Sa-Token常用操作和源码分析
Sa-Token集成Redis
一、前言
参考笔记链接:
1、https://www.cnblogs.com/skyblue-li/p/17432214.html
2、https://blog.csdn.net/qq_42985872/article/details/131453413?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_utm_term~default-1-131453413-blog-139888993.235v43pc_blog_bottom_relevance_base1&spm=1001.2101.3001.4242.2&utm_relevant_index=3
注:第一个是说明sa-token中的集成说明的,第二个阐述了Sa-Token集成Redis的部分源码说明。
下面我将用我的理解来解释为什么可以使用Redis来进行token的存储。
进入到核心代码:
StpUtil.login(userInfo.getUserId(), new SaLoginModel().setIsLastingCookie(loginParam.getRememberMe())
.setTimeout(DEFAULT_LOGIN_SESSION_TIMEOUT));
从这里进入,这里有两段核心代码:
/**
* 会话登录,并指定所有登录参数 Model
*
* @param id 账号id,建议的类型:(long | int | String)
* @param loginModel 此次登录的参数Model
*/
public void login(Object id, SaLoginModel loginModel) {
// 1、创建会话
String token = createLoginSession(id, loginModel);
// 2、在当前客户端注入 token
setTokenValue(token, loginModel);
}
核心逻辑两个步骤:1、创建会话;2、在当前客户端注入token值。
二、创建会话
// 6、保存 token -> id 的映射关系,方便日后根据 token 找账号 id
saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
这一步很重要:
/**
* 存储 token - id 映射关系
*
* @param tokenValue token值
* @param loginId 账号id
* @param timeout 会话有效期 (单位: 秒)
*/
public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {
getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);
}
这里的方法:getSaTokenDao()获取得到SaTokenDao非常关键。SaTokenDao是一个接口,在当前系统中存在两个实现类:SaTokenDaoDefaultImpl和SaTokenDaoRedisJackson。
@Component
public class SaTokenDaoRedisJackson implements SaTokenDao {}
那么为什么添加Redis坐标依赖之后就可以使用到了Redis了呢?
还得从自动配置包开始:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.dev33.satoken.spring.SaBeanRegister,\
cn.dev33.satoken.spring.SaBeanInject,\
cn.dev33.satoken.spring.sso.SaSsoBeanRegister,\
cn.dev33.satoken.spring.sso.SaSsoBeanInject,\
cn.dev33.satoken.spring.oauth2.SaOAuth2BeanRegister,\
cn.dev33.satoken.spring.oauth2.SaOAuth2BeanInject
在自动注入SaBeanInject对应的bean时可以看到
@Autowired(
required = false
)
public void setSaTokenDao(SaTokenDao saTokenDao) {
SaManager.setSaTokenDao(saTokenDao);
}
@Autowired(
required = false
)
public void setStpInterface(StpInterface stpInterface) {
SaManager.setStpInterface(stpInterface);
}
那么具体看下对应的SaTokenDao对应的实现:
/**
* 持久化组件
*/
private volatile static SaTokenDao saTokenDao;
// 默认首先先走这一步
public static void setSaTokenDao(SaTokenDao saTokenDao) {
setSaTokenDaoMethod(saTokenDao);
SaTokenEventCenter.doRegisterComponent("SaTokenDao", saTokenDao);
}
private static void setSaTokenDaoMethod(SaTokenDao saTokenDao) {
if (SaManager.saTokenDao != null) {
SaManager.saTokenDao.destroy();
}
SaManager.saTokenDao = saTokenDao;
if (SaManager.saTokenDao != null) {
// 然后回去执行Redis实现类中的方法。但是redis没有对应的实现方法,依然是一个空方法
SaManager.saTokenDao.init();
}
}
// 当没有添加Redis对应的坐标依赖时,才会去加载默认的实现类。也就是在内存中进行存储
public static SaTokenDao getSaTokenDao() {
if (saTokenDao == null) {
synchronized (SaManager.class) {
if (saTokenDao == null) {
setSaTokenDaoMethod(new SaTokenDaoDefaultImpl());
}
}
}
return saTokenDao;
}
三、在当前客户端注入token值
在这一步中,我们可以看到Sa-Token是如何在cookie和header头中写入对应的token对应的值的。
/**
* 在当前会话写入指定 token 值
*
* @param tokenValue token 值
* @param loginModel 登录参数
*/
public void setTokenValue(String tokenValue, SaLoginModel loginModel){
// 先判断一下,如果提供 token 为空,则不执行任何动作
if(SaFoxUtil.isEmpty(tokenValue)) {
return;
}
// 1、将 token 写入到当前请求的 Storage 存储器里
setTokenValueToStorage(tokenValue);
// 2. 将 token 写入到当前会话的 Cookie 里
if (getConfigOrGlobal().getIsReadCookie()) {
// 利用httpresponse将其添加到cookie
setTokenValueToCookie(tokenValue, loginModel.getCookieTimeout());
}
// 3. 将 token 写入到当前请求的响应头中
if(loginModel.getIsWriteHeaderOrGlobalConfig()) {
// 利用httpresponse将其添加到响应头中去
setTokenValueToResponseHeader(tokenValue);
}
}
所以我们可以在cookie、header中看到对应的token值。
3.1、获取得到和存储用户信息
在前面的存储过程中,只是将token和用户id做了一个绑定关系,但是有时候想要在session中获取得到用户信息。使用下面这种方式
UserInfo userInfo = new UserInfo();
StpUtil.login(userInfo.getUserId(), new SaLoginModel().setIsLastingCookie(loginParam.getRememberMe())
.setTimeout(DEFAULT_LOGIN_SESSION_TIMEOUT));
StpUtil.getSession().set(userInfo.getUserId().toString(), userInfo);
这里就可以将对应的用户信息进行保存。
为什么可以做到分布式存储?无非是sa-token作者对其进行了封装
public static SaSession getSession() {
return stpLogic.getSession();
}
点击进去,查看下具体方法
public SaSession getSession(boolean isCreate) {
return getSessionByLoginId(getLoginId(), isCreate);
}
直接点击到这里,然后看下源码
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
return getSessionBySessionId(splicingKeySession(loginId), isCreate, session -> {
// 这里是该 Account-Session 首次创建时才会被执行的方法:
// 设定这个 SaSession 的各种基础信息:类型、账号体系、账号id
session.setType(SaTokenConsts.SESSION_TYPE__ACCOUNT);
session.setLoginType(getLoginType());
session.setLoginId(loginId);
});
}
可以看到splicingKeySession方法前缀的拼接:
public String splicingKeySession(Object loginId) {
return getConfigOrGlobal().getTokenName() + ":" + loginType + ":session:" + loginId;
}
这里loginId的来源也很简单,因为之前已经建立了token和ID的映射关系,所以直接获取得到即可。
这里拼接对应的key,想要从Redis中来获取得到对应的session:
public SaSession getSessionBySessionId(String sessionId, boolean isCreate, Consumer<SaSession> appendOperation) {
// 如果提供的 sessionId 为 null,则直接返回 null
if(SaFoxUtil.isEmpty(sessionId)) {
return null;
}
// 先检查这个 SaSession 是否已经存在,如果不存在且 isCreate=true,则新建并返回
SaSession session = getSaTokenDao().getSession(sessionId);
if(session == null && isCreate) {
// 创建这个 SaSession
session = SaStrategy.instance.createSession.apply(sessionId);
// 追加操作
if(appendOperation != null) {
appendOperation.accept(session);
}
// 将这个 SaSession 入库
getSaTokenDao().setSession(session, getConfigOrGlobal().getTimeout());
}
return session;
}
首次创建肯定是没有的,创建完成并将其保存之后会将其保存起来。然后可以看到对应的入库方法
/**
* 写入 SaSession,并设定存活时间(单位: 秒)
* @param session 要保存的 SaSession 对象
* @param timeout 过期时间(单位: 秒)
*/
default void setSession(SaSession session, long timeout) {
setObject(session.getId(), session, timeout);
}
会发现可以进入到Redis中的方法中来:
public void setObject(String key, Object object, long timeout) {
if (timeout != 0L && timeout > -2L) {
if (timeout == -1L) {
this.objectRedisTemplate.opsForValue().set(key, object);
} else {
this.objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
}
}
}
所以也就意味着Session中可以挂在数据,在Session类中有一个字段dataMap清晰说明了可以用来挂在数据
/**
* 所有挂载数据
*/
private final Map<String, Object> dataMap = new ConcurrentHashMap<>();
所以可以看到当用来存值的时候使用方式:
StpUtil.getSession().set(userInfo.getUserId().toString(), userInfo);
看下这里的写方法,恰恰使用的也就是datamap方法:
/**
* 写值
* @param key 名称
* @param value 值
* @return 对象自身
*/
@Override
public SaSession set(String key, Object value) {
dataMap.put(key, value);
update();
return this;
}
这里也就意味着可以存储多条数据信息以及如果以后用户信息发生了修改之后,这里也需要再次将Session进行更新。
四、过滤器实现权限操作
参考链接:https://www.cnblogs.com/liuguangzhi/articles/18415627
因为网关中使用的是reacter,不是传统的web框架。所以在sa-token中使用了过滤器来进行权限和角色的校验,sa-token提供的filter有SaReactorFilter。
可以看下对应的体系结构:
public interface WebFilter {
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}
这里是org.springframework.web.server包下的WebFilter接口,也就是说gateway网关实现的接口,会挨个遍历对应的接口实现类,调用对应的filter方法。
而SaReactorFilter就实现了这个接口,可以看下对应的实现方式:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 写入WebFilterChain对象
exchange.getAttributes().put(SaReactorHolder.CHAIN_KEY, chain);
// ---------- 全局认证处理
try {
// 写入全局上下文 (同步)
SaReactorSyncHolder.setContext(exchange);
// 执行全局过滤器《《《《==========================核心逻辑==========================
beforeAuth.run(null);
SaRouter.match(includeList).notMatch(excludeList).check(r -> {
auth.run(null);
});
} catch (StopMatchException e) {
// StopMatchException 异常代表:停止匹配,进入Controller
} catch (Throwable e) {
// 1. 获取异常处理策略结果
String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));
// 2. 写入输出流
// 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json
// 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) {
exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);
}
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes())));
} finally {
// 清除上下文
SaReactorSyncHolder.clearContext();
}
// ---------- 执行
// 写入全局上下文 (同步)
SaReactorSyncHolder.setContext(exchange);
// 执行
return chain.filter(exchange).contextWrite(ctx -> {
// 写入全局上下文 (异步)
ctx = ctx.put(SaReactorHolder.CONTEXT_KEY, exchange);
return ctx;
}).doFinally(r -> {
// 清除上下文
SaReactorSyncHolder.clearContext();
});
}
这里还是比较简单的操作方式