微信登录
从账号密码到微信扫码:登录认证体系的配置、实践与升级
本文面向没有接入过微信登录的同学。我们沿着“打开登录页 → 扫码确认 → 首次绑定 → 进入首页 → 刷新页面 → 退出登录”的顺序,解释谁在操作、请求传了什么、后端检查什么,以及怎样确认成功。
全文采用通用教学口径,不对应某个现有系统的完整接口契约。示例接口统一以 /auth 开头;字段、Cookie 名称和有效期均可按实际业务调整,不能直接替换已有生产配置。
本文重点是电脑网站的微信扫码登录,不是小程序登录,也不是微信内置浏览器的网页授权。三者可能涉及不同的应用类型、授权能力与接口。
一、先认识四个参与者
前端登录页运行在浏览器里,不是浏览器之外的另一台机器。真正需要分清的是电脑浏览器、手机微信、业务后端与微信开放平台。
| 参与者 | 负责什么 | 不负责什么 |
|---|---|---|
| 电脑浏览器及其中的登录页 | 展示二维码、发请求、接收跳转、保存并携带 Cookie | 不自行决定用户身份和业务权限,不保存 AppSecret |
| 手机微信 | 扫描二维码,让用户确认或取消授权 | 不直接给电脑创建业务系统 Session |
| 业务后端 | 校验回调、调用微信、查找本地账号、建立会话、检查权限 | 不把微信身份直接当成本地管理员权限 |
| 微信开放平台 | 识别微信用户,为授权流程签发临时凭证 | 不管理业务系统里的本地账号、角色和数据归属 |
它们不是简单的四级转发关系,而是各自承担不同交互:
电脑浏览器(运行登录页) ↔ 业务后端
↕ ↕
微信授权页面 微信平台身份接口
↕
手机微信
业务后端 ↔ 数据库、Redis
数据库负责保存账号、绑定关系等长期数据。Redis 是一种数据存储组件,这里用来保存短期授权记录、验证码或服务端会话;它不是微信平台,也不是一种登录协议。
浏览器可以保存登录凭证。本文把随机 Session ID 放在 HttpOnly Cookie 中,由浏览器管理,页面 JavaScript 不需要读取它。AppSecret 则只保留在后端。
二、先看普通账号密码登录
用户在前端填写账号密码,浏览器提交给后端。后端验证成功以后,不需要用户每次点击页面都重新输入密码,而是建立一个可以在后续请求中识别的登录会话。
用户填写账号密码
↓
浏览器提交给后端
↓
后端检查密码、账号状态和所属范围
↓
加载用户、角色和权限
↓
生成不可预测的随机 Session ID
↓
Redis 保存服务端会话记录并设置有效期
↓
响应通过 Set-Cookie 把 Session ID 交给浏览器
↓
浏览器后续自动携带符合发送规则的 Cookie
本文约定登录接口为:
POST /auth/login
Content-Type: application/x-www-form-urlencoded
username=example_user&password=EXAMPLE_ONLY
上面的账号密码是占位示例,不能作为真实账号使用。username 表示账号,password 表示用户输入的密码;后端验证密码时,应使用安全的密码哈希校验,而不是在数据库明文保存密码。
示例使用 URL 编码表单,是为了说明一种可兼容历史表单解析器的提交方式。JSON 也是可选方式,但前后端必须约定一致;请求体格式不一致时,后端可能读不到参数。
登录接口返回成功后,页面还要查询当前用户,确认浏览器确实保存并携带了有效凭证:
GET /auth/me
本文采用以下公开用户资料作为示例,真实系统的字段名称应以接口约定为准:
{
"code": 200,
"data": {
"userId": 1001,
"username": "example_user",
"tenantId": 2001,
"roleIds": [3]
}
}
userId 是本地用户编号,tenantId 表示用户当前所在的数据隔离空间,roleIds 是角色编号列表。它们来自后端校验后的会话,而不是由前端自报后直接相信。
三、Cookie、Session、JWT、Token、localStorage 分别是什么
登录认证首先回答“请求来自谁”,后续权限校验再回答“这个人允许做什么”。下面这些名词分属不同层面,不能当成只能选一个的替代方案。
| 名称 | 本质 | 在登录中的作用 |
|---|---|---|
| Token | 凭证的统称 | 用来证明某种身份或授权 |
| Session | 服务端会话记录 | 保存当前用户、权限范围和有效期等信息 |
| Session ID | 会话的随机编号 | 帮助后端找到对应会话 |
| Cookie | 浏览器保存并按规则自动发送的数据 | 可以承载 Session ID,也可以承载其他凭证 |
| JWT | 一种令牌格式 | 可以通过签名保护其中的身份和授权声明 |
| localStorage | 浏览器中由页面脚本读写的存储 | 不会自动随 HTTP 请求发送,本身不具备认证能力 |
| Redis | 数据存储组件 | 可以存 Session、临时授权状态或其他缓存 |
因此,“Session + Cookie + Redis”并不矛盾:Session 是记录,Cookie 是浏览器携带编号的方式,Redis 是后端保存记录的位置。
3.1 Session 怎样保存在 Redis 中
后端验证登录成功后,生成安全随机的 Session ID。不要直接使用用户 ID、手机号、时间戳或容易推测的字符串作为会话凭证。
一种设计是使用下面的 Redis 键:
auth:session:{domainCode}:{SHA256(sessionId)}
domainCode 区分业务范围,SHA256(sessionId) 表示会话编号的摘要。摘要不是加密,也不替代安全随机值;这种设计避免在 Redis 键名中直接出现原始会话编号。
对应记录示例:
{
"userId": 1001,
"tenantId": 2001,
"domainCode": "web",
"roleIds": [3],
"createdAt": "2026-01-01T00:00:00Z",
"expiresAt": "2026-01-02T00:00:00Z"
}
这里的时间只是演示固定 24 小时有效期。Redis 还应设置相应 TTL,即记录存活时间;不要仅在 JSON 里写一个截止时间,却永远不清理缓存。
会话记录不应保存明文密码或微信 AppSecret。如果系统支持代操作或切换数据范围,可以额外记录真实操作者,但它不属于所有登录系统都必须具备的字段。
Redis 可以部署在受控服务器,也可以使用托管服务;Java 后端常通过 RedisTemplate 等客户端访问。浏览器不应直接连接 Redis。
3.2 后端如何把编号交给浏览器
后端在 HTTPS 登录响应中添加:
Set-Cookie: session_id=RANDOM_SESSION_ID; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400; Secure
RANDOM_SESSION_ID 是说明位置的占位符,实际值必须随机生成。浏览器按 Cookie 规则保存它,后续请求携带的是 Cookie 请求头,而不是重复发送 Set-Cookie。
Cookie: session_id=RANDOM_SESSION_ID
| 属性 | 作用 | 注意事项 |
|---|---|---|
Path=/ |
匹配当前主机下的所有路径 | 不是资源权限校验,也不是跨域许可 |
HttpOnly |
阻止页面脚本直接读取这枚 Cookie | 不代表 XSS 无法借浏览器发送请求 |
Secure |
限制通过安全连接发送 | HTTPS 部署应正确启用,代理转发信息也需正确配置 |
SameSite=Lax |
限制部分跨站携带行为 | 允许符合条件的跨站顶层安全导航,不是完整 CSRF 防护 |
Max-Age=86400 |
示例中浏览器保留 86400 秒 | 与服务端有效期是两个需要协调的限制 |
未设置 Domain |
默认只发送给设置它的主机 | 不会自动在不同子域之间共享 |
关键区别是“交给浏览器”和“暴露给页面脚本”不是同一件事。后端需要通过 Cookie 交付 Session ID,但不应再把它放入公开 JSON、动态脚本或日志中。Cookie 行为可参考 MDN:Set-Cookie。
CSRF 指外部网站诱导浏览器,借用自动携带的 Cookie 执行用户并不想做的操作。登录、退出、绑定和业务写接口都需要按风险落实防护,不能只看有没有有效 Cookie。
优先使用框架维护的 CSRF 防护,例如验证与当前会话关联的 CSRF Token,并校验可信请求来源。这个 Token 用于确认请求,不是 Session ID,不应拿登录凭证代替它。OWASP:CSRF 防护
后面的 OAuth state 只保护扫码授权关联,不能替代其他接口的 CSRF 防护。本文请求片段主要展示业务参数,尚未给出整套 CSRF 实现,不能直接当成完整生产代码。
3.3 JWT 与旧 tokenKey 方案有什么区别
常见签名型 JWT 由 Header.Payload.Signature 三部分组成。Header 描述算法等信息,Payload 保存声明,Signature 用来验证完整性和来源。头部和载荷通常只是 Base64URL 编码,可以被读取,不应包含密码或密钥。JWT 标准
一种常见用法是客户端直接携带 JWT,后端验证签名、有效期和预期声明后再检查权限。是否需要查询数据库或撤销记录,取决于系统设计,不能说使用 JWT 就一定不访问服务端存储。
本文讨论的历史兼容模型不同:
后端生成 JWT
↓
JWT 保存在 Redis 中
↓
另外生成随机 tokenKey,交给浏览器
↓
后续请求携带 tokenKey
↓
后端根据 tokenKey 从 Redis 取出 JWT,再验证
因此,tokenKey 不是 JWT 原文;这个旧方案本来就依赖 Redis。升级到统一 Session 的重点是会话结构、入口、权限上下文和生命周期治理,不能简单说成“从纯无状态变成有状态”。

此图保留自原稿,作为概念辅助;具体旧凭证的数据流以上文 tokenKey 说明为准,不据此推定某个系统已经采用客户端直传 JWT。
四、打开页面时,怎样恢复登录状态
刷新页面会重新运行前端代码,但不会因此自动丢失尚未到期的 Cookie。后端仍可以根据浏览器携带的 Session ID 恢复登录态。
常见入口包括当前用户 JSON 接口,或者为历史页面提供公开会话资料的动态 JavaScript。这里保留动态脚本方案,帮助理解历史入口如何接入统一会话。
4.1 动态脚本提供的是公开快照
浏览器加载 /auth/session.js,按规则携带 Cookie
↓
后端根据 Session ID 查询会话
↓
核对有效期、用户状态、数据范围与权限
↓
生成只包含公开会话资料的 JavaScript
↓
浏览器执行脚本,页面据此展示或跳转
示例脚本内容:
window.authRuntime = {
user: {
userId: 1001,
tenantId: 2001,
roleIds: [3],
},
};
未登录时,示例返回 window.authRuntime = { user: null };。脚本中不能包含原始 Session ID、旧 tokenKey、AppSecret 或其他可用于冒用身份的凭证。
动态会话响应应避免被共享缓存复用,设置合适的 Cache-Control: no-store 和 JavaScript 内容类型。把数据拼入脚本时还要安全序列化、转义危险字符,不能直接拼接用户昵称等输入。
4.2 页面判断不能代替后端鉴权
下面是放在受保护页面上的最小教学示例。它使用普通同步脚本顺序,先执行会话脚本,再检查公开用户资料,不代表某个前端框架的完整路由守卫。
<script src="/auth/session.js"></script>
<script>
const user = window.authRuntime?.user;
if (!user?.userId) {
window.location.replace("/login.html");
}
</script>
生产实现还要处理脚本加载失败、超时、闪屏和安全回跳。脚本异常时不能继续显示敏感内容,也不能把一个可任意修改的 window 变量当成身份凭证。
前端守卫决定显示什么,后端鉴权决定能拿到什么数据。即使有人删除跳转代码,业务后端仍必须对受保护请求验证真实登录态与资源权限。
页面打开后,会话也可能随后到期。前端因此还要处理业务接口的未登录响应,而不是只在首次加载时检查一次。
五、接入微信前,要完成哪些配置
现在假设小明打开电脑登录页、选择微信扫码,并在手机确认。微信可以告诉后端“这是哪个微信用户”,但“对应哪个本地账号、能使用什么功能”需要业务系统自己判断。

5.1 平台、部署和开发准备可以并行
| 操作人员 | 要做的事 | 完成标志 |
|---|---|---|
| 平台配置人员 | 在微信开放平台创建网站应用,按要求提交资料、审核并配置回调域名 | 取得可用的 AppID、AppSecret 和相应授权能力 |
| 部署人员 | 准备域名解析、HTTPS、前后端请求转发,以及数据库和 Redis 连接 | 登录页能打开,接口能到正确后端,后端能够访问微信 API |
| 开发人员 | 接入登录入口、应用配置、回调接口、首次绑定与短信流程 | 完成授权前置检查后,可以进行完整联调 |
上述工作可以分工同时推进,但真正联调时需要它们全部就绪。微信平台当前的菜单、资料、费用和审核要求,以微信开放平台实际说明为准。
5.2 回调域名、回调地址、登录页地址不是一回事
| 名称 | 示例 | 用途 |
|---|---|---|
| 授权回调域名 | login-demo.example.com |
按平台要求约束授权结果允许返回的域名 |
| 后端回调地址 | https://login-demo.example.com/auth/wechat/callback/{openAppId} |
接收 code 和 state,完成服务端处理 |
| 前端登录页地址 | https://login-demo.example.com/login.html |
展示登录结果、错误提示或绑定表单 |
本篇回调示例带有内部应用记录 ID:{openAppId}。它不是微信 AppID,也不是用户 ID;实际接入时要替换占位符。单应用系统也可以设计不含这个路径参数的回调接口。
第一次联调可以让前端页面和回调位于同一个 HTTPS 主机,减少 Cookie 丢失与跨域配置问题。跨域不是不能实现,但还需要共同处理凭据携带、允许来源以及 Cookie 的域和站点规则。
5.3 配置分成三层,不要全部放进一个公开 JSON
| 配置层 | 内容 | 谁可以接触 |
|---|---|---|
| 前端公开配置 | 微信入口开关、SDK 地址 | 浏览器可以读取 |
| 后端私密与应用配置 | AppID、AppSecret、回调地址、允许开关 | 后端受控管理;AppSecret 不返回页面 |
| 每次扫码临时数据 | 随机 state、应用关联、有效时间 | 每次由后端创建,随流程到期或被消费 |
前端公开配置示例:
{
"features": {
"wechatLogin": true
},
"wechat": {
"scriptUrl": "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
}
}
JSON 里不能放 HTML 注释或 // 注释。解释应写在代码块外;如果为了说明使用带注释示例,必须明确它不是标准 JSON。
后端开关和时长配置可以类似:
wechat:
open-login:
enabled: true
state-ttl-seconds: 300
pending-ttl-seconds: 600
这段 YAML 只是开关和有效期示例,不是完成接入所需的全部配置。AppSecret 需通过受保护的服务端配置管理方式注入,不能写进公开仓库、前端脚本或截图。
如果应用配置已经存入数据库,要明确运行时实际读取的是哪一份配置。修改 YAML 不一定会覆盖数据库里的已有值;这类优先级应通过实现和运行环境核对,不能靠猜测。
前端公开配置可以由后端通过 JSON 接口或动态脚本下发。本文使用 GET /auth/frontend-config.js 作为脚本入口示例;这只是教学约定,不代表某个项目已经存在该路径。
六、微信登录全流程:从展示二维码到进入首页
先从用户视角看一遍:
- 电脑打开登录页,切换到微信扫码。
- 页面取得本次授权配置并显示二维码。
- 手机微信扫码,用户确认或取消授权。
- 未绑定的微信先验证手机号并关联本地账号。
- 后端建立会话,电脑确认登录态后进入首页。
- 刷新页面时,浏览器继续携带有效 Cookie。
- 退出时,后端撤销当前会话对应凭证。

此图保留自原稿,图片内部内容未在本次修改中重新编辑。下文按通用接口示例解释各步骤;配置和示例不代表实际平台审核或线上联调已经完成。

第一步:前端向后端申请本次扫码配置
用户进入微信登录区域后,前端请求:
GET /auth/wechat/config
后端先检查总开关和可用应用,再安全随机生成本次 state。返回示例:
{
"code": 200,
"data": {
"enabled": true,
"appId": "WECHAT_APP_ID",
"scope": "snsapi_login",
"redirectUri": "https://login-demo.example.com/auth/wechat/callback/EXAMPLE_APP_RECORD_ID",
"state": "SERVER_GENERATED_RANDOM_STATE",
"expiresIn": 300
}
}
以上大写值均为占位符。响应外层的业务状态 code: 200 与微信回调中的临时授权 code 不是同一个概念。
| 字段 | 含义 |
|---|---|
enabled |
后端当前是否允许发起微信登录 |
appId |
微信平台上的网站应用标识,可以用于前端展示二维码 |
scope |
申请的授权范围,本文网站扫码示例使用 snsapi_login |
redirectUri |
授权后由浏览器访问的后端完整回调地址 |
state |
每次由后端随机生成的授权关联凭证,不是短信验证码 |
expiresIn |
本次后端授权上下文还能使用多少秒,不代表微信所有凭证的有效期 |
同一次配置请求中,后端还要完成两个关联动作:
Redis 键:auth:wechat:state:{SHA256(state)}
Redis 值:domainCode、openAppId、创建时间
Redis TTL:示例 300 秒
响应 Cookie 名:wx_state
Cookie 值:state 原值
Cookie 有效期:与本次流程协调设置
这是同一个请求中的两项工作,不表示它们一定使用并行线程执行。应先确保服务端临时记录创建成功,再交付给浏览器使用。
wx_state 也应由后端设置 HttpOnly、HTTPS 下的 Secure、合适的 SameSite、Path 和短有效期。本例顶层 GET 回调可配合 SameSite=Lax;如果限制 Path,必须覆盖回调路径。
HttpOnly 保护的是 Cookie 的脚本读取入口;本次 state 仍需通过配置响应交给微信组件。配置响应应禁止缓存,流程结束或重置时清理对应临时状态,不要把 state 当作正式会话凭证。
第二步:前端使用官方组件显示二维码
前端需要准备二维码容器、加载 SDK,再把本次配置交给 WxLogin。不要硬编码 state,也不要从前端传入一个任意 AppSecret。
下面是最小初始化示例,不包含完整生产重试、多标签协调与错误分类:
<script src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
<div id="login_container"></div>
<button id="refresh_qr" type="button">获取或刷新二维码</button>
<p id="login_message" role="status"></p>
<script>
const refreshButton = document.getElementById("refresh_qr");
const message = document.getElementById("login_message");
refreshButton.addEventListener("click", async () => {
refreshButton.disabled = true;
message.textContent = "正在获取二维码…";
try {
if (typeof window.WxLogin !== "function") {
throw new Error("微信组件加载失败,请刷新页面重试");
}
const response = await fetch("/auth/wechat/config", {
credentials: "include",
cache: "no-store",
});
if (!response.ok) throw new Error("获取扫码配置失败");
const result = await response.json();
const config = result.data;
if (result.code !== 200 || !config?.enabled) {
throw new Error("微信登录暂不可用");
}
document.getElementById("login_container").replaceChildren();
new window.WxLogin({
self_redirect: false,
id: "login_container",
appid: config.appId,
scope: config.scope,
redirect_uri: encodeURIComponent(config.redirectUri),
state: config.state,
style: "black",
});
message.textContent = "请用手机微信扫码,并在手机上确认";
} catch (error) {
message.textContent =
error instanceof Error ? error.message : "请稍后重试";
} finally {
refreshButton.disabled = false;
}
});
</script>
id 指向已有容器;appid、scope、redirect_uri、state 来自本次后端配置。示例使用 self_redirect: false 让授权后在顶层窗口跳转;选择嵌入式跳转时还要评估 Cookie 和页面承载方式。
credentials: "include" 表示请求携带符合规则的浏览器凭据,不是 JavaScript 手动读取 HttpOnly Cookie。它也不能单独解决跨域问题,服务器与 Cookie 配置仍需配合。
第三步:用户扫码确认,电脑浏览器访问回调
电脑展示二维码时,用户在手机微信上完成扫码和确认。授权流程随后引导电脑浏览器访问后端回调地址:
GET /auth/wechat/callback/EXAMPLE_APP_RECORD_ID?code=TEMP_AUTH_CODE&state=SERVER_GENERATED_RANDOM_STATE
Cookie: wx_state=SERVER_GENERATED_RANDOM_STATE
| 参数 | 来源 | 用途 |
|---|---|---|
路径中的 openAppId |
后端配置的内部应用记录 | 定位本次接入应用,必须与服务端授权记录匹配 |
查询参数 code |
微信授权流程 | 由后端向微信换取身份,不能重复当作登录凭证使用 |
查询参数 state |
之前后端生成,随授权过程返回 | 关联本次登录尝试 |
Cookie 中的 wx_state |
发起尝试的电脑浏览器保存 | 帮助后端校验发起浏览器与回调是否一致 |
手机确认不会直接创建业务会话。到这里,后端仍需验证回调;页面 URL 显示“成功”也不能替代这一步。
第四步:后端校验 Cookie、state 与 Redis 记录
后端收到请求后,不能直接相信路径和查询参数,要按顺序验证:
检查 code、state 是否存在
↓
检查 URL state 与当前浏览器 wx_state Cookie 是否一致
↓
按 state 摘要原子读取并删除 Redis 记录
↓
检查记录是否存在、是否有效、所属应用和业务范围是否匹配
↓
通过:继续换取微信身份
失败:终止本次流程,提示重新获取二维码
“原子读取并删除”是指两步作为不可分割的操作完成。否则两个重复回调可能同时读到同一条有效记录,导致重复处理。
可以使用 Redis 的相应原子命令或 Lua 脚本实现,具体取决于部署版本。GETDEL 能读取字符串值并删除对应键,但它本身不会替你校验 Cookie、应用或用户权限。Redis:GETDEL
state 成功消费后不能重复使用。后续调用失败时要提示用户重新开始,不能把已消费的旧 state 当作可以无限重试的凭证。
第五步:后端用 code 换取微信身份
上一阶段证明“这是有效的授权尝试”,这一阶段才确认“扫码的是哪个微信用户”。业务后端使用受控配置中的 AppID、AppSecret 和本次 code 与微信交互。
业务后端携带 AppID、AppSecret、code 调用微信
↓
取得微信用户标识与调用微信接口的短期凭据
↓
按需查询头像、昵称等资料
↓
校验响应,再查找本地账号绑定
这些是后端到微信的请求,不是浏览器直接带着 AppSecret 调用微信。浏览器开发者工具中看不到这段服务端请求是正常现象,排查需要使用脱敏的服务端日志。
| 身份或凭证 | 含义 |
|---|---|
openid |
用户在某个微信应用下的标识,不能直接当作本地 userId |
unionid |
满足微信相关条件时可用于跨应用识别的标识,可能缺失 |
| 微信 access token | 后端调用相关微信接口的短期凭据 |
| 本地 Session ID | 浏览器访问业务系统的会话凭证 |
不要把微信 access token 与本地 Session ID 混用,也不要仅凭相同昵称或头像自动合并账号。跨应用合并涉及业务规则,不能假设拿到 unionid 就应自动合并所有身份。
第六步:查找微信与本地账号的绑定关系
业务系统需要保存以下关系:
微信应用标识 + openid → 本地 userId
后端查询后分两条正常路径处理:
找到绑定 → 检查绑定与账号状态 → 核对范围和权限 → 统一登录
没有绑定 → 建立待绑定记录 → 验证手机号 → 绑定或注册
已经绑定也不代表永远可以登录。账号停用、绑定禁用或所属范围不匹配时,应拒绝,而不是因为微信认证通过就绕过本地规则。
第七步:首次登录,先进入待绑定状态
没有绑定时,后端先暂存微信身份,生成随机待绑定 ticket,并通过 Cookie 交给浏览器。它还不是正式会话,不能凭它访问受保护业务接口。

Redis:auth:wechat:pending:{SHA256(ticket)}
记录:应用、微信身份、创建时间、后续已验证信息
初始有效期:示例 600 秒
浏览器 Cookie:wx_bind=ticket
wx_bind 同样由后端设置 HttpOnly、Secure、合适的 SameSite、Path 和短有效期;Path 必须覆盖待绑定查询和完成绑定接口。结束流程后清理,不交给页面脚本自行管理原始 ticket。
此时浏览器还停留在后端回调请求中。后端通过 Set-Cookie 交付 ticket,同时返回重定向,让电脑回到 /login.html?wechat=bind,由登录页展示绑定流程。
前端识别 wechat=bind 提示后,带 Cookie 查询待绑定状态。URL 参数只负责提示页面做什么,不证明用户拥有待绑定身份:
GET /auth/wechat/pending
返回示例:
{
"code": 200,
"data": {
"pending": true,
"nickname": "示例昵称",
"avatarUrl": "https://example.com/avatar.png",
"expiresIn": 600
}
}
后端不需要把原始待绑定 ticket 再放到公开响应里。头像和昵称用于提示“正在绑定哪个微信”,不是允许前端决定账号归属的证据。
第八步:验证手机号,再绑定或创建账号
手机号验证是本文采用的绑定策略,不是微信登录协议要求每个网站都必须执行的步骤。它用于确认当前操作人能够接收该手机号的短信,再按本地规则关联账号。
填写手机号 → 完成滑块验证 → 申请短信 → 输入短信验证码
↓
后端验证短信并查询账号
滑块降低自动化滥用风险,短信验证码验证手机号控制权;两者不能相互替代。短信申请和绑定接口还需要限流、防重复提交及有效期校验。
POST /auth/wechat/complete
Content-Type: application/json
{
"phone": "用户填写的手机号",
"smsCode": "用户收到的验证码"
}
验证成功后,按账号情况处理:
| 账号情况 | 后端处理 | 页面结果 |
|---|---|---|
| 已存在且可用 | 校验范围,保存绑定,建立会话 | 返回 LOGGED_IN |
| 不存在 | 在后端记录已验证手机号,要求补资料 | 返回 COMPLETE_PROFILE |
| 停用、冲突等异常 | 拒绝绑定,不建立会话 | 展示明确且不泄露敏感信息的提示 |
新账号补资料的通用示例:
{
"name": "用户填写的昵称",
"password": "用户设置的密码"
}
第二次仍提交到 /auth/wechat/complete。具体注册字段和校验规则由业务约定,本文不包含专属业务字段;已验证手机号必须从后端记录读取,不能允许前端任意替换身份。
创建账号和保存绑定应在同一个数据库事务中完成。数据库唯一约束用于防止同一应用下的微信身份被并发绑定到多个账号,不能只靠“先查是否存在,再写入”。
成功后返回:
{
"code": 200,
"data": {
"action": "LOGGED_IN"
}
}
LOGGED_IN 是前后端约定的处理结果,不是登录凭证。若绑定记录已经提交,但后续会话创建失败,不应随意回滚删除正常绑定;需提示重试,并通过重新扫码按已绑定账号继续。
第九步:微信和密码登录进入同一套会话处理
账号密码验证通过 ─┐
├→ 定位本地账号 → 加载权限 → 建立统一会话
微信认证并绑定成功 ┘
区别在于“如何证明身份”,相同点在于“最终使用哪个本地账号、如何访问业务”。两条路复用会话轮换、过期、退出和权限校验,避免出现两套互相不一致的登录状态。

后端生成新的随机 Session ID,保存会话并设置 Cookie。首次绑定完成后,应清理待绑定记录和 Cookie,不让临时流程凭证无限保留。
登录或切换权限范围时,还可轮换当前会话编号,避免一直复用登录前或旧上下文中的编号。是否撤销其他设备会话,要作为独立业务规则设计。
第十步:前端确认真实会话,再进入目标页面
后端可能把浏览器重定向到 /login.html?wechat=success,或者返回上一节的动作结果。但任何人都能修改 URL,所以前端仍需调用 /auth/me 确认后端能恢复真实身份。
本示例在确认用户后,再并行读取角色与页面配置:
GET /auth/me,确认有效 userId
↓
并行发起
┌─────────┴─────────┐
读取角色 读取页面配置
└─────────┬─────────┘
等待两项结束
↓
按错误策略处理失败结果
↓
校验安全回跳地址,进入首页
这是本示例的初始化安排,不是所有系统必须使用的固定接口。配置失败能否采用默认值、角色读取失败是否允许继续,需要明确约定;真正的业务授权始终由后端执行。
回跳地址应限制为允许的本站路径,不能直接跳向用户提供的任意 URL,避免登录页被利用为外站跳转入口。消费完 wechat、reason 等提示参数后,可清理 URL,减少重复处理。
七、二维码“防失效”:安全恢复,不是永不过期
扫码流程中存在不同的时钟,不能把它们混为一个倒计时。

| 对象 | 示例或来源 | 到期影响 |
|---|---|---|
| 微信二维码 | 按微信平台规则 | 原二维码可能无法继续授权 |
| 微信 code | 按微信平台规则 | 无法继续换取微信身份 |
| 后端 state | 本文初始 300 秒 | 回调无法通过本次流程校验 |
| 待绑定记录 | 本文初始 600 秒 | 不能继续原来的绑定流程 |
| 正式 Session | 本文固定 24 小时 | 后续业务请求需重新认证 |
300、600、86400 秒都是本文示例,不是微信统一规定。Session 固定有效期不等于每次访问自动续期;如需滑动续期,还要设计最长寿命及撤销规则。
7.1 为什么刷新二维码会让旧码失败
第一次获取:二维码 A + state A,浏览器 Cookie=A
↓
点击刷新:二维码 B + state B,浏览器 Cookie=B
↓
继续扫描旧 A:回调 state A ≠ Cookie B → 拒绝
扫描新 B:状态匹配且有效 → 继续校验
同一浏览器多个标签页可能共享同名 Cookie,因此两页相互刷新也会产生影响。这个失败说明登录尝试不再一致,不能为了“防失效”而跳过校验。
7.2 前端应该怎样改善体验
- 显示加载状态,避免一次操作发出重复请求。
- 根据
expiresIn提示本次上下文剩余时间。 - 到期后遮住旧码,提示重新获取。
- 页面从后台回来时,重新检查是否已到期。
- 对请求使用序号或取消机制,避免迟到的旧响应覆盖新二维码。
- 协调多标签页,或者明确提醒刷新会使其他尝试失效。
- 获取新码时同时取得新 state,不只是刷新二维码图片。
这些是可逐步补齐的设计项,不表示前面的最小 SDK 示例已经全部实现。不要在用户手机确认期间盲目高频自动刷新,避免主动打断有效授权。
请求序号主要防止旧响应覆盖页面内容;丢弃响应数据或取消 fetch,不保证浏览器没有处理旧响应的 Set-Cookie。同名 Cookie 还需通过串行发起、服务端尝试记录和多标签协调处理,不能只改页面显示。
7.3 临时记录续期也要考虑 Cookie
如果验证码验证成功后延长待绑定 Redis 记录,却没有同步检查浏览器 ticket Cookie 的寿命,可能出现“后端数据还在,浏览器却不再携带凭证”的情况。
因此,服务端记录、浏览器凭证和页面剩余时间必须协调设计。前端倒计时只负责提示,最终有效性由后端判断。
八、退出登录、过期和权限变化
退出不是只清 localStorage,也不是只跳转登录页。示例请求:
POST /auth/logout
后端应针对当前请求识别到的会话执行:
撤销当前 Session 记录
↓
兼容期撤销与本次登录关联的旧凭证
↓
清除对应 Cookie
↓
前端清理用户展示缓存并返回登录页
清除 Cookie 时,要匹配原先的名称、Path 和 Domain 等范围,通常通过到期响应实现。若退出接口失败,页面应提示重试,不应宣称已经完成服务端注销。
只有 Cookie 到期、Redis TTL 到期和业务规定的撤销策略相互配合,才构成完整的会话生命周期。账号停用、权限变化等也需要在服务端重新检查,而不是永久信任某次登录时的旧快照。
当前浏览器退出不自动等于所有设备退出。只撤销新 Session,也不一定会撤销仍然有效的旧 tokenKey;多设备和新旧凭证关联撤销必须单独设计。
九、新旧认证和历史入口怎样渐进兼容
一次性删除旧逻辑,可能让尚未更新的页面、旧书签或长连接立即失效。渐进迁移的做法是先统一后端身份处理,再明确新旧入口的兼容顺序。

9.1 凭证优先级不能含糊
本文采用下面的示例策略:
存在新 Session Cookie
→ 只验证新 Session
→ 无效则本次请求拒绝,不继续尝试旧凭证
没有新 Session Cookie,但有旧 tokenKey Cookie
→ 根据 tokenKey 查 Redis 中的 JWT 并验证
前两者都没有
→ 按兼容约定尝试 Authorization Header 中的旧 tokenKey
旧 Cookie 也存在但无效时,不应无限尝试其他凭证直到某个通过。无论哪条链路成功,都应恢复统一的本地用户、数据范围和权限上下文。
还有一个细节:如果新 Cookie 被清除,下一次请求可能改用仍有效的旧凭证。因此不能把“本次不降级”和“旧凭证已经被永久撤销”混为一谈。
9.2 历史入口需要逐项检查
| 历史调用方式 | 兼容措施 | 要验证什么 |
|---|---|---|
| 旧登录 URL 或书签 | 网关或路由转向新入口 | 回跳参数保留且经过安全校验 |
| 旧表单提交 | 后端保留表单解析,或前端继续兼容提交格式 | 参数名称、编码与错误响应一致 |
| 旧 Cookie / Header | 保留受控兼容验证路径 | 不能绕过用户状态与数据范围检查 |
| 旧动态会话脚本 | 返回公开快照 | 不泄露原始凭证,失效时能正确跳转 |
| 不同形式的未登录响应 | 统一前端失效处理 | 区分 HTTP 401 与约定的业务错误码 |
这些是需要实施和验收的措施,不是仅写出文档就算完成。旧通道还应有退出条件,例如调用量、客户端迁移进度和最后支持版本。
十、为什么普通接口正常,长连接仍可能有问题
长连接建立后,不会像普通短请求那样每次都重新执行完整 HTTP 认证。所以“能连上”和“连接整个生命周期受控”是两件事。
10.1 流式 HTTP 与 SSE
SSE 是服务端通过 HTTP 持续向浏览器发送事件。前端使用流式 fetch 时,需要按部署方式正确携带 Cookie;使用 EventSource 时,则按该 API 的凭据规则配置。MDN:使用 SSE
// 只示意凭据携带,未包含完整流读取、取消和错误处理。
fetch("/auth/example-stream", {
credentials: "include",
});
后端在订阅前认证,并验证任务或资源是否属于当前用户允许的范围。进入异步线程前,应显式传递必要的可信上下文,处理时设置,结束时清理。
不能认为线程池中的任务天然继承正确用户,也不能直接相信前端传来的 tenantId。线程复用后忘记清理,可能让后一个任务使用前一个任务的上下文。
10.2 WebSocket
WebSocket 先进行握手,再建立双向连接。新入口可以在符合浏览器 Cookie 规则的握手中验证会话,避免把长期登录凭证放进 URL。
浏览器握手还要校验 Origin,也就是发起连接的页面来源,并与明确的允许名单比较。有效 Cookie 不能证明页面来源可信,普通 HTTP 的 CORS 配置也不能代替 WebSocket 来源校验。OWASP:WebSocket 安全
非浏览器客户端可能不带 Origin,或自行构造它,应有独立的认证与接入规则;不能为了兼容而让所有浏览器来源都直接通过。
旧入口若仍携带 tokenKey,服务端需要验证旧凭证,再核对其对应用户和范围,不能直接相信 URL 中的 userId 或 tenantId。
新入口:握手带 Cookie → 验证 Session ─┐
├→ 认证成功后建立连接
旧入口:携带 tokenKey → 验证旧凭证 ──┘
10.3 退出之后,已有连接怎么办
删除 Session 不一定会主动断开现有连接。若业务要求立即撤销,还需要建立用户或会话到连接的关联,并通过主动关闭、周期复核或消息级校验等机制执行。
心跳通常用于发现断线或保持连接,不等于重新认证或延长会话。旧 URL 凭证、响应中的凭证字段和日志中的敏感参数,也应纳入后续安全治理。
十一、登录模块怎样独立发布和快速回滚
登录页改一个提示文字,不应在架构上必然要求重建所有业务页面。可以把登录页及其 JS、CSS 整理为独立静态构建单元,并保持稳定的后端接口。

11.1 发布过程要做什么
- 单独构建登录模块,输出完整 HTML、JS、CSS。
- 计算资源内容哈希,确保 HTML 引用对应版本的资源。
- 归档本次产物,并保留上一版本。
- 在测试环境验证支持的新旧前端与后端组合。
- 按受控部署流程发布,检查旧入口、缓存和回跳。
- 观察账号登录、微信回调、绑定和长连接的实际结果。
内容哈希可以帮助区分资源版本,但还要配置缓存策略。敏感会话响应应禁止缓存;静态登录 HTML、JS、CSS 的缓存方式需和版本引用一起设计,避免页面长期引用不匹配的资源。
11.2 失败时怎样回滚
发现新版本异常
↓
恢复上一版本成套 HTML、JS、CSS
↓
确认入口和缓存指向正确版本
↓
重新验证登录、扫码、回跳与长连接
不能只恢复一个 JS,却保留引用新资源的 HTML。自动回滚还需要健康检查、触发条件、切换机制和演练,有独立构建命令不等于已经具备这些能力。
静态资源回滚 ≠ 后端回滚 ≠ 数据库回滚。改过接口、会话格式或表结构时,需要分别分析兼容性,不能通过删除数据来假装恢复旧版本。
十二、亲手联调时,按什么顺序检查
下面是待执行的测试清单,不表示这些检查已经在真实环境完成。只在获得授权的测试环境操作,不展示或分享完整 Cookie、AppSecret、短信验证码或授权 code。
| 场景 | 操作 | 应观察到的结果 |
|---|---|---|
| 未登录访问 | 打开受保护页面 | 进入登录页,受保护接口拒绝匿名读取 |
| 账号登录 | 提交有效账号密码 | Cookie 被保存,当前用户接口返回有效身份 |
| 微信已绑定 | 扫码并确认 | 回调经过校验,建立统一会话 |
| 微信未绑定 | 扫码后完成手机号验证 | 正确进入已有账号绑定或新账号注册分支 |
| 错误与停用 | 错误验证码、停用账号、冲突绑定 | 拒绝处理,不从错误分支进入登录成功 |
| 伪造成功标记 | 手动修改 URL 的 success 参数 | 无有效会话时不能进入受保护功能 |
| 重放与到期 | 重放回调或等待 state 失效 | 后端拒绝,页面能重新发起授权 |
| 刷新与多标签 | 刷新后扫旧码、两页交替刷新 | 状态不匹配时安全拒绝,不跳过校验 |
| 页面恢复 | 刷新业务页面 | 有效会话继续使用,过期会话重新认证 |
| 退出 | 调用退出后再次请求接口 | 当前凭证按设计撤销,页面清理展示缓存 |
| 长连接 | 建连、断线重连、会话到期和退出 | 分别符合握手与存量连接撤销策略 |
| 回滚 | 在测试环境恢复上一整套产物 | 登录、旧入口、缓存和回跳仍正确 |
开发者工具可以查看请求顺序和 Cookie 是否被浏览器接受,但不要把完整网络导出文件直接分享出去。业务后端与微信之间的调用要通过脱敏日志排查,不应打印含密钥的完整请求 URL。
十三、面试时怎样解释这次升级
13.1 简历可以怎样写
下面是表达模板,只保留自己实际参与并能够举证的部分:
参与登录认证体系渐进式升级,协助统一账号登录、微信扫码登录及服务端会话管理,适配旧凭证、历史入口和流式 / WebSocket 场景;推进登录页独立静态构建与成套资源回滚方案,降低登录模块与主应用的发版耦合。
如果没有实际做过部署和回滚验收,应写“设计回滚方案”或“参与准备”,不要直接写“完成秒级自动回滚”。没有监控或发布记录,也不要虚构安全提升比例、故障率或恢复时长。
13.2 两分钟回答示例
我参与的是一次渐进式登录认证升级,不只是增加微信二维码。主要目标是让不同入口共用用户、权限和会话处理,并在迁移时兼容历史客户端。
我实际负责的是【填写真实职责】。账号密码验证成功,或者微信身份完成本地账号关联后,都会进入统一登录逻辑。微信只确认第三方身份,业务权限仍由本地账号和后端规则决定。
新链路使用随机 Session ID,由 Cookie 携带,会话保存在 Redis。旧 tokenKey 通道在兼容期保留,它不是 JWT 原文,而是后端查找 Redis 中 JWT 的凭据。新会话优先的策略支持客户端分批迁移。
微信扫码时,后端生成随机 state,绑定浏览器并设置有效期,回调校验后原子消费。首次绑定验证手机号,避免直接相信昵称或客户端指定的用户 ID。二维码过期则重新发起流程,而不是绕过校验。
长连接分别处理流式请求的 Cookie 和异步上下文,以及 WebSocket 握手认证。退出后已有连接即时失效,需要额外撤销机制,不能只依赖删除 Cookie。
发布方面,让登录页单独构建,统一管理 HTML、JS、CSS 版本。回滚要整组恢复并复测,后端和数据库变更单独评估。收益使用实际构建、发布和故障记录说明,不虚构数字。
13.3 常见追问
| 问题 | 回答重点 |
|---|---|
| 到底有没有用 JWT? | 新链路以 Session 为主,本文旧方案是 tokenKey 查 Redis 中的 JWT;前端不直接管理 JWT 不等于后端没用过 |
| 旧方案也用 Redis,为什么升级? | 重点是统一会话、权限上下文、凭据携带、轮换和退出,以及多入口治理,不是简单删除一个 JWT 类 |
| state 有什么作用? | 把回调与发起浏览器的授权尝试绑定起来,安全随机、短期有效并原子消费;不是全站 CSRF 的全部方案 |
| 为什么还要手机号? | 本文选择它作为本地账号绑定策略,证明号码控制权,不是微信协议强制所有网站使用 |
| HttpOnly 就安全了吗? | 还需 HTTPS、合适的 Cookie 属性、CSRF 防护、后端权限校验和敏感响应治理 |
| 二维码怎样防失效? | 明确时钟、到期提示、重新生成 state、处理并发和多标签,而不是让旧码永久有效 |
| 退出就会断开 WebSocket 吗? | 不一定,已有连接需要单独的关闭或复核机制,握手认证不等于全生命周期治理 |
| 独立构建就能快速回滚吗? | 还需版本归档、部署切换、缓存一致性、兼容性和实际演练 |
十四、复习:能回答这些问题,才算理解了流程
- 手机确认之后,是谁携带 code 请求业务后端?
- 为什么浏览器可以保存 Session ID,但页面脚本不需要读取它?
- state Cookie、待绑定 Cookie、正式 Session Cookie 分别允许做什么?
- 为什么微信 openid 不能直接当成本地 userId?
- 为什么数据库先查询再插入,仍然不能代替唯一约束?
- 哪些步骤可以并行,哪些必须等待上一步结果?
- 退出当前会话、全设备下线、断开已有长连接有什么区别?
- 为什么恢复旧前端文件不等于撤销数据库迁移?
遇到不理解的地方,可以按“谁发起请求 → 参数从哪里来 → 后端检查什么 → 失败会怎样 → 用什么结果验证”重新讲一遍,而不是只背接口名称。