基于ThreadLocal和JWT登录的问题,微服务登录架构解决方案

https://www.bilibili.com/video/BV1f3411G7xk


公司之前是以JWT + ThreadLocal 做的登录系统,在使用的过程发现了如下的问题,下面我们一起来看看,后面也会给出更好的解决方案。


一、基于JWT + ThreadLocal实现登录


1-1、JWT

所谓的JWT是 json web token 的缩写,你可以理解成把一个数据进行一系列的加密后生成的一个字符串,所以你也可以把它解密成原本的数据。


1-2、ThreadLocal

这个可能很多人听过,但却很少人用过,其实很简单,在多线程的情况下,如果你不想用同步的方式解决就可以用ThreadLocal线程本地变量来解决并发的问题。

你可以理解成它是一个特殊的map,它的key就是线程本身,value就是你想存储的数据。


1-3、实现思路

实现起来很简单,一个登录接口生成token,一个拦截器,每次解析token放入当前线程的 threadLocal,然后在方法里面随时随地的获取当前用户信息了。

很简单,只要我们定义一个类,给它设置静态方法,就OK了。

public class UserSessionContext {
    private static ThreadLocal<UserSessionVO> userSessionVOThreadLocal = new ThreadLocal<>();

    public static void set(UserSessionVO userSessionVO) {
        userSessionVOThreadLocal.set(userSessionVO);
    }

    public static UserSessionVO get() {
        return userSessionVOThreadLocal.get();
    }

    public static void remove() {
        userSessionVOThreadLocal.remove();
    }
}



二、存在的问题


2-1、内存泄漏

所谓内存泄漏就是很多无用的数据,也无法被回收。

在Java里面,有四种引用

  • 强引用:被强引用的对象不会被回收。(new 的方式就是强引用)
  • 软应用:被软引用的对象只有内存不足的时候才会被回收
  • 弱引用:被弱引用的对象下次垃圾回收的时候一定会被回收
  • 虚引用:虚引用和垃圾回收没有什么关系,只是在回收的时候会得到一个系统通知

我们的线程并不是频繁创建的,是有一个工作线程池的,当一个线程处理完后就被放回了线程池,下一个线程又调用 set方法设置数据,而之前的数据并没有被释放,因为是new创建的所以还有一个强引用指向它,导致它无法被使用却也不能被回收。


解决办法: 在拦截器的后处理里面调用threadlocal的remove方法删除即可


2-2、后面的请求使用了前面的登录数据

因为在最开始的方案里面,没有在拦截器后处理去 remove ThreadLocal里面的数据,而又存在某些请求既可以登陆又可以不登陆,就导致后面的请求使用了之前的登陆数据,导致数据异常。


2-3、多个系统权限问题

一般系统都是有用户端和管理端,我们在管理端可以修改用户权限,修改后我们同时更新JWT,这样访问管理端是没有问题。

但是在用户端就不行,没办法清除数据就导致用户端的权限不一致了。


2-4、其它

其实现在大部分都是使用redis做数据存储,token一个随机字符串就搞定了,但有时候我们没必要或者不想引入新的技术。另外本地内存肯定是要更快的。

上述的问题也都有办法可以解决的,想一想?在逻辑上都可以解决的。


三、完美的解决方案

设想一个这样的场景,我们后台服务有30+,并不是所有接口都需要进行权限校验(大部分接口是需要进行权限校验的),你应当如何设置一个完美的结局方案呢?

思想还是和之前一样,生成一个token(uuid/jwt),然后一个拦截器进行拦截,再来一个自定义注解判断是否需要拦截。

不同在于,我们把这个生成token、解析token的步骤抽离出来,做成一个公共包,这样每个需要用到的服务直接引入,然后在需要拦截的方法加一个注解就好了。


服务架构图
基于ThreadLocal和JWT登录的问题,微服务登录架构解决方案_第1张图片

3-1、common

关于登录部分一些公共的部分就放在这里,比如登录拦截注解、自定义异常、返回的ResponseObj类(就是一个自定义返回值里面有 code、data、message)

之所以单独拎出来也是为了好维护,因为我们的clientservice 都要引入,后续也可能会对基础包里面新增修改啥的,这样就只需要维护一个公共包就好了,不用修改两处。


3-2、service

这个服务很简单,但是很重要,它是一个独立运行提供http服务的服务。

它提供两个主要的接口

  1. 生成token,你之前的token怎么生成,它这个就怎么生成,一样的
  2. 解析token,我们无非是获取到了token然后再去把它转换成用户信息(不管是JWT还是存入redis都一样)

3-3、client

这个是很巧妙的一部分,我们把client使用 mvn deploy 命令推到远程仓库,这样谁需要使用,直接在pom里面引入就好了。

我们项目引入了 client,而client引入了 common,所以我们项目也可以使用自定义注解,只需要在需要拦截的地方加入自定义登录拦截注解就好了。

把之前的拦截器复制到client里面,对于解析token的地方改成使用http调用service服务(当然也可以使用openFeign这样的方式调用)。

底包有一个很重要的思想:引入就使用,如果不使用就不要引入,所以我们需要在自动注入里面直接配置好注入这个拦截器。


四、其它


4-1、代码

因为这个涉及到公司源码,不方面公开,原理就是这样,可以根据原理搭建一个,前提准备知识是关于 starter 的构建,可以看看这个 https://blog.csdn.net/Tomwildboar/article/details/122901248


4-2、关于单点登录

在写这个文章的时候,突然想到单点登录,毕竟都是解决登录问题嘛,有什么区别呢?

  • 单点登录:单点登录解决的是多个系统使用同一套账号体系来登录。
  • 上面登录:今天介绍的登录是为了解决多个服务之间的鉴权,比如我们现在一个系统40多个服务,服务之间调用,以及前后端登录交互。

Java进阶之单点登录详解

你可能感兴趣的:(场景题,技术学习,微服务,java,ThreadLocal,JWT,登陆架构)