微信授权登录和微信扫码登录代码详细分析
一、整体架构
1.1 技术栈
- 后端 : Flask + JWT + YunGouOS API
- 前端 : uni-app (Vue3) + 微信JS SDK (wxLogin.js)
- 第三方服务 : YunGouOS (微信开放平台接口封装)
1.2 登录方式对比

二、数据模型
2.1 WechatUser 模型
python
class WechatUser(db.Model):
id = db.Column(db.Integer, primary_key=True)
openid = db.Column(db.String(128), unique=True,
nullable=False) # 微信唯一标识
unionid = db.Column(db.String(128)) # 开放平台统一标识
nickname = db.Column(db.String(128)) # 昵称
avatar_url = db.Column(db.String(512)) # 头像
gender = db.Column(db.Integer, default=0) # 性别
country = db.Column(db.String(64)) # 国家
province = db.Column(db.String(64)) # 省份
city = db.Column(db.String(64)) # 城市
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) # 关
联系统用户
2.2 扫码会话存储
python
# 内存存储扫码登录状态(生产环境建议使用Redis)
qrcode_login_sessions = {}
# 会话结构
{
'scene_id': 'weblogin_1777994028_xxx', # 场景ID
'status': 'waiting', # waiting/confirmed/expired
'openid': None, # 微信openid
'user_id': None, # 系统用户ID
'token': None, # JWT token
'username': None, # 用户名
'created_at': 1777994028, # 创建时间
'expires_at': 1777994328 # 过期时间(5分钟)
}
三、YunGouOS 签名算法
3.1 签名生成
python
def create_sign(params, partner_key):
# 1. 复制参数,移除sign
sign_params = params.copy()
sign_params.pop('sign', None)
# 2. 过滤空值参数
filtered_params = {k: v for k, v in sign_params.items() if v
is not None and v != ''}
# 3. 按参数名ASCII码升序排序
sorted_params = sorted(filtered_params.items())
# 4. 拼接字符串:key1=value1&key2=value2...
string_a = '&'.join([f"{k}={v}" for k, v in sorted_params])
# 5. 拼接密钥
string_sign_temp = f"{string_a}&key={partner_key}"
# 6. MD5加密并转大写
sign = hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest
().upper()
return sign
3.2 签名规则
- 只有必填参数参与签名
- 可选参数(如 params )不参与签名
- 参数按 ASCII 码升序排序
- 最后拼接 &key=partner_key
- MD5 加密后转大写
四、微信授权登录 (H5)
4.1 后端接口
1. 获取授权链接
接口 : GET /api/wechat/h5/auth-url
功能 :
- 调用 YunGouOS get_oauth_url() 获取授权链接
- 授权类型: mp-base (基础授权,静默获取openid)
返回数据 :
json
{
"msg": "获取授权链接成功",
"auth_url": "https://open.weixin.qq.com/connect/oauth2/authorize?..."
}
2.授权回调
接口 : GET /api/wechat/oauth/callback
功能 :
- 接收微信回调参数 code
- 调用 YunGouOS get_oauth_info() 查询授权信息
- 解析 openid
- 查找或创建 WechatUser
- 检查是否已绑定系统账号
- 已绑定:生成JWT token,重定向到前端
- 未绑定:重定向到绑定页面
重定向URL :
- 已绑定: http://localhost:5173/#/pages/auth/wechat-login?token=xxx\&user_id=1\&username=admin
- 未绑定: http://localhost:5173/#/pages/auth/wechat-login?openid=xxx\&need_bind=1
3.H5登录接口
接口 : POST /api/wechat/h5/login
功能 :
- 接收前端传来的 code
- 调用 YunGouOS get_oauth_info() 查询授权信息
- 查找或创建 WechatUser
- 检查是否已绑定系统账号
- 返回 JSON 响应
返回数据 :
json
// 已绑定
{
"msg": "登录成功",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "admin",
"avatar": "..."
}
}
// 未绑定
{
"msg": "未绑定系统账号",
"openid": "o-_-itxxxxxxxxxxxxxx",
"need_bind": true
}
4.2 前端实现
文件 : uniapp/pages/auth/wechat-login.vue
核心流程
vue
onMounted(async () => {
// 1. 检测是否在微信浏览器中
// 检测是否在微信浏览器中
// if (!isWechatBrowser()) {
// status.value = 'error';
// errorMessage.value = '请在微信客户端中打开此页面进行授权登录';
// return;
// }
// 2. 检查是否有回调参数(从后端重定向回来)
if (hasCallbackParams) {
handleCallback(); // 处理token/openid/error参数
} else {
// 3. 检查是否有code参数(微信授权回调)
if (code) {
handleWechatLogin(code); // 调用后端登录
} else {
// 4. 没有code,跳转到微信授权
const authUrl = await wechatApi.getAuthUrl();
window.location.href = authUrl;
}
}
});
状态处理

Token 保存
js
// 使用 userStore 同步状态
userStore.token = res.token;
userStore.userInfo = res.user;
// 持久化到本地存储
setToken(res.token);
setUserInfo(res.user);
五、微信扫码登录 (PC)





5.1 后端接口
初始化扫码登录
接口 : POST /api/wechat/weblogin/init
功能 :
- 生成唯一 scene_id
- 创建会话并存储到 qrcode_login_sessions
- 调用 YunGouOS get_web_login() 获取扫码参数
- 构建微信开放平台扫码授权URL
- 返回参数给前端
返回数据 :
json
{
"msg": "获取成功",
"scene_id": "weblogin_1777994028_xxx",
"auth_url": "https://open.weixin.qq.com/connect/qrconnect?...",
"appId": "wx7xxxxxxxxxxxx",
"scope": "snsapi_login",
"state": "B4C2B73xxxxxxxxxxxxxxx6AA8B",
"redirect_uri": "https://api.wx.yungouos.com/callback/wxmp/oauth",
"expires_in": 300
}
扫码登录回调
接口 : GET /api/wechat/weblogin/callback
功能 :
- 接收微信回调参数 code 和 state
- 调用 YunGouOS get_oauth_info() 查询授权信息
- 解析 scene_id 和 openid
- 查找会话并检查是否过期
- 查找或创建 WechatUser
- 检查是否已绑定系统账号
- 如果已绑定:更新会话状态为 confirmed ,生成JWT token
- 返回简单HTML页面(避免iframe显示JSON)
返回内容 :
html
<!DOCTYPE html>
<html>
<head>
<title>扫码成功</title>
</head>
<body>
<div class="container">
<div class="icon">✓</div>
<h1>扫码成功</h1>
<p>请在原页面查看登录状态</p>
</div>
</body>
</html>
扫码状态轮询
接口 : GET /api/wechat/weblogin/status?scene_id=xxx
功能 :
- 根据 scene_id 查询会话状态
- 检查是否过期
- 返回当前状态和登录信息
返回数据 :
json
{
"status": "confirmed", // waiting/confirmed/expired
"msg": "登录成功",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "admin"
}
}
微信账号绑定
接口 : POST /api/wechat/bind
功能 :
- 验证系统账号密码
- 查找微信用户
- 绑定微信到系统账号(设置 wechat_user.user_id )
- 生成JWT token并返回
5.2 前端实现
文件 : uniapp/pages/auth/qrcode-login.vue
核心流程
js
// 动态加载微信JS文件
const loadWxLoginScript = () => {
return new Promise((resolve, reject) => {
if (typeof WxLogin !== "undefined") {
resolve();
return;
}
const script = document.createElement("script");
script.src =
"https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js";
script.onload = () => resolve();
script.onerror = () => reject(new Error("加载微信JS文件失败"));
document.head.appendChild(script);
});
};
生成二维码
js
const generateQrcode = async () => {
// 1. 调用后端获取扫码参数
const res = await wechatApi.initWebLogin();
// 2. 动态加载微信JS SDK
if (typeof WxLogin === 'undefined') {
await loadWxLoginScript();
}
// 3. 设置状态渲染容器
status.value = 'waiting';
await nextTick();
// 4. 初始化微信二维码
new WxLogin({
self_redirect: true, // 在当前iframe内跳转
id: 'wx_qrcode_container',
appid: res.appId,
scope: res.scope,
redirect_uri: encodeURIComponent(res.redirect_uri),
state: res.state,
style: 'black',
href: '',
fast_login: 0,
});
// 5. 开始轮询和倒计时
startPolling(res.scene_id);
startCountdown(res.expires_in);
};
状态轮询
js
const startPolling = (sid) => {
pollTimer = setInterval(async () => {
const res = await wechatApi.checkWebloginStatus({ scene_id: sid });
if (res.status === 'confirmed') {
// 登录成功,保存token并跳转
userStore.token = res.token;
userStore.userInfo = res.user;
setToken(res.token);
setUserInfo(userStore.userInfo);
setTimeout(() => {
window.location.href = '/#/pages/index/index';
}, 1500);
} else if (res.status === 'expired') {
// 二维码过期
status.value = 'expired';
clearInterval(pollTimer);
}
}, 3000);
};
倒计时
js
const startCountdown = (seconds) => {
countdown.value = seconds;
if (countdownTimer) {
clearInterval(countdownTimer);
}
countdownTimer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(countdownTimer);
}
}, 1000);
};
状态流转
PlainText
loading → waiting → (iframe显示扫码成功) → confirmed → success → 跳转首页
↓
expired → 显示刷新按钮
六、YunGouOS API 调用
6.1 获取授权链接
python
def get_oauth_url(mch_id, callback_url, partner_key, auth_type='mp-base', params=None):
# 只有必填参数参与签名
sign_params = {
'mch_id': mch_id,
'callback_url': callback_url
}
sign = create_sign(sign_params, partner_key)
# 请求参数包含所有参数
request_params = {
'mch_id': mch_id,
'callback_url': callback_url,
'type': auth_type,
'sign': sign
}
if params:
request_params['params'] = params
response = requests.post(
'https://api.wx.yungouos.com/api/wx/getOauthUrl',
data=request_params,
timeout=10
)
return response.json()
6.2 查询授权信息
python
def get_oauth_info(mch_id, code, partner_key):
# 必填参数参与签名
sign_params = {
'mch_id': mch_id,
'code': code
}
sign = create_sign(sign_params, partner_key)
request_params = {
'mch_id': mch_id,
'code': code,
'sign': sign
}
response = requests.get(
'https://api.wx.yungouos.com/api/wx/
getOauthInfo',
params=request_params,
timeout=10
)
return response.json()
6.3 获取扫码登录参数
python
def get_web_login(mch_id, callback_url, partner_key,
params=None):
# 只有必填参数参与签名
sign_params = {
'mch_id': mch_id,
'callback_url': callback_url
}
sign = create_sign(sign_params, partner_key)
request_params = {
'mch_id': mch_id,
'callback_url': callback_url,
'sign': sign
}
if params:
request_params['params'] = params
response = requests.post(
'https://api.wx.yungouos.com/api/wx/
getWebLogin',
data=request_params,
timeout=10
)
return response.json()
七、配置参数
7.1 环境变量
python
# 后端配置
YUNGOUOS_MCH_ID = '100xxxxxx' # 商户号
YUNGOUOS_PARTNER_KEY = '你的密钥' # 商户密钥
WECHAT_OAUTH_CALLBACK = 'http://localhost:5555/api/wechat/oauth/callback'
WECHAT_WEB_LOGIN_CALLBACK = 'https://api.wx.yungouos.com/callback/wxmp/oauth'
WECHAT_QRCODE_EXPIRE_TIME = 300 # 5分钟
FRONTEND_URL = 'http://localhost:5173'
八、安全机制
8.1 会话安全
- 会话存储在内存(生产环境建议用Redis)
- 5分钟自动过期
- 每次扫码生成唯一 scene_id
8.2 签名安全
- YunGouOS 签名使用 MD5 + partner_key
- 参数按key排序后拼接
- 防止参数篡改
8.3 Token安全
- 使用 JWT (flask_jwt_extended)
- Token 包含用户ID
- 前端存储在 localStorage 和 userStore
九、流程图
9.1 微信授权登录流程
用户访问微信登录页面
↓
检测是否在微信浏览器中
↓
调用 /api/wechat/h5/auth-url 获取授权链接
↓
跳转到微信授权页面
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx6xxxxxxxxxb4a&redirect_uri=https://api.wx.yungouos.com/callback/wxmp/oauth&response_type=code&scope=snsapi_base&state=B695C4xxxxxxxxxx4140E6F2&connect_redirect=1#wechat_redirect
↓
用户授权,微信回调 YunGouOS
↓
YunGouOS 回调后端 /api/wechat/oauth/callback
http://localhost:5555/api/wechat/oauth/callback?code=B695Cxxxxxxxxxxxxxxx3FD4140E6F2
↓
后端查询授权信息,生成JWT token
↓
重定向到前端 wechat-login.vue
http://localhost:5173/#/pages/auth/wechat-login?token=eyJ0eXAxxxxxxxxxxxxxxx&user_id=1&username=admin
↓
前端处理URL参数,保存token
↓
跳转到首页
带token和用户信息重定向到前端,前端解析url保存token、用户信息
跳转到首页
9.2 微信扫码登录流程
用户访问扫码登录页面
↓
前端调用 /api/wechat/weblogin/init
↓
后端生成scene_id,调用YunGouOS获取参数
↓
前端加载微信JS SDK,渲染二维码
↓
用户用微信扫描二维码
↓
微信跳转到 YunGouOS 回调地址
↓
YunGouOS 回调后端 /api/wechat/weblogin/callback
↓
后端查询授权信息,更新会话状态
↓
后端返回HTML页面(iframe显示)
↓
前端轮询检测到 confirmed 状态
↓
保存token,跳转到首页
十、待优化项
- 会话存储 : 当前使用内存,生产环境应改用 Redis
- 错误处理 : 增加更详细的错误日志和用户提示
- 二维码刷新 : 支持手动刷新和自动刷新
- 多端同步 : 支持手机扫码后PC端自动登录
- 安全加固 : 增加IP限制、频率限制等
- 日志完善 : 统一日志格式,增加请求追踪ID