Sa-Token浅谈

news2024/11/26 8:38:07

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

文章目录

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

简介

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

image-20221213155942848

使用

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

首先添加依赖:

<!-- 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);

功能图如下所示:

sa-token-js

源码解释

解释登录原理

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:

image-20221214103335083

能够观察到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();

image-20221214104609553

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()) {
            // 获取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<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获取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;
}

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-value
    dataMap.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(),主要实现一些日志打印:

image-20221214140145488

还有一个空实现的SaTokenListenerForSimple监听器,后续我们如果想要做一些自定义扩展,就可以继承SaTokenListenerForSimple做一些属于我们自己的业务监听器处理:

image-20221214140335284

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使用(源码解析 + 万字)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88997.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

23个常见的 JavaScript 函数

本文收集了23个日常开发中非常常用的功能&#xff0c;其中一些可能很复杂&#xff0c;另一些可能很简单&#xff0c;但我相信它们都会或多或少对每个人都有帮助。 01、生成随机颜色 当网站需要生成随机颜色时&#xff0c;我们可以通过以下代码来执行此操作。 02、数组重新排序…

文件包含漏洞包含日志文件获取Shell实战

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是文件包含漏洞包含日志文件获取Shell实战。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#…

Vue渲染器(四):双端diff算法

渲染器&#xff08;四&#xff09;&#xff1a;双端diff算法 在上一章中&#xff0c;我们介绍了简单diff算法的实现原理。它利用vnode的key属性&#xff0c;尽可能多地复用DOM&#xff0c;并通过移动DOM的方式来完成更新&#xff0c;从而减少不断地创建和销毁DOM元素带来的性能…

(python + 雷电模拟器)frida下载与安装

frida下载 我这边是用pycharm下载的 我是直接下载最新的&#xff0c;暂时没发现什么异常 在安装成功界面查看frida版本 此时电脑端frida下载完成。打开github&#xff0c;搜索到frida&#xff0c;点击发行版 根据你的frida版本&#xff0c;对url进行修改进入你需要的版本…

方格涂色(冬季每日一题 30)

给定一个 nnnnnn 的方格矩阵&#xff0c;最初所有方格都是白色的。 现在需要将矩阵边界上的一些方格涂成黑色&#xff0c;从而使得&#xff1a; 最上一行恰好有 UUU 个方格是黑色的。最右一列恰好有 RRR 个方格是黑色的。最下一行恰好有 DDD 个方格是黑色的。最左一列恰好有 …

Android入门第47天-Fragment的基本使用

简介 我们的Android入门一步步已经进入中级。我们讲完了所有的基本组件的基本使用、Activity、Service、BroadCast。今天我们来到了Fragment篇章。Fragment和Activity比到底是一个什么样的存在呢&#xff1f;我们以一个很小的例子来说通Fragment。 Fragment是什么 Fragment可…

智能家居DIY系列之智能灯泡

一、什么是智能灯 传统的灯泡是通过手动打开和关闭开关来工作。有时&#xff0c;它们可以通过声控、触控、红外等方式进行控制&#xff0c;或者带有调光开关&#xff0c;让用户调暗或调亮灯光。 智能灯泡内置有芯片和通信模块&#xff0c;可与手机、家庭智能助手、或其他智能…

浅析JWT Attack

前言 在2022祥云杯时遇到有关JWT的题&#xff0c;当时没有思路&#xff0c;对JWT进行学习后来对此进行简单总结&#xff0c;希望能对正在学习JWT的师傅们有所帮助。 JWT JWT&#xff0c;即JSON WEB TOKEN&#xff0c;它是一种用于通信双方之间传递安全信息的简洁的、URL安全…

创新研发负载分担机制,天翼云IPv6网络带宽再升级!

网络作为社会信息化的基础&#xff0c;已成为人们日常生活不可或缺的一部分。网络通过模拟信号将信息转为电流进行传播&#xff0c;在这个过程中&#xff0c;网卡便充当了解码器的作用&#xff0c;能够将电信号转换为计算机能够识别的数字信号。 网卡&#xff0c;即网络接口卡&…

关于LabVIEW大作业/课设/论文的写作框架整理(主体三部曲)

文章目录 一、前言二、写作框架2.1 介绍函数以及工具箱2.2 介绍相关原理2.3 系统设计和案例演示三、总结一、前言 因为在Labview临近要交大作业,发现自己根本不会写,程序等的已经准备好了,但是对于写作一直不知道查了查知网文章,让我有了个大概了解,在此帖出来,希望能帮…

1569_AURIX_TC275_电源管理与系统控制单元

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 之前看了不少类似的寄存器信息&#xff0c;总体来说阅读价值不是很大&#xff0c;查询的价值多一些。如果是进行编码&#xff0c;这样的寄存器信息需要查一下&#xff0c;在功能了解的时候…

java面试强基(22)

为什么要使用多线程呢? 先从总体上来说&#xff1a; 从计算机底层来说&#xff1a; 线程可以比作是轻量级的进程&#xff0c;是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外&#xff0c;多核 CPU 时代意味着多个线程可以同时运行&#xff0c;这减少了线程…

漏洞丨实例分析cve2012-0158

作者&#xff1a;黑蛋 作者&#xff1a;黑蛋 一、漏洞简介 CVE-2012-0158是一个office栈溢出漏洞&#xff0c;Microsoft Office 2003 sp3是2007年9月18日由微软公司创作的一个办公软件&#xff0c;他的MSCOMCTL.ocx中的MSCOMCTL.ListView控件检查失误&#xff0c;由于读取长…

MySQL数据库Linux系统安装tar包

MySQL数据库Linux系统安装tar包 使用的远程工具是mabaxterm,使用此工具连接linux服务器&#xff0c; 第一步先把mysql安装包拖到远程工具的目录里&#xff1a;/usr/local 第二步&#xff1a;cd到local目录下解压数据库mysql 命令&#xff1a; cd …/usr/local 解压数据库masq…

endo BCN-PEG4-COOH,1881221-47-1,endo BCN-四聚乙二醇-羧酸特点分享

●外观以及性质&#xff1a; endo BCN-PEG4-acid含有BCN基团和羧酸基团&#xff0c;酸基团可以在偶联条件下与胺反应形成酰胺键。BCN基团可以发生点击化学反应。 【产品理化指标】&#xff1a; ●中文名&#xff1a;endo BCN-四聚乙二醇-羧酸 ●英文名&#xff1a;endo BCN-P…

APS智能排产帮助LNG船舶生产厂家充分利用产能,提升生产效益

前一段时间&#xff0c;由于欧洲各国集中储备天然气准备过冬&#xff0c;引发全球对LNG船舶&#xff0c;也就是液化天然气运输船的需求持续增加。一艘LNG船单日租金成本已跃升至近40万美元&#xff08;约合人民币283万元&#xff09;&#xff0c;同比增长340%以上&#xff0c;一…

测试面试 | 某 BAT 大厂测试开发面试真题与重点解析

image1080677 64.8 KB 本文作者 J2W 为霍格沃兹测试学院《测试开发实战进阶》班优秀学员&#xff0c;4 个多月从初出茅庐、勉勉强强的初级测试开发快速成长&#xff0c;成功拿下某 BAT 大厂中级测试开发岗位 Offer&#xff0c;并获得学院奖学金。助教老师对其一致评价是「学习非…

程序人生:自学上岸自动化测试薪资20K,我的经验值得想进阶的朋友借鉴...

经常有人问过这样一个问题&#xff1a;‘’自动化测试是真的这么厉害吗&#xff1f;如何从零成为自动化测试工程师&#xff1f;” 我之前写过这样一篇文章【从功能测试进阶自动化测试&#xff0c;熬夜7天整理出这一份超全学习指南【附网盘资源】】 厉害不厉害在于你有没有扎实…

java-爬虫-es

文章目录1.数据来源&#xff1a;数据库、mq、爬虫2.爬虫&#xff1a;获取想要的页面数据1.导入依赖2.爬取核心部分编码3.测试解析成功4.封装对象5.引入es配置类6.将HtmlParseUtil注册到spring7.爬取的数据入es库8.空白文件初始化vue文献&#xff1a;https://www.kuangstudy.com…

Java程序员的技术进阶成长路线

据不完全统计&#xff0c;截至目前(2017.07)为止&#xff0c;中国Java程序员的数量已经超过了100万。而且&#xff0c;随着IT培训业的持续发展和大量的应届毕业生进入社会&#xff0c;Java程序员面临的竞争压力越来越大。那么&#xff0c;作为一名Java初级程序员&#xff0c;怎…