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";
    }

    @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();
		}
	}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

elementui,iview等 表格单元格合并之固定列

要的效果如下 需要合并 show weak 及 Siginin这三列 上代码 <template><Table:columns"columns":span-method"handleSpan":data"data"bordersize"small"ref"table"></Table> </template> <sc…

Linux备份---异地

参考文档&#xff1a;Linux环境实现mysql所在服务器定时同步数据文件到备份服务器&#xff08;异地容灾备份场景&#xff09;_mysql异地备份-CSDN博客 通过SSH进行连接&#xff1a; 应用服务器&#xff1a; 通过ssh-keygen -t rsay建立ssh通信的密钥 密钥建立后&#xff0c;…

JavaScript-输入输出语句

输出语句 document.write( 输出的内容 ) 语法&#xff1a;document.write( 输出的内容) 作用&#xff1a;内容会显示在网页上 如果输出的内容是标签&#xff0c;也会被解析为网页元素 代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head>&…

cubemx配置stm32f407VET6实现can通信

背景&#xff1a; 项目上需要把原先的TMC5160电机驱动器替换为购买的电机控制模块&#xff08;该模块采用canopen通信&#xff09; 移植canopen的前提是can通信正常&#xff0c;现在添加一下can通信&#xff08;先用标准帧&#xff0c;250K bit/S的波特率测试&#xff09; 原理…

【回溯】1255. 得分最高的单词集合

本文涉及知识点 回溯 力扣难道&#xff1a;1881 LeetCode1255. 得分最高的单词集合 你将会得到一份单词表 words&#xff0c;一个字母表 letters &#xff08;可能会有重复字母&#xff09;&#xff0c;以及每个字母对应的得分情况表 score。 请你帮忙计算玩家在单词拼写游戏…

系统管理(System Keeping):Codigger资源与配置管理(上)

系统管理&#xff08;System Keeping&#xff09;&#xff0c;作为Codigger不可或缺的一部分&#xff0c;为开发者提供全面而高效的资源与配置管理体验。下面&#xff0c;让我们从它的其中三方面来一探究竟其强大的功能如何助力开发者提升工作效率。 一、环境配置&#xff1a;全…

Linux交叉编译

一. 交叉编译 1.使用环境要求 新版本的orangepi-build是在Ubuntu22.04的x64电脑或虚拟机上运行的 lsb_release -a //查看自己的虚拟机版本 因为编译出的SDK大概有16G大小&#xff0c;因此&#xff0c;至少给虚拟机分配50G的大小。 2.获取Linux SDK 方法一&#xff1a;从…

React框架-Next 学习-1

创建一个 Next.js 应用,node版本要高&#xff0c;16.5以上 npm淘宝镜像切为https://registry.npmmirror.com npm config set registry https://registry.npmmirror.com npx create-next-applatest//安装后 使用npm run dev 启动 Next.js 是围绕着 页面&#xff08;pages&am…

智慧园区EasyCVR视频智能管理方案:构建高效安全园区新视界

一、背景分析 园区作为城市的基本单元&#xff0c;是最重要的人口和产业聚集区。根据行业市场调研&#xff0c;90%以上城市居民工作与生活在园区进行&#xff0c;80%以上的GDP和90%以上的创新在园区内产生&#xff0c;可以说“城市&#xff0c;除了马路都是园区”。 园区形态…

高通QCS6490开发(二)AI板卡接口

QCS6490是高通公司针对高端物联网终端而优化的SoC&#xff0c;在性能和功耗上有最优的平衡。《高通QCS6490 AIoT应用开发》是一系列AIoT应用开发文章&#xff0c;介绍如何基于QCS6490平台做AIIoT的应用开发。 本文主要介绍FV01开发板的内部和外部接口。 内部的板载接口如下 接口…

怎么做私域?先来了解私域运营模式!

现在&#xff0c;很多企业都在做私域&#xff0c;但仍旧有很多人会问&#xff1a;我的私域到底要怎么做&#xff1f; 关于这个问题&#xff0c;不同产品无论在消费频次与客单价上&#xff0c;还是在决策链路的长度和复杂度上&#xff0c;都有巨大的差异&#xff0c;消费者需要…

GPT-4o模型介绍和使用方法

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

java项目之企业资产管理系统(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的企业资产管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 管理员功能有个人中心&…

程序员兼职引起的纠纷?

最近跟朋友聊天&#xff0c;说遇到一些因兼职工作而引发的争议&#xff0c;因为我本人也曾涉足过兼职领域&#xff0c;因此对程序员兼职时可能遇到的各种情况和应遵循的“套路”准则还有有一些发言权的&#xff0c;所以想和大家聊聊如何安全“兼职”的1/2事项~ ✅顺便内推个机会…

前端面试题(二十三)(答案版)

面试形式&#xff1a;线上电话面试&#xff1a;一面&#xff1a;时长30分钟 面试评价&#xff1a;精准考察项目所需技术理论工作实践 面试官的提问大纲&#xff1a;本公司项目要求本人简历 工作经验&#xff1a;2-4年 公司名称&#xff1a;深圳XX&#xff08;想知道的就滴喔…

SHELL编程(一)

目录 一、 Linux操作系统&#xff08;一&#xff09;内核与操作系统&#xff08;二&#xff09;操作系统的功能 二、Linux高级命令&#xff08;一&#xff09; 离线安装 dpkg1. 安装2. 使用3. 查看安装详细信息4. 安装路径5. 不完全删除6. 完全删除 &#xff08;二&#xff09;…

(内地家长)为什么不建议做香港优才计划?香港身份的孩子不是全都能低分上名校!

&#xff08;内地家长&#xff09;为什么不建议做香港优才计划&#xff1f;香港身份的孩子不能都低分上名校&#xff01; 大部分申请香港优才的朋友&#xff0c;应该是冲着孩子教育、高考升学来的。 确实&#xff0c;香港优才申请后拿到的香港身份&#xff0c;对于孩子读书教…

HT3S-ECS-MDN网关引领智能称重新篇章欧姆龙EtherCAT PLC的集成应用案例

在现代化工业生产中&#xff0c;精确的数据采集和高效的通信系统是确保生产流程顺利运行的关键。特别是在称重环节&#xff0c;数据的准确性和实时性对于生产质量和成本控制至关重要。今天&#xff0c;我们将为您介绍一个成功的案例&#xff0c;展示HT3S-ECS-MDN网关如何连接称…

git常用命令及其ignore文件

1.git本地操作命令 # 查看git的版本 git --version # 生成空的本地仓库 git init # 将文件添加到暂存区 git add 文件 # 将暂存区里的文件提交到本地仓库 git commit -m "描述"2.git远程仓库命令 # 添加远程仓库 git remote add origin http://192.168.1.130:9000/…

MySQL 8.0 全新特性详解

MySQL 8.0带来了许多令人兴奋的新特性和优化功能&#xff0c;下面我将逐一详细介绍每个特性&#xff1a; 一、原生数据字典 MySQL 8.0 引入了原生数据字典&#xff0c;取代了之前使用的.frm、.par、.opt等文件来存储元数据。这一改进使得元数据的访问和管理更加高效和直接。原…