点开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<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<RequestAttributes>("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());
源码链:
SaLoginModel的isLastingCookie属性是Boolean的,注意是Boolean而不是boolean,所以默认值是null!
新建SaLoginModel后,会走到这个方法:
public void login(Object id, SaLoginModel loginModel)
上面代码说明了,loginModel会根据config调用自身的build方法。
真相大白,sa-token默认就是记住我的模式。
哈哈,刚刚给作者发了个issue:
步骤 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方法,源码链如下
找到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的部分。
什么是SaTokenDao?
这个应该是sa-token的内部持久化容器,我不明白为啥作者要用xxxDao来命名,我还以为是存到数据库呢。我个人觉得用xxxContext,xxxBeanFactory来命名比较合适。
SaTokenDao的默认实现是SaTokenDaoDefaultImpl,里面维护了一个Map,用的是ConcurrentHashMap,看来这个部分是很重要的,看得出作者处处都在想着线程安全。
我们获取loginId,用的是get方法
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
确实也有道理。