sa-token权限认证框架,最简洁,最实用讲解

2024-05-15 17:12

本文主要是介绍sa-token权限认证框架,最简洁,最实用讲解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述
在这里插入图片描述
查看源码,可知,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";}@GetMappingpublic 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版)*/@Beanpublic 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 /*/@Beanpublic 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) 的方式来全局获取到这个 StpLogicSaManager.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()*/@Overridepublic 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初始化LoginModelSaTokenConfig config = getConfigOrGlobal();loginModel.build(config);// 3、创建或者从缓存中取出一个token字符串String tokenValue = distUsableToken(id, loginModel);// 4、获取此账号的 Account-Session , 更新timeoutSaSession session = getSessionByLoginId(id, true, loginModel.getTimeoutOrGlobalConfig());session.updateMinTimeout(loginModel.getTimeout());// 5、更新saSession的token-signTokenSign 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、一切处理完毕,返回会话凭证 tokenreturn tokenValue;}

创建token,在StpLogic类中有一个方法可以生成不同格式的token,所谓的token,在sa-token中指的是一个随机字符串,由特定算法生成

	/*** 创建 Token 的策略*/public SaCreateTokenFunction createToken = (loginId, loginType) -> {// 根据配置的tokenStyle生成不同风格的tokenString tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle();switch (tokenStyle) {// uuidcase 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) + "__";// 默认,还是uuiddefault: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 才是允许复用旧 tokenif(getConfigOfIsShare()) {// 根据 账号id + 设备类型,尝试获取旧的 tokenString tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());// 如果有值,那就直接复用if(SaFoxUtil.isNotEmpty(tokenValue)) {return tokenValue;}// 如果没值,那还是要继续往下走,尝试新建 token// ↓↓↓}}// 4、如果代码走到此处,说明未能成功复用旧 token,需要根据算法新建 tokenreturn 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++) {// 生成 tokenString 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,则直接返回 nullif(SaFoxUtil.isEmpty(sessionId)) {throw new SaTokenException("SessionId 不能为空").setCode(SaErrorCode.CODE_11072);}// 先检查这个 SaSession 是否已经存在,如果不存在且 isCreate=true,则新建并返回SaSession session = getSaTokenDao().getSession(sessionId);if(session == null && isCreate) {// 创建这个 SaSessionsession = SaStrategy.instance.createSession.apply(sessionId);// 追加操作if(appendOperation != null) {appendOperation.accept(session);}// 如果未提供 timeout,则根据相应规则设定默认的 timeoutif(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 {// 否则使用全局配置的 timeouttimeout = 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 值 */@Overridepublic Object get(String key) {return dataMap.get(key);}/*** 写值 * @param key   名称* @param value 值* @return 对象自身*/@Overridepublic SaSession set(String key, Object value) {dataMap.put(key, value);update();return this;}/*** 写值 (只有在此 key 原本无值的情况下才会写入)* @param key   名称* @param value 值* @return 对象自身*/@Overridepublic SaSession setByNull(String key, Object value) {if( ! has(key)) {dataMap.put(key, value);update();}return this;}/*** 删值* @param key 要删除的key* @return 对象自身*/@Overridepublic 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生成不同风格的tokenString tokenStyle = SaManager.getStpLogic(loginType).getConfigOrGlobal().getTokenStyle();switch (tokenStyle) {// uuidcase 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) + "__";// 默认,还是uuiddefault: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=LaxStringBuilder 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 都没有提交,则直接返回 nullString tokenValue = getTokenValue();if(tokenValue == null) {return null;}// 3、根据 token 找到对应的 loginId,如果 loginId 为 null 或者属于异常标记里面,均视为未登录, 统一返回 nullObject loginId = getLoginIdNotHandle(tokenValue);if( ! isValidLoginId(loginId) ) {return null;}// 4、如果 token 已被冻结,也返回 nullif(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 存储器里删除 TokenSaStorage 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;}/*** 获取底层源对象 */@Overridepublic Object getSource() {return request;}/*** 在 [Request作用域] 里写入一个值 */@Overridepublic SaStorageForServlet set(String key, Object value) {request.setAttribute(key, value);return this;}/*** 在 [Request作用域] 里获取一个值 */@Overridepublic Object get(String key) {return request.getAttribute(key);}/*** 在 [Request作用域] 里删除一个值 */@Overridepublic 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-SessionSaSession 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) -> {// 空集合直接返回falseif(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 时,直接返回 trueif(patt == null && str == null) {return true;}// 两者其一为 null 时,直接返回 falseif(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-Sessionsession.logoutByTokenSignCountToZero();}}

这篇关于sa-token权限认证框架,最简洁,最实用讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/992421

相关文章

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

浅析如何使用Swagger生成带权限控制的API文档

《浅析如何使用Swagger生成带权限控制的API文档》当涉及到权限控制时,如何生成既安全又详细的API文档就成了一个关键问题,所以这篇文章小编就来和大家好好聊聊如何用Swagger来生成带有... 目录准备工作配置 Swagger权限控制给 API 加上权限注解查看文档注意事项在咱们的开发工作里,API

修改若依框架Token的过期时间问题

《修改若依框架Token的过期时间问题》本文介绍了如何修改若依框架中Token的过期时间,通过修改`application.yml`文件中的配置来实现,默认单位为分钟,希望此经验对大家有所帮助,也欢迎... 目录修改若依框架Token的过期时间修改Token的过期时间关闭Token的过期时js间总结修改若依

java如何通过Kerberos认证方式连接hive

《java如何通过Kerberos认证方式连接hive》该文主要介绍了如何在数据源管理功能中适配不同数据源(如MySQL、PostgreSQL和Hive),特别是如何在SpringBoot3框架下通过... 目录Java实现Kerberos认证主要方法依赖示例续期连接hive遇到的问题分析解决方式扩展思考总

Redis的Zset类型及相关命令详细讲解

《Redis的Zset类型及相关命令详细讲解》:本文主要介绍Redis的Zset类型及相关命令的相关资料,有序集合Zset是一种Redis数据结构,它类似于集合Set,但每个元素都有一个关联的分数... 目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZ

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

Java后端接口中提取请求头中的Cookie和Token的方法

《Java后端接口中提取请求头中的Cookie和Token的方法》在现代Web开发中,HTTP请求头(Header)是客户端与服务器之间传递信息的重要方式之一,本文将详细介绍如何在Java后端(以Sp... 目录引言1. 背景1.1 什么是 HTTP 请求头?1.2 为什么需要提取请求头?2. 使用 Spr