个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang -
Overview
继续上文Sa-Token登录pre,有了前面的基础,就可以完整的了解satoken
的登录流程了。
项目启动
可以看到satoken
的一些配置和组件都已经注入,这个日志是怎么做的?下次可以讲一下,使用的是观察者模式。
login
一开始我还想直接从源码角度来的,发现不太合适,还是结合项目debug
吧!
前面业务登录直接过,到satoken
登录。
StpUtil
的所有login
重载方法最后归结于StpLogic
的public void login(Object id, SaLoginModel loginModel)
,如下:
SaLoginModel
决定登录的一些细节,登录设备、是否持久化Cookie
、指定此次登录token
有效期、此次token
最低活跃频率等等。
1、创建会话
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
public String createLoginSession(Object id, SaLoginModel loginModel) {
checkLoginArgs(id, loginModel); SaTokenConfig config = getConfigOrGlobal(); loginModel.build(config);
String tokenValue = distUsableToken(id, loginModel); SaSession session = getSessionByLoginId(id, true); session.updateMinTimeout(loginModel.getTimeout()); TokenSign tokenSign = new TokenSign(tokenValue, loginModel.getDeviceOrDefault(), loginModel.getTokenSignTag()); session.addTokenSign(tokenSign);
saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
if(isOpenCheckActiveTimeout()) { setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig()); }
SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
if(config.getMaxLoginCount() != -1) { logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount()); } return tokenValue; }
|
代码注释已经相当清晰了,接下来就依此来吧。
1.1、检查参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| protected void checkLoginArgs(Object id, SaLoginModel loginModel) {
if(SaFoxUtil.isEmpty(id)) { throw new SaTokenException("loginId 不能为空").setCode(SaErrorCode.CODE_11002); }
if(NotLoginException.ABNORMAL_LIST.contains(id.toString())) { throw new SaTokenException("loginId 不能为以下值:" + NotLoginException.ABNORMAL_LIST); }
if( ! SaFoxUtil.isBasicType(id.getClass())) { SaManager.log.warn("loginId 应该为简单类型,例如:String | int | long,不推荐使用复杂类型:" + id.getClass()); }
if( ! isSupportExtra()) { Map<String, Object> extraData = loginModel.getExtraData(); if(extraData != null && extraData.size() > 0) { SaManager.log.warn("当前 StpLogic 不支持 extra 扩展参数模式,传入的 extra 参数将被忽略"); } }
if( ! getConfigOrGlobal().getDynamicActiveTimeout() && loginModel.getActiveTimeout() != null) { SaManager.log.warn("当前全局配置未开启动态 activeTimeout 功能,传入的 activeTimeout 参数将被忽略"); }
}
|
1、检查空
2、对账号id
检查,异常情况有下
3、账号id
不能是除8
大基本基本数据类型、8
大包装类、String
之外的复杂类型。
4、是否支持扩展参数是在StpLogic
层面的,不是全局配置,默认不集成jwt
时是不支持扩展参数的。
5、项目启动日志打印的全局配置dynamicActiveTimeout=false
,所以登录时会有以下日志。
SA [WARN] -->: 当前全局配置未开启动态 activeTimeout 功能,传入的
activeTimeout 参数将被忽略
1.2、初始化loginModel
1 2 3
| SaTokenConfig config = getConfigOrGlobal(); loginModel.build(config);
|
根据全局配置补充loginModel
,如:token
有效期,登录后是否写入响应头。
根据就近原则,全局配置优先级不如自定义loginModel
,这个要知道。
当下配置,默认token
有效期1小时,不写入响应头。
1.3、账号分配可用token
1 2
| String tokenValue = distUsableToken(id, loginModel);
|
这个注释相当严谨!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| protected String distUsableToken(Object id, SaLoginModel loginModel) {
Boolean isConcurrent = getConfigOrGlobal().getIsConcurrent(); if( ! isConcurrent) { replaced(id, loginModel.getDevice()); } if(SaFoxUtil.isNotEmpty(loginModel.getToken())) { return loginModel.getToken(); }
if(isConcurrent) {
if(getConfigOfIsShare()) {
String tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
if(SaFoxUtil.isNotEmpty(tokenValue)) { return tokenValue; }
} } return SaStrategy.instance.generateUniqueToken.execute( "token", getConfigOfMaxTryTimes(), () -> { return createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData()); }, tokenValue -> { return getLoginIdNotHandle(tokenValue) == null; } ); }
|
同端互斥登录
全局配置的是不允许同一用户并发登录(挤掉旧登录),注意!挤掉旧登录只能挤掉同设备登录,就是手机挤掉手机登录,不会挤掉PC登录。所以方法是这样的replaced(Object loginId, String device)
。这也就是官网的同端互斥登录。
因为我是配置了不允许同端登录is-concurrent: false
,那么进入replaced
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
public void replaced(Object loginId, String device) { SaSession session = getSessionByLoginId(loginId, false); if(session != null) {
for (TokenSign tokenSign: session.getTokenSignListByDevice(device)) {
String tokenValue = tokenSign.getValue();
session.removeTokenSign(tokenValue);
if(isOpenCheckActiveTimeout()) { clearLastActive(tokenValue); }
updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);
SaTokenEventCenter.doReplaced(loginType, loginId, tokenValue); }
} }
|
getSessionByLoginId
是一个非常重要的方法,有很多引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return getSessionBySessionId(splicingKeySession(loginId), isCreate, session -> { session.setType(SaTokenConsts.SESSION_TYPE__ACCOUNT); session.setLoginType(getLoginType()); session.setLoginId(loginId); }); }
|
实际上重要的是其重载方法,如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
public SaSession getSessionBySessionId(String sessionId, boolean isCreate, Consumer<SaSession> appendOperation) {
if(SaFoxUtil.isEmpty(sessionId)) { return null; }
SaSession session = getSaTokenDao().getSession(sessionId);
if(session == null && isCreate) { session = SaStrategy.instance.createSession.apply(sessionId);
if(appendOperation != null) { appendOperation.accept(session); }
getSaTokenDao().setSession(session, getConfigOrGlobal().getTimeout()); } return session; }
|
这个方法设计的就挺有意思,第二个参数可以决定是否创建新的session
,第三个参数是四大函数式接口之一的消费者。Java8
函数式接口、lambada
、stream
都应该了解的。
如下方法可知,在查不到sessionId
的session
时使用SaStrategy
类来创建session
,这个SaStrategy
类实际上是一个单例类,其中有很多策略方法,这个可以下次单独拎出来讲的。
因为这是我第一次登录,sessionKey
是这样的Authorization:login:session:1
并没有在redis
,而且这次仅仅是查看是否已有session
,所以不用创建。
那么replaced
就过了。
新建token
本次未预定token
值,而且是同端互斥登录,所以直接到新建token
。
createTokenValue
又是SaStrategy
的一个策略。
还记得satoken自定义
Token 风格吗?就体现在这里了。
getLoginIdNotHandle
这里通过拼接tokenKey
,查redis
检查token
是否已经存在,来保证唯一的。
generateUniqueToken
也是SaStrategy
的一个策略。
其函数式定义是这样的
根据传入的参数生成如下一个token
。
1.4、获取Account-Session,续期
1 2 3
| SaSession session = getSessionByLoginId(id, true); session.updateMinTimeout(loginModel.getTimeout());
|
发现没?又是getSessionByLoginId
,不过这次传入isCreate=true
。
创建session
这次需要创建session
了
创建session
策略如下,其实其中还包含了一个发布事件的动作
SA [INFO] -->: SaSession [Authorization:login:session:1]
创建成功
续期
1.5、在Account-Session上记录本次登录token
1 2 3
| TokenSign tokenSign = new TokenSign(tokenValue, loginModel.getDeviceOrDefault(), loginModel.getTokenSignTag()); session.addTokenSign(tokenSign);
|
前面讲过了Sa-Token 中的
Session会话
模型详解,Account-Session
归属账号的,一个账号可以存在多个token
,而本次token
当然也是属于这个账号,所以将此tokenSign
加入Account-Session
中,表示这个token
归属于同一账号。
1.6、保存token-id映射
1 2
| saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
|
如下拼接redisKey
存入账号id
。
1.7、写入token最后活跃时间
1 2 3 4
| if(isOpenCheckActiveTimeout()) { setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig()); }
|
如下
1.8、发布全局事件
1 2
| SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
|
这个可以再开一篇,观察者模式。
1.9、检查会话数量
1 2 3 4
| if(config.getMaxLoginCount() != -1) { logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount()); }
|
这个方法注解很清晰了!
当我们要清除一个token
时,我们应该做些什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public void logoutByMaxLoginCount(Object loginId, SaSession session, String device, int maxLoginCount) {
if(session == null) { session = getSessionByLoginId(loginId, false); if(session == null) { return; } }
List<TokenSign> list = session.getTokenSignListByDevice(device);
for (int i = 0; i < list.size() - maxLoginCount; i++) {
String tokenValue = list.get(i).getValue();
session.removeTokenSign(tokenValue);
if(isOpenCheckActiveTimeout()) { clearLastActive(tokenValue); }
deleteTokenToIdMapping(tokenValue);
deleteTokenSession(tokenValue);
SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); }
session.logoutByTokenSignCountToZero(); }
|
1.10、返回会话凭证token
至此终于创建会话结束。。。
2、在当前客户端注入 token
回到login
的第二步setTokenValue
。
1 2 3 4 5 6 7
| public void login(Object id, SaLoginModel loginModel) { String token = createLoginSession(id, loginModel);
setTokenValue(token, loginModel); }
|
如下,根据配置,讲token
写入Storage
。
SpringMVC
环境下,就是SaStorageForServlet
,实际操作的是HttpServletRequest
,点开就看到了。
至于Cookie
,就是特殊的头信息,这些都类似的。
那么至此一次登录真的就完成了。
分析总结
登录日志
登录成功后控制台会打印如下日志,这就是提到但又没细说的事件发布机制做到的。
Redis数据
查看redis
数据有下
session
splicingKeySession
:${tokenName}:login:session:${loginId}
Authorization:login:session:1
存储值如下,这个就是账号id
对应的Account-Session
。
token映射id
splicingKeySession
:${tokenName}:login:token:${tokenValue}
对应前面1.6保存token-id
映射
last-active
splicingKeySession
:${tokenName}:login:last-active:${tokenValue}
对应前面1.7写入token
最后活跃时间
token-session
splicingKeySession
:${tokenName}:login:token-session:${tokenValue}
这里的dataMap
里的login_user
保留着当前登录用户信息,对应上篇文章的“缓存权限数据”章节。
获取缓存中的登录用户信息就可以使用以下方法了!
1 2 3 4 5
| SaSession session = StpUtil.getTokenSession(); if (ObjectUtil.isNull(session)) { return null; } LoginUser loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
|
顶人下线
也就是说重复调用login
登录,会有什么效果,redis
会有什么变化呢?
因为这次是同端登录,可以看到session
的tokenSign
变为最新的,token
虽然还有两条,但其中一条指向的不再是账号id
,而是-4
,表示被顶下线,按理讲这里的token-session
应该是要被清除了的,源码里这里个步骤被注释了,不太理解。保留疑问。
过期登录
写不动了😂
场景太多了,自己实践吧!
写在最后
拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。
个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang -
Overview