最近在研究微信小程序授权登录的后端实现,网上有很多资料但是都比较散,所以查了很多资料自己实现了出来,顺便在这里记录一下总体逻辑。
以下是小程序官方给出的实现流程,这个和我最终实现的逻辑有所差别,不过可以作为参考:
获取登陆凭证
在小程序端用户登录时调用wx.login
,调用的返回值内会有一个code
,这就是需要传递给后端的内容,code只能被使用一次,失效之后只能重新调用wx.login
来获取。
后端接收到code
之后可以通过调用微信官方提供的auth.code2Session
接口,在其中传入在开发者平台获取到自己小程序的appId
和appSecret
以及前端传递过来的code
进行请求,即可获取到登陆凭证openid
和session_key
。
GET https://api.weixin.qq.com/sns/jscode2session
openid
是用户唯一标识符,后续可以用于生成自定义登录态,而session_key
是会话秘钥,需要用它来解密用户数据,在每次调用wx.login
后session_key
都会发生变化,但实际上自定义登录态的实现在这里并不需要用到它,每次登录之后用code获取到的新数据进行使用就行了。
数据解密
session_key
是微信侧用于标识用户微信授权登录是否过期的,只有在有效期内才能够作为秘钥解密出用户数据,而这个过期时间没有定值,是根据用户使用小程序的频次动态变化的;而微信官方也不建议将在后端获取到的session_key
下发到小程序或是对外暴露,因此我们的会话秘钥只在每次登录的时候用到。
小程序端通过wx.login
登陆以后就可以调用wx.getUserInfo
可以获取到经加密的用户数据encryptedData
以及解密向量iv
,可以将他们与code
一并携带向后端请求登录接口,后端则可以根据iv
和session_key
解密出用户信息。
在之前可以直接从encryptedData
中解析出用户名、用户头像等等一系列信息,但是目前encryptedData
中的敏感信息都是空值,可能唯一有用的就是用户性别,因此进行数据解密的用处不大。现在需要拿到用户头像和昵称则必须让用户自行完善信息。
头像方面,现在需要将一个button
组件open-type
的值设置为 chooseAvatar
,当用户选择需要使用的头像之后,可以通过bind:chooseavatar
事件回调获取到头像信息的临时路径;昵称方面,需要将input
组件type
的值设置为nickname
。将这两个数据存到数据库中即可。
自定义登录态
目前我们拿到了所有的用户信息,我们可以通过openid
在数据库中检索看是否有匹配项,若没有则插入一条新数据到表中,否则就更新信息。为了标识用户登录状态,在这里引入JWT
来实现,由于openid
是唯一用户标识,所以这里直接用它来生成自定义登录态即可,当然也可以在其中存放一些必要的数据以便使用。
为了防止token
被篡改,还需要加上数字签名,可以在配置文件中定义一个长度在64以上的秘钥,通过HS512
算法进行加密即可,我们的用户信息则存入claim
中。在后续直接通过秘钥进行校验与解析即可。由于JWT
本身是无状态的,而如果用户要做退出登录之类的操作我们无法直接让已生成的token
过期,只能等它过期,因此可以通过Redis
来做状态记录。
在生成token
时将其存入Redis
中并设定一个过期时间,而后续每次校验token
前先看Redis
中是否存在对应的key
值,如果存在这说明登录态有效。如果要在后台手动对用户进行登出操作,只需要将对应的token
从Redis
中删除即可。生成的token
将作为登录接口的响应数据返回,小程序只需要在后续的接口请求中携带token,若接口返回了Unauthorized
就再次引导用户进行登录即可。
配置拦截器
为了确保已登录用户才能访问对应接口信息,还需要配置一层拦截器,拦截器应实现HandlerInterceptor
并重写preHandle
方法,请求会在Controller
方法处理之前进入preHandle
中。首先就需要从请求头中取出token
并对其进行解析,若Redis中没有对应的数据或是解析失败则拦截请求返回一个Unauthorized
状态;如果解析出有效的数据则将数据放入自定义的ThreadLocal
类中保存,这样在后续操作中直接从ThreadLocal
中取出即可而无需进行不必要的数据库查询。
public class UserThread {
private static final ThreadLocal userThreadLocal = new ThreadLocal<>();
public static User getUserThreadLocal() {
return UserThread.userThreadLocal.get();
}
public static void setUserThreadLocal(User user) {
UserThread.userThreadLocal.set(user);
}
public static void removeUserThreadLocal() {
UserThread.userThreadLocal.remove();
}
}
最后别忘记在继承自WebMvcConfigurationSupport
的配置类中重写addInterceptors
方法来注册拦截器并放行登录路径,以免登录请求也经过拦截器。
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/api/login"
);
}
}
这样一来整个登录流程就大功告成了,除此以外这一登陆流程还应该整合Spring Security
或者shiro
这样的安全框架,这样的话逻辑就会更完整也更安全一些,不过那就不是这篇文章的重点了,下次再见👋