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

news2024/9/23 3:32:11

在这里插入图片描述
在这里插入图片描述
查看源码,可知,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();
		}
	}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何使用AspectJ做切面,打印jar包中方法的执行日记

最近在工作中遇到一个redis缓存中的hash key莫名其妙被删除的问题&#xff0c;我们用了J2Cache&#xff0c;二级缓存用的是redis。hash key莫名其妙被删除又没有日志&#xff0c;就想到做一个切面在调用redis删除hash key的方法的时候&#xff0c;打印日志&#xff0c;并且把调…

揭秘SmartEDA魅力:为何众多学校青睐这款电路仿真软件?

在当今数字化、信息化的教育时代&#xff0c;电子电路仿真软件已成为电子学教学不可或缺的重要工具。其中&#xff0c;SmartEDA电路仿真软件以其强大的功能、用户友好的界面以及丰富的教育资源&#xff0c;赢得了众多学校的青睐。那么&#xff0c;究竟是什么原因让SmartEDA成为…

解决Win11下SVN状态图标显示不出来

我们正常SVN在Windows资源管理器都是有显示状态图标的&#xff0c; 如果不显示状态图标&#xff0c;可能你的注册表的配置被顶下去了&#xff0c;我们查看一下注册表 运行CMD > regedit 打开注册表编辑器 然后打开这个路径&#xff1a;计算机\HKEY_LOCAL_MACHINE\SOFTWARE…

【LeetCode刷题】27. 移除元素

1. 题目链接2. 题目描述3. 解题方法4. 代码 1. 题目链接 27. 移除元素 2. 题目描述 3. 解题方法 暴力法直接解决&#xff0c;用双层for循环&#xff0c;外层for循环找val&#xff0c;内层for循环做删除操作。双指针法&#xff0c;fast和slow。fast找不是val的值&#xff0c;…

在Ubuntu上的QT创建工程并打包项目

一、环境准备 参考UbuntuQT安装 二、创建项目&#xff0c;点击choose 设置项目名字路径等&#xff0c;点击下一步 默认&#xff0c;点击下一步 设置函数名字&#xff0c;保持默认&#xff0c;下一步 保持默认&#xff0c;点击下一步 继续&#xff0c;下一步 点击完成 三…

22 优化日志文件统计程序-按月份统计每个用户每天的访问次数

读取任务一中序列文件&#xff0c;统计每个用户每天的访问次数&#xff0c;最终将2021/1和2021/2的数据分别输出在两个文件中。 一、创建项目步骤&#xff1a; 1.创建项目 2.修改pom.xml文件 <packaging>jar</packaging> <dependencies><dependency>…

听劝!普通人千万别随意入门网络安全

一、什么是网络安全 网络安全是一种综合性的概念&#xff0c;涵盖了保护计算机系统、网络基础设施和数据免受未经授权的访问、攻击、损害或盗窃的一系列措施和技术。经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”…

Linux-页(page)和页表

本文在页表方面参考了这篇博客&#xff0c;特别鸣谢&#xff01; 【Linux】页表的深入分析 1. 页帧和页框 页帧&#xff08;page frame&#xff09;是内存的最小可分配单元&#xff0c;也开始称作页框&#xff0c;Linux下页帧的大小为4KB。 内核需要将他们用于所有的内存需求&a…

【Git教程】(十九)合并小型项目 — 概述及使用要求,执行过程及其实现,替代解决方案 ~

Git教程 合并小型项目 1️⃣ 概述2️⃣ 使用要求3️⃣ 执行过程及其实现 在项目的初始阶段&#xff0c;往往需要针对重要的设计决策和技术实现原型实验。当原型评估结束后&#xff0c;需要将那些成功的原型合并起来称为整个项目的初始版本。 在这样的情景中&#xff0c;各个原…

什么是ARP攻击,怎么做好主机安全,受到ARP攻击有哪些解决方案

在数字化日益深入的今天&#xff0c;网络安全问题愈发凸显其重要性。其中&#xff0c;ARP攻击作为一种常见的网络攻击方式之一&#xff0c;往往给企业和个人用户带来不小的困扰。ARP协议是TCP/IP协议族中的一个重要协议&#xff0c;负责把网络层(IP层)的IP地址解析为数据链路层…

Vmvare—windows中打不开摄像头

1、检查本地摄像头是否能正常打开 设备管理器—查看—显示隐藏设备—选中照相机—启动 USB2.0 HD UVC—打开相机查看 2、检查虚拟机的设置 虚拟机—虚拟机—可移动设备—USB2.0 HD UVC—勾选在状态栏中显示 虚拟机—打开windows主机—右小角选中圆圈图标—勾选连接主机 此时…

办公园区建筑科技风效果(html+threejs)

办公楼科技风(Htmlthreejs) 初始化三维场景 function init() {container document.getElementById(container);camera new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 150000000);camera.position.set(550, 600, 690);scene new THREE.Sce…

MES系统追溯管理功能,迅速定位问题源头

一、MES系统概述 MES系统是一种实现车间生产智能化、信息化的管理系统&#xff0c;通过对生产现场的数据进行实时采集、处理和分析&#xff0c;为企业管理者提供准确、及时的生产信息。MES系统主要包括生产订单管理、物料追溯、质量管理、设备管理、物料管理、人员管理等功能模…

探索数据结构(让数据结构不再成为幻想)

目录 什么是数据结构 数据与结构 什么是算法 复杂度分析 时间复杂度与空间复杂度 时间复杂度 思考&#xff1a; 空间复杂度 常数阶O(1) 对数阶O(logn) 线性阶O(n) 以下为空间复杂度为O(n) 线性对数阶O(nlogn) 平方阶O(n) 指数阶O(2^n) 什么是数据结构 数据结构…

Python送你小花花

快到520了&#xff0c;准备好送上你的爱意了吗&#xff1f; 还记得去年从网上模仿了一篇python使用turtle画的小花花程序&#xff0c;当时还没有转行到程序员行业&#xff0c;刚刚入门学习编程&#xff0c;还在纠结是学习python、Java还是C#的时候。 总会被一些猎奇的内容吸引&…

uniapp如何打包预约按摩H5?

uniapp如何打包预约按摩H5&#xff1f; 开发工具&#xff1a;HBuilderX 一、如何修改域名配置&#xff1f; 1、修改公众号AppID、页面访问路径 1&#xff09;gzh_appid: 公众号AppID siteroot: 域名&#xff0c;需更换为你自己的域名以及公众号APPID&#xff0c;域名格式【htt…

雍禾植发张东宏:以诚相待毛发患者

医学道路上的奋斗往往需要坚定的信念和不懈的努力。对于张东宏医生来说&#xff0c;医学并非止步于书本知识&#xff0c;而是一次次与患者对话、一次次实操中的历练和积累。在他的成长历程中&#xff0c;医学之路如同一棵参天大树&#xff0c;每一步都是扎实的打磨&#xff0c;…

windows设置Redis服务后台自启动

问题 在日常开发过程中&#xff0c;redis是我们常用的缓存工具&#xff0c;但是由于redis对于Linux系统进行开发的&#xff0c;在Linux系统里可以通过修改redis.conf从而从而实现后台启动。 daemonize no 改成 daemonize yes 但是在window上如何也进行后台运行呢&#xff0c…

Arduino红外遥控器,控制继电器水泵

我们将讨论如何使用Arduino和IRremote库来实现通过红外遥控器控制继电器的开关。通过这个项目&#xff0c;你将学会如何接收和解码红外信号&#xff0c;并根据接收到的信号控制继电器&#xff08;这里的继电器可以换成其他传感器&#xff09;的状态。 项目简介 我们将使用Ard…