小程序没有 cookie。这句话第一次真正咬到我,是在把一套 Web 的登录逻辑往小程序上搬的时候。后端同学照旧发了 Set-Cookie,前端这边死活拿不到会话,来回折腾了大半天才反应过来,小程序的请求压根不走浏览器那套 cookie 机制。登录这件事在小程序里得重做一遍。wx.login 拿到的 code 到底是什么、为什么必须绕一趟自己的服务器、session_key 为什么一个字节都不该下发到前端,这几点没搞清楚,写出来的登录要么不安全,要么隔三差五掉登录态。这篇把整条链路从头到尾拆一遍。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 小程序登录为什么不能照搬 Web 的那一套
wx.login拿到的 code 是什么,它凭什么能换来 openid- 服务端用 code 换
openid和session_key的那一步,为什么必须放在服务端 session_key和AppSecret泄露到前端会发生什么- 自定义登录态(3rd_session / token)该怎么设计
wx.checkSession在整条链路里的位置,登录态失效怎么补- 一段可以直接抄走的完整登录代码,以及 2018 年这套写法今天还剩多少能用
先交代一句时效性。这篇写于 2018 年,链路的骨架(code 换 session、自建登录态)这些年没变,但获取用户信息那部分改了好几轮。文中出现的
wx.getUserInfo现在已经不是推荐写法了,第七节会专门说现状,正文里的老代码我一个字没删,方便你对着老项目排查。
# 一、小程序登录和 Web 登录差在哪
Web 登录的心智很简单。用户提交账号密码,服务端验一下,种个 cookie 或者返回一个 token,之后每个请求带上它,齐活。整条链路里只有两方,浏览器和你的服务器。
小程序不一样,中间多了一个微信。
用户在小程序里是不输账号密码的,他点一下就进来了,这个「一下」背后的身份是微信给的。所以链路上至少有三方,小程序客户端、你的服务器、微信的接口服务。你的服务器没法直接问用户「你是谁」,只能拿着小程序给的一张凭证去问微信。
还有几个更琐碎但更容易踩的差别:
- 小程序没有 cookie,
Set-Cookie发过来也不会自动带回去,登录态只能自己在header或者 body 里手动传 - 小程序没有
localStorage,对应的是wx.setStorageSync这一套,是小程序自己的存储 - 小程序的代码包是可以被解出来的,任何写死在前端的密钥都等于公开
第三条是这篇里最需要记住的一条。后面讲 AppSecret 的时候还会回到它。
# 二、完整登录流程
先看整条链路的时序。下面这张图是登录流程的全貌,从小程序调 wx.login 开始,到你的服务器把自定义登录态发回小程序结束。

把图拆成文字,一共是五步:
- 小程序内通过
wx.login接口获得code - 将
code传入后台,后台对微信服务器发起一个https请求换取openid、session_key(解密encryptedData、iv得到的) - 后台生成一个自身的
3rd_session(以此为key值保持openid和session_key),返回给前端。PS:微信方的openid和session_key并没有发回给前端小程序 - 小程序拿到
3rd_session之后保持在本地 - 小程序请求登录区内接口,通过
wx.checkSession检查登录态,如果失效重新走上述登录流程,否则带上3rd_session到后台进行登录验证
这五步每一步都有它非在那儿不可的理由,下面逐个说。
# 2.1 第一步,wx.login 拿 code
wx.login 是整条链路的发起动作,它不弹窗、不需要用户点确认,调了就返回。
wx.login({
success(res){
console.log(res)
//code:"fda41033Z0fdak3dfae01dffaaWXQA1vwQ4dfae0Akg3e0Z0k3E"
//errMsg:"login:ok"
}
})
拿到的 code 就是那张凭证。它有三个性质决定了后面所有的设计:
- 临时。有效期很短,官方文档写的是 5 分钟,过期就废
- 一次性。拿去换过一次 session 之后就作废了,第二次换会直接报错
- 不含敏感信息。code 本身看不出用户是谁,被截获了也没用,因为换 session 还需要
AppSecret
这里有个坑我见过好几次。前端给登录请求加了自动重试,网络抖一下重发一次,同一个 code 被送去换了两次,第二次微信返回 code 已被使用,用户就卡在登录页出不来了。正确的做法是重试之前重新调一次 wx.login 拿新 code,而不是复用旧的。具体的错误码请查官方文档的错误码表,别照抄我这里的文字描述去写判断分支。
# 2.2 第二步,服务端用 code 换 openid 和 session_key
这一步是整条链路的核心,也是最容易写错地方的一步。你的服务器拿着 code、appid、AppSecret 去调微信的 auth.code2Session 接口,微信校验通过后返回 openid 和 session_key。
原文里列的传输参数一共 7 个,我把它抄在下面并逐条注上用途:
appid 小程序唯一标识
secret 小程序的 app secret
js_code //wx.login登录时获取的 code,用于后续获取session_key
//下面两个参数用户服务器端签名校验用户信息的
signature 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息。
rawData 不包括敏感信息的原始数据字符串,用于计算签名。
//下面两个参数是用于解密获取openId和UnionId的
encryptedData 包括敏感数据在内的完整用户信息的加密数据
iv 加密算法的初始向量
这 7 个参数不是一次全用上的。appid 和 secret 是你服务器自己的配置,前端根本不该知道;signature 和 rawData 是校验用户信息完整性的,可选;真正需要前端传给你服务器的,其实只有下面三个:
js_code // wx.login登录时获取的 code,用于后续获取session_key
encryptedData 包括敏感数据在内的完整用户信息的加密数据
iv 加密算法的初始向量
- 可精简为以上三个参数
- 其余的签名校验的参数可省略,而
appid和secret可以直接写在服务器
encryptedData 和 iv 是配套的。它们是微信给你的一份加密数据,只有用 session_key 才解得开,解开之后能拿到包含 unionid 在内的完整用户信息。这也解释了 session_key 为什么叫 key,它就是解密用的钥匙。
至于服务端拿到之后怎么落库、怎么发 token,原文一句「服务端处理返回 token、sessionId 过程省略」带过了,第四节我把这块补上。
服务端处理返回 token、sessionId 过程省略…
# 2.3 拿 openid 之前先弄清它是什么
很多人第一次接触会默认 openid 就是用户 ID,这个理解在单个小程序里够用,但放到整个微信生态就不对了。
openid 是「某个微信号在某个应用里的唯一标识」。同一个人,在你的小程序和你的公众号里,openid 是两个完全不同的值。真正跨应用唯一的是 UnionId,它需要小程序和公众号绑定在同一个微信开放平台账号下才会下发。
所以数据库设计的时候,我的建议是一开始就把 union_id 这一列留出来。哪怕现在你只有一个小程序,后面业务扩到公众号再补迁移,比一开始多加一列痛苦得多。
# 三、session_key 和 AppSecret 绝不能下发到前端
这一节单独拎出来讲,因为它是这篇里唯一一条属于安全红线的内容。
先说结论:AppSecret 只能存在于你的服务器,session_key 只能存在于你的服务器,两者都不允许出现在小程序端的任何位置,包括代码、请求响应体、日志和本地缓存。
# 3.1 AppSecret 泄露会发生什么
AppSecret 是小程序的密钥,它和 appid 配对,代表「我就是这个小程序的拥有者」。拿到它可以做的事包括但不限于:直接调 auth.code2Session 换任意用户的 session、调各种服务端开放接口、拿 access_token 发模板消息。
我见过一种偷懒写法,为了省一个后端接口,直接在小程序里拼 https://api.weixin.qq.com/sns/jscode2session?...&secret=xxx 发请求。这么写调试期确实跑得通,但小程序的代码包是能被解包的,AppSecret 就明晃晃躺在里面。等于把家门钥匙贴在门上。
顺带说一句,AppSecret 也别提交进 git 仓库,哪怕是私有仓库。走环境变量或者配置中心。它虽然可以在公众平台后台重置,但一重置所有在途的服务端请求会同时失效,线上会明显抖一下。
# 3.2 session_key 泄露会发生什么
session_key 的危害路径不一样,但一样严重。它是解密 encryptedData 的钥匙,同时也是校验数据签名的密钥。泄露之后,攻击者可以伪造出一份「看起来签名合法」的用户数据发给你的服务器,你的校验逻辑会认为它是微信下发的真数据。
所以微信官方的措辞是「开发者不应该把 session_key 传输到小程序客户端等服务器外的环境」。这句话字面意思就够清楚了,不用过度解读。
# 3.3 那前端到底该拿到什么
答案是你自己签发的那个 token,仅此而已。
openid 严格来说不算私密信息,下发到前端也不会立刻出事,但我的习惯是能不发就不发。前端拿着 openid 唯一能做的事就是把它塞进请求参数,而这恰恰是你不希望的,因为一旦业务接口信任前端传来的 openid,改一个字符就能读别人的数据。让前端只持有 token,服务端从 token 反查 openid,这条边界会干净很多。
# 四、自定义登录态怎么设计
微信只负责告诉你「这次来的是谁」,它不负责帮你维持会话。用户总不能每打开一个页面就重新登录一次,所以自定义登录态这一层必须你自己造。原文叫它 3rd_session,现在更常见的叫法是 token,是同一个东西。
# 4.1 最小可用的映射结构
服务端拿到 openid 和 session_key 之后,生成一个随机字符串当 key,把这两个值挂在它下面:
{
"token_1": {
"openid": "获取到的openid 1",
"session_key": "获取到的session_key 1"
},
"token_2": {
"openid": "获取到的openid 2",
"session_key": "获取到的session_key 2"
}
}
这个结构只是用来说明映射关系,真跑起来千万别用一个内存对象来存。进程一重启映射全丢,多实例部署的时候各存各的,用户在 A 机器登录、请求打到 B 机器就是 401。生产环境这份映射一般放 Redis,顺手把过期时间也设上,登录态的有效期就自然有了。
# 4.2 token 怎么传
小程序没有 cookie,token 只能手动带。URL Query、body、header 三种都能传,我的建议是走 header。
为什么?URL Query 会被访问日志、反向代理、各种埋点系统原样记录下来,token 就这么散得到处都是。放 header 至少不会进 access log 的 URL 字段。