前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

微信小程序登录流程详解 从 wx.login 到自定义登录态

首页2018-08-13 00:01:20Front-End
小程序微信小程序登录

小程序没有 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 到开发者服务器下发 3rd_session

把图拆成文字,一共是五步:

  • 小程序内通过 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 字段。

fe
  • 一、小程序登录和 Web 登录差在哪
  • 二、完整登录流程
    • 2.1 第一步,wx.login 拿 code
    • 2.2 第二步,服务端用 code 换 openid 和 session_key
    • 2.3 拿 openid 之前先弄清它是什么
  • 三、session_key 和 AppSecret 绝不能下发到前端
    • 3.1 AppSecret 泄露会发生什么
    • 3.2 session_key 泄露会发生什么
    • 3.3 那前端到底该拿到什么
  • 四、自定义登录态怎么设计
    • 4.1 最小可用的映射结构
    • 4.2 token 怎么传
    • 4.3 token 过期和 session_key 过期是两件事
  • 五、登录态校验和 checkSession
  • 六、完整登录代码示例
  • 七、这套写法今天还剩多少能用
  • 总结
  • 参考

← Immutable之回顾 持久化数据结构与结构共享原理小程序之自定义组件 →