初探微信小程序授权登录与SpringBoot后端交互流程

最近在研究微信小程序授权登录的后端实现,网上有很多资料但是都比较散,所以查了很多资料自己实现了出来,顺便在这里记录一下总体逻辑。

以下是小程序官方给出的实现流程,这个和我最终实现的逻辑有所差别,不过可以作为参考:

img


获取登陆凭证

在小程序端用户登录时调用wx.login,调用的返回值内会有一个code,这就是需要传递给后端的内容,code只能被使用一次,失效之后只能重新调用wx.login来获取。

后端接收到code之后可以通过调用微信官方提供的auth.code2Session接口,在其中传入在开发者平台获取到自己小程序的appIdappSecret以及前端传递过来的code进行请求,即可获取到登陆凭证openidsession_key

GET https://api.weixin.qq.com/sns/jscode2session

openid是用户唯一标识符,后续可以用于生成自定义登录态,而session_key是会话秘钥,需要用它来解密用户数据,在每次调用wx.loginsession_key都会发生变化,但实际上自定义登录态的实现在这里并不需要用到它,每次登录之后用code获取到的新数据进行使用就行了。


数据解密

session_key是微信侧用于标识用户微信授权登录是否过期的,只有在有效期内才能够作为秘钥解密出用户数据,而这个过期时间没有定值,是根据用户使用小程序的频次动态变化的;而微信官方也不建议将在后端获取到的session_key下发到小程序或是对外暴露,因此我们的会话秘钥只在每次登录的时候用到。

小程序端通过wx.login登陆以后就可以调用wx.getUserInfo可以获取到经加密的用户数据encryptedData以及解密向量iv,可以将他们与code一并携带向后端请求登录接口,后端则可以根据ivsession_key解密出用户信息。

img

在之前可以直接从encryptedData中解析出用户名、用户头像等等一系列信息,但是目前encryptedData中的敏感信息都是空值,可能唯一有用的就是用户性别,因此进行数据解密的用处不大。现在需要拿到用户头像和昵称则必须让用户自行完善信息。

头像方面,现在需要将一个button组件open-type的值设置为 chooseAvatar,当用户选择需要使用的头像之后,可以通过bind:chooseavatar事件回调获取到头像信息的临时路径;昵称方面,需要将input组件type的值设置为nickname。将这两个数据存到数据库中即可。

avatar

avatar


自定义登录态

目前我们拿到了所有的用户信息,我们可以通过openid在数据库中检索看是否有匹配项,若没有则插入一条新数据到表中,否则就更新信息。为了标识用户登录状态,在这里引入JWT来实现,由于openid是唯一用户标识,所以这里直接用它来生成自定义登录态即可,当然也可以在其中存放一些必要的数据以便使用。

为了防止token被篡改,还需要加上数字签名,可以在配置文件中定义一个长度在64以上的秘钥,通过HS512算法进行加密即可,我们的用户信息则存入claim中。在后续直接通过秘钥进行校验与解析即可。由于JWT本身是无状态的,而如果用户要做退出登录之类的操作我们无法直接让已生成的token过期,只能等它过期,因此可以通过Redis来做状态记录。

在生成token时将其存入Redis中并设定一个过期时间,而后续每次校验token前先看Redis中是否存在对应的key值,如果存在这说明登录态有效。如果要在后台手动对用户进行登出操作,只需要将对应的tokenRedis中删除即可。生成的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这样的安全框架,这样的话逻辑就会更完整也更安全一些,不过那就不是这篇文章的重点了,下次再见👋

文章链接:https://blog.syrizelink.top/index.php/2023/11/324/
🔔本博客文章仅用作个人学习/知识分享使用,不保证其正确性以及时效性
✏️部分素材来源于网络,如有侵权请联系我删除
🌏未经作者同意时,如要转载请务必标明出处
上一篇