查看源码,可知,sa
sa-token框架
- 测试代码
- 源码配置
- 自动装配
- SaTokenConfig
- SaTokenConfigFactory
- SaManager
- 工具类
- SaFoxUtil
- StpUtil
- SaResult
- StpLogic
- 持久层
- 定时任务
- 会话登录
- 生成token
- 创建account-session
- 事件驱动模型
- 写入token
- SaSession
- SaCookie
- SaTokenDao
- SaStrage
- SaManager
- 持有者
- SaRouter
- 拼接响应头cookie
- 验证登录
- 退出登录
- 权限认证
- StpInterface
- 权限校验
- 角色校验
- 注解鉴权
- 踢人下线
测试代码
/**
* 处理用户认证鉴权请求
* */
@RestController
@RequestMapping("/user")
@Slf4j
@Validated
public class UserController {
@PostMapping("/dologin")
public String doLogin(@RequestBody LoginDTO loginDTO){
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
if (username.equals("test1")&&password.equals("123")){
StpUtil.login(10002);
return "登录成功";
}
return "success";
}
@GetMapping
public boolean isLogin(){
boolean login = StpUtil.isLogin();
log.info("当前会话是否登录:{}",login);
return login;
}
@GetMapping("/token-info")
public Object getTokenInfo(){
SaResult saResult = SaResult.data(StpUtil.getTokenInfo());
return saResult;
}
@GetMapping("/logout")
public Object logout(){
StpUtil.logout();
return SaResult.ok();
}
}
源码配置
自动装配
在springboot项目中,项目启动时,会初始化并注入sa-token中的一些对象
特别是saTokenConfig对象,封装了全局配置参数
/**
* 注册Sa-Token所需要的Bean
* <p> Bean 的注册与注入应该分开在两个文件中,否则在某些场景下会造成循环依赖
* @author click33
*
*/
public class SaBeanRegister {
/**
* 获取配置Bean
*
* @return 配置对象
*/
@Bean
@ConfigurationProperties(prefix = "sa-token")
public SaTokenConfig getSaTokenConfig() {
return new SaTokenConfig();
}
/**
* 获取 json 转换器 Bean (Jackson版)
*
* @return json 转换器 Bean (Jackson版)
*/
@Bean
public SaJsonTemplate getSaJsonTemplateForJackson() {
try {
// 部分开发者会在 springboot 项目中排除 jackson 依赖,所以这里做一个判断:
// 1、如果项目中存在 jackson 依赖,则使用 jackson 的 json 转换器
// 2、如果项目中不存在 jackson 依赖,则使用默认的 json 转换器
// to:防止因为 jackson 依赖问题导致项目无法启动
Class.forName("com.fasterxml.jackson.databind.ObjectMapper");
return new SaJsonTemplateForJackson();
} catch (ClassNotFoundException e) {
return new SaJsonTemplateDefaultImpl();
}
}
/**
* 应用上下文路径加载器
* @return /
*/
@Bean
public ApplicationContextPathLoading getApplicationContextPathLoading() {
return new ApplicationContextPathLoading();
}
}
/**
* 注入 Sa-Token 所需要的 Bean
*
* @author click33
* @since 1.34.0
*/
public class SaBeanInject {
/**
* 组件注入
* <p> 为确保 Log 组件正常打印,必须将 SaLog 和 SaTokenConfig 率先初始化 </p>
*
* @param log log 对象
* @param saTokenConfig 配置对象
*/
public SaBeanInject(
@Autowired(required = false) SaLog log,
@Autowired(required = false) SaTokenConfig saTokenConfig
){
if(log != null) {
SaManager.setLog(log);
}
if(saTokenConfig != null) {
SaManager.setConfig(saTokenConfig);
}
}
/**
* 注入持久化Bean
*
* @param saTokenDao SaTokenDao对象
*/
@Autowired(required = false)
public void setSaTokenDao(SaTokenDao saTokenDao) {
SaManager.setSaTokenDao(saTokenDao);
}
/**
* 注入权限认证Bean
*
* @param stpInterface StpInterface对象
*/
@Autowired(required = false)
public void setStpInterface(StpInterface stpInterface) {
SaManager.setStpInterface(stpInterface);
}
/**
* 注入上下文Bean
*
* @param saTokenContext SaTokenContext对象
*/
@Autowired(required = false)
public void setSaTokenContext(SaTokenContext saTokenContext) {
SaManager.setSaTokenContext(saTokenContext);
}
/**
* 注入二级上下文Bean
*
* @param saTokenSecondContextCreator 二级上下文创建器
*/
@Autowired(required = false)
public void setSaTokenContext(SaTokenSecondContextCreator saTokenSecondContextCreator) {
SaManager.setSaTokenSecondContext(saTokenSecondContextCreator.create());
}
/**
* 注入侦听器Bean
*
* @param listenerList 侦听器集合
*/
@Autowired(required = false)
public void setSaTokenListener(List<SaTokenListener> listenerList) {
SaTokenEventCenter.registerListenerList(listenerList);
}
/**
* 注入临时令牌验证模块 Bean
*
* @param saTemp saTemp对象
*/
@Autowired(required = false)
public void setSaTemp(SaTempInterface saTemp) {
SaManager.setSaTemp(saTemp);
}
/**
* 注入 Same-Token 模块 Bean
*
* @param saSameTemplate saSameTemplate对象
*/
@Autowired(required = false)
public void setSaIdTemplate(SaSameTemplate saSameTemplate) {
SaManager.setSaSameTemplate(saSameTemplate);
}
/**
* 注入 Sa-Token Http Basic 认证模块
*
* @param saBasicTemplate saBasicTemplate对象
*/
@Autowired(required = false)
public void setSaHttpBasicTemplate(SaHttpBasicTemplate saBasicTemplate) {
SaHttpBasicUtil.saHttpBasicTemplate = saBasicTemplate;
}
/**
* 注入 Sa-Token Digest Basic 认证模块
*
* @param saHttpDigestTemplate saHttpDigestTemplate 对象
*/
@Autowired(required = false)
public void setSaHttpBasicTemplate(SaHttpDigestTemplate saHttpDigestTemplate) {
SaHttpDigestUtil.saHttpDigestTemplate = saHttpDigestTemplate;
}
/**
* 注入自定义的 JSON 转换器 Bean
*
* @param saJsonTemplate JSON 转换器
*/
@Autowired(required = false)
public void setSaJsonTemplate(SaJsonTemplate saJsonTemplate) {
SaManager.setSaJsonTemplate(saJsonTemplate);
}
/**
* 注入自定义的 参数签名 Bean
*
* @param saSignTemplate 参数签名 Bean
*/
@Autowired(required = false)
public void setSaSignTemplate(SaSignTemplate saSignTemplate) {
SaManager.setSaSignTemplate(saSignTemplate);
}
/**
* 注入自定义的 StpLogic
* @param stpLogic /
*/
@Autowired(required = false)
public void setStpLogic(StpLogic stpLogic) {
StpUtil.setStpLogic(stpLogic);
}
/**
* 利用自动注入特性,获取Spring框架内部使用的路由匹配器
*
* @param pathMatcher 要设置的 pathMatcher
*/
@Autowired(required = false)
@Qualifier("mvcPathMatcher")
public void setPathMatcher(PathMatcher pathMatcher) {
SaPathMatcherHolder.setPathMatcher(pathMatcher);
}
}
SaTokenConfig
源码中用于缓存配置属性的类,和配置文件中的配置项一一对应
/** token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀) */
private String tokenName = "satoken";
/** token 有效期(单位:秒) 默认30天,-1 代表永久有效 */
private long timeout = 60 * 60 * 24 * 30;
/**
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
*/
private long activeTimeout = -1;
/**
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
*/
private Boolean dynamicActiveTimeout = false;
/**
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
*/
private Boolean isConcurrent = true;
/**
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
*/
private Boolean isShare = true;
/**
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
*/
private int maxLoginCount = 12;
/**
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
*/
private int maxTryTimes = 12;
/**
* 是否尝试从请求体里读取 token
*/
private Boolean isReadBody = true;
/**
* 是否尝试从 header 里读取 token
*/
private Boolean isReadHeader = true;
/**
* 是否尝试从 cookie 里读取 token
*/
private Boolean isReadCookie = true;
/**
* 是否在登录后将 token 写入到响应头
*/
private Boolean isWriteHeader = false;
/**
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
*/
private String tokenStyle = "uuid";
/**
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
*/
private int dataRefreshPeriod = 30;
/**
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
*/
private Boolean tokenSessionCheckLogin = true;
/**
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
*/
private Boolean autoRenew = true;
/**
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
*/
private String tokenPrefix;
/**
* 是否在初始化配置时在控制台打印版本字符画
*/
private Boolean isPrint = true;
/**
* 是否打印操作日志
*/
private Boolean isLog = false;
/**
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
*/
private String logLevel = "trace";
/**
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
*/
private int logLevelInt = 1;
/**
* 是否打印彩色日志
*/
private Boolean isColorLog = null;
/**
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
*/
private String jwtSecretKey;
/**
* Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456
*/
private String httpBasic = "";
/**
* Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456
*/
private String httpDigest = "";
/**
* 配置当前项目的网络访问地址
*/
private String currDomain;
/**
* Same-Token 的有效期 (单位: 秒)
*/
private long sameTokenTimeout = 60 * 60 * 24;
/**
* 是否校验 Same-Token(部分rpc插件有效)
*/
private Boolean checkSameToken = false;
SaTokenConfigFactory
用了一个工厂模式,负责读取配置文件,并且缓存起来(Map<String,String>)
/**
* 工具方法: 将指定路径的properties配置文件读取到Map中
*
* @param propertiesPath 配置文件地址
* @return 一个Map
*/
private static Map<String, String> readPropToMap(String propertiesPath) {
Map<String, String> map = new HashMap<>(16);
try {
InputStream is = SaTokenConfigFactory.class.getClassLoader().getResourceAsStream(propertiesPath);
if (is == null) {
return null;
}
Properties prop = new Properties();
prop.load(is);
for (String key : prop.stringPropertyNames()) {
map.put(key, prop.getProperty(key));
}
} catch (IOException e) {
throw new SaTokenException("配置文件(" + propertiesPath + ")加载失败", e).setCode(SaErrorCode.CODE_10021);
}
return map;
}
单单用Map缓存还不够方便,还要封装成对象,利用反射的field.set(),以及根据字段类型来填充属性,这和springIoC的依赖注入中的ByType思路一样
/**
* 工具方法: 将 Map 的值映射到一个 Model 上
*
* @param map 属性集合
* @param obj 对象, 或类型
* @return 返回实例化后的对象
*/
private static Object initPropByMap(Map<String, String> map, Object obj) {
if (map == null) {
map = new HashMap<>(16);
}
// 1、取出类型
Class<?> cs;
if (obj instanceof Class) {
// 如果是一个类型,则将obj=null,以便完成静态属性反射赋值
cs = (Class<?>) obj;
obj = null;
} else {
// 如果是一个对象,则取出其类型
cs = obj.getClass();
}
// 2、遍历类型属性,反射赋值
for (Field field : cs.getDeclaredFields()) {
String value = map.get(field.getName());
if (value == null) {
// 如果为空代表没有配置此项
continue;
}
try {
Object valueConvert = SaFoxUtil.getValueByType(value, field.getType());
field.setAccessible(true);
field.set(obj, valueConvert);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new SaTokenException("属性赋值出错:" + field.getName(), e).setCode(SaErrorCode.CODE_10022);
}
}
return obj;
}
}
SaManager
管理全局对象,类似于spring中的ApplicationContext
工具类
SaFoxUtil
sa-token核心工具类,诸如生成token等核心操作会封装进这个工具类
生成指定长度的随机token:
/**
* 生成指定长度的随机字符串
*
* @param length 字符串的长度
* @return 一个随机字符串
*/
public static String getRandomString(int length) {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = ThreadLocalRandom.current().nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
StpUtil
Sa-Token 权限认证工具类
提供了用于认证,鉴权,注销,踢下线,查询会话状态,获取会话和令牌对象,封禁账号等核心功能的工具方法,是Sa-token中连接其他核心api的中转站
通过看源码,我发现这个工具类甚至有点多余,因为,具体逻辑位于StpLogic,而这个工具类只是负责调用StpLogic
public class StpUtil{
/**
* 多账号体系下的类型标识
*/
public static final String TYPE = "login";
/**
* 底层使用的 StpLogic 对象
*/
public static StpLogic stpLogic = new StpLogic(TYPE);
/**
* 获取当前 StpLogic 的账号类型
*
* @return /
*/
public static String getLoginType(){
return stpLogic.getLoginType();
}
/**
* 安全的重置 StpLogic 对象
*
* <br> 1、更改此账户的 StpLogic 对象
* <br> 2、put 到全局 StpLogic 集合中
* <br> 3、发送日志
*
* @param newStpLogic /
*/
public static void setStpLogic(StpLogic newStpLogic) {
// 1、重置此账户的 StpLogic 对象
stpLogic = newStpLogic;
// 2、添加到全局 StpLogic 集合中
// 以便可以通过 SaManager.getStpLogic(type) 的方式来全局获取到这个 StpLogic
SaManager.putStpLogic(newStpLogic);
// 3、$$ 发布事件:更新了 stpLogic 对象
SaTokenEventCenter.doSetStpLogic(stpLogic);
}
/**
* 获取 StpLogic 对象
*
* @return /
*/
public static StpLogic getStpLogic() {
return stpLogic;
}
// ------------------- 获取 token 相关 -------------------
/**
* 返回 token 名称,此名称在以下地方体现:Cookie 保存 token 时的名称、提交 token 时参数的名称、存储 token 时的 key 前缀
*
* @return /
*/
public static String getTokenName() {
return stpLogic.getTokenName();
}
/**
* 在当前会话写入指定 token 值
*
* @param tokenValue token 值
*/
public static void setTokenValue(String tokenValue){
stpLogic.setTokenValue(tokenValue);
}
/**
* 在当前会话写入指定 token 值
*
* @param tokenValue token 值
* @param cookieTimeout Cookie存活时间(秒)
*/
public static void setTokenValue(String tokenValue, int cookieTimeout){
stpLogic.setTokenValue(tokenValue, cookieTimeout);
}
/**
* 在当前会话写入指定 token 值
*
* @param tokenValue token 值
* @param loginModel 登录参数
*/
public static void setTokenValue(String tokenValue, SaLoginModel loginModel){
stpLogic.setTokenValue(tokenValue, loginModel);
}
/**
* 获取当前请求的 token 值
*
* @return 当前tokenValue
*/
public static String getTokenValue() {
return stpLogic.getTokenValue();
}
/**
* 获取当前请求的 token 值 (不裁剪前缀)
*
* @return /
*/
public static String getTokenValueNotCut(){
return stpLogic.getTokenValueNotCut();
}
/**
* 获取当前会话的 token 参数信息
*
* @return token 参数信息
*/
public static SaTokenInfo getTokenInfo() {
return stpLogic.getTokenInfo();
}
// ------------------- 登录相关操作 -------------------
// --- 登录
/**
* 会话登录
*
* @param id 账号id,建议的类型:(long | int | String)
*/
public static void login(Object id) {
stpLogic.login(id);
}
}
SaResult
sa-token提供了SaResult用于构建统一结果返回类
/**
* 对请求接口返回 Json 格式数据的简易封装。
*
* <p>
* 所有预留字段:<br>
* code = 状态码 <br>
* msg = 描述信息 <br>
* data = 携带对象 <br>
* </p>
*
* @author click33
* @since 1.22.0
*/
public class SaResult extends LinkedHashMap<String, Object> implements Serializable{
// 序列化版本号
private static final long serialVersionUID = 1L;
// 预定的状态码
public static final int CODE_SUCCESS = 200;
public static final int CODE_ERROR = 500;
/**
* 构建
*/
public SaResult() {
}
/**
* 构建
* @param code 状态码
* @param msg 信息
* @param data 数据
*/
public SaResult(int code, String msg, Object data) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
}
/**
* 根据 Map 快速构建
* @param map /
*/
public SaResult(Map<String, ?> map) {
this.setMap(map);
}
/**
* 获取code
* @return code
*/
public Integer getCode() {
return (Integer)this.get("code");
}
/**
* 获取msg
* @return msg
*/
public String getMsg() {
return (String)this.get("msg");
}
/**
* 获取data
* @return data
*/
public Object getData() {
return this.get("data");
}
/**
* 给code赋值,连缀风格
* @param code code
* @return 对象自身
*/
public SaResult setCode(int code) {
this.put("code", code);
return this;
}
/**
* 给msg赋值,连缀风格
* @param msg msg
* @return 对象自身
*/
public SaResult setMsg(String msg) {
this.put("msg", msg);
return this;
}
/**
* 给data赋值,连缀风格
* @param data data
* @return 对象自身
*/
public SaResult setData(Object data) {
this.put("data", data);
return this;
}
/**
* 写入一个值 自定义key, 连缀风格
* @param key key
* @param data data
* @return 对象自身
*/
public SaResult set(String key, Object data) {
this.put(key, data);
return this;
}
/**
* 获取一个值 根据自定义key
* @param <T> 要转换为的类型
* @param key key
* @param cs 要转换为的类型
* @return 值
*/
public <T> T get(String key, Class<T> cs) {
return SaFoxUtil.getValueByType(get(key), cs);
}
/**
* 写入一个Map, 连缀风格
* @param map map
* @return 对象自身
*/
public SaResult setMap(Map<String, ?> map) {
for (String key : map.keySet()) {
this.put(key, map.get(key));
}
return this;
}
// ============================ 静态方法快速构建 ==================================
// 构建成功
public static SaResult ok() {
return new SaResult(CODE_SUCCESS, "ok", null);
}
public static SaResult ok(String msg) {
return new SaResult(CODE_SUCCESS, msg, null);
}
public static SaResult code(int code) {
return new SaResult(code, null, null);
}
public static SaResult data(Object data) {
return new SaResult(CODE_SUCCESS, "ok", data);
}
// 构建失败
public static SaResult error() {
return new SaResult(CODE_ERROR, "error", null);
}
public static SaResult error(String msg) {
return new SaResult(CODE_ERROR, msg, null);
}
// 构建指定状态码
public static SaResult get(int code, String msg, Object data) {
return new SaResult(code, msg, data);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "{"
+ "\"code\": " + this.getCode()
+ ", \"msg\": " + transValue(this.getMsg())
+ ", \"data\": " + transValue(this.getData())
+ "}";
}
/**
* 转换 value 值:
* 如果 value 值属于 String 类型,则在前后补上引号
* 如果 value 值属于其它类型,则原样返回
*
* @param value 具体要操作的值
* @return 转换后的值
*/
private String transValue(Object value) {
if(value == null) {
return null;
}
if(value instanceof String) {
return "\"" + value + "\"";
}
return String.valueOf(value);
}
}
StpLogic
Sa-Token 权限认证,逻辑实现类
Sa-Token 的核心,框架大多数功能均由此类提供具体逻辑实现。
sa-token中有关权限认证的核心逻辑均位于此类
创建token令牌和saSession会话对象
/**
* 创建指定账号 id 的登录会话数据
*
* @param id 账号id,建议的类型:(long | int | String)
* @param loginModel 此次登录的参数Model
* @return 返回会话令牌
*/
public String createLoginSession(Object id, SaLoginModel loginModel) {
// 1、检查参数类型
checkLoginArgs(id, loginModel);
// 2、使用全局SATokenConfig初始化LoginModel
SaTokenConfig config = getConfigOrGlobal();
loginModel.build(config);
// 3、创建或者从缓存中取出一个token字符串
String tokenValue = distUsableToken(id, loginModel);
// 4、获取此账号的 Account-Session , 更新timeout
SaSession session = getSessionByLoginId(id, true, loginModel.getTimeoutOrGlobalConfig());
session.updateMinTimeout(loginModel.getTimeout());
// 5、更新saSession的token-sign
TokenSign tokenSign = new TokenSign(tokenValue, loginModel.getDeviceOrDefault(), loginModel.getTokenSignTag());
session.addTokenSign(tokenSign);
// 6、缓存token和账号id的映射关系
saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
// 7、更新账号活跃时间,以便进行活跃度检查
if(isOpenCheckActiveTimeout()) {
setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig());
}
// 8、发布登陆成功事件
SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
// 9、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉
if(config.getMaxLoginCount() != -1) {
logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());
}
// 10、一切处理完毕,返回会话凭证 token
return tokenValue;
}
创建token,在StpLogic类中有一个方法可以生成不同格式的token,所谓的token,在sa-token中指的是一个随机字符串,由特定算法生成
/**
* 创建 Token 的策略
*/
public SaCreateTokenFunction createToken = (loginId, loginType) -> {
// 根据配置的tokenStyle生成不同风格的token
String tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle();
switch (tokenStyle) {
// uuid
case SaTokenConsts.TOKEN_STYLE_UUID:
return UUID.randomUUID().toString();
// 简单uuid (不带下划线)
case SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID:
return UUID.randomUUID().toString().replaceAll("-", "");
// 32位随机字符串
case SaTokenConsts.TOKEN_STYLE_RANDOM_32:
return SaFoxUtil.getRandomString(32);
// 64位随机字符串
case SaTokenConsts.TOKEN_STYLE_RANDOM_64:
return SaFoxUtil.getRandomString(64);
// 128位随机字符串
case SaTokenConsts.TOKEN_STYLE_RANDOM_128:
return SaFoxUtil.getRandomString(128);
// tik风格 (2_14_16)
case SaTokenConsts.TOKEN_STYLE_TIK:
return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__";
// 默认,还是uuid
default:
SaManager.getLog().warn("配置的 tokenStyle 值无效:{},仅允许以下取值: " +
"uuid、simple-uuid、random-32、random-64、random-128、tik", tokenStyle);
return UUID.randomUUID().toString();
}
};
持久层
包括会话对象,token在内的数据的存储由sa-token提供的持久层负责,一般是直接存到内存中,也支持放到redis或者其他数据库中
持久层负责对象数据的读写
/**
* Sa-Token 持久层接口
*
* <p>
* 此接口的不同实现类可将数据存储至不同位置,如:内存Map、Redis 等等。
* 如果你要自定义数据存储策略,也需通过实现此接口来完成。
* </p>
*
* @author click33
* @since 1.10.0
*/
public interface SaTokenDao {
/** 常量,表示一个 key 永不过期 (在一个 key 被标注为永远不过期时返回此值) */
long NEVER_EXPIRE = -1;
/** 常量,表示系统中不存在这个缓存(在对不存在的 key 获取剩余存活时间时返回此值) */
long NOT_VALUE_EXPIRE = -2;
// --------------------- 字符串读写 ---------------------
/**
* 获取 value,如无返空
*
* @param key 键名称
* @return value
*/
String get(String key);
/**
* 写入 value,并设定存活时间(单位: 秒)
*
* @param key 键名称
* @param value 值
* @param timeout 数据有效期(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
*/
void set(String key, String value, long timeout);
/**
* 更新 value (过期时间不变)
* @param key 键名称
* @param value 值
*/
void update(String key, String value);
/**
* 删除 value
* @param key 键名称
*/
void delete(String key);
/**
* 获取 value 的剩余存活时间(单位: 秒)
* @param key 指定 key
* @return 这个 key 的剩余存活时间
*/
long getTimeout(String key);
/**
* 修改 value 的剩余存活时间(单位: 秒)
* @param key 指定 key
* @param timeout 过期时间(单位: 秒)
*/
void updateTimeout(String key, long timeout);
// --------------------- 对象读写 ---------------------
/**
* 获取 Object,如无返空
* @param key 键名称
* @return object
*/
Object getObject(String key);
/**
* 写入 Object,并设定存活时间 (单位: 秒)
* @param key 键名称
* @param object 值
* @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
*/
void setObject(String key, Object object, long timeout);
/**
* 更新 Object (过期时间不变)
* @param key 键名称
* @param object 值
*/
void updateObject(String key, Object object);
/**
* 删除 Object
* @param key 键名称
*/
void deleteObject(String key);
/**
* 获取 Object 的剩余存活时间 (单位: 秒)
* @param key 指定 key
* @return 这个 key 的剩余存活时间
*/
long getObjectTimeout(String key);
/**
* 修改 Object 的剩余存活时间(单位: 秒)
* @param key 指定 key
* @param timeout 剩余存活时间
*/
void updateObjectTimeout(String key, long timeout);
// --------------------- SaSession 读写 (默认复用 Object 读写方法) ---------------------
/**
* 获取 SaSession,如无返空
* @param sessionId sessionId
* @return SaSession
*/
default SaSession getSession(String sessionId) {
return (SaSession)getObject(sessionId);
}
/**
* 写入 SaSession,并设定存活时间(单位: 秒)
* @param session 要保存的 SaSession 对象
* @param timeout 过期时间(单位: 秒)
*/
default void setSession(SaSession session, long timeout) {
setObject(session.getId(), session, timeout);
}
/**
* 更新 SaSession
* @param session 要更新的 SaSession 对象
*/
default void updateSession(SaSession session) {
updateObject(session.getId(), session);
}
/**
* 删除 SaSession
* @param sessionId sessionId
*/
default void deleteSession(String sessionId) {
deleteObject(sessionId);
}
/**
* 获取 SaSession 剩余存活时间(单位: 秒)
* @param sessionId 指定 SaSession
* @return 这个 SaSession 的剩余存活时间
*/
default long getSessionTimeout(String sessionId) {
return getObjectTimeout(sessionId);
}
/**
* 修改 SaSession 剩余存活时间(单位: 秒)
* @param sessionId 指定 SaSession
* @param timeout 剩余存活时间
*/
default void updateSessionTimeout(String sessionId, long timeout) {
updateObjectTimeout(sessionId, timeout);
}
// --------------------- 会话管理 ---------------------
/**
* 搜索数据
* @param prefix 前缀
* @param keyword 关键字
* @param start 开始处索引
* @param size 获取数量 (-1代表从 start 处一直取到末尾)
* @param sortType 排序类型(true=正序,false=反序)
*
* @return 查询到的数据集合
*/
List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType);
// --------------------- 生命周期 ---------------------
/**
* 当此 SaTokenDao 实例被装载时触发
*/
default void init() {
}
/**
* 当此 SaTokenDao 实例被卸载时触发
*/
default void destroy() {
}
}
定时任务
定时释放部分内存资源
sa-token定时任务做得比较简陋,只是单开一个异步线程,间歇式清理而已
/**
* 初始化定时任务,定时清理过期数据
*/
public void initRefreshThread() {
// 如果开发者配置了 <=0 的值,则不启动定时清理
if(SaManager.getConfig().getDataRefreshPeriod() <= 0) {
return;
}
// 启动定时刷新
this.refreshFlag = true;
this.refreshThread = new Thread(() -> {
for (;;) {
try {
try {
// 如果已经被标记为结束
if( ! refreshFlag) {
return;
}
// 执行清理
refreshDataMap();
} catch (Exception e) {
e.printStackTrace();
}
// 休眠N秒
int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();
if(dataRefreshPeriod <= 0) {
dataRefreshPeriod = 1;
}
Thread.sleep(dataRefreshPeriod * 1000L);
} catch (Exception e) {
e.printStackTrace();
}
}
});
this.refreshThread.start();
}
会话登录
处理登录
/**
* 会话登录,并指定所有登录参数 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);
}
生成token
首次登录,会直接创建一个新token
非首次,会从缓存中或数据库中读取token
先创建token和session对象
/**
* 为指定账号 id 的登录操作,分配一个可用的 token
*
* @param id 账号id
* @param loginModel 此次登录的参数Model
* @return 返回 token
*/
protected String distUsableToken(Object id, SaLoginModel loginModel) {
// 1、获取全局配置的 isConcurrent 参数
// 如果配置为:不允许一个账号多地同时登录,则需要先将这个账号的历史登录会话标记为:被顶下线
Boolean isConcurrent = getConfigOrGlobal().getIsConcurrent();
if( ! isConcurrent) {
replaced(id, loginModel.getDevice());
}
// 2、如果调用者预定了要生成的 token,则直接返回这个预定的值,框架无需再操心了
if(SaFoxUtil.isNotEmpty(loginModel.getToken())) {
return loginModel.getToken();
}
// 3、只有在配置了 [ 允许一个账号多地同时登录 ] 时,才尝试复用旧 token,这样可以避免不必要地查询,节省开销
if(isConcurrent) {
// 3.1、看看全局配置的 IsShare 参数,配置为 true 才是允许复用旧 token
if(getConfigOfIsShare()) {
// 根据 账号id + 设备类型,尝试获取旧的 token
String tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
// 如果有值,那就直接复用
if(SaFoxUtil.isNotEmpty(tokenValue)) {
return tokenValue;
}
// 如果没值,那还是要继续往下走,尝试新建 token
// ↓↓↓
}
}
// 4、如果代码走到此处,说明未能成功复用旧 token,需要根据算法新建 token
return SaStrategy.instance.generateUniqueToken.execute(
"token",
getConfigOfMaxTryTimes(),
() -> {
return createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());
},
tokenValue -> {
return getLoginIdNotHandle(tokenValue) == null;
}
);
}
/**
* 生成唯一式 token 的算法
*/
public SaGenerateUniqueTokenFunction generateUniqueToken = (elementName, maxTryTimes, createTokenFunction, checkTokenFunction) -> {
// 为方便叙述,以下代码注释均假设在处理生成 token 的场景,但实际上本方法也可能被用于生成 code、ticket 等
// 循环生成
for (int i = 1; ; i++) {
// 生成 token
String token = createTokenFunction.get();
// 如果 maxTryTimes == -1,表示不做唯一性验证,直接返回
if (maxTryTimes == -1) {
return token;
}
// 如果 token 在DB库查询不到数据,说明是个可用的全新 token,直接返回
if (checkTokenFunction.apply(token)) {
return token;
}
// 如果已经循环了 maxTryTimes 次,仍然没有创建出可用的 token,那么抛出异常
if (i >= maxTryTimes) {
throw new SaTokenException(elementName + " 生成失败,已尝试" + i + "次,生成算法过于简单或资源池已耗尽");
}
}
};
创建account-session
一个账号对于一个saSession会话对象
// ------------------- Account-Session 相关 -------------------
/**
* 获取指定 key 的 SaSession, 如果该 SaSession 尚未创建,isCreate = 是否立即新建并返回
*
* @param sessionId SessionId
* @param isCreate 是否新建
* @param timeout 如果这个 SaSession 是新建的,则使用此值作为过期值(单位:秒),可填 null,代表使用全局 timeout 值
* @param appendOperation 如果这个 SaSession 是新建的,则要追加执行的动作,可填 null,代表无追加动作
* @return Session对象
*/
public SaSession getSessionBySessionId(String sessionId, boolean isCreate, Long timeout, Consumer<SaSession> appendOperation) {
// 如果提供的 sessionId 为 null,则直接返回 null
if(SaFoxUtil.isEmpty(sessionId)) {
throw new SaTokenException("SessionId 不能为空").setCode(SaErrorCode.CODE_11072);
}
// 先检查这个 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);
}
// 如果未提供 timeout,则根据相应规则设定默认的 timeout
if(timeout == null) {
// 如果是 Token-Session,则使用对用 token 的有效期,使 token 和 token-session 保持相同ttl,同步失效
if(SaTokenConsts.SESSION_TYPE__TOKEN.equals(session.getType())) {
timeout = getTokenTimeout(session.getToken());
if(timeout == SaTokenDao.NOT_VALUE_EXPIRE) {
timeout = getConfigOrGlobal().getTimeout();
}
} else {
// 否则使用全局配置的 timeout
timeout = getConfigOrGlobal().getTimeout();
}
}
// 将这个 SaSession 入库
getSaTokenDao().setSession(session, timeout);
}
return session;
}
新建saSession对象,和token一样,也是在saStrage中创建
/**
* 创建 Session 的策略
*/
public SaCreateSessionFunction createSession = (sessionId) -> {
return new SaSession(sessionId);
};
事件驱动模型
运用订阅发布模式
Sa-Token 事件中心 事件发布器
提供侦听器注册、事件发布能力
SaTokenEventCenter
所谓监听器listener,等效于观察者模式中的观察者列表
// --------- 注册侦听器
private static List<SaTokenListener> listenerList = new ArrayList<>();
监听器也是交由SpringIoC管理
/**
* 注册一组侦听器
* @param listenerList /
*/
public static void registerListenerList(List<SaTokenListener> listenerList) {
if(listenerList == null) {
throw new SaTokenException("注册的侦听器集合不可以为空").setCode(SaErrorCode.CODE_10031);
}
for (SaTokenListener listener : listenerList) {
if(listener == null) {
throw new SaTokenException("注册的侦听器不可以为空").setCode(SaErrorCode.CODE_10032);
}
}
SaTokenEventCenter.listenerList.addAll(listenerList);
}
每发布一个事件,就调用观察者的相应方法
/**
* 事件发布:创建了一个新的 SaSession
* @param id SessionId
*/
public static void doCreateSession(String id) {
for (SaTokenListener listener : listenerList) {
listener.doCreateSession(id);
}
}
缓存saSession到Map
public void setObject(String key, Object object, long timeout) {
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
dataMap.put(key, object);
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}
写入token
登录成功后获得一个token,将token输出
将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()) {
setTokenValueToCookie(tokenValue, loginModel.getCookieTimeout());
}
// 3. 将 token 写入到当前请求的响应头中
if(loginModel.getIsWriteHeaderOrGlobalConfig()) {
setTokenValueToResponseHeader(tokenValue);
}
}
将token写入cookie
/**
* 将 token 写入到当前会话的 Cookie 里
*
* @param tokenValue token 值
* @param cookieTimeout Cookie存活时间(单位:秒,填-1代表为内存Cookie,浏览器关闭后消失)
*/
public void setTokenValueToCookie(String tokenValue, int cookieTimeout){
SaCookieConfig cfg = getConfigOrGlobal().getCookie();
SaCookie cookie = new SaCookie()
.setName(getTokenName())
.setValue(tokenValue)
.setMaxAge(cookieTimeout)
.setDomain(cfg.getDomain())
.setPath(cfg.getPath())
.setSecure(cfg.getSecure())
.setHttpOnly(cfg.getHttpOnly())
.setSameSite(cfg.getSameSite())
;
SaHolder.getResponse().addCookie(cookie);
}
SaSession
Session Model,会话作用域的读取值对象,在一次会话范围内: 存值、取值。数据在注销登录后失效。
在 Sa-Token 中,SaSession 分为三种,分别是:
- Account-Session: 指的是框架为每个 账号id 分配的 SaSession。
- Token-Session: 指的是框架为每个 token 分配的 SaSession。
- Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 SaSession。
注意:以上分类仅为框架设计层面的概念区分,实际上它们的数据存储格式都是一致的。
Session对象用于一个账号登录到注销期间的数据共享,实际是用于多个同源请求之间共享数据
实际是将数据缓存到一个Map中
一个账号对应一个account-session对象,对应多个token-session对象
account-session实际意义是代表账号,token-session实际意义是代表登录同一账号的不同设备,token-sign与设备一一对应
token,token-session,token-sign都是和设备一一对应,一个账号可以拥有多个token
public class SaSession implements SaSetValueInterface, Serializable {
/**
* 此 SaSession 的 id
*/
private String id;
/**
* 此 SaSession 的 类型
*/
private String type;
/**
* 所属 loginType
*/
private String loginType;
/**
* 所属 loginId (当此 SaSession 属于 Account-Session 时,此值有效)
*/
private Object loginId;
/**
* 所属 Token (当此 SaSession 属于 Token-Session 时,此值有效)
*/
private String token;
/**
* 此 SaSession 的创建时间(13位时间戳)
*/
private long createTime;
/**
* 所有挂载数据
*/
private final Map<String, Object> dataMap = new ConcurrentHashMap<>();
// ----------------------- 存取值 (类型转换)
// ---- 重写接口方法
/**
* 取值
* @param key key
* @return 值
*/
@Override
public Object get(String key) {
return dataMap.get(key);
}
/**
* 写值
* @param key 名称
* @param value 值
* @return 对象自身
*/
@Override
public SaSession set(String key, Object value) {
dataMap.put(key, value);
update();
return this;
}
/**
* 写值 (只有在此 key 原本无值的情况下才会写入)
* @param key 名称
* @param value 值
* @return 对象自身
*/
@Override
public SaSession setByNull(String key, Object value) {
if( ! has(key)) {
dataMap.put(key, value);
update();
}
return this;
}
/**
* 删值
* @param key 要删除的key
* @return 对象自身
*/
@Override
public SaSession delete(String key) {
dataMap.remove(key);
update();
return this;
}
}
SaCookie
/**
* Cookie Model,代表一个 Cookie 应该具有的所有参数
*
* @author click33
* @since 1.16.0
*/
public class SaCookie {
/**
* 响应头中存储cookie的key值
*/
public static final String HEADER_NAME = "Set-Cookie";
/**
* 名称
*/
private String name;
/**
* 值
*/
private String value;
/**
* 有效时长 (单位:秒),-1 代表为临时Cookie 浏览器关闭后自动删除
*/
private int maxAge = -1;
/**
* 域
*/
private String domain;
/**
* 路径
*/
private String path;
/**
* 是否只在 https 协议下有效
*/
private Boolean secure = false;
/**
* 是否禁止 js 操作 Cookie
*/
private Boolean httpOnly = false;
/**
* 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制)
*/
private String sameSite;
}
SaTokenDao
dao层用于数据持久化
Sa-Token 持久层接口
此接口的不同实现类可将数据存储至不同位置,如:内存Map、Redis 等等。 如果你要自定义数据存储策略,也需通过实现此接口来完成
提供了一个默认实现类,用于将数据缓存到一个ConcurrentHashMap中
public interface SaTokenDao {
/** 常量,表示一个 key 永不过期 (在一个 key 被标注为永远不过期时返回此值) */
long NEVER_EXPIRE = -1;
/** 常量,表示系统中不存在这个缓存(在对不存在的 key 获取剩余存活时间时返回此值) */
long NOT_VALUE_EXPIRE = -2;
// --------------------- 字符串读写 ---------------------
/**
* 获取 value,如无返空
*
* @param key 键名称
* @return value
*/
String get(String key);
/**
* 写入 value,并设定存活时间(单位: 秒)
*
* @param key 键名称
* @param value 值
* @param timeout 数据有效期(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
*/
void set(String key, String value, long timeout);
/**
* 更新 value (过期时间不变)
* @param key 键名称
* @param value 值
*/
void update(String key, String value);
/**
* 删除 value
* @param key 键名称
*/
void delete(String key);
/**
* 获取 value 的剩余存活时间(单位: 秒)
* @param key 指定 key
* @return 这个 key 的剩余存活时间
*/
long getTimeout(String key);
/**
* 修改 value 的剩余存活时间(单位: 秒)
* @param key 指定 key
* @param timeout 过期时间(单位: 秒)
*/
void updateTimeout(String key, long timeout);
// --------------------- 对象读写 ---------------------
/**
* 获取 Object,如无返空
* @param key 键名称
* @return object
*/
Object getObject(String key);
/**
* 写入 Object,并设定存活时间 (单位: 秒)
* @param key 键名称
* @param object 值
* @param timeout 存活时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
*/
void setObject(String key, Object object, long timeout);
/**
* 更新 Object (过期时间不变)
* @param key 键名称
* @param object 值
*/
void updateObject(String key, Object object);
/**
* 删除 Object
* @param key 键名称
*/
void deleteObject(String key);
/**
* 获取 Object 的剩余存活时间 (单位: 秒)
* @param key 指定 key
* @return 这个 key 的剩余存活时间
*/
long getObjectTimeout(String key);
/**
* 修改 Object 的剩余存活时间(单位: 秒)
* @param key 指定 key
* @param timeout 剩余存活时间
*/
void updateObjectTimeout(String key, long timeout);
// --------------------- SaSession 读写 (默认复用 Object 读写方法) ---------------------
/**
* 获取 SaSession,如无返空
* @param sessionId sessionId
* @return SaSession
*/
default SaSession getSession(String sessionId) {
return (SaSession)getObject(sessionId);
}
/**
* 写入 SaSession,并设定存活时间(单位: 秒)
* @param session 要保存的 SaSession 对象
* @param timeout 过期时间(单位: 秒)
*/
default void setSession(SaSession session, long timeout) {
setObject(session.getId(), session, timeout);
}
/**
* 更新 SaSession
* @param session 要更新的 SaSession 对象
*/
default void updateSession(SaSession session) {
updateObject(session.getId(), session);
}
/**
* 删除 SaSession
* @param sessionId sessionId
*/
default void deleteSession(String sessionId) {
deleteObject(sessionId);
}
/**
* 获取 SaSession 剩余存活时间(单位: 秒)
* @param sessionId 指定 SaSession
* @return 这个 SaSession 的剩余存活时间
*/
default long getSessionTimeout(String sessionId) {
return getObjectTimeout(sessionId);
}
/**
* 修改 SaSession 剩余存活时间(单位: 秒)
* @param sessionId 指定 SaSession
* @param timeout 剩余存活时间
*/
default void updateSessionTimeout(String sessionId, long timeout) {
updateObjectTimeout(sessionId, timeout);
}
// --------------------- 会话管理 ---------------------
/**
* 搜索数据
* @param prefix 前缀
* @param keyword 关键字
* @param start 开始处索引
* @param size 获取数量 (-1代表从 start 处一直取到末尾)
* @param sortType 排序类型(true=正序,false=反序)
*
* @return 查询到的数据集合
*/
List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType);
// --------------------- 生命周期 ---------------------
/**
* 当此 SaTokenDao 实例被装载时触发
*/
default void init() {
}
/**
* 当此 SaTokenDao 实例被卸载时触发
*/
default void destroy() {
}
SaStrage
Sa-Token 策略对象
定义一些关键算法,例如生成token等
此类统一定义框架内的一些关键性逻辑算法,方便开发者进行按需重写,例:
// SaStrategy全局单例,所有方法都用以下形式重写
SaStrategy.instance.setCreateToken((loginId, loginType) -》 {
// 自定义Token生成的算法
return “xxxx”;
});
十分奇特的是,SaStrage维护了一些匿名内部类对象,他们的方法体用lambda表示
采用单例模式,通过单例去调用这些匿名类
public final class SaStrategy{
/**
* 获取 SaStrategy 对象的单例引用
*/
public static final SaStrategy instance = new SaStrategy();
// ----------------------- 所有策略
/**
* 创建 Token 的策略
*/
public SaCreateTokenFunction createToken = (loginId, loginType) -> {
// 根据配置的tokenStyle生成不同风格的token
String tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle();
switch (tokenStyle) {
// uuid
case SaTokenConsts.TOKEN_STYLE_UUID:
return UUID.randomUUID().toString();
// 简单uuid (不带下划线)
case SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID:
return UUID.randomUUID().toString().replaceAll("-", "");
// 32位随机字符串
case SaTokenConsts.TOKEN_STYLE_RANDOM_32:
return SaFoxUtil.getRandomString(32);
// 64位随机字符串
case SaTokenConsts.TOKEN_STYLE_RANDOM_64:
return SaFoxUtil.getRandomString(64);
// 128位随机字符串
case SaTokenConsts.TOKEN_STYLE_RANDOM_128:
return SaFoxUtil.getRandomString(128);
// tik风格 (2_14_16)
case SaTokenConsts.TOKEN_STYLE_TIK:
return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__";
// 默认,还是uuid
default:
SaManager.getLog().warn("配置的 tokenStyle 值无效:{},仅允许以下取值: " +
"uuid、simple-uuid、random-32、random-64、random-128、tik", tokenStyle);
return UUID.randomUUID().toString();
}
};
}
SaManager
管理 Sa-Token 所有全局组件,可通过此类快速获取、写入各种全局组件对象
public class SaManagr{
/**
* 全局配置对象
*/
public volatile static SaTokenConfig config;
/**
* 持久化组件
*/
private volatile static SaTokenDao saTokenDao;
权限数据源组件
private volatile static StpInterface stpInterface;
/**
* 一级上下文 SaTokenContextContext
*/
private volatile static SaTokenContext saTokenContext;
/**
* StpLogic 集合, 记录框架所有成功初始化的 StpLogic
*/
public static Map<String, StpLogic> stpLogicMap = new LinkedHashMap<>();
}
持有者
SaHolder持有者对象,实际是用于包裹请求,响应的对象
Sa-Token 上下文持有类,你可以通过此类快速获取当前环境下的 SaRequest、SaResponse、SaStorage、SaApplication 对象。
底层是通过SaManager从context取数据
/**
* Sa-Token 上下文持有类,你可以通过此类快速获取当前环境下的 SaRequest、SaResponse、SaStorage、SaApplication 对象。
*
* @author click33
* @since 1.18.0
*/
public class SaHolder {
/**
* 获取当前请求的 SaTokenContext 上下文对象
* @see SaTokenContext
*
* @return /
*/
public static SaTokenContext getContext() {
return SaManager.getSaTokenContextOrSecond();
}
/**
* 获取当前请求的 Request 包装对象
* @see SaRequest
*
* @return /
*/
public static SaRequest getRequest() {
return SaManager.getSaTokenContextOrSecond().getRequest();
}
/**
* 获取当前请求的 Response 包装对象
* @see SaResponse
*
* @return /
*/
public static SaResponse getResponse() {
return SaManager.getSaTokenContextOrSecond().getResponse();
}
/**
* 获取当前请求的 Storage 包装对象
* @see SaStorage
*
* @return /
*/
public static SaStorage getStorage() {
return SaManager.getSaTokenContextOrSecond().getStorage();
}
/**
* 获取全局 SaApplication 对象
* @see SaApplication
*
* @return /
*/
public static SaApplication getApplication() {
return SaApplication.defaultInstance;
}
}
SaRouter
路由匹配操作工具类
提供了一系列的路由匹配操作方法,一般用在全局拦截器、过滤器做路由拦截鉴权。
简单示例:
// 指定一条 match 规则
SaRouter
.match("/**") // 拦截的 path 列表,可以写多个
.notMatch("/user/doLogin") // 排除掉的 path 列表,可以写多个
.check(r->StpUtil.checkLogin()); // 要执行的校验动作,可以写完整
/**
* 路由匹配
* @param patterns 路由匹配符集合
* @return 对象自身
*/
public static SaRouterStaff match(List<String> patterns) {
return new SaRouterStaff().match(patterns);
}
/**
* 执行校验函数 (无参)
* @param fun 要执行的函数
* @return 对象自身
*/
public SaRouterStaff check(SaFunction fun) {
if(isHit) {
fun.run();
}
return this;
}
拼接响应头cookie
拼接成最终响应头Set-Cookie的内容
/**
* 转换为响应头 Set-Cookie 参数需要的值
* @return /
*/
public String toHeaderValue() {
this.builder();
if(SaFoxUtil.isEmpty(name)) {
throw new SaTokenException("name不能为空").setCode(SaErrorCode.CODE_12002);
}
if(value != null && value.contains(";")) {
throw new SaTokenException("无效Value:" + value).setCode(SaErrorCode.CODE_12003);
}
// Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax
StringBuilder sb = new StringBuilder();
sb.append(name).append("=").append(value);
if(maxAge >= 0) {
sb.append("; Max-Age=").append(maxAge);
String expires;
if(maxAge == 0) {
expires = Instant.EPOCH.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME);
} else {
expires = OffsetDateTime.now().plusSeconds(maxAge).format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
sb.append("; Expires=").append(expires);
}
if(!SaFoxUtil.isEmpty(domain)) {
sb.append("; Domain=").append(domain);
}
if(!SaFoxUtil.isEmpty(path)) {
sb.append("; Path=").append(path);
}
if(secure) {
sb.append("; Secure");
}
if(httpOnly) {
sb.append("; HttpOnly");
}
if(!SaFoxUtil.isEmpty(sameSite)) {
sb.append("; SameSite=").append(sameSite);
}
return sb.toString();
}
Set-Cookie:
satoken=035e17cd-40be-49dc-9a44-a0338479d6d4;
Max-Age=2592000;
Expires=Thu, 13 Jun 2024 19:58:54 +0800;
Path=/
cookie具有自动携带的特点,在之后的请求,会自动添加进请求头中
验证登录
通过token从缓存中找到对应的账号id,同时也会校验token和账号id的有效性,所有数据均来源于缓存
所以可以说sa-token是基于缓存来校验用户身份的
/**
* 获取当前会话账号id, 如果未登录,则返回null
*
* @return 账号id
*/
public Object getLoginIdDefaultNull() {
// 1、先判断一下当前会话是否正在 [ 临时身份切换 ], 如果是则返回临时身份
if(isSwitch()) {
return getSwitchLoginId();
}
// 2、如果前端连 token 都没有提交,则直接返回 null
String tokenValue = getTokenValue();
if(tokenValue == null) {
return null;
}
// 3、根据 token 找到对应的 loginId,如果 loginId 为 null 或者属于异常标记里面,均视为未登录, 统一返回 null
Object loginId = getLoginIdNotHandle(tokenValue);
if( ! isValidLoginId(loginId) ) {
return null;
}
// 4、如果 token 已被冻结,也返回 null
if(getTokenActiveTimeoutByToken(tokenValue) == SaTokenDao.NOT_VALUE_EXPIRE) {
return null;
}
// 5、执行到此,证明此 loginId 已经是个正常合法的账号id了,可以返回
return loginId;
}
从请求中获取token
/**
* 获取当前请求的 token 值 (不裁剪前缀)
*
* @return /
*/
public String getTokenValueNotCut(){
// 获取相应对象
SaStorage storage = SaHolder.getStorage();
SaRequest request = SaHolder.getRequest();
SaTokenConfig config = getConfigOrGlobal();
String keyTokenName = getTokenName();
String tokenValue = null;
// 1. 先尝试从 Storage 存储器里读取
if(storage.get(splicingKeyJustCreatedSave()) != null) {
tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave()));
}
// 2. 再尝试从 请求体 里面读取
if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadBody()){
tokenValue = request.getParam(keyTokenName);
}
// 3. 再尝试从 header 头里读取
if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadHeader()){
tokenValue = request.getHeader(keyTokenName);
}
// 4. 最后尝试从 cookie 里读取
if(SaFoxUtil.isEmpty(tokenValue) && config.getIsReadCookie()){
tokenValue = request.getCookieValue(keyTokenName);
}
// 5. 至此,不管有没有读取到,都不再尝试了,直接返回
return tokenValue;
}
从这里可以看出来sa-token是依据token来对用户登陆状态进行判定的,sa-token会storage,请求体,请求头中或者是cookie中获取token值
其中storage存的其实就是request请求对象
退出登录
清除cookie,token,以及相关的缓存信息
/**
* 在当前客户端会话注销
*/
public void logout() {
// 1、如果本次请求连 Token 都没有提交,那么它本身也不属于登录状态,此时无需执行任何操作
String tokenValue = getTokenValue();
if(SaFoxUtil.isEmpty(tokenValue)) {
return;
}
// 2、如果打开了 Cookie 模式,则先把 Cookie 数据清除掉
if(getConfigOrGlobal().getIsReadCookie()){
SaCookieConfig cfg = getConfigOrGlobal().getCookie();
SaCookie cookie = new SaCookie()
.setName(getTokenName())
.setValue(null)
// 有效期指定为0,做到以增代删
.setMaxAge(0)
.setDomain(cfg.getDomain())
.setPath(cfg.getPath())
.setSecure(cfg.getSecure())
.setHttpOnly(cfg.getHttpOnly())
.setSameSite(cfg.getSameSite())
;
SaHolder.getResponse().addCookie(cookie);
}
// 3、然后从当前 Storage 存储器里删除 Token
SaStorage storage = SaHolder.getStorage();
storage.delete(splicingKeyJustCreatedSave());
// 4、清除当前上下文的 [ 活跃度校验 check 标记 ]
storage.delete(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY);
// 5、清除这个 token 的其它相关信息
logoutByTokenValue(tokenValue);
}
storage底层就是维护了一个request对象,所有的操作实际都是在操作request域
/**
* 对 SaStorage 包装类的实现(Jakarta-Servlet 版)
*
* @author click33
* @since 1.34.0
*/
public class SaStorageForServlet implements SaStorage {
/**
* 底层Request对象
*/
protected HttpServletRequest request;
/**
* 实例化
* @param request request对象
*/
public SaStorageForServlet(HttpServletRequest request) {
this.request = request;
}
/**
* 获取底层源对象
*/
@Override
public Object getSource() {
return request;
}
/**
* 在 [Request作用域] 里写入一个值
*/
@Override
public SaStorageForServlet set(String key, Object value) {
request.setAttribute(key, value);
return this;
}
/**
* 在 [Request作用域] 里获取一个值
*/
@Override
public Object get(String key) {
return request.getAttribute(key);
}
/**
* 在 [Request作用域] 里删除一个值
*/
@Override
public SaStorageForServlet delete(String key) {
request.removeAttribute(key);
return this;
}
}
会话清除
/**
* 会话注销,根据指定 Token
*
* @param tokenValue 指定 token
*/
public void logoutByTokenValue(String tokenValue) {
// 1、清除这个 token 的最后活跃时间记录
if(isOpenCheckActiveTimeout()) {
clearLastActive(tokenValue);
}
// 2、清除这个 token 的 Token-Session 对象
deleteTokenSession(tokenValue);
// 3、清除 token -> id 的映射关系
String loginId = getLoginIdNotHandle(tokenValue);
if(loginId != null) {
deleteTokenToIdMapping(tokenValue);
}
// 4、判断一下:如果此 token 映射的是一个无效 loginId,则此处立即返回,不需要再往下处理了
if( ! isValidLoginId(loginId) ) {
return;
}
// 5、$$ 发布事件:某某账号的某某 token 注销下线了
SaTokenEventCenter.doLogout(loginType, loginId, tokenValue);
// 6、清理这个账号的 Account-Session 上的 token 签名,并且尝试注销掉 Account-Session
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
session.removeTokenSign(tokenValue);
session.logoutByTokenSignCountToZero();
}
}
权限认证
StpInterface
权限数据加载源接口
在使用权限校验 API 之前,你必须实现此接口,告诉框架哪些用户拥有哪些权限。 框架默认不对数据进行缓存,如果你的数据是从数据库中读取的,一般情况下你需要手动实现数据的缓存读写。
public interface StpInterface {
/**
* 返回指定账号id所拥有的权限码集合
*
* @param loginId 账号id
* @param loginType 账号类型
* @return 该账号id具有的权限码集合
*/
List<String> getPermissionList(Object loginId, String loginType);
/**
* 返回指定账号id所拥有的角色标识集合
*
* @param loginId 账号id
* @param loginType 账号类型
* @return 该账号id具有的角色标识集合
*/
List<String> getRoleList(Object loginId, String loginType);
}
权限校验
位于StpUtil中
// ------------------- 权限认证操作 -------------------
/**
* 获取:当前账号的权限码集合
*
* @return /
*/
public static List<String> getPermissionList() {
return stpLogic.getPermissionList();
}
/**
* 获取:指定账号的权限码集合
*
* @param loginId 指定账号id
* @return /
*/
public static List<String> getPermissionList(Object loginId) {
return stpLogic.getPermissionList(loginId);
}
/**
* 判断:当前账号是否含有指定权限, 返回 true 或 false
*
* @param permission 权限码
* @return 是否含有指定权限
*/
public static boolean hasPermission(String permission) {
return stpLogic.hasPermission(permission);
}
/**
* 判断:指定账号 id 是否含有指定权限, 返回 true 或 false
*
* @param loginId 账号 id
* @param permission 权限码
* @return 是否含有指定权限
*/
public static boolean hasPermission(Object loginId, String permission) {
return stpLogic.hasPermission(loginId, permission);
}
/**
* 判断:当前账号是否含有指定权限 [ 指定多个,必须全部具有 ]
*
* @param permissionArray 权限码数组
* @return true 或 false
*/
public static boolean hasPermissionAnd(String... permissionArray){
return stpLogic.hasPermissionAnd(permissionArray);
}
/**
* 判断:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ]
*
* @param permissionArray 权限码数组
* @return true 或 false
*/
public static boolean hasPermissionOr(String... permissionArray){
return stpLogic.hasPermissionOr(permissionArray);
}
/**
* 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
*
* @param permission 权限码
*/
public static void checkPermission(String permission) {
stpLogic.checkPermission(permission);
}
/**
* 校验:当前账号是否含有指定权限 [ 指定多个,必须全部验证通过 ]
*
* @param permissionArray 权限码数组
*/
public static void checkPermissionAnd(String... permissionArray) {
stpLogic.checkPermissionAnd(permissionArray);
}
/**
* 校验:当前账号是否含有指定权限 [ 指定多个,只要其一验证通过即可 ]
*
* @param permissionArray 权限码数组
*/
public static void checkPermissionOr(String... permissionArray) {
stpLogic.checkPermissionOr(permissionArray);
}
角色校验
// ------------------- 角色认证操作 -------------------
/**
* 获取:当前账号的角色集合
*
* @return /
*/
public static List<String> getRoleList() {
return stpLogic.getRoleList();
}
/**
* 获取:指定账号的角色集合
*
* @param loginId 指定账号id
* @return /
*/
public static List<String> getRoleList(Object loginId) {
return stpLogic.getRoleList(loginId);
}
/**
* 判断:当前账号是否拥有指定角色, 返回 true 或 false
*
* @param role 角色
* @return /
*/
public static boolean hasRole(String role) {
return stpLogic.hasRole(role);
}
/**
* 判断:指定账号是否含有指定角色标识, 返回 true 或 false
*
* @param loginId 账号id
* @param role 角色标识
* @return 是否含有指定角色标识
*/
public static boolean hasRole(Object loginId, String role) {
return stpLogic.hasRole(loginId, role);
}
/**
* 判断:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ]
*
* @param roleArray 角色标识数组
* @return true或false
*/
public static boolean hasRoleAnd(String... roleArray){
return stpLogic.hasRoleAnd(roleArray);
}
/**
* 判断:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ]
*
* @param roleArray 角色标识数组
* @return true或false
*/
public static boolean hasRoleOr(String... roleArray){
return stpLogic.hasRoleOr(roleArray);
}
/**
* 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
*
* @param role 角色标识
*/
public static void checkRole(String role) {
stpLogic.checkRole(role);
}
/**
* 校验:当前账号是否含有指定角色标识 [ 指定多个,必须全部验证通过 ]
*
* @param roleArray 角色标识数组
*/
public static void checkRoleAnd(String... roleArray){
stpLogic.checkRoleAnd(roleArray);
}
/**
* 校验:当前账号是否含有指定角色标识 [ 指定多个,只要其一验证通过即可 ]
*
* @param roleArray 角色标识数组
*/
public static void checkRoleOr(String... roleArray){
stpLogic.checkRoleOr(roleArray);
}
一个账号对应多个角色,一个角色对应多个权限
角色鉴权粒度大于权限
权限校验的关键逻辑位于SaStrage
/**
* 判断:集合中是否包含指定元素(模糊匹配)
*/
public SaHasElementFunction hasElement = (list, element) -> {
// 空集合直接返回false
if(list == null || list.size() == 0) {
return false;
}
// 先尝试一下简单匹配,如果可以匹配成功则无需继续模糊匹配
if (list.contains(element)) {
return true;
}
// 开始模糊匹配
for (String patt : list) {
if(SaFoxUtil.vagueMatch(patt, element)) {
return true;
}
}
// 走出for循环说明没有一个元素可以匹配成功
return false;
};
模糊匹配指的是*号等符号的匹配
/**
* 字符串模糊匹配
* <p>example:
* <p> user* user-add -- true
* <p> user* art-add -- false
* @param patt 表达式
* @param str 待匹配的字符串
* @return 是否可以匹配
*/
public static boolean vagueMatch(String patt, String str) {
// 两者均为 null 时,直接返回 true
if(patt == null && str == null) {
return true;
}
// 两者其一为 null 时,直接返回 false
if(patt == null || str == null) {
return false;
}
// 如果表达式不带有*号,则只需简单equals即可 (这样可以使速度提升200倍左右)
if( ! patt.contains("*")) {
return patt.equals(str);
}
// 深入匹配
return vagueMatchMethod(patt, str);
}
对于*号的匹配则更加复杂
/**
* 字符串模糊匹配
*
* @param pattern /
* @param str /
* @return /
*/
private static boolean vagueMatchMethod( String pattern, String str) {
int m = str.length();
int n = pattern.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 1; i <= n; ++i) {
if (pattern.charAt(i - 1) == '*') {
dp[0][i] = true;
} else {
break;
}
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (pattern.charAt(j - 1) == '*') {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
} else if (str.charAt(i - 1) == pattern.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[m][n];
}
注解鉴权
注解鉴权 —— 优雅的将鉴权与业务代码分离!
- @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
- @SaCheckRole(“admin”): 角色校验 —— 必须具有指定角色标识才能进入该方法。
- @SaCheckPermission(“user:add”): 权限校验 —— 必须具有指定权限才能进入该方法。
- @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
- @SaCheckHttpBasic: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。
- @SaCheckHttpDigest: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。
- @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
- @SaCheckDisable(“comment”):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
- Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
踢人下线
/**
* 踢人下线,根据账号id 和 设备类型
* <p> 当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5 </p>
*
* @param loginId 账号id
* @param device 设备类型 (填 null 代表踢出该账号的所有设备类型)
*/
public void kickout(Object loginId, String device) {
// 1、获取此账号的 Account-Session,上面记录了此账号的所有登录客户端数据
SaSession session = getSessionByLoginId(loginId, false);
if(session != null) {
// 2、遍历此账号所有从这个 device 设备上登录的客户端,清除相关数据
for (TokenSign tokenSign: session.getTokenSignListByDevice(device)) {
// 2.1、获取此客户端的 token 值
String tokenValue = tokenSign.getValue();
// 2.2、从 Account-Session 上清除 token 签名
session.removeTokenSign(tokenValue);
// 2.3、清除这个 token 的最后活跃时间记录
if(isOpenCheckActiveTimeout()) {
clearLastActive(tokenValue);
}
// 2.4、将此 token 标记为:已被踢下线
updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);
// 2.5、此处不需要清除它的 Token-Session 对象
// deleteTokenSession(tokenValue);
// 2.6、$$ 发布事件:xx 账号的 xx 客户端被踢下线了
SaTokenEventCenter.doKickout(loginType, loginId, tokenValue);
}
// 3、如果代码走到这里的时候,此账号已经没有客户端在登录了,则直接注销掉这个 Account-Session
session.logoutByTokenSignCountToZero();
}
}