public void setTokenValue(String tokenValue, int cookieTimeout){
SaTokenConfig config = getConfig();
// 将token保存到[存储器]里
SaStorage storage = SaHolder.getStorage();
// 判断是否配置了token前缀
String tokenPrefix = config.getTokenPrefix();
if(SaFoxUtil.isEmpty(tokenPrefix)) {
storage.set(splicingKeyJustCreatedSave(), tokenValue);
} else {
// 如果配置了token前缀,则拼接上前缀一起写入
storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue);
}
// 注入Cookie
if(config.getIsReadCookie()){
SaResponse response = SaHolder.getResponse();
response.addCookie(getTokenName(), tokenValue, “/”, config.getCookieDomain(), cookieTimeout);
}
}
SaStorage是一个接口,set方法是把token存入request对象中。
关键是下面一段:
esponse.addCookie(getTokenName(), tokenValue, “/”, config.getCookieDomain(), cookieTimeout);
这句话利用cookie保存了当前登录用户的token。
谷歌浏览器查看cookie方式:右上角有三个点的按钮 - 设置
搜索localhost,找到satoken,这就是上面代码中getTokenName()方法的返回值
步骤 6 sa-token为什么能获取到response对象?SaHolder的秘密。。。(深挖,看不懂就跳过,没事)
===========================================================
之前我也一直想不通一个问题,sa-token用起来也太方便了吧,就这么一句话,什么都搞定了。我也不需要去关心session,也不要管HttpServletResponse啥的。
秘密就在这:
SaResponse response = SaHolder.getResponse();
SaHolder调用getResponse方法得到SaResponse, 这个SaResponse是一个接口
package cn.dev33.satoken.context.model;
/**
-
Response 包装类
-
@author kong
*/
public interface SaResponse {
/**
-
获取底层源对象
-
@return see note
*/
public Object getSource();
/**
-
删除指定Cookie
-
@param name Cookie名称
*/
public void deleteCookie(String name);
/**
-
写入指定Cookie
-
@param name Cookie名称
-
@param value Cookie值
-
@param path Cookie路径
-
@param domain Cookie的作用域
-
@param timeout 过期时间 (秒)
*/
public void addCookie(String name, String value, String path, String domain, int timeout);
/**
-
在响应头里写入一个值
-
@param name 名字
-
@param value 值
-
@return 对象自身
*/
public SaResponse setHeader(String name, String value);
/**
-
在响应头写入 [Server] 服务器名称
-
@param value 服务器名称
-
@return 对象自身
*/
public default SaResponse setServer(String value) {
return this.setHeader(“Server”, value);
}
/**
-
重定向
-
@param url 重定向地址
-
@return 任意值
*/
public Object redirect(String url);
}
我们目前只用了addCookie方法,然后再看SaResponse的实现类
只有一个实现类,addCookie方法如下:
/**
- 写入指定Cookie
*/
@Override
public void addCookie(String name, String value, String path, String domain, int timeout) {
Cookie cookie = new Cookie(name, value);
if(SaFoxUtil.isEmpty(path) == true) {
path = “/”;
}
if(SaFoxUtil.isEmpty(domain) == false) {
cookie.setDomain(domain);
}
cookie.setPath(path);
cookie.setMaxAge(timeout);
response.addCookie(cookie);
}
和我们预期的是一致的。
现在的问题是,SaHolder究竟是怎么getResponse的?
代码如下:
public static SaResponse getResponse() {
return SaManager.getSaTokenContext().getResponse();
}
原来是saTokenContext的绝活,再看SaManager的getSaTokenContext方法:
public static SaTokenContext getSaTokenContext() {
if (saTokenContext == null) {
synchronized (SaManager.class) {
if (saTokenContext == null) {
setSaTokenContext(new SaTokenContextDefaultImpl());
}
}
}
return saTokenContext;
}
我以为秘密在 new SaTokenContextDefaultImpl() 里面。
SaTokenContextDefaultImpl是Sa-Token 上下文处理器 [默认实现类]。
结果一看代码,懵逼了:
敢情这是在嘲讽我吗,这估计是不正常的情况下才会走到这吧,肯定不是。那如果不是用的SaTokenContextDefaultImpl,难道saTokenContext本来就有值的?
saTokenContext是一个接口,有三个实现类:
因为这是SpringBoot项目怎么看也像是SaTokenContextForSpring
重新登录看看,发现
这个方法返回了SaResponseForServlet对象,属于SaResponse的实现类,所以在上面SaHolder的getResponse方法获取的实际是SaResponseForServlet对象。又是多态,多态好是好,封装了底层的实现。只给出接口类的简单操作,就是如果翻源码会有点麻烦,需要一层层地找。
SaResponseForServlet的实例化代码如下
那么关键就在于SpringMVCUtil是怎么getResponse的?
关键就在于RequestContextHolder了,它调用了getRequestAttributes方法,得到 servletRequestAttributes对象,查看数据发现
哦,到这一步就有点豁然开朗了,response的值是:com.alibaba.druid.support.http.WebStatFilter$StatHttpServletResponseWrapper@3cca73f3
至于这个玩意又是什么东东,老实说,我目前还没有完全搞明白,目前只知道这个类的位置是在druid的jar包里面:
点开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实现如下:
注释说返回绑定当前线程的RequestAttributes(请求参数),用到了requestAttributesHolder
private static final ThreadLocal requestAttributesHolder =
new NamedThreadLocal(“Request attributes”);
这个requestAttributesHolder是一个ThreadLocal,是线程本地变量,并且设置了final和static。设置final是因为不希望被修改,设置static是为了方便其他地方也能调用它。
ThreadLocal是java.lang包下面的,已经和框架无关了。其get方法源码如下:
简单说一下,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。
我们理解到这一步已经足够了。
步骤 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());
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

最后
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
O等大厂,18年进入阿里一直到现在。**
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-UbE64mMS-1712664712564)]
[外链图片转存中…(img-5JuU91nF-1712664712565)]
[外链图片转存中…(img-w2YHqVmE-1712664712565)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

最后
[外链图片转存中…(img-2XxHLunu-1712664712566)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!