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

简介: 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


确实也有道理。



相关文章
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
1257 29
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
518 4
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
|
存储 前端开发 JavaScript
在线教育网课系统源码开发指南:功能设计与技术实现深度解析
在线教育网课系统是近年来发展迅猛的教育形式的核心载体,具备用户管理、课程管理、教学互动、学习评估等功能。本文从功能和技术两方面解析其源码开发,涵盖前端(HTML5、CSS3、JavaScript等)、后端(Java、Python等)、流媒体及云计算技术,并强调安全性、稳定性和用户体验的重要性。
|
机器学习/深度学习 自然语言处理 算法
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
3742 1
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
503 2
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
1243 2
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
1478 1
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析

推荐镜像

更多
  • DNS