主要介绍Sa-Token的鉴权使用以及实现原理。 文章目录 简介使用源码解释创建会话1.前置检查2.获取配置3.分配token4.获取 User-Session5.设置token-id映射关系6.登录成功事件发布7.检查会话数
主要介绍Sa-Token的鉴权使用以及实现原理。
官网介绍的非常详细,主要突出这是一个轻量级鉴权框架的特点,详情可自行访问:https://sa-token.dev33.cn/doc.html#/
旨在简单使用,大部分功能均可以在一行代码内实现,这里举几个官网示例:
首先添加依赖:
cn.dev33 sa-token-spring-boot-starter 1.33.0
yaml配置文件:
server: # 端口 port: 8081############## Sa-Token 配置 (文档: Https://sa-token.cc) ##############sa-token: # token名称 (同时也是cookie名称) token-name: satoken # token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 activity-timeout: -1 # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: true # token风格 token-style: uuid # 是否输出操作日志 is-log: false
// 会话登录,参数填登录人的账号id StpUtil.login(10001);
// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常StpUtil.checkLogin();
// 将账号id为 10077 的会话踢下线 StpUtil.kickout(10077);
功能图如下所示:
解释登录原理
StpUtil.login(usId);
这短短的一句话代码蕴藏了多少玄机,我们一探究竟。
首先我们观察现象,模拟一个登录入口/login,里面做一个最简单的动作,就是将前端传入的usId作为用户id并交给StpUtil执行登录逻辑,并最终将usId返回。
@GetMapping("/login")private String login(String usId){ StpUtil.login(usId); return usId;}
访问 http://localhost:8081/login?usId=123,成功返回结果123。
随后我们F12查看浏览器的控制台,Application->打开Cookies:
能够观察到Cookies自动新增了一个键为satoken的cookie键值对,值类似于uuid随机字符串,此处示例为9ea38efb-228f-4131-b844-903467caf205,过期时间设置了30天,可对应前面配置文件的配置信息,satoken的cookie名称以及过期时间均支持配置调整。
接下来我们分析源码:
首先通过StpUtil作为入口,实际上通过实例化StpLogic来进行调用:
// StpUtil.javapublic static StpLogic stpLogic = new StpLogic("login");public static void login(Object id) { stpLogic.login(id);}
随后在StpLogic中,通过传入的Object id以及SaLoginModel进行构建会话:
// StpLogic.javapublic void login(Object id) { this.login(id, new SaLoginModel());}// 通过这两个方法进行会话建立 createLoginSession()/setTokenValue()public void login(Object id, SaLoginModel loginModel) { // 1、创建会话 String token = this.createLoginSession(id, loginModel); // 2、在当前客户端注入Token setTokenValue(token, loginModel);}
分析StpLogic.createLoginSession()方法
public String createLoginSession(Object id, SaLoginModel loginModel) { // 1.这个设计可以借鉴一下,直接可以判断id值为null就抛异常,将异常封装起来 SaTokenException.throwByNull(id, "账号id不能为空", 11002); // 2.获取配置信息 SaTokenConfig config = this.getConfig(); loginModel.build(config); // 3.生成token(若已经有存在生效的token则使用原先的token) String tokenValue = this.distUsableToken(id, loginModel); // 4.获取 User-Session/首次登录则创建会话,使用自定义封装的SaSession对象接收 SaSession session = this.getSessionByLoginId(id, true); session.updateMinTimeout(loginModel.getTimeout()); session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault()); // 5.设置token -> id 映射关系 this.saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout()); // 调用getSaTokenDao().set设置tokenValue与失效时间的关系 this.setLastActivityToNow(tokenValue); // 6.事件发布 SaTokenEventCenter.doLogin(this.loginType, id, tokenValue, loginModel); // 7.检查此账号会话数量是否超出最大值,-1表示不限会话数量 if (config.getMaxLoginCount() != -1) { loGoutByMaxLoginCount(id, session, (String)null, config.getMaxLoginCount()); } return tokenValue;}
判断Object id是否为null
SaTokenException.throwByNull(id, "账号id不能为空", 11002);public static void throwByNull(Object value, String message, int code) { if(SaFoxUtil.isEmpty(value)) { throw new SaTokenException(message).setCode(code); }}
StpLogic.getConfig():
// 2.获取配置信息SaTokenConfig config = this.getConfig();
StpLogic.distUsableToken():
protected String distUsableToken(Object id, SaLoginModel loginModel) { Boolean isConcurrent = this.getConfig().getIsConcurrent(); if (!isConcurrent) { this.replaced(id, loginModel.getDevice()); } if (SaFoxUtil.isNotEmpty(loginModel.getToken())) { return loginModel.getToken(); } else { if (isConcurrent && this.getConfigOfIsshare() && !loginModel.isSetExtraData()) { // 获取token String tokenValue = this.getTokenValueByLoginId(id, loginModel.getDeviceOrDefault()); // 已经存在会话,将之前生成的token返回 if (SaFoxUtil.isNotEmpty(tokenValue)) { return tokenValue; } }// *新会话,生成一个新token,可借鉴,见下方解析 return this.createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData()); }}
解析StpLogic.createTokenValue():
public class StpLogic { // ... public String createTokenValue(Object loginId, String device, long timeout, Map extraData) { // SaStrategy.me:使用SaStrategy的单例引用 // SaStrategy.me.createToken:调用createToken()方法 // SaStrategy.me.createToken.apply:使得结果生效 return SaStrategy.me.createToken.apply(loginId, loginType); }}public final class SaStrategy { public static final SaStrategy me = new SaStrategy(); public BiFunction
StpLogic.getSessionByLoginId()
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) { return getSessionBySessionId(splicingKeySession(loginId), isCreate);}public SaSession getSessionBySessionId(String sessionId, boolean isCreate) { // getSaTokenDao使用了懒加载初始化SaTokenDao对象,最终由new SaTokenDaoDefaultImpl()进行实现具体方法 // 并根据sessionId获取session SaSession session = getSaTokenDao().getSession(sessionId); // session暂未建立,进行session新建 if(session == null && isCreate) { // 与上方的createToken使用了同样的设计 session = SaStrategy.me.createSession.apply(sessionId); // 设置session与session过期时效 getSaTokenDao().setSession(session, getConfig().getTimeout()); } return session;}
StpLogic.saveTokenToIdMapping()
public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {// 如果继续往下深挖其实set方法的实现底层就是一个new ConcurrentHashMap() // 并且封装了一个new ConcurrentHashMap()来记录key与过期时间的关系 // 并且设置的过期过期时间 getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);}
这里浅浅看一下设置的过期时间如何实现:
public Map dataMap = new ConcurrentHashMap();public Map expireMap = new ConcurrentHashMap();@Overridepublic void set(String key, String value, long timeout) { if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { return; } // 设置key-value dataMap.put(key, value); // 设置key-到期时间 expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));}void clearKeyByTimeout(String key) { Long expirationTime = expireMap.get(key); // 清除条件:如果不为空 && 不是[永不过期] && 已经超过过期时间 if(expirationTime != null && expirationTime != SaTokenDao.NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) { dataMap.remove(key); expireMap.remove(key); }}// ------------------------ String 读写操作 @Overridepublic String get(String key) { // 首先判断一下key是否已经过期 clearKeyByTimeout(key); return (String)dataMap.get(key);}
SaTokenEventCenter.doLogin
// --------- 注册侦听器 private static List listenerList = new ArrayList<>();public static void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { for (SaTokenListener listener : listenerList) { listener.doLogin(loginType, loginId, tokenValue, loginModel); }}
主要目的是登录成功后的一些后置处理方法回调等,通过观察我们可以发现只要注册到SaTokenEventCenter.listenerList中即可在遍历中执行监听器的doLogin()方法。目前默认添加控制台日志侦听器,new SaTokenListenerForLog(),主要实现一些日志打印:
还有一个空实现的SaTokenListenerForSimple监听器,后续我们如果想要做一些自定义扩展,就可以继承SaTokenListenerForSimple做一些属于我们自己的业务监听器处理:
StpLogic.logoutByMaxLoginCount()
public void logoutByMaxLoginCount(Object loginId, SaSession session, String device, int maxLoginCount) { if(session == null) { session = getSessionByLoginId(loginId, false); if(session == null) { return; } } List list = session.tokenSignListCopyByDevice(device); // 遍历操作 for (int i = 0; i < list.size(); i++) { // 只操作前n条 if(i >= list.size() - maxLoginCount) { continue; } // 清理: token签名、token最后活跃时间 String tokenValue = list.get(i).getValue(); session.removeTokenSign(tokenValue); clearLastActivity(tokenValue); // 删除Token-Id映射 & 清除Token-Session deleteTokenToIdMapping(tokenValue); deleteTokenSession(tokenValue); // $$ 发布事件:指定账号注销 SaTokenEventCenter.doLogout(loginType, loginId, tokenValue); } // 注销 Session session.logoutByTokenSignCountToZero();}
分析StpLogic.setTokenValue()方法
public void setTokenValue(String tokenValue, SaLoginModel loginModel){ if(SaFoxUtil.isEmpty(tokenValue)) { return; } // 1. 将 Token 保存到 [存储器] 里 setTokenValueToStorage(tokenValue); // 2. 将 Token 保存到 [Cookie] 里 此处对应 if (getConfig().getIsReadCookie()) { setTokenValueToCookie(tokenValue, loginModel.getCookieTimeout()); } // 3. 将 Token 写入到响应头里 if(loginModel.getIsWriteHeaderOrGlobalConfig()) { setTokenValueToResponseHeader(tokenValue); }}
参考资料:
来源地址:https://blog.csdn.net/imVainiycos/article/details/128319460
--结束END--
本文标题: Sa-Token浅谈
本文链接: https://lsjlt.com/news/373988.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-04-01
2024-04-03
2024-04-03
2024-01-21
2024-01-21
2024-01-21
2024-01-21
2023-12-23
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0