Sa-Token核心功能解剖二( Session会话、 持久层Redis扩展 、全局侦听器 、全局过滤器、多账号体系认证、单点登录)
文章目录
- 概要
- 功能结构图
- 5.Session会话
- 6.持久层扩展
- 7.全局侦听器
- 8.全局过滤器
- 9.多账号体系认证
- 10.单点登录
概要
Sa-Token核心功能解剖(二),主要有:
- Session会话 —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。
- 持久层扩展 —— 可集成 Redis,重启数据不丢失。
- 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
- 全局过滤器 —— 方便的处理跨域,全局设置安全响应头等操作。
- 多账号体系认证 —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
- 单点登录 —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
功能结构图
5.Session会话
- 全端共享Session(Account-Session): PC 和 APP 登录的账号id一致,它们对应的都是同一个Session,两端可以非常轻松的同步数据。
- 单端独享Session(Token-Session):同一账号在不同设备登录,产生单端独享的token和Token-Session,实现不同端会话Session的分隔、独享。
- 自定义Session( Custom-Session),方便的存取值。
Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据(如用户信息登录关联的User信息),提高程序性能。
Sa-Token 中,Session 分为三种,分别是:
- Account-Session: 每个 账号id 分配的 Session,调用StpUtil.login(id)登录会话时才会产生。
- Token-Session: 每个 token 分配的 Session
- Custom-Session: 以一个特定的值作为SessionId,来分配的Session。
Session会话模型详解
Sa-Token 常量类SaTokenConsts:
public class SaTokenConsts {
//SaSession 的类型: Account-Session
public static final String SESSION_TYPE__ACCOUNT = "Account-Session";
//SaSession 的类型: Token-Session
public static final String SESSION_TYPE__TOKEN = "Token-Session";
//SaSession 的类型: Custom-Session
public static final String SESSION_TYPE__CUSTOM = "Custom-Session";
......
}
1)全端共享Session(Account-Session)
//会话登录
//id 账号id,建议的类型:(long | int | String)
public void login(Object id) {
login(id, createSaLoginParameter());
}
--->
/**
* 创建指定账号 id 的登录会话数据
* @param id 账号id,建议的类型:(long | int | String)
* @param loginParameter 此次登录的参数Model
* @return 返回会话令牌
*/
public String createLoginSession(Object id,
SaLoginParameter loginParameter) {
.....
// 3、获取此账号的 Account-Session , 续期
SaSession session = getSessionByLoginId(id, true,
loginParameter.getTimeout());
.....
return session;
}
--->
//获取指定账号 id 的 Account-Session, 如果该 SaSession
//尚未创建,isCreate=是否新建并返回
public SaSession getSessionByLoginId(Object loginId,
boolean isCreate, Long timeout) {
if(SaFoxUtil.isEmpty(loginId)) {
throw new SaTokenException("Account-Session 获取失败:
loginId 不能为空");
}
return getSessionBySessionId(
splicingKeySession(loginId), isCreate, timeout,
session -> {
//Account-Session 首次创建时才会被执行的方法:
session.setType(SaTokenConsts.SESSION_TYPE__ACCOUNT);
session.setLoginType(getLoginType());
session.setLoginId(loginId);
});
}
--->
//拼接: 在保存 Account-Session 时,应该使用的 key
//key = Token:login:session:582729269957048452(uuid值)
public String splicingKeySession(Object loginId) {
return getConfigOrGlobal().getTokenName()
+ ":" + loginType + ":session:" + loginId;
}
Redis展示:
2)单端独享Session(Token-Session)
Sa-Token针对会话登录,不仅为账号id分配了Account-Session,同时还为每个token分配了不同的Token-Session。当同一账号在两个设备登录,但是它们产生两个不同token和对应的Token-Session,
实现不同端会话Session的分隔、独享。
前提:用户登录完成,并将产生的Token存储至请求作用域的读取值对象SaStorage[Storage Model]内
//登录web端 – 成功 --产生token
StpUtil.login(defUser.getId(), “PC”);
追溯用户唯一值token 存储到SaTokenContext 上下文
public void login(Object id, SaLoginModel loginModel) {
......
//当前客户端注入 token
this.setTokenValue(token, loginModel);
}
--->
public void setTokenValue(String tokenValue,
SaLoginModel loginModel) {
if (!SaFoxUtil.isEmpty(tokenValue)) {
//token 写入到当前请求的 Storage 存储器里
this.setTokenValueToStorage(tokenValue);
.....
}
}
---->
//将 token 写入到当前请求的 Storage 存储器里
public void setTokenValueToStorage(String tokenValue){
//实例化saTokenContext
SaTokenContext saTokenContext = SaManager.
getSaTokenContextOrSecond();
// 1、获取当前请求的 Storage 存储器
SaStorage storage = saTokenContext.getStorage();
// 2、保存 token
String tokenPrefix = getConfigOrGlobal().getTokenPrefix();
if( SaFoxUtil.isEmpty(tokenPrefix) ) {
storage.set(splicingKeyJustCreatedSave(), tokenValue);
} else {
storage.set(splicingKeyJustCreatedSave(),
tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT
+ tokenValue);
}
// 3、以无前缀的方式再写入一次
storage.set(SaTokenConsts.JUST_CREATED_NOT_PREFIX,
tokenValue);
}
token 写入到当前请求的 Storage 存储器展示图:
实例化saToken上下文-saTokenContext过程说明:
SaTokenContext saTokenContext = SaManager.getSaTokenContext-
OrSecond()
- starter.jar包自动装配saToken上下文注册类-SaTokenContextRegister
- SaTokenContextRegister类@Bean注入SaTokenContextForSpringIn-
JakartaServlet实例化实现SaTokenContext接口。
SaTokenContextForSpringInJakartaServlet类:
public class SaTokenContextForSpringInJakartaServlet
implements SaTokenContext {
public SaRequest getRequest() {
return new SaRequestForServlet(
SpringMVCUtil.getRequest());
}
public SaResponse getResponse() {
return new SaResponseForServlet(
SpringMVCUtil.getResponse());
}
public SaStorage getStorage() {
return new SaStorageForServlet(
SpringMVCUtil.getRequest());
}
.........
}
获取当前请求的 Storage 存储器:SaStorage storage = saTokenContext.getStorage();
public class SaTokenContextForSpringInJakartaServlet implements SaTokenContext {
/**
* 获取当前请求的 Storage 包装对象
*/
@Override
public SaStorage getStorage() {
return new SaStorageForServlet(SpringMVCUtil.getRequest());
}
...........
}
-------->
SaStorage类[Storage Model],请求作用域的读取值对象
public interface SaStorage extends SaSetValueInterface {
/** 取值 */
@Override
Object get(String key);
/** 写值 */
@Override
SaStorage set(String key, Object value);
}
用户调用 SaSession tokenSession = StpUtil.getTokenSession();
,从同一请求作用域的读取值对象SaStorage[Storage Model]内获取tokenValue
SaSession tokenSession = StpUtil.getTokenSession();
-------->
//1.获取同一请求作用域的读取值对象SaStorage内tokenValue
//2.SaSession 尚未创建,isCreate代表是否新建并返回
public SaSession getTokenSession(boolean isCreate) {
String tokenValue = this.getTokenValue();
return this.getTokenSessionByToken(tokenValue, isCreate);
}
-------->
//获取同一请求作用域的读取值对象SaStorage内tokenValue
public String getTokenValueNotCut(){
// 获取相应对象
SaStorage storage = SaHolder.getStorage();
SaRequest request = SaHolder.getRequest();
String tokenValue = null;
// 1. 尝试Storage存储器里读取Attrube属性为JUST_CREATED_、JUST_CREATED_NOT_PREFIX_
//等Key属性的值,即tokenValue
if(storage.get(splicingKeyJustCreatedSave()) != null) {
tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave()));
}........
return tokenValue;
}
获取tokenValue后创建tokenSession并存储到Redis
public SaSession getTokenSession(boolean isCreate) {
String tokenValue = getTokenValue();
............
return getTokenSessionByToken(tokenValue, isCreate);
}
-------->
public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {
return this.getSessionBySessionId(
this.splicingKeyTokenSession(tokenValue), isCreate, (Long)null, (session) -> {
// 这里是该 Token-Session 首次创建时才会被执行的方法:
session.setType("Token-Session");
session.setLoginType(this.getLoginType());
session.setToken(tokenValue);
});
}
-------->
//拼接:在保存 Token-Session 时,应该使用的 key
public String splicingKeyTokenSession(String tokenValue) {
return this.getConfigOrGlobal().getTokenName() + ":"
+ this.loginType + ":token-session:" + tokenValue;
}
-------->
/**
* 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,isCreate = 是否立即新建并返回
* @param sessionId SessionId
* @param isCreate 是否新建
* @return Session对象
*/
public SaSession getSessionBySessionId(String sessionId, boolean isCreate,
Long timeout, Consumer<SaSession> appendOperation) {
// 先检查这个 SaSession 是否已经存在,如果不存在且 isCreate=true,则新建并返回
SaSession session = getSaTokenDao().getSession(sessionId);
if(session == null && isCreate) {
// 创建这个 SaSession
session = SaStrategy.instance.createSession.apply(sessionId);
...........
// 将这个 SaSession 入库
getSaTokenDao().setSession(session, timeout);
}
return session;//Redis存储返token-session,并返回
}
同一账号在不同设备登录后独享的Token和Token-session 的Redis展示:
3)自定义Session( Custom-Session)
Custom-Session不依赖特定的 账号id 或者 token,而是依赖于你提供的SessionId,
由 SaSessionCustomUtil.getSessionById(“SessionId”);
流程忽略
6.持久层扩展
可集成 Redis,重启数据不丢失。
pom.xml 依赖
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.39.0</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
获取token指定的SaSession
SaSession tokenSession = StpUtil.getTokenSessionByToken(token);
---------->
/**
* 获取指定 token 的 Token-Session,如果该 SaSession 尚未创建,isCreate代表是否新建并返回
* @param tokenValue token值
* @param isCreate 是否新建
* @return session对象
*/
public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {
return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate, null, session -> {
session.setType(SaTokenConsts.SESSION_TYPE__TOKEN);
session.setLoginType(getLoginType());
session.setToken(tokenValue);
});
}
---------->
SaSession session = getSaTokenDao().getSession(sessionId);
---------->
//返回当前 StpLogic 使用的持久化对象
public SaTokenDao getSaTokenDao() {
return SaManager.getSaTokenDao();
}
---------->
public interface SaTokenDao {
default SaSession getSession(String sessionId) {
return (SaSession)getObject(sessionId);
}
}
---------->
@Component
public class SaTokenDaoRedisJackson implements SaTokenDao {
public Object getObject(String key) {
return this.objectRedisTemplate.opsForValue().get(key);
}
}
Sa-Token 持久层接口SaTokenDao实现类:SaTokenDaoRedisJackson、SaTokenDaoDefaultImpl。
- SaTokenDaoRedisJackson:持久层接口实现类(基于redis),由sa-token-redis-jackson.1.39.0.jar包自动装配[org.springframework.boot.autoconfigure.EnableAutoConfiguration
- =cn.dev33.satoken.dao.SaTokenDaoRedisJackson]
- SaTokenDaoDefaultImpl:持久层接口默认实现类(基于内存 Map,系统重启后数据丢失)
7.全局侦听器
在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。
侦听器实现参考
用户退出流程
/**
* [work] 会话注销,根据账号id 和 注销参数
* @param loginId 账号id
* @param logoutParameter 注销参数
*/
public void _logout(Object loginId, SaLogoutParameter logoutParameter) {
// 1、获取此账号的 Account-Session,上面记录了此账号的所有登录客户端数据
SaSession session = getSessionByLoginId(loginId, false);
session.logoutByTerminalCountToZero();
}
--------->
/** 注销Session (从持久库删除) */
public void logout() {
SaManager.getSaTokenDao().deleteSession(this.id);
// $$ 发布事件
SaTokenEventCenter.doLogoutSession(id);
}
--------->
//Sa-Token 事件中心 事件发布器:提供侦听器注册、事件发布能力
public class SaTokenEventCenter {
public static void doLogoutSession(String id) {
for (SaTokenListener listener : listenerList) {
listener.doLogoutSession(id);
}
}
}
--------->
public class SaTokenListenerForLog implements SaTokenListener {
@Override
public void doLogoutSession(String id) {
log.info("SaSession [{}] 注销成功", id);
}
}
8.全局过滤器
Sa-Token全局过滤器来实现路由拦截器鉴权。
方便的处理跨域,全局设置安全响应头等操作。
实现参考
9.多账号体系认证
一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
实现参考
10.单点登录
在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。
参考