sa-token使用(源码解析 + 万字)二

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: sa-token使用(源码解析 + 万字)

点开WebStatFilter,发现里面有个内部类StatHttpServletResponseWrapper,原来那个$是内部类的意思


public final static class StatHttpServletResponseWrapper extends HttpServletResponseWrapper implements HttpServletResponse {
        //初始值应该设置为:HttpServletResponse.SC_OK,而不是 0。
        private int status = HttpServletResponse.SC_OK;
        public StatHttpServletResponseWrapper(HttpServletResponse response){
            super(response);
        }
        public void setStatus(int statusCode) {
            super.setStatus(statusCode);
            this.status = statusCode;
        }
        @SuppressWarnings("deprecation")
        public void setStatus(int statusCode, String statusMessage) {
            super.setStatus(statusCode, statusMessage);
            this.status = statusCode;
        }
        public void sendError(int statusCode, String statusMessage) throws IOException {
            super.sendError(statusCode, statusMessage);
            this.status = statusCode;
        }
        public void sendError(int statusCode) throws IOException {
            super.sendError(statusCode);
            this.status = statusCode;
        }
        public int getStatus() {
            return status;
        }
    }


这个StatHttpServletResponseWrapper类继承了HttpServletResponseWrapper,而HttpServletResponseWrapper又继承了ServletResponseWrapper,ServletResponseWrapper实现了HttpServletResponse(嗯??见到 HttpServletResponse了,终于看到了老朋友,不容易)


因为这个项目使用了Druid数据源,所以肯定是某个时间点把这个类new出来了,因为多态的关系,不会影响其他功能,这个咱们先讲到这。


好了,回到SpringMVCUtil的getResponse方法:


/**
 * 获取当前会话的 response
 * @return response
 */
public static HttpServletResponse getResponse() {
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if(servletRequestAttributes == null) {
        throw new SaTokenException("非Web上下文无法获取Response");
    }
    return servletRequestAttributes.getResponse();
}


我知道你一定有很多的疑惑,比如RequestContextHolder是啥,怎么就getRequestAttributes了,servletRequestAttributes又是啥,凭什么就可以getResponse?


别着急,咱一个一个来。


首先是RequestContextHolder,它的getRequestAttributes实现如下:


1.png


注释说返回绑定当前线程的RequestAttributes(请求参数),用到了requestAttributesHolder


private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
            new NamedThreadLocal<RequestAttributes>("Request attributes");


这个requestAttributesHolder是一个ThreadLocal,是线程本地变量,并且设置了final和static。设置final是因为不希望被修改,设置static是为了方便其他地方也能调用它。


ThreadLocal是java.lang包下面的,已经和框架无关了。其get方法源码如下:


2.png


简单说一下,ThreadLocal是和当前线程相关的,具体原理我们就先不说了,等以后重新开一个章节单独。现在,你只需要知道,Spring框架的org.springframework.web.context.request帮我们完成了这个事情,他就是拿到response了。而sa-token框架是直接取用了Spring框架的Response。


这个Response是和当前线程相关的,每个用户访问Tomcat,走到Controller,service,dao再返回数据,这整个过程都是在一个线程里面,和其他用户的访问无关,这个叫做线程隔离。


咳咳,最后我们捋一捋:


讲了这么多,其实我们的问题就是SaHolder为什么能获取response对象,现在直接说结论,因为SaHolder调用了SaManager.getSaTokenContext(),得到了SaTokenContext才可以通过getResponse方法获取SaResponse,而SaTokenContext的真实身份其实是SaTokenContextForSpring,SaTokenContextForSpring重写了getResponse,就是在这个方法去调用Spring的Response。


3.png


我们理解到这一步已经足够了。


步骤 7 sa-token默认配置


如果你不单独做配置,就采用默认配置,默认配置是写在SaTokenConfig中的。


/** token名称 (同时也是cookie名称) */
private String tokenName = "satoken";
/** token的长久有效期(单位:秒) 默认30天, -1代表永久 */
private long timeout = 60 * 60 * 24 * 30;
/**
 * token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
 * (例如可以设置为1800代表30分钟内无操作就过期)
 */
private long activityTimeout = -1;
/** 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) */
private Boolean isConcurrent = true;
/** 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */
private Boolean isShare = true;
/** 是否尝试从请求体里读取token */
private Boolean isReadBody = true;
/** 是否尝试从header里读取token */
private Boolean isReadHead = true;
/** 是否尝试从cookie里读取token */
private Boolean isReadCookie = true;
/** token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */
private String tokenStyle = "uuid";
/** 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 */
private int dataRefreshPeriod = 30;
/** 获取[token专属session]时是否必须登录 (如果配置为true,会在每次获取[token-session]时校验是否登录) */
private Boolean tokenSessionCheckLogin = true;
/** 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作)  */
private Boolean autoRenew = true;
/** 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景 */
private String cookieDomain;
/** token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */
private String tokenPrefix;
/** 是否在初始化配置时打印版本字符画 */
private Boolean isPrint = true;
/** 是否打印操作日志 */
private Boolean isLog = false;
/**
 * jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) 
 */
private String jwtSecretKey;
/**
 * Id-Token的有效期 (单位: 秒)
 */
private long idTokenTimeout = 60 * 60 * 24;


步骤 8 考考你,现在有记住我的功能吗?


Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:


  • 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失
  • 永久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览Cookie也不会消失

因此,记住我的功能对应的就是永久Cookie

登录的时候,我们的代码是这样写的:


StpUtil.login(userReal.getId());


源码链:


4.png


5.png


SaLoginModel的isLastingCookie属性是Boolean的,注意是Boolean而不是boolean,所以默认值是null!


新建SaLoginModel后,会走到这个方法:


public void login(Object id, SaLoginModel loginModel)


6.png


上面代码说明了,loginModel会根据config调用自身的build方法。


7.png


真相大白,sa-token默认就是记住我的模式。


哈哈,刚刚给作者发了个issue:


8.png


步骤 9 如何获取登录用户ID?


页面:my.jsp

该页面可以查看个人信息,对应接口为 /user/getUserInfo.do


/**
 * 查询当前用户信息
 * @param user
 * @return
 */
@PostMapping("getUserInfo.do")
public User getUserInfo(){
    int loginIdAsInt = StpUtil.getLoginIdAsInt();
    User user = service.getUserById(loginIdAsInt);
    return user;
}


这个getLoginIdAsInt方法,源码链如下


9.png


10.png


找到stpLogin::getLoginId 方法


/** 
 * 获取当前会话账号id, 如果未登录,则抛出异常 
 * @return 账号id
 */
public Object getLoginId() {
    // 如果正在[临时身份切换], 则返回临时身份 
    if(isSwitch()) {
        return getSwitchLoginId();
    }
    // 如果获取不到token,则抛出: 无token
    String tokenValue = getTokenValue();
    if(tokenValue == null) {
        throw NotLoginException.newInstance(loginType, NotLoginException.NOT_TOKEN);
    }
    // 查找此token对应loginId, 如果找不到则抛出:无效token 
    String loginId = getLoginIdNotHandle(tokenValue);
    if(loginId == null) {
        throw NotLoginException.newInstance(loginType, NotLoginException.INVALID_TOKEN, tokenValue);
    }
    // 如果是已经过期,则抛出已经过期 
    if(loginId.equals(NotLoginException.TOKEN_TIMEOUT)) {
        throw NotLoginException.newInstance(loginType, NotLoginException.TOKEN_TIMEOUT, tokenValue);
    }
    // 如果是已经被顶替下去了, 则抛出:已被顶下线 
    if(loginId.equals(NotLoginException.BE_REPLACED)) {
        throw NotLoginException.newInstance(loginType, NotLoginException.BE_REPLACED, tokenValue);
    }
    // 如果是已经被踢下线了, 则抛出:已被踢下线 
    if(loginId.equals(NotLoginException.KICK_OUT)) {
        throw NotLoginException.newInstance(loginType, NotLoginException.KICK_OUT, tokenValue);
    }
    // 检查是否已经 [临时过期]
    checkActivityTimeout(tokenValue);
    // 如果配置了自动续签, 则: 更新[最后操作时间] 
    if(getConfig().getAutoRenew()) {
        updateLastActivityToNow(tokenValue);
    }
    // 至此,返回loginId 
    return loginId;
}


核心就是getLoginIdNotHandle方法,点开


/**
  * 获取指定Token对应的账号id (不做任何特殊处理) 
  * @param tokenValue token值 
  * @return 账号id
  */
public String getLoginIdNotHandle(String tokenValue) {
    return getSaTokenDao().get(splicingKeyTokenValue(tokenValue));
}


这边获取了saTokenDao对象,SaTokenDao是一个接口,默认实现是SaTokenDaoDefaultImpl(好像也只有这么一个实现类)


让我们来看看是怎么实现的,找到了SaManager关于SaTokenDao的部分。


11.png


什么是SaTokenDao?

这个应该是sa-token的内部持久化容器,我不明白为啥作者要用xxxDao来命名,我还以为是存到数据库呢。我个人觉得用xxxContext,xxxBeanFactory来命名比较合适。


SaTokenDao的默认实现是SaTokenDaoDefaultImpl,里面维护了一个Map,用的是ConcurrentHashMap,看来这个部分是很重要的,看得出作者处处都在想着线程安全。


我们获取loginId,用的是get方法


12.png


key就是tokenValue(已经经过splicingKeyTokenValue方法修饰过,加了前缀TokenName和loginType)


真相大白,在login的时候,sa-token就在saTokenDao中根据tokenValue注册了loginId,其他地方需要取的时候,只需要问saTokenDao拿就行了。


本次实验中,


key=satoken:login:token:50b2dcf9-922b-4aaa-b000-33526199dd52,

value=6


步骤 10 sa-token作者回复


今天发现,sa-token作者回复我的issue


13.png


确实也有道理。



相关文章
|
15天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
16天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
28天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
43 3
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
59 5
|
2月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
120 5
|
2月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
2月前
|
算法 Java 程序员
Map - TreeSet & TreeMap 源码解析
Map - TreeSet & TreeMap 源码解析
37 0
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0

推荐镜像

更多