基于SpringBoot的影像注册系统04 sa-token使用(源码解析 + 万字)

*/

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的实现类

image

只有一个实现类,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 上下文处理器 [默认实现类]。

结果一看代码,懵逼了:

image

敢情这是在嘲讽我吗,这估计是不正常的情况下才会走到这吧,肯定不是。那如果不是用的SaTokenContextDefaultImpl,难道saTokenContext本来就有值的?

image

saTokenContext是一个接口,有三个实现类:

image

因为这是SpringBoot项目怎么看也像是SaTokenContextForSpring

image

重新登录看看,发现

image

这个方法返回了SaResponseForServlet对象,属于SaResponse的实现类,所以在上面SaHolder的getResponse方法获取的实际是SaResponseForServlet对象。又是多态,多态好是好,封装了底层的实现。只给出接口类的简单操作,就是如果翻源码会有点麻烦,需要一层层地找。

SaResponseForServlet的实例化代码如下

image

那么关键就在于SpringMVCUtil是怎么getResponse的?

image

关键就在于RequestContextHolder了,它调用了getRequestAttributes方法,得到 servletRequestAttributes对象,查看数据发现

image

哦,到这一步就有点豁然开朗了,response的值是:com.alibaba.druid.support.http.WebStatFilter$StatHttpServletResponseWrapper@3cca73f3

至于这个玩意又是什么东东,老实说,我目前还没有完全搞明白,目前只知道这个类的位置是在druid的jar包里面:

image

image

点开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又继承了ServletResponseWrapperServletResponseWrapper实现了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实现如下:

image

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

private static final ThreadLocal requestAttributesHolder =

new NamedThreadLocal(“Request attributes”);

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

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

image

简单说一下,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。

image

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

步骤 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());

源码链:

image

image

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

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

public void login(Object id, SaLoginModel loginModel)

image

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

image

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

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

image

image

步骤 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方法,源码链如下

image

image

找到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));

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

蚂蚁面试比较重视基础,所以Java那些基本功一定要扎实。蚂蚁的工作环境还是挺赞的,因为我面的是稳定性保障部门,还有许多单独的小组,什么三年1班,很有青春的感觉。面试官基本水平都比较高,基本都P7以上,除了基础还问了不少架构设计方面的问题,收获还是挺大的。


经历这次面试我还通过一些渠道发现了需要大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

蚂蚁金服5面,总结了49个面试题,遇到的面试官都是P7级别以上

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**[外链图片转存中…(img-Dr8EZxo7-1713610987314)]

[外链图片转存中…(img-ZqYqllir-1713610987315)]

[外链图片转存中…(img-GnEDCSO0-1713610987315)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

蚂蚁面试比较重视基础,所以Java那些基本功一定要扎实。蚂蚁的工作环境还是挺赞的,因为我面的是稳定性保障部门,还有许多单独的小组,什么三年1班,很有青春的感觉。面试官基本水平都比较高,基本都P7以上,除了基础还问了不少架构设计方面的问题,收获还是挺大的。


经历这次面试我还通过一些渠道发现了需要大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

[外链图片转存中…(img-gjCTr7nF-1713610987316)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值