主要介绍Sa-Token的鉴权使用以及实现原理。

文章目录

  • 简介
  • 使用
  • 源码解释
    • 创建会话
      • 1.前置检查
      • 2.获取配置
      • 3.分配token
      • 4.获取 User-Session
      • 5.设置token-id映射关系
      • 6.登录成功事件发布
      • 7.检查会话数量
    • 客户端注入Token

简介

官网介绍的非常详细,主要突出这是一个轻量级鉴权框架的特点,详情可自行访问:https://sa-token.dev33.cn/doc.html#/

使用

旨在简单使用,大部分功能均可以在一行代码内实现,这里举几个官网示例:

首先添加依赖:

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.33.0</version>
</dependency>

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.java
public static StpLogic stpLogic = new StpLogic("login");public static void login(Object id) {stpLogic.login(id);
}

随后在StpLogic中,通过传入的Object id以及SaLoginModel进行构建会话:

// StpLogic.java
public 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;
}

1.前置检查

判断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);}
}

2.获取配置

StpLogic.getConfig():

// 2.获取配置信息
SaTokenConfig config = this.getConfig();

3.分配token

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()) {// 获取tokenString 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<String, Object> 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<Object, String, String> createToken = (loginId, loginType) -> {// 根据配置的tokenStyle生成不同风格的token String tokenStyle = SaManager.getConfig().getTokenStyle();// uuid if(SaTokenConsts.TOKEN_STYLE_UUID.equals(tokenStyle)) {return UUID.randomUUID().toString();}// 简单uuid (不带下划线)if(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID.equals(tokenStyle)) {return UUID.randomUUID().toString().replaceAll("-", "");}// 32位随机字符串if(SaTokenConsts.TOKEN_STYLE_RANDOM_32.equals(tokenStyle)) {return SaFoxUtil.getRandomString(32);}// 64位随机字符串if(SaTokenConsts.TOKEN_STYLE_RANDOM_64.equals(tokenStyle)) {return SaFoxUtil.getRandomString(64);}// 128位随机字符串if(SaTokenConsts.TOKEN_STYLE_RANDOM_128.equals(tokenStyle)) {return SaFoxUtil.getRandomString(128);}// tik风格 (2_14_16)if(SaTokenConsts.TOKEN_STYLE_TIK.equals(tokenStyle)) {return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__";}// 默认,还是uuid return UUID.randomUUID().toString();};}

4.获取 User-Session

StpLogic.getSessionByLoginId()

public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {return getSessionBySessionId(splicingKeySession(loginId), isCreate);
}/**
* 获取指定key的Session, 如果Session尚未创建,isCreate=是否新建并返回
* @param sessionId SessionId
* @param isCreate 是否新建
* @return Session对象
*/
public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {// getSaTokenDao使用了懒加载初始化SaTokenDao对象,最终由new SaTokenDaoDefaultImpl()进行实现具体方法// 并根据sessionId获取sessionSaSession 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;
}

5.设置token-id映射关系

StpLogic.saveTokenToIdMapping()

public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {// 如果继续往下深挖其实set方法的实现底层就是一个new ConcurrentHashMap<String, Object>()// 并且封装了一个new ConcurrentHashMap<String, Long>()来记录key与过期时间的关系// 并且设置的过期过期时间getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);
}

这里浅浅看一下设置的过期时间如何实现:

/**
* 数据集合
*/
public Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();/**
* 过期时间集合 (单位: 毫秒) , 记录所有key的到期时间 [注意不是剩余存活时间]
*/
public Map<String, Long> expireMap = new ConcurrentHashMap<String, Long>();@Override
public void set(String key, String value, long timeout) {if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {return;}// 设置key-valuedataMap.put(key, value);// 设置key-到期时间expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}/**
* 如果指定key已经过期,则立即清除它。在每个get方法前都首先调用一下这个方法
* @param key 指定key
*/
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 读写操作 @Override
public String get(String key) {// 首先判断一下key是否已经过期clearKeyByTimeout(key);return (String)dataMap.get(key);
}

6.登录成功事件发布

SaTokenEventCenter.doLogin

// --------- 注册侦听器
private static List<SaTokenListener> listenerList = new ArrayList<>();/**
* 每次登录时触发
* @param loginType 账号类别
* @param loginId 账号id
* @param tokenValue 本次登录产生的 token 值
* @param loginModel 登录参数
*/
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做一些属于我们自己的业务监听器处理:

7.检查会话数量

StpLogic.logoutByMaxLoginCount()

/**
* 会话注销,根据账号id 和 设备类型 和 最大同时在线数量
*
* @param loginId 账号id
* @param session 此账号的 Session 对象,可填写null,框架将自动获取
* @param device 设备类型 (填null代表注销所有设备类型)
* @param maxLoginCount 保留最近的几次登录
*/
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.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();
}

客户端注入Token

分析StpLogic.setTokenValue()方法

/**
* 在当前会话写入当前TokenValue
* @param tokenValue token值
* @param loginModel 登录参数
*/
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);}
}

参考资料:

  • Sa-Token官网介绍
  • sa-token使用(源码解析 + 万字)

Sa-Token浅谈相关推荐

  1. 用户数据表设计借鉴 浅谈数据库用户表结构设计,第三方登录 基于 Token 的身份验证

    最近对用户数据表的设计比较感兴趣,看到了两篇比较好的文章. 浅谈数据库用户表结构设计,第三方登录 转载于: https://www.cnblogs.com/jiqing9006/p/5937733.h ...

  2. music算法_“要热爱 请深爱”系列(5)浅谈模拟退火算法

    黄乐天 浅谈模拟退火算法 背景 在实际生活中, 数学问题中,我们常常会遇到(一定范围内)函数求最值的问题.一般可以用数学方式解答,但如果遇到如下恶心的函数: 它的函数图像是这样的: 我们只好用计算机科 ...

  3. 浅谈嵌套命名实体识别(Nested NER)

    ©PaperWeekly 原创 · 作者|张成蹊 单位|北京大学硕士生 研究方向|自然语言处理 序 命名实体识别(Named Entity Recognition, 下称 NER)任务,主要目的是从一 ...

  4. php ci如何保证数据安全,浅谈php(codeigniter)安全性注意事项

    1.httponly session一定要用httponly的否则可能被xxs攻击,利用js获取cookie的session_id. 要用框架的ci_session,更长的位数,httponly,这些 ...

  5. 浅谈SQL注入风险 - 一个Login拿下Server(转)

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:"老师你这SQL有注 ...

  6. 浅谈C#更改令牌ChangeToken

    前言 在上篇文章浅谈C#取消令牌CancellationTokenSource[1]一文中我们讲解了CancellationTokenSource,它的主要功能就是分发一个令牌,当我取消令牌我可以进行 ...

  7. 浅谈“三层结构”原理与用意(转帖)

    浅谈"三层结构"原理与用意 序 在刚刚步入"多层结构"Web应用程序开发的时候,我阅读过几篇关于"asp.net三层结构开发"的文章.但其多 ...

  8. web应用服务器计算资源核算,浅谈网络计算与应用.doc

    浅谈网络计算与应用.doc 浅谈网络计算与应用 摘要:作为一种新型的分布计算技术,网格计算将地理上分布的.异构的资源 用高速网络连接在一起,集成一台高速的超级计算机.分析了网格计算的意义. 体系结构. ...

  9. 浅谈网络通信中的流量整形

    前言 在前面的<浅谈网络通信中的 ACK.NACK 和 REX>一文中,我们知道了网络通信中的丢包重传的相关理论和方法,既在网络发生丢包的情况下的补救措施,本文则往前进一步,介绍下如何通过 ...

  10. laytpl语法_浅谈laytpl 模板空值显示null的解决方法及简单的js表达式

    浅谈laytpl 模板空值显示null的解决方法及简单的js表达式 laytpl 模板语法 {{ d.field }} 输出一个普通字段,不转义html 官方的说明 但d.field 为空时会显示nu ...

最新文章

  1. MySQL 存储引擎(MyISAM、InnoDB、NDBCluster)
  2. Nagios Apache报Internal Server Error错误的解决方法
  3. 光耦驱动单向可控硅_华越国际一文带路:可控硅触发设计技巧
  4. Python Pytest调用fixture之@pytest.mark.usefixtures()、叠加usefixtures、@pytest.fixture(autouse=True)用法详解
  5. java this的用法
  6. “听话”的苏宁少东家
  7. 广州软件性能测试培训,Loadrunner企业级性能测试课程 广州八神软件性能测试实战教程 炼数性能测试视频...
  8. python中factor函数_Python基础教程
  9. 火了!评分9.7,这本Python书终于玩大了!
  10. AJAX Wrapper for .NET
  11. MongoDB查询及索引优化
  12. 如何elf文件转换为asm汇编文件
  13. 百度地图显示多个标注点
  14. LWN:终于能够防护 straight-line 预测执行漏洞了!
  15. C#二次开发CAD常用的方法和注意事项
  16. 关于『数据结构』:图论
  17. linux python3安装proton_深度deepin系统中通过Lutris(wine、proton)运行逆水寒的方法 ......
  18. Sqlalchemy 使用add_columns函数
  19. 开通微信服务号需要准备的材料
  20. SWMM引擎之二——在读SWMM模拟结果时应注意的问题

热门文章

  1. Java web--利用java操作excel文档
  2. 【MCU】单片机看门狗工作原理
  3. ROS机器人平台发展趋势
  4. linux临时关闭防火墙,和永久关闭防火墙
  5. 英特尔和amd学计算机,笔记本处理器intel和amd哪个好_有什么区别|性能对比-太平洋电脑网...
  6. Spring Webflux 响应式编程 (二) - WebFlux编程实战
  7. C语言-vs的常用快捷键
  8. 中兴5G解决方案打造新体验,构建新生态
  9. Zotero使用GB/T7714 2005模板插入参考文献出现 作者名全部大写问题、et al.变成汉字‘等‘、多出参考文章的doi 问题 的解决方案
  10. tcp ip协议 服务器和客户端区别,网络与TCP/IP协议-总结