最近在研究微信小程序授权登录的后端实现,网上有很多资料但是都比较散,所以查了很多资料自己实现了出来,顺便在这里记录一下总体逻辑。
以下是小程序官方给出的实现流程,这个和我最终实现的逻辑有所差别,不过可以作为参考:

获取登陆凭证
在小程序端用户登录时调用wx.login,调用的返回值内会有一个code,这就是需要传递给后端的内容,code只能被使用一次,失效之后只能重新调用wx.login来获取。
后端接收到code之后可以通过调用微信官方提供的auth.code2Session接口,在其中传入在开发者平台获取到自己小程序的appId和appSecret以及前端传递过来的code进行请求,即可获取到登陆凭证openid和session_key。
GET https://api.weixin.qq.com/sns/jscode2sessionopenid是用户唯一标识符,后续可以用于生成自定义登录态,而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这样的安全框架,这样的话逻辑就会更完整也更安全一些,不过那就不是这篇文章的重点了,下次再见?
