上图是微信开发文档提供的图。
最近开发一款小程序,看了许久的微信文档,这里来记录一下其中的登录与授权过程。
总体流程:

  1. 前端执行wx.login()获取code传给后端
  2. 后端通过微信官方的登录凭证校验接口获取到session_key与openid,将session_key与openid保存下来。然后自定义登录状态(一开始我也先不明白这里该怎么做,后面我会介绍我的做法,欢迎大佬指正)并返回给前端。
  3. 前端以后每次请求都会携带该自定义登录状态,后端进行登录状态的判断,正常就返回业务数据,否则重新登陆,获取新的登录状态。

接下来看几个官方的文档:

一、理论

1、前端:wx.login(Object object)

本接口从基础库版本 2.3.1 起支持在小程序插件中使用

调用接口获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等。用户数据的加解密通讯需要依赖会话密钥完成。更多使用方法详见 小程序登录。

在小程序插件中使用时,需要在用户信息功能页中获得用户授权之后调用。否则将返回 fail。详见 用户信息功能页

参数

Object object
属性 类型 默认值 必填 说明 最低版本
timeout number 超时时间,单位ms 1.9.90
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

object.success 回调函数

参数

Object res
属性 类型 说明
code string 用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 auth.code2Session,使用 code 换取 openid 和 session_key 等信息

示例代码

wx.login({success (res) {if (res.code) {//发起网络请求wx.request({url: 'https://test.com/onLogin',data: {code: res.code}})} else {console.log('登录失败!' + res.errMsg)}}
})

2、后端:auth.code2Session

本接口应在服务器端调用,详细说明参见服务端API。

登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。更多使用方法详见 小程序登录。

请求地址

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

请求参数

属性 类型 默认值 必填 说明
appid string 小程序 appId
secret string 小程序 appSecret
js_code string 登录时获取的 code
grant_type string 授权类型,此处只需填写 authorization_code

返回值

Object

返回的 JSON 数据包

属性 类型 说明
openid string 用户唯一标识
session_key string 会话密钥
unionid string 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明。
errcode number 错误码
errmsg string 错误信息
errcode 的合法值
说明 最低版本
-1 系统繁忙,此时请开发者稍候再试
0 请求成功
40029 code 无效
45011 频率限制,每个用户每分钟100次

3、前端:wx.checkSession(Object object)

检查登录态是否过期。

通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效。具体时效逻辑由微信维护,对开发者透明。开发者只需要调用 wx.checkSession 接口检测当前用户登录态是否有效。

登录态过期后开发者可以再调用 wx.login 获取新的用户登录态。调用成功说明当前 session_key 未过期,调用失败说明 session_key 已过期。更多使用方法详见 小程序登录。

参数

Object object
属性 类型 默认值 必填 说明
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

示例代码

wx.checkSession({success () {//session_key 未过期,并且在本生命周期一直有效},fail () {// session_key 已经失效,需要重新执行登录流程wx.login() //重新登录}
})

会话密钥 session_key 有效性

开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。

  1. wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
  2. 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
  3. 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
  4. 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

4、后端:auth.checkSessionKey

本接口应在服务器端调用,详细说明参见服务端API。

校验服务器所保存的登录态 session_key 是否合法。为了保持 session_key 私密性,接口不明文传输 session_key,而是通过校验登录态签名完成。

请求地址

GET https://api.weixin.qq.com/wxa/checksession?access_token=ACCESS_TOKEN&signature=SIGNATURE&openid=OPENID&sig_method=SIG_METHOD

请求参数

属性 类型 默认值 必填 说明
access_token string 接口调用凭证
openid string 用户唯一标识符
signature string 用户登录态签名
sig_method string 用户登录态签名的哈希方法,目前只支持 hmac_sha256

返回值

Object

返回的 JSON 数据包

属性 类型 说明
errcode number 错误码
errmsg string 错误信息

errcode 的合法值

说明 最低版本
0 ok 请求成功
87009 invalid signature 签名错误

调用示例

curl -G 'https://api.weixin.qq.com/wxa/checksession?access_token=OsAoOMw4niuuVbfSxxxxxxxxxxxxxxxxxxx&signature=fefce01bfba4670c85b228e6ca2b493c90971e7c442f54fc448662eb7cd72509&openid=oGZUI0egBJY1zhBYw2KhdUfwVJJE&sig_method=hmac_sha256'

返回示例

正确时的返回JSON数据包如下:

{"errcode": 0, "errmsg": "ok"}

错误时的返回JSON数据包如下(示例为签名错误):

{"errcode": 87009, "errmsg": "invalid signature"}

5、前端:wx.authorize(Object object)

基础库 1.2.0 开始支持,低版本需做兼容处理。

提前向用户发起授权请求。调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口。如果用户之前已经同意授权,则不会出现弹窗,直接返回成功。更多用法详见 用户授权。 > 小程序插件可以使用 wx.authorizeForMiniProgram

参数

Object object
属性 类型 默认值 必填 说明
scope string 需要获取权限的 scope,详见 scope 列表
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

示例代码

// 可以通过 wx.getSetting 先查询一下用户是否授权了 "scope.record" 这个 scope
wx.getSetting({success(res) {if (!res.authSetting['scope.record']) {wx.authorize({scope: 'scope.record',success () {// 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问wx.startRecord()}})}}
})

6、权限

部分接口需要经过用户授权同意才能调用。我们把这些接口按使用范围分成多个 scope ,用户选择对 scope 来进行授权,当授权给一个 scope 之后,其对应的所有接口都可以直接使用。

此类接口调用时:

  • 如果用户未接受或拒绝过此权限,会弹窗询问用户,用户点击同意后方可调用接口;
  • 如果用户已授权,可以直接调用接口;
  • 如果用户已拒绝授权,则不会出现弹窗,而是直接进入接口 fail 回调。请开发者兼容用户拒绝授权的场景。

获取用户授权设置

开发者可以使用 wx.getSetting 获取用户当前的授权状态。

打开设置界面

用户可以在小程序设置界面(「右上角」 - 「关于」 - 「右上角」 - 「设置」)中控制对该小程序的授权状态。

开发者可以调用 wx.openSetting 打开设置界面,引导用户开启授权。

提前发起授权请求

开发者可以使用 wx.authorize 在调用需授权 API 之前,提前向用户发起授权请求。

scope 列表

scope 对应接口 描述
scope.userInfo wx.getUserInfo 用户信息
scope.userLocation wx.getLocation 地理位置
scope.werun wx.getWeRunData 微信运动步数
scope.writePhotosAlbum wx.saveImageToPhotosAlbum 保存到相册

授权有效期

一旦用户明确同意或拒绝过授权,其授权关系会记录在后台,直到用户主动删除小程序。

最佳实践

在真正需要使用授权接口时,才向用户发起授权申请,并在授权申请中说明清楚要使用该功能的理由。

注意事项

  1. wx.authorize({scope: "scope.userInfo"}),不会弹出授权窗口,请使用 wx.createUserInfoButton
  2. 需要授权 scope.userLocation 时必须配置地理位置用途说明。

我根据对上面文档的理解,重新介绍一下最开始的那幅图,以及我的解决方案:

  1. 前端发起wx.login请求获得code(5分钟内有效)
  2. 前端携带code请求后端登录接口,后端拿到code去请求登录品质校验接口,校验code,正常则返回session_key与openid。
  3. 根据openid生成token返回给前端(这里的token就是我定义的登录状态)
  4. 前端以后每次请求都要携带上我的token
  5. session_key失效这需要重新登录
  1. 为什么利用openid生产token?
    session_key微信官方强烈要求不能返回给前端,如果使用session_key生产token,会有被破译token的危险,openid是用户唯一标识,比起session_key没那么重要,session_key还会用于对微信敏感数据进行解密,所以session_key很重要。
  2. token时效怎么设置?
    在生成token时,不设置失效时间,让token与session_key同步,前端每次进入小程序时使用wx.checkSession检验session_key是否失效,失效则重新登陆,生成新的session_key与token,token与session_key以及openid都被我存入了数据库中。
  3. 每次生成的token一样?
    前面说到了我是使用openid生成token的,那么如果每次的密文一样,则生成的token就会不变,这是可以考虑使用uuid作为密文,或者使用session_key作为密文。

授权过程:前端授权,存入用户信息进入数据库。(后端使用session_key验证数据一致性,一致则存入数据库,否则返回异常)

二、代码

1、登录请求处理:


@Value("${wechat.appid}")
private String appid;@Value("${wechat.secret}")
private String secret;@Override
public String onLogin(String code) throws IOException, BaseException {String url = "https://api.weixin.qq.com/sns/jscode2session" +"?appid="+appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code";String result = "";BufferedReader in = null;try {URL url1 = new URL(url);URLConnection urlConnection = url1.openConnection();in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));String line;while ((line = in.readLine()) != null) {result += line;}} catch (Exception e) {throw e;} finally {try {in.close();} catch (IOException e) {e.printStackTrace();}}JSON parse = JSONUtil.parse(result);Integer errcode = parse.getByPath("errcode", Integer.class);if (errcode == null){// 用户唯一标识String openid = parse.getByPath("openid", String.class);// 会话密钥String sessionKey = parse.getByPath("session_key", String.class);// 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回String unionid = parse.getByPath("unionid", String.class);// 通过oppenid与session_key计算tokenString token = JwtUtils.getToken(openid, sessionKey);SignaturePO signaturePO = signatureDAO.queryById(openid);if (signaturePO != null){// 该用户以及注册过了// 更新session_key与tokensignaturePO.setSessionKey(sessionKey);signaturePO.setToken(token);int update = signatureDAO.update(signaturePO);if (update != 1){throw new BaseException(500, "更新session_key与token失败");}return token;}else {// 该用户未被注册,将该用户的session_key与token添加到数据库SignaturePO po = new SignaturePO();po.setOpenid(openid);po.setSessionKey(sessionKey);po.setToken(token);int insert = signatureDAO.insert(po);if (insert != 1){throw new BaseException(500, "更新session_key与token失败");}return token;}}else if (errcode == -1){throw new BaseException(errcode, "系统繁忙,稍候再试");}else if (errcode == 40029){throw new BaseException(errcode, "code无效");}else if (errcode == 45011){throw new BaseException(errcode, "频率限制,每个用户每分钟100次");}else {throw new BaseException(500, "服务器异常");}
}

2、拦截器拦截

@Component
public class JWTInterceptor implements HandlerInterceptor {@Resourceprivate SignatureDAO signatureDAO;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("Authorization");if (token != null && !"".equals(token)) {// 查询数据库中是否有该tokenSignaturePO signaturePO = signatureDAO.queryByToken(token);if (signaturePO != null){// 该token可以正常使用request.setAttribute(SESSIONKEY, signaturePO.getSessionKey());return true;}else {throw new BaseException(ResultEnum.CHECK_EROR);}}return false;}
}

注意:这里会出现依赖注入问题。解决办法

3、生产token

 public static String getToken(String openId, String session_key) throws UnsupportedEncodingException {String token = JWT.create().withKeyId(openId).withIssuer("weixin").withIssuedAt(new Date()).withClaim("openid", openId).sign(Algorithm.HMAC256(session_key));return token;}

4、sha1计算签名

public static String getSha1(String str) {char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9','a', 'b', 'c', 'd', 'e', 'f' };try {MessageDigest mdTemp = MessageDigest.getInstance("SHA1");mdTemp.update(str.getBytes("UTF-8"));byte[] md = mdTemp.digest();int j = md.length;char buf[] = new char[j * 2];int k = 0;for (int i = 0; i < j; i++) {byte byte0 = md[i];buf[k++] = hexDigits[byte0 >>> 4 & 0xf];buf[k++] = hexDigits[byte0 & 0xf];}return new String(buf);} catch (Exception e) {return null;}}

5、授权校验数据一致性

@Overridepublic void authorize(ResDTO resDTO, HttpServletRequest request) throws BaseException {String sessionKey = JwtUtils.getSessionKey(request);//signature = sha1( rawData + session_key )String signature = resDTO.getSignature();String signature2  = Sha1.getSha1(resDTO.getRawData() + sessionKey);if (signature.equals(signature2)){// 数据一致String encryptedData = resDTO.getEncryptedData();String iv = resDTO.getIv();// 解密敏感数据// 格式:// {'openId': 'oGZUI0egBJY1zhBYw2KhdUfwVJJE',// 'nickName': 'Band', 'gender': 1, 'language': 'zh_CN',// 'city': 'Guangzhou', 'province': 'Guangdong', 'country': 'CN',// 'avatarUrl': 'http://wx.qlogo.cn/mmopen/vi_32/aSKcBBPpibyKNicHNTMM0qJVh8Kjgiak2AHWr8MHM4WgMEm7GFhsf8OYrySdbvAMvTsw3mo8ibKicsnfN5pRjl1p8HQ/0',// 'unionId': 'ocMvos6NjeKLIBqg5Mr9QjxrP1FA',// 'watermark': {'timestamp': 1477314187, 'appid': 'wx4f4bc4dec97d474b'}}JSONObject userInfoByEncryptedData = WXDecryptUtil.getUserInfoByEncryptedData(encryptedData, sessionKey, iv);String openId = userInfoByEncryptedData.getString("openId");UserInfoPO userInfoPO = resDTO.getUserInfoPO();// 设置openiduserInfoPO.setOpenid(openId);//将用户信息添加到数据库int insert = userInfoDAO.insert(userInfoPO);if (insert != 1){//添加失败throw new BaseException(500, "添加用户信息失败");}}else {throw new BaseException(500, "数据不一致");}}

6、微信实现解密敏感数据

public class WXDecryptUtil {public static JSONObject getUserInfoByEncryptedData(String encryptedData, String sessionKey, String iv){// 被加密的数据byte[] dataByte = Base64.decode(encryptedData);// 加密秘钥byte[] keyByte = Base64.decode(sessionKey);// 偏移量byte[] ivByte = Base64.decode(iv);try {// 如果密钥不足16位,那么就补足.  这个if 中的内容很重要int base = 16;if (keyByte.length % base != 0) {int groups = keyByte.length / base + 1;byte[] temp = new byte[groups * base];Arrays.fill(temp, (byte) 0);System.arraycopy(keyByte, 0, temp, 0, keyByte.length);keyByte = temp;}// 初始化Security.addProvider(new BouncyCastleProvider());Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding","BC");SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");parameters.init(new IvParameterSpec(ivByte));cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化byte[] resultByte = cipher.doFinal(dataByte);if (null != resultByte && resultByte.length > 0) {String result = new String(resultByte, StandardCharsets.UTF_8);return JSONObject.parseObject(result);}} catch (Exception e) {e.printStackTrace();}return null;}
}

微信小程序Token登录验证相关推荐

  1. PHP实现微信小程序授权登录

    PHP实现微信小程序授权登录(示例) public function login(){//接收参数$code = input('code'); //code码$nickName = input('ni ...

  2. SpringBoot微信小程序授权登录

    SpringBoot微信小程序授权登录 一.appId 1.1.自己是管理者:微信公众平台,申请或登录自己的微信小程序,在开发者管理中即可看到 2.2.自己是开发者:让管理员将自己加入到小程序开发者管 ...

  3. 微信小程序用户登录信息过期处理

    微信小程序用户登录信息过期处理 由于小程序对获取用户信息的新规定,获取用户信息必须通过一个button调出获取窗口,然而用户的token会过期,而本地存在的缓存可能会让用户误以为自己仍处于登录状态,但 ...

  4. 微信小程序的登录界面实现

    微信小程序的登录界面实现 <view class="container"><view class="wrapper"><view ...

  5. python写微信小程序源码示例_python实现微信小程序用户登录、模板推送

    python实现微信小程序用户登录.模板推送 来源:中文源码网    浏览: 次    日期:2019年11月5日 [下载文档:  python实现微信小程序用户登录.模板推送.txt ] (友情提示 ...

  6. 基于uni-app实现微信小程序一键登录和退出登录功能

    起因 目前正在使用uni-app开发一个微信小程序,开发到登录模块时通过查阅uni-app官方教程.微信小程序官方文档.网上的教程终于是实现了微信小程序的登录模块,现总结分享给大家,共同学习. 总体思 ...

  7. php(ThinkPHP)实现微信小程序的登录过程

    源码也在我的github中给出 https://github.com/wulongtao/think-wxminihelper 下面结合thinkPHP框架来实现以下微信小程序的登录流程,这些流程是结 ...

  8. 新版微信小程序授权登录流程及问题汇总(getUserProfile)

    问题来源:前不久去面试的时候有面试官问我你有自己的博客啥的吗?只能很尴尬的说没有.其实一直想有一个属于自己的博客啥的去记录自己在开发过程中遇到的问题,正好现在微信小程序比较流行,就花了两天自己搞了一个 ...

  9. SpringCloud 微信小程序授权登录 获取openId SessionKey【SpringCloud系列13】

    SpringCloud 大型系列课程正在制作中,欢迎大家关注与提意见. 自我提升方法推荐:神奇的早起 早上 5:00 -5:20 起床刷牙 5:30-6:00 晨练(跑步.跳绳.骑自行车.打球等等) ...

  10. 微信公众账号后台怎么解除小程序_微信小程序 后台登录(非微信账号)实例详解...

    微信小程序 后台登录 实现效果图: 最近写了一个工具类的小程序,按需求要求不要微信提供的微信账号登录,需要调取后台登录接口来登录.由于小程序大部分都是调取微信信息登录,很少有调用自己后台来登录的,所以 ...

最新文章

  1. TeamViewer介绍:远程控制计算机
  2. LaTeX Test
  3. 前端性能优化—将CSS文件放在顶部
  4. 蓝桥杯第四届初赛-买不到的数目-数论
  5. CODING 受邀参加《腾讯全球数字生态大会》
  6. 基于深度学习的FAQ问答系统
  7. js中追加写入文件(字符串追加)_note
  8. 进程和线程的主要区别
  9. 液晶8K电视也能打造家庭影院?一起“宅”过电影情人节吧
  10. Java实现提取拼音首字母
  11. ISSCC 2017论文导读 Session 14: A 28nm SoC with a 1.2GHz Prediction Sparse Deep-Neural-Network Engine
  12. Shiro笔记 教程
  13. 关于信度分析的多种方法
  14. 在IE浏览器访问网址时显示证书错误,导航已阻止
  15. 如何查看电脑的SN码?
  16. 3个思考方向,轻松实现快速涨粉
  17. teamlab什么意思_不好意思,我们的2019毕业季聚会,和前辈的不一样
  18. http请求 状态码204
  19. 计算机 90学时培训总结,90学时培训心得体会(通用5篇)
  20. 安装IE11提示“Internet Explorer在安装前需要更新”

热门文章

  1. 分享一些可用的淘宝(1688)关于订单信息获取的相关接口(开放API收费)
  2. 【ibokan】好的用户界面-界面设计的一些技巧
  3. Edit Control响应全选(Ctrl+A)
  4. 我不是九爷 带你了解 docker实战命令
  5. 华为2018年服务器销售额,2018Q4华为服务器收入全球第三 同比增45.9%
  6. 怎么快速解决dns被劫持问题?
  7. B端产品的筛选场景调研与设计优化实践
  8. The Rust Programming Language - 第7章 使用包、crate和模块管理不断增长的项目 - 7.1 包和crate
  9. 趣味点名软件_网传川大教授用刷脸软件点名 无人逃课
  10. XP IIS下配置.net的问题总结与简单解决方法