当前位置: 首页 > 工具软件 > Sa-Token > 使用案例 >

Sa-token 之 SaManager

龙洛城
2023-12-01

SaManager 负责 Sa-token 所有全局组件管理,其中包含如下:
1、负责全局配置的组件 SaTokenConfig;

2、负责持久化处理的组件 SaTokenDao;

3、负责权限认证的组件 StpInterface;

4、负责框架行为的组件 SaTokenAction;

5、负责上下文处理器的组件 SaTokenContext 与 SaTokenSecondContext;

6、负责认证活动监听的组件 SaTokenListener;

7、负责临时令牌验证的组件 SaTempInterface;

8、负责认证处理逻辑的组件 StpLogic;

接下来分别对以上8个组件逐个介绍。

目录

一、SaTokenConfig 全局配置

二、SaTokenDao 持久化处理

三、StpInterface 权限认证

四、SaTokenAction 框架行为

五、SaTokenContext与SaTokenSecondContext 上下文处理器

六、SaTokenListener 认证活动监听

七、SaTempInterface 临时令牌验证

八、StpLogic 认证处理逻辑

(一)Token 相关

(二)登录相关

(三)Session 相关

 1、Token-Session

2、User-Session

(四)Key 相关


一、SaTokenConfig 全局配置

通过源码注释说明可以大致了解各项配置的含义(以下仅对关键配置重点说明)

public class SaTokenConfig implements Serializable {

	private static final long serialVersionUID = -6541180061782004705L;

	/** token名称 (同时也是cookie名称) */
	private String tokenName = "satoken";

	/** token的长久有效期(单位:秒) 默认30天, -1代表永久 */
	private long timeout = 60 * 60 * 24 * 30;

	/**
	 * token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
	 * (例如可以设置为1800代表30分钟内无操作就过期)
	 */
	private long activityTimeout = -1;

	/** 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) */
	private Boolean isConcurrent = true;

	/** 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) */
	private Boolean isShare = true;

	/** 是否尝试从请求体里读取token */
	private Boolean isReadBody = true;

	/** 是否尝试从header里读取token */
	private Boolean isReadHead = true;

	/** 是否尝试从cookie里读取token */
	private Boolean isReadCookie = true;

	/** token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) */
	private String tokenStyle = "uuid";

	/** 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理 */
	private int dataRefreshPeriod = 30;

	/** 获取[token专属session]时是否必须登录 (如果配置为true,会在每次获取[token-session]时校验是否登录) */
	private Boolean tokenSessionCheckLogin = true;

	/** 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作)  */
	private Boolean autoRenew = true;

	/** token前缀, 格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx) */
	private String tokenPrefix;

	/** 是否在初始化配置时打印版本字符画 */
	private Boolean isPrint = true;

	/** 是否打印操作日志 */
	private Boolean isLog = false;

	/**
	 * jwt秘钥 (只有集成 jwt 模块时此参数才会生效)   
	 */
	private String jwtSecretKey;
	
	/**
	 * Id-Token的有效期 (单位: 秒)
	 */
	private long idTokenTimeout = 60 * 60 * 24;

	/**
	 * Http Basic 认证的账号和密码 
	 */
	private String basic = "";

	/** 配置当前项目的网络访问地址 */
	private String currDomain;

	/** 是否校验Id-Token(部分rpc插件有效) */
	private Boolean checkIdToken = false;

	/**
	 * Cookie配置对象 
	 */
	public SaCookieConfig cookie = new SaCookieConfig();
	
	/**
	 * SSO单点登录配置对象 
	 */
	public SaSsoConfig sso = new SaSsoConfig();
}
SaTokenConfig全局配置
配置项默认值说明配置方式
tokenNamesatoken

token键的前缀,

例如:Session、Id-Token、Sso-Ticket等的键值

sa-token.token-name
tokenPrefix

token值的前缀,

默认无前缀,

例如:

设置为 Bearer,那么生成的token值为:satoken: Bearer xxxx-xxxx-xxxx-xxxx

sa-token.token-prefix
timeout60*60*24*30

token的长久有效期限(单位秒),

默认30天,

-1代表不限制

sa-token.timeout
activityTimeout-1

token的临时有效期限(单位秒),

指定时间内无操作的过期时间,

默认 -1,代表不限制

sa-token.activity-timeout
idTokenTimeout60*60*24

Id-Token的有效期(单位秒),

可用于微服务之间调用鉴权,

默认 1天

sa-token.id-token-timeout
autoRenewtrue

是否开启自动续签,

默认开启

sa-token.auto-renew
basic""

Http Basic 认证的默认账号和密码

格式为 user:password

可搭配使用 @SaCheckBasic 注解实现自定义认证配置

sa-token.basic
currDomain

配置当前项目的网络访问地址,

影响采用SaToken默认提供的SaSsoHandle中客户端访问服务端获取授权以及校验Ticket,

默认从客户端请求中获取,当服务端与客户端访问地址不一致时,可通过配置服务端地址的方式进行覆盖。

sa-token.curr-domain
cookieSaCookieConfig配置sa-token.cookie.
ssoSaSsoConfig配置sa-token.sso.
public class SaCookieConfig {
	
    /**
     * 域(写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景)
     */
    private String domain; 

    /**
     * 路径 
     */
    private String path;

    /**
     * 是否只在 https 协议下有效 
     */
    private Boolean secure = false; 
    
    /**
     * 是否禁止 js 操作 Cookie 
     */
    private Boolean httpOnly = false; 
    
    /**
     * 第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制)
     */
	private String sameSite;
}
SaCookieConfig全局配置
配置项默认值说明配置方式
domain

写入Cookie时显式指定作用域

1、不可指定顶级域名,

2、指定顶级+二级域名,二级子域名都可获取Cookie

3、指定二级子域名,仅指定域名可获取Cookie

sa-token.cookie.domain
path

写入Cookie时显示指定路径

非指定path的请求不会携带Cookie

sa-token.cookie.path
securefalse

是否只在 https 协议下有效

默认不限制

sa-token.cookie.secure
httpOnlyfalse是否禁止 javascript 操作 Cookiesa-token.cookie.http-only
sameSite

第三方限制级别:

Strict=完全禁止,只允许同站(顶级+二级域名相同)请求携Cookie;

Lax=部分允许;

None=不限制,必须配置 secure 为true;

sa-token.cookie.same-site
public class SaSsoConfig implements Serializable {

	private static final long serialVersionUID = -6541180061782004705L;

	
	// ----------------- Server端相关配置 
	
	/**
	 * Ticket有效期 (单位: 秒) 
	 */
	public long ticketTimeout = 60 * 5;
	
	/**
	 * 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket) 
	 */
	public String allowUrl = "*";

	/**
	 * 是否打开单点注销功能 
	 */
	public Boolean isSlo = true; 
	
	/**
	 * 是否打开模式三(此值为 true 时将使用 http 请求:校验ticket值、单点注销、获取userinfo) 
	 */
	public Boolean isHttp = false; 

	/**
	 * 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验) 
	 */
	public String secretkey;

	
	// ----------------- Client端相关配置 

	/**
	 * 配置 Server 端单点登录授权地址 
	 */
	public String authUrl;

	/**
	 * 是否打开单点注销功能 
	 */
	// public Boolean isSlo = true;  // 同上 

	/**
	 * 是否打开模式三(此值为 true 时将使用 http 请求:校验ticket值、单点注销、获取userinfo) 
	 */
	// public Boolean isHttp = false;  // 同上 

	/**
	 * 接口调用秘钥 (用于SSO模式三单点注销的接口通信身份校验) 
	 */
	// public String secretkey;  // 同上 

	/**
	 * 配置 Server 端的 ticket 校验地址 
	 */
	public String checkTicketUrl;

	/**
	 * 配置 Server 端查询 userinfo 地址 
	 */
	public String userinfoUrl;
	
	/**
	 * 配置 Server 端单点注销地址 
	 */
	public String sloUrl;

	/**
	 * 配置当前 Client 端的单点注销回调URL (为空时自动获取) 
	 */
	public String ssoLogoutCall;
}
SaSsoConfig全局配置
配置项默认值说明配置方式
isSlotrue

是否开启单点注销功能

特别提示:客户端需要开启模式三(isHttp=true)

1、客户端在进行 ticket 校验请求时,将向服务端注册注销回调地址

2、服务端将在账号注销过程多播调用注销用户Session中所有已注册的客户端注销回调地址

sa-token.sso.is-slo
isHttpfalse

是否开启模式三(基于http请求校验ticket)

sa-token.sso.is-http
secretkey

接口调用密钥(用于SSO模式三单点注销的接口通信身份校验,服务端与客户端配置密钥需保持一致,否则校验失败)

sa-token.sso.secretkey
httpOnlyfalse是否禁止 javascript 操作 Cookiesa-token.cookie.http-only
sameSite

第三方限制级别:

Strict=完全禁止,只允许同站(顶级+二级域名相同)请求携Cookie;

Lax=部分允许;

None=不限制,必须配置 secure 为true;

sa-token.cookie.same-site

二、SaTokenDao 持久化处理

与Spring Security 认证授权框架类似,Sa-Token的对用户登录、授权后的状态是通过在服务端进行保留实现,SaTokenDao就是为其持久化处理这一关键要素进行了通用封装。整体上包含:字符串的读写、对象的读写、Session读写,项目框架(以及插件)中提供了三种实现:

1、SaTokenDaoDefaultImpl 默认实现

2、SaTokenDaoRedis Redis存储+JDK默认序列化方式实现

3、SaTokenDaoRedisJackson Redis存储+jackson序列化方式实现

重点关注 SaTokenDaoRedisJackson 实现中的 init() 方式,基于RedisConnnetionFactory初始化处理过程:

1、指定序列化方式:

(1)键:StringRedisSerializer

(2)值:GenericJackson2JsonRedisSerializer

2、增强值序列化方法

(1)通过反射获取Mapper对象

(2)时间类型序列化与反序列化模版配置

3、构建Redis操作对象

(1)String类型操作对象 StringRedisTemplate

(2)Object类型操作对象 RedisTemplate<String, Object>

4、初始化SaTokenDao相关组件

public interface SaTokenDao {

	/** 常量,表示一个key永不过期 (在一个key被标注为永远不过期时返回此值) */ 
	public static final long NEVER_EXPIRE = -1;
	
	/** 常量,表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值) */ 
	public static final long NOT_VALUE_EXPIRE = -2;

	
	// --------------------- 字符串读写 ---------------------
	
	/**
	 * 获取Value,如无返空 
	 * @param key 键名称 
	 * @return value
	 */
	public String get(String key);

	/**
	 * 写入Value,并设定存活时间 (单位: 秒)
	 * @param key 键名称 
	 * @param value 值 
	 * @param timeout 过期时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
	 */
	public void set(String key, String value, long timeout);

	/**
	 * 更新Value (过期时间不变) 
	 * @param key 键名称 
	 * @param value 值 
	 */
	public void update(String key, String value);

	/**
	 * 删除Value 
	 * @param key 键名称 
	 */
	public void delete(String key);
	
	/**
	 * 获取Value的剩余存活时间 (单位: 秒) 
	 * @param key 指定key 
	 * @return 这个key的剩余存活时间 
	 */
	public long getTimeout(String key);
	
	/**
	 * 修改Value的剩余存活时间 (单位: 秒) 
	 * @param key 指定key
	 * @param timeout 过期时间 
	 */
	public void updateTimeout(String key, long timeout);

	
	// --------------------- 对象读写 ---------------------

	/**
	 * 获取Object,如无返空 
	 * @param key 键名称 
	 * @return object
	 */
	public Object getObject(String key);

	/**
	 * 写入Object,并设定存活时间 (单位: 秒)
	 * @param key 键名称 
	 * @param object 值 
	 * @param timeout 存活时间 (值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
	 */
	public void setObject(String key, Object object, long timeout);

	/**
	 * 更新Object (过期时间不变) 
	 * @param key 键名称 
	 * @param object 值 
	 */
	public void updateObject(String key, Object object);

	/**
	 * 删除Object 
	 * @param key 键名称 
	 */
	public void deleteObject(String key);
	
	/**
	 * 获取Object的剩余存活时间 (单位: 秒)
	 * @param key 指定key 
	 * @return 这个key的剩余存活时间 
	 */
	public long getObjectTimeout(String key);
	
	/**
	 * 修改Object的剩余存活时间 (单位: 秒)
	 * @param key 指定key
	 * @param timeout 过期时间 
	 */
	public void updateObjectTimeout(String key, long timeout);

	
	// --------------------- Session读写 ---------------------

	/**
	 * 获取Session,如无返空 
	 * @param sessionId sessionId
	 * @return SaSession
	 */
	public default SaSession getSession(String sessionId) {
		return (SaSession)getObject(sessionId);
	}

	/**
	 * 写入Session,并设定存活时间 (单位: 秒) 
	 * @param session 要保存的Session对象
	 * @param timeout 过期时间 (单位: 秒)
	 */
	public default void setSession(SaSession session, long timeout) {
		setObject(session.getId(), session, timeout);
	}

	/**
	 * 更新Session
	 * @param session 要更新的session对象
	 */
	public default void updateSession(SaSession session) {
		updateObject(session.getId(), session);
	}
	
	/**
	 * 删除Session
	 * @param sessionId sessionId
	 */
	public default void deleteSession(String sessionId) {
		deleteObject(sessionId);
	}

	/**
	 * 获取Session剩余存活时间 (单位: 秒) 
	 * @param sessionId 指定Session 
	 * @return 这个Session的剩余存活时间 
	 */
	public default long getSessionTimeout(String sessionId) {
		return getObjectTimeout(sessionId);
	}
	
	/**
	 * 修改Session剩余存活时间 (单位: 秒) 
	 * @param sessionId 指定Session 
	 * @param timeout 过期时间 
	 */
	public default void updateSessionTimeout(String sessionId, long timeout) {
		updateObjectTimeout(sessionId, timeout);
	}
	
	
	// --------------------- 会话管理 ---------------------

	/**
	 * 搜索数据 
	 * @param prefix 前缀 
	 * @param keyword 关键字 
	 * @param start 开始处索引 (-1代表查询所有)
	 * @param size 获取数量 
	 * @return 查询到的数据集合 
	 */
	public List<String> searchData(String prefix, String keyword, int start, int size);
}
public class SaTokenDaoRedisJackson implements SaTokenDao {

	public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
	public static final String DATE_PATTERN = "yyyy-MM-dd";
	public static final String TIME_PATTERN = "HH:mm:ss";
	public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
	public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
	public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);

	/**
	 * ObjectMapper对象 (以public作用域暴露出此对象,方便开发者二次更改配置)
	 */
	public ObjectMapper objectMapper;
	
	/**
	 * String专用
	 */
	public StringRedisTemplate stringRedisTemplate;	

	/**
	 * Object专用 
	 */
	public RedisTemplate<String, Object> objectRedisTemplate;
	
	/**
	 * 标记:是否已初始化成功
	 */
	public boolean isInit;
	
	@Autowired
	public void init(RedisConnectionFactory connectionFactory) {
		
		// 指定相应的序列化方案 
		StringRedisSerializer keySerializer = new StringRedisSerializer();
		GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
		// 通过反射获取Mapper对象, 增加一些配置, 增强兼容性 
		try {
			Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
			field.setAccessible(true);
			ObjectMapper objectMapper = (ObjectMapper) field.get(valueSerializer);
			this.objectMapper = objectMapper;
			// 配置[忽略未知字段]
			this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
			// 配置[时间类型转换]
			JavaTimeModule timeModule = new JavaTimeModule();
			// LocalDateTime序列化与反序列化
			timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
			timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
			// LocalDate序列化与反序列化
			timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
			timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
			// LocalTime序列化与反序列化
			timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
			timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
			this.objectMapper.registerModule(timeModule);
		} catch (Exception e) {
			System.err.println(e.getMessage());
		}
		// 构建StringRedisTemplate
		StringRedisTemplate stringTemplate = new StringRedisTemplate();
		stringTemplate.setConnectionFactory(connectionFactory);
		stringTemplate.afterPropertiesSet();
		// 构建RedisTemplate
		RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
		template.setConnectionFactory(connectionFactory);
		template.setKeySerializer(keySerializer);
		template.setHashKeySerializer(keySerializer);
		template.setValueSerializer(valueSerializer);
		template.setHashValueSerializer(valueSerializer);
		template.afterPropertiesSet();
		
		// 开始初始化相关组件 
		if(this.isInit == false) {
			this.stringRedisTemplate = stringTemplate;
			this.objectRedisTemplate = template;
			this.isInit = true;
		}
	}
}

三、StpInterface 权限认证

提供了获取用户权限集以及用户角色标识接口定义,可自定义该接口的实现并结合 SaCheckPermission以及SaCheckRole注解使用,集成权限认证功能。

public interface StpInterface {

	/**
	 * 返回指定账号id所拥有的权限码集合 
	 * 
	 * @param loginId  账号id
	 * @param loginType 账号类型
	 * @return 该账号id具有的权限码集合
	 */
	public List<String> getPermissionList(Object loginId, String loginType);

	/**
	 * 返回指定账号id所拥有的角色标识集合 
	 * 
	 * @param loginId  账号id
	 * @param loginType 账号类型
	 * @return 该账号id具有的角色标识集合
	 */
	public List<String> getRoleList(Object loginId, String loginType);

}

SaToken 框架支持多账户认证体系,例如在需区分内部用户以及外部用户的使用场景下,loginType 为不同账户类型标识,可基于 loginType 分别提供不同账户类型的权限、角色信息。 

四、SaTokenAction 框架行为

提供了框架内部的关键性处理逻辑定义,包括如下:

1、createToken 创建 Token

2、createSession 创建 Session

3、hasElement 判断集合是否包含指定元素

4、checkMethodAnnotation 注解鉴权内部实现

5、validateAnnotation 注解校验

该接口标记为 @Deprecated,替代组件为 SaStrategy,目前其内部默认实现基于 SaTokenAction,个人项目可通过覆盖其中策略方法进行自定义实现。

@Deprecated
public interface SaTokenAction {

	/**
	 * 创建一个Token 
	 * @param loginId 账号id 
	 * @param loginType 账号类型 
	 * @return token
	 */
	public String createToken(Object loginId, String loginType); 
	
	/**
	 * 创建一个Session 
	 * @param sessionId Session的Id
	 * @return 创建后的Session 
	 */
	public SaSession createSession(String sessionId); 
	
	/**
	 * 判断:集合中是否包含指定元素(模糊匹配) 
	 * @param list 集合
	 * @param element 元素
	 * @return 是否包含
	 */
	public boolean hasElement(List<String> list, String element);

	/**
	 * 对一个Method对象进行注解检查(注解鉴权内部实现) 
	 * @param method Method对象
	 */
	public void checkMethodAnnotation(Method method);
	
	/**
	 * 从指定元素校验注解 
	 * @param target /
	 */
	public void validateAnnotation(AnnotatedElement target);
	
}
public final class SaStrategy {

	private SaStrategy() {
	}

	/**
	 * 获取 SaStrategy 对象的单例引用 
	 */
	public static final SaStrategy me = new SaStrategy();

	// 
	// 所有策略  
	// 
	
	/**
	 * 创建 Token 的策略 
	 * <p> 参数 [账号id, 账号类型] 
	 */
	public BiFunction<Object, String, String> createToken = (loginId, loginType) -> {
		return SaManager.getSaTokenAction().createToken(loginId, loginType);
	};
	
	/**
	 * 创建 Session 的策略 
	 * <p> 参数 [SessionId] 
	 */
	public Function<String, SaSession> createSession = (sessionId) -> {
		return SaManager.getSaTokenAction().createSession(sessionId);
	};

	/**
	 * 判断:集合中是否包含指定元素(模糊匹配) 
	 * <p> 参数 [集合, 元素] 
	 */
	public BiFunction<List<String>, String, Boolean> hasElement = (list, element) -> {
		return SaManager.getSaTokenAction().hasElement(list, element);
	};

	/**
	 * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) 
	 * <p> 参数 [Method句柄] 
	 */
	public Consumer<Method> checkMethodAnnotation = (method) -> {

		// 先校验 Method 所属 Class 上的注解 
		me.checkElementAnnotation.accept(method.getDeclaringClass());

		// 再校验 Method 上的注解  
		me.checkElementAnnotation.accept(method);
	};

	/**
	 * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) 
	 * <p> 参数 [element元素] 
	 */
	public Consumer<AnnotatedElement> checkElementAnnotation = (element) -> {
		// 为了兼容旧版本 
		SaManager.getSaTokenAction().validateAnnotation(element);
	};

	/**
	 * 从元素上获取注解(注解鉴权内部实现) 
	 * <p> 参数 [element元素,要获取的注解类型] 
	 */
	public BiFunction<AnnotatedElement, Class<? extends Annotation> , Annotation> getAnnotation = (element, annotationClass)->{
		// 默认使用jdk的注解处理器 
		return element.getAnnotation(annotationClass);
	};
	

	// 
	// 重写策略 set连缀风格 
	// 
	
	/**
	 * 重写创建 Token 的策略 
	 * <p> 参数 [账号id, 账号类型] 
	 * @param createToken / 
	 * @return 对象自身 
	 */
	public SaStrategy setCreateToken(BiFunction<Object, String, String> createToken) {
		this.createToken = createToken;
		return this;
	}

	/**
	 * 重写创建 Session 的策略 
	 * <p> 参数 [SessionId] 
	 * @param createSession / 
	 * @return 对象自身 
	 */
	public SaStrategy setCreateSession(Function<String, SaSession> createSession) {
		this.createSession = createSession;
		return this;
	}

	/**
	 * 判断:集合中是否包含指定元素(模糊匹配) 
	 * <p> 参数 [集合, 元素] 
	 * @param hasElement /
	 * @return 对象自身 
	 */
	public SaStrategy setHasElement(BiFunction<List<String>, String, Boolean> hasElement) {
		this.hasElement = hasElement;
		return this;
	}

	/**
	 * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) 
	 * <p> 参数 [Method句柄] 
	 * @param checkMethodAnnotation /
	 * @return 对象自身 
	 */
	public SaStrategy setCheckMethodAnnotation(Consumer<Method> checkMethodAnnotation) {
		this.checkMethodAnnotation = checkMethodAnnotation;
		return this;
	}

	/**
	 * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) 
	 * <p> 参数 [element元素] 
	 * @param checkElementAnnotation / 
	 * @return 对象自身 
	 */
	public SaStrategy setCheckElementAnnotation(Consumer<AnnotatedElement> checkElementAnnotation) {
		this.checkElementAnnotation = checkElementAnnotation;
		return this;
	}

	/**
	 * 从元素上获取注解(注解鉴权内部实现) 
	 * <p> 参数 [element元素,要获取的注解类型] 
	 * @param getAnnotation / 
	 * @return 对象自身 
	 */
	public SaStrategy setGetAnnotation(BiFunction<AnnotatedElement, Class<? extends Annotation> , Annotation> getAnnotation) {
		this.getAnnotation = getAnnotation;
		return this;
	}
	
	
}

五、SaTokenContext与SaTokenSecondContext 上下文处理器

上下文处理器是维持会话跟踪的关键,SaTokenContext 中封装定义如下:
1、getRequest 获取当前会话的「请求对象」

2、getResponse 获取当前会话的「响应对象」

3、getStorage 获取当前会话的「存储器对象」

4、matchPath 检验路由是否匹配

5、isValid 判断当前上下文是否有效

SaTokenSecondContext 完全继承至 SaTokenContext,用于区分 一级/二级上下文;类似微服务项目中,同时存在基于 http 的 web 访问以及 dubbo 的 rpc 调时,就需要提供二级上下文兼容支持。如果采用 feign 的服务间 rpc 调用,则无需二级上下文支持。

public interface SaTokenContext {

	/**
	 * 获取当前请求的 [Request] 对象
	 * 
	 * @return see note 
	 */
	public SaRequest getRequest();

	/**
	 * 获取当前请求的 [Response] 对象
	 * 
	 * @return see note 
	 */
	public SaResponse getResponse();

	/**
	 * 获取当前请求的 [存储器] 对象 
	 * 
	 * @return see note 
	 */
	public SaStorage getStorage();

	/**
	 * 校验指定路由匹配符是否可以匹配成功指定路径 
	 * 
	 * @param pattern 路由匹配符 
	 * @param path 需要匹配的路径 
	 * @return see note 
	 */
	public boolean matchPath(String pattern, String path);

	/**
	 * 此上下文是否有效 
	 * @return / 
	 */
	public default boolean isValid() {
		return false;
	}
	
}
public interface SaTokenSecondContext extends SaTokenContext {
	
}

六、SaTokenListener 认证活动监听

封装定义了认证活动中的关键性操作行为监听,包括登录、注销、被踢下线、禁用等操作。可自定义各操作的实现,扩展功能支持,例如:用户认证日志、账号安全通知等。

public interface SaTokenListener {

	/**
	 * 每次登录时触发 
	 * @param loginType 账号类别
	 * @param loginId 账号id
	 * @param loginModel 登录参数
	 */
	public void doLogin(String loginType, Object loginId, SaLoginModel loginModel);
			
	/**
	 * 每次注销时触发 
	 * @param loginType 账号类别
	 * @param loginId 账号id
	 * @param tokenValue token值
	 */
	public void doLogout(String loginType, Object loginId, String tokenValue);
	
	/**
	 * 每次被踢下线时触发 
	 * @param loginType 账号类别 
	 * @param loginId 账号id 
	 * @param tokenValue token值 
	 */
	public void doKickout(String loginType, Object loginId, String tokenValue);

	/**
	 * 每次被顶下线时触发
	 * @param loginType 账号类别
	 * @param loginId 账号id
	 * @param tokenValue token值
	 */
	public void doReplaced(String loginType, Object loginId, String tokenValue);

	/**
	 * 每次被封禁时触发
	 * @param loginType 账号类别
	 * @param loginId 账号id
	 * @param disableTime 封禁时长,单位: 秒
	 */
	public void doDisable(String loginType, Object loginId, long disableTime);
	
	/**
	 * 每次被解封时触发
	 * @param loginType 账号类别
	 * @param loginId 账号id
	 */
	public void doUntieDisable(String loginType, Object loginId);
	
	/**
	 * 每次创建Session时触发
	 * @param id SessionId
	 */
	public void doCreateSession(String id);
	
	/**
	 * 每次注销Session时触发
	 * @param id SessionId
	 */
	public void doLogoutSession(String id);
	
}

七、SaTempInterface 临时令牌验证

封装定义了临时令牌的生成、删除、解析、获取过期时间等接口定义,默认基于SaTokenDao支持持久化处理,sa-token-temp-jwt 插件中提供了基于 jwt 的自校验实现。以「邀请链接」的临时令牌应用分析使用特点:

1、有效期时间短,通常在5分钟,10分钟等;

2、需要隐蔽掉具备规律性的关键信息,比如某游戏工会的邀请链接中需要提供的工会标识信息,如果明文传输 id=1000 形式,就可以通过 修改 id=1001 尝试加入其他工会。在邀请链接中携带随机生成的临时令牌替代明文标识,服务端通过解析临时令牌获取其对应明文标识再做业务处理,支持规律性信息的隐蔽目的。

public interface SaTempInterface {

	/**
	 * 根据value创建一个token 
	 * @param value 指定值
	 * @param timeout 有效期,单位:秒 
	 * @return 生成的token
	 */
	public default String createToken(Object value, long timeout) {
		
		// 生成 token 
		String token = SaStrategy.me.createToken.apply(null, null);
		
		// 持久化映射关系 
		String key = splicingKeyTempToken(token);
		SaManager.getSaTokenDao().setObject(key, value, timeout);
		
		// 返回 
		return token;
	}
	
	/**
	 *  解析token获取value 
	 * @param token 指定token 
	 * @return  See Note 
	 */
	public default Object parseToken(String token) {
		String key = splicingKeyTempToken(token);
		return SaManager.getSaTokenDao().getObject(key);
	}

	/**
	 * 解析token获取value,并转换为指定类型 
	 * @param token 指定token 
	 * @param cs 指定类型 
	 * @param <T> 默认值的类型 
	 * @return  See Note 
	 */
	public default<T> T parseToken(String token, Class<T> cs) {
		return SaFoxUtil.getValueByType(parseToken(token), cs);
	}
	
	/**
	 * 获取指定 token 的剩余有效期,单位:秒 
	 * <p> 返回值 -1 代表永久,-2 代表token无效 
	 * @param token see note 
	 * @return see note 
	 */
	public default long getTimeout(String token) {
		String key = splicingKeyTempToken(token);
		return SaManager.getSaTokenDao().getObjectTimeout(key);
	}

	/**
	 * 删除一个 token
	 * @param token 指定token 
	 */
	public default void deleteToken(String token) {
		String key = splicingKeyTempToken(token);
		SaManager.getSaTokenDao().deleteObject(key);
	}
	
	/**  
	 * 获取映射关系的持久化key 
	 * @param token token值
	 * @return key
	 */
	public default String splicingKeyTempToken(String token) {
		return SaManager.getConfig().getTokenName() + ":temp-token:" + token;
	}

	/**
	 * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效)  
	 */
	public default String getJwtSecretKey() {
		return null;
	}
}

八、StpLogic 认证处理逻辑

StpLogic 是 Sa-token 框架各个组件能够协同配合,提供认证鉴权功能的关键。

在 SaManager 中 StpLogic 是以 Map 集合的形式初始化,Map 的键为 StpLogic 的 loginType(账号类型),值为 StpLogic 操作对象,这就是 Sa-token 支持多账号认证体系的基础,基于不同的loginType,实现不同账号类型的认证逻辑隔离。

public class SaManager {
	
	/**
	 * StpLogic集合, 记录框架所有成功初始化的StpLogic 
	 */
	public static Map<String, StpLogic> stpLogicMap = new HashMap<String, StpLogic>();
	
	/**
	 * 向集合中 put 一个 StpLogic 
	 * @param stpLogic StpLogic
	 */
	public static void putStpLogic(StpLogic stpLogic) {
		stpLogicMap.put(stpLogic.getLoginType(), stpLogic);
	}

	/**
	 * 根据 LoginType 获取对应的StpLogic,如果不存在则抛出异常 
	 * @param loginType 对应的账号类型 
	 * @return 对应的StpLogic
	 */
	public static StpLogic getStpLogic(String loginType) {
		// 如果type为空则返回框架内置的 
		if(loginType == null || loginType.isEmpty()) {
			return StpUtil.stpLogic;
		}
		
		// 从SaManager中获取 
		StpLogic stpLogic = stpLogicMap.get(loginType);
		if(stpLogic == null) {
			/*
			 * 此时有两种情况会造成 StpLogic == null 
			 * 1. loginType拼写错误,请改正 (建议使用常量) 
			 * 2. 自定义StpUtil尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种
			 * 		(1) 从main方法里调用一次
			 * 		(2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化 
			 */
			throw new SaTokenException("未能获取对应StpLogic,type="+ loginType);
		}
		
		// 返回 
		return stpLogic;
	}
	
	
}

以下分场景介绍 StpLogic 中接口功能:

(一)Token 相关

  • 创建 Token
	/**
	 * 创建一个TokenValue 
	 * @param loginId loginId 
	 * @param device 设备标识 
	 * @param timeout 过期时间 
	 * @return 生成的tokenValue 
	 */
 	public String createTokenValue(Object loginId, String device, long timeout) {
 		return SaStrategy.me.createToken.apply(loginId, loginType);
	}

                默认基于 SaStrategy 创建 Token 策略实现,生成无实际含义的随机字符串。可通过以下方式自定义实现:

        (1)新建 StpLogic 子类 重写 createTokenValue 方法;例如 sa-token-jwt 插件中提供的StpLogicJwtForStyle 重写

        (2)由于 SaStrategy 全局单例,可通过以下方式实现其创建 Token 策略的重写

SaStrategy.me.setCreateToken((loginId, loginType) -> {
		// 自定义Token生成的算法 
		return "xxxx";
});
  • 存储 Token 至当前会话
    • 保存至 Storage
      • 保存在 request 域
      • 仅在当前请求中有效,重定向客户端后将无法获取到 Token
    • 保存至 Cookie
      • 保存在 cookie 域
      • 在 Cookie 有效期内,符合 Cookie 安全策略的请求中都可以再次获取
    /**
 	 * 在当前会话写入当前TokenValue 
 	 * @param tokenValue token值 
 	 */
	public void setTokenValue(String tokenValue){
		setTokenValue(tokenValue, (int)SaManager.getConfig().getTimeout());
	}
	
 	/**
 	 * 在当前会话写入当前TokenValue 
 	 * @param tokenValue token值 
 	 * @param cookieTimeout Cookie存活时间(秒)
 	 */
	public void setTokenValue(String tokenValue, int cookieTimeout){
		
		if(SaFoxUtil.isEmpty(tokenValue)) {
			return;
		}
		
		// 1. 将token保存到[存储器]里  
		setTokenValueToStorage(tokenValue);
		
		// 2. 将 Token 保存到 [Cookie] 里 
		if (getConfig().getIsReadCookie()) {
			setTokenValueToCookie(tokenValue, cookieTimeout);
		}
	}

 	/**
 	 * 将 Token 保存到 [Storage] 里 
 	 * @param tokenValue token值 
 	 */
	public void setTokenValueToStorage(String tokenValue){
		// 1. 将token保存到[存储器]里  
		SaStorage storage = SaHolder.getStorage();
		
		// 2. 如果打开了 Token 前缀模式,则拼接上前缀
		String tokenPrefix = getConfig().getTokenPrefix();
		if(SaFoxUtil.isEmpty(tokenPrefix) == false) {
			storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue);	
		} else {
			storage.set(splicingKeyJustCreatedSave(), tokenValue);	
		}
		
		// 3. 写入 (无前缀) 
		storage.set(SaTokenConsts.JUST_CREATED_NOT_PREFIX, tokenValue);  
	}
	
 	/**
 	 * 将 Token 保存到 [Cookie] 里 
 	 * @param tokenValue token值 
 	 * @param cookieTimeout Cookie存活时间(秒)
 	 */
	public void setTokenValueToCookie(String tokenValue, int cookieTimeout){
		SaCookieConfig cfg = getConfig().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);
	}
  • 获取当前会话的 Token

        分优先级从以下途径获取

  1. Storage 获取
  2. RequestParam 获取
  3. Header 获取
  4. Cookie 获取
    /**
	 * 获取当前TokenValue
	 * @return 当前tokenValue
	 */
	public String getTokenValue(){
		// 1. 获取
		String tokenValue = getTokenValueNotCut();
		
		// 2. 如果打开了前缀模式,则裁剪掉 
		String tokenPrefix = getConfig().getTokenPrefix();
		if(SaFoxUtil.isEmpty(tokenPrefix) == false) {
			// 如果token并没有按照指定的前缀开头,则视为未提供token 
			if(SaFoxUtil.isEmpty(tokenValue) || tokenValue.startsWith(tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT) == false) {
				tokenValue = null;
			} else {
				// 则裁剪掉前缀 
				tokenValue = tokenValue.substring(tokenPrefix.length() + SaTokenConsts.TOKEN_CONNECTOR_CHAT.length());
			}
		}
		
		// 3. 返回 
		return tokenValue;
	}
	
	/**
	 * 获取当前TokenValue (不裁剪前缀)
	 * @return / 
	 */
	public String getTokenValueNotCut(){
		// 0. 获取相应对象 
		SaStorage storage = SaHolder.getStorage();
		SaRequest request = SaHolder.getRequest();
		SaTokenConfig config = getConfig();
		String keyTokenName = getTokenName();
		String tokenValue = null;
		
		// 1. 尝试从Storage里读取 
		if(storage.get(splicingKeyJustCreatedSave()) != null) {
			tokenValue = String.valueOf(storage.get(splicingKeyJustCreatedSave()));
		}
		// 2. 尝试从请求体里面读取 
		if(tokenValue == null && config.getIsReadBody()){
			tokenValue = request.getParam(keyTokenName);
		}
		// 3. 尝试从header里读取 
		if(tokenValue == null && config.getIsReadHead()){
			tokenValue = request.getHeader(keyTokenName);
		}
		// 4. 尝试从cookie里读取 
		if(tokenValue == null && config.getIsReadCookie()){
			tokenValue = request.getCookieValue(keyTokenName);
		}
		
		// 5. 返回 
		return tokenValue;
	}

(二)登录相关

  • 会话登录
    • 支持多端设备模式
      • 不同端设备登录每次新建 Token,一端设备进行注销,不影响其他端
    • 开启并发登录模式(默认启用)
      • 共享模式(默认):(同端设备)共用同一 Token,一方进行注销,整体都不可登录
      • 独立模式:(同端设备)每次新建 Token,各方进行注销,互不影响
    • 禁用并发登录模式:(同端设备)一方登录,将顶替掉另一登录方
	/**
	 * 会话登录 
	 * @param id 账号id,建议的类型:(long | int | String)
	 */
	public void login(Object id) {
		login(id, new SaLoginModel());
	}

	/**
	 * 会话登录,并指定登录设备 
	 * @param id 账号id,建议的类型:(long | int | String)
	 * @param device 设备标识 
	 */
	public void login(Object id, String device) {
		login(id, new SaLoginModel().setDevice(device));
	}

	/**
	 * 会话登录,并指定是否 [记住我] 
	 * @param id 账号id,建议的类型:(long | int | String)
	 * @param isLastingCookie 是否为持久Cookie 
	 */
	public void login(Object id, boolean isLastingCookie) {
		login(id, new SaLoginModel().setIsLastingCookie(isLastingCookie));
	}
	
	/**
	 * 会话登录,并指定所有登录参数Model 
	 * @param id 登录id,建议的类型:(long | int | String)
	 * @param loginModel 此次登录的参数Model 
	 */
	public void login(Object id, SaLoginModel loginModel) {
		
		SaTokenException.throwByNull(id, "账号id不能为空");
		
		// ------ 0、前置检查:如果此账号已被封禁. 
		if(isDisable(id)) {
			throw new DisableLoginException(loginType, id, getDisableTime(id));
		}
		
		// ------ 1、初始化 loginModel 
		SaTokenConfig config = getConfig();
		loginModel.build(config);
		
		// ------ 2、生成一个token  
		String tokenValue = null;
		// --- 如果允许并发登录 
		if(config.getIsConcurrent()) {
			// 如果配置为共享token, 则尝试从Session签名记录里取出token 
			if(getConfigOfIsShare()) {
				tokenValue = getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
			}
		} else {
			// --- 如果不允许并发登录,则将这个账号的历史登录标记为:被顶下线 
			replaced(id, loginModel.getDevice());
		}
		// 如果至此,仍未成功创建tokenValue, 则开始生成一个 
		if(tokenValue == null) {
			tokenValue = createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout());
		}
		
		// ------ 3. 获取 User-Session , 续期 
		SaSession session = getSessionByLoginId(id, true);
		session.updateMinTimeout(loginModel.getTimeout());
		
		// 在 User-Session 上记录token签名 
		session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());
		
		// ------ 4. 持久化其它数据 
		// token -> id 映射关系  
		saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
		
		// 在当前会话写入tokenValue 
		setTokenValue(tokenValue, loginModel.getCookieTimeout());

		// 写入 [token-last-activity] 
		setLastActivityToNow(tokenValue); 
		
		// $$ 通知监听器,账号xxx 登录成功 
		SaManager.getSaTokenListener().doLogin(loginType, id, loginModel);
	}
  • 会话注销
    • 注销当前会话
    • 注销指定会话
      • 注销方式
        • 注销指定账号:账号下属所有登录都将强制下线
        • 注销指定账号登录设备:账号下属指定设备登录都将强制下线(关注:独立模式的并发登录
        • 注销指定会话:与指定 Token 关联的会话都将强制下线(关注:共享模式的并发登录
	/** 
	 * 会话注销 
	 */
	public void logout() {
		// 如果连token都没有,那么无需执行任何操作 
		String tokenValue = getTokenValue();
 		if(SaFoxUtil.isEmpty(tokenValue)) {
 			return;
 		}
 		
 		// 从当前 [storage存储器] 里删除 
 		SaHolder.getStorage().delete(splicingKeyJustCreatedSave());
 		
 		// 如果打开了Cookie模式,则把cookie清除掉 
 		if(getConfig().getIsReadCookie()){
 			SaHolder.getResponse().deleteCookie(getTokenName());
		}
 		
 		// 清除这个token的相关信息
 		logoutByTokenValue(tokenValue);
	}

	/**
	 * 会话注销,根据账号id 
	 * 
	 * @param loginId 账号id 
	 */
	public void logout(Object loginId) {
		logout(loginId, null);
	}
	
	/**
	 * 会话注销,根据账号id 和 设备标识 
	 * 
	 * @param loginId 账号id 
	 * @param device 设备标识 (填null代表所有注销设备) 
	 */
	public void logout(Object loginId, String device) {
		clearTokenCommonMethod(loginId, device, tokenValue -> {
	 		// 删除Token-Id映射 & 清除Token-Session 
			deleteTokenToIdMapping(tokenValue);
			deleteTokenSession(tokenValue);
			SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue);
		}, true);
	}
	
	/**
	 * 会话注销,根据指定 Token 
	 * 
	 * @param tokenValue 指定token
	 */
	public void logoutByTokenValue(String tokenValue) {
		// 1. 清理 token-last-activity
		clearLastActivity(tokenValue); 	
		
		// 2. 注销 Token-Session 
		deleteTokenSession(tokenValue);

		// if. 无效 loginId 立即返回 
 		String loginId = getLoginIdNotHandle(tokenValue);
 	 	if(isValidLoginId(loginId) == false) {
 	 		if(loginId != null) {
 	 			deleteTokenToIdMapping(tokenValue);
 	 		}
 			return;
 		}
 		// 3. 清理token-id索引 
 	 	deleteTokenToIdMapping(tokenValue);
 	 	
 	 	// $$ 通知监听器,某某Token注销下线了 
		SaManager.getSaTokenListener().doLogout(loginType, loginId, tokenValue);

		// 4. 清理User-Session上的token签名 & 尝试注销User-Session 
 	 	SaSession session = getSessionByLoginId(loginId, false);
 	 	if(session != null) {
 	 	 	session.removeTokenSign(tokenValue); 
 			session.logoutByTokenSignCountToZero();
 	 	}
	}
  • 踢人下线:会话标记为 KICK_OUT,在 Token 有效期内,再次访问将反馈“Token已被踢下线”
    • 指定账号:影响账号下属所有会话
    • 指定设备:影响账号下属指定设备相关会话(关注:独立模式的并发登录
    • 指定会话:影响与指定 Token 关联的所有会话(关注:共享模式的并发登录
	/**
	 * 踢人下线,根据账号id 
	 * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-5 </p>
	 * 
	 * @param loginId 账号id 
	 */
	public void kickout(Object loginId) {
		kickout(loginId, null);
	}
	
	/**
	 * 踢人下线,根据账号id 和 设备标识 
	 * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-5 </p>
	 * 
	 * @param loginId 账号id 
	 * @param device 设备标识 (填null代表踢出所有设备) 
	 */
	public void kickout(Object loginId, String device) {
		clearTokenCommonMethod(loginId, device, tokenValue -> {
			// 将此 token 标记为已被踢下线  
			updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);
	 		SaManager.getSaTokenListener().doKickout(loginType, loginId, tokenValue);
		}, true);
	}

	/**
	 * 踢人下线,根据指定 Token 
	 * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-5 </p>
	 * 
	 * @param tokenValue 指定token
	 */
	public void kickoutByTokenValue(String tokenValue) {
		// 1. 清理 token-last-activity
		clearLastActivity(tokenValue); 	
		
		// 2. 不注销 Token-Session 

 		// if. 无效 loginId 立即返回 
 		String loginId = getLoginIdNotHandle(tokenValue);
 	 	if(isValidLoginId(loginId) == false) {
 			return;
 		}
 	 	
 		// 3. 给token打上标记:被踢下线 
 	 	updateTokenToIdMapping(tokenValue, NotLoginException.KICK_OUT);
		
 	 	// $$. 否则通知监听器,某某Token被踢下线了 
		SaManager.getSaTokenListener().doKickout(loginType, loginId, tokenValue);

		// 4. 清理User-Session上的token签名 & 尝试注销User-Session 
 	 	SaSession session = getSessionByLoginId(loginId, false);
 	 	if(session != null) {
 	 	 	session.removeTokenSign(tokenValue); 
 			session.logoutByTokenSignCountToZero();
 	 	}
	}
  • 顶人下线:会话标记为 BE_REPLACED,在 Token 有效期内,再次访问将反馈“Token已被顶下线”
    • 仅指定账号:影响账号下属所有会话
    • 同时指定设备:影响账号下属指定设备相关会话(关注:独立模式的并发登录
	/**
	 * 顶人下线,根据账号id 和 设备标识 
	 * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-4 </p>
	 * 
	 * @param loginId 账号id 
	 * @param device 设备标识 (填null代表顶替所有设备) 
	 */
	public void replaced(Object loginId, String device) {
		clearTokenCommonMethod(loginId, device, tokenValue -> {
			// 将此 token 标记为已被顶替 
			updateTokenToIdMapping(tokenValue, NotLoginException.BE_REPLACED);
	 		SaManager.getSaTokenListener().doReplaced(loginType, loginId, tokenValue);
		}, false);
	}

(三)Session 相关

Sa-Token 框架中,在 StpLogic 中有以下两类 Session:

  • Token-Session:为每个 Token 分配的 Session
  • User-Session:为每个 Session 分配的 Session

两类 Session 的创建、获取都以 sessionId 作为依据。

/** 
	 * 获取指定key的Session, 如果Session尚未创建,isCreate=是否新建并返回
	 * @param sessionId SessionId
	 * @param isCreate 是否新建
	 * @return Session对象 
	 */
	public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {
		SaSession session = getSaTokenDao().getSession(sessionId);
		if(session == null && isCreate) {
			session = SaStrategy.me.createSession.apply(sessionId);
			getSaTokenDao().setSession(session, getConfig().getTimeout());
		}
		return session;
	}

	/** 
	 * 获取指定key的Session, 如果Session尚未创建,则返回null
	 * @param sessionId SessionId
	 * @return Session对象 
	 */
	public SaSession getSessionBySessionId(String sessionId) {
		return getSessionBySessionId(sessionId, false);
	}

	/** 
	 * 获取指定账号id的User-Session, 如果Session尚未创建,isCreate=是否新建并返回
	 * @param loginId 账号id
	 * @param isCreate 是否新建
	 * @return Session对象
	 */
	public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
		return getSessionBySessionId(splicingKeySession(loginId), isCreate);
	}

接下来分别介绍这两类 Session,梳理两者之间是如何配合以支撑 Sa-token 框架的认证。

 1、Token-Session

Token-Session 与 tokenValue 强相关:

  • Token-Session 的创建、更新、获取都需要以 loginId 作为依据
  • Token-Session 的 sessionId 是以 tokenValue 为基础,拼接上特定字符串生成,因此可通过 tokenValue 获取到其对应 Token-Session
    /** 
	 * 获取指定Token-Session,如果Session尚未创建,isCreate代表是否新建并返回
	 * @param tokenValue token值
	 * @param isCreate 是否新建 
	 * @return session对象
	 */
	public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {
		return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate);
	}
	
	/** 
	 * 获取指定Token-Session,如果Session尚未创建,则新建并返回 
	 * @param tokenValue Token值
	 * @return Session对象  
	 */
	public SaSession getTokenSessionByToken(String tokenValue) {
		return getSessionBySessionId(splicingKeyTokenSession(tokenValue), true);
	}

	/** 
	 * 获取当前Token-Session,如果Session尚未创建,isCreate代表是否新建并返回 
	 * @param isCreate 是否新建 
	 * @return Session对象  
	 */
	public SaSession getTokenSession(boolean isCreate) {
		// 如果配置了需要校验登录状态,则验证一下
		if(getConfig().getTokenSessionCheckLogin()) {
			checkLogin();
		} else {
			// 如果配置忽略token登录校验,则必须保证token不为null (token为null的时候随机创建一个) 
			String tokenValue = getTokenValue();
			if(tokenValue == null || Objects.equals(tokenValue, "")) {
				// 随机一个token送给Ta 
				tokenValue = createTokenValue(null, null, getConfig().getTimeout());
				// 写入 [最后操作时间]
				setLastActivityToNow(tokenValue);  
				// 在当前会话写入这个tokenValue 
				int cookieTimeout = (int)(getConfig().getTimeout() == SaTokenDao.NEVER_EXPIRE ? Integer.MAX_VALUE : getConfig().getTimeout());
				setTokenValue(tokenValue, cookieTimeout);
			}
		}
		// 返回这个token对应的Token-Session 
		return getSessionBySessionId(splicingKeyTokenSession(getTokenValue()), isCreate);
	}
	
	/** 
	 * 获取当前Token-Session,如果Session尚未创建,则新建并返回
	 * @return Session对象 
	 */
	public SaSession getTokenSession() {
		return getTokenSession(true);
	}
	
	/**
	 * 删除Token-Session 
	 * @param tokenValue token值 
	 */
	public void deleteTokenSession(String tokenValue) {
		getSaTokenDao().delete(splicingKeyTokenSession(tokenValue));
	}

	/**  
	 * 拼接key: tokenValue的专属session 
	 * @param tokenValue token值
	 * @return key
	 */
	public String splicingKeyTokenSession(String tokenValue) {
		return getConfig().getTokenName() + ":" + loginType + ":token-session:" + tokenValue;
	}

2、User-Session

User-Session 与 loginId 强相关:

  • User-Session 的创建、更新、获取都需要以 loginId 作为依据
  • User-Session 的 sessionId 是以 loginId 为基础,拼接上特定字符串生成,因此可通过 loginId 获取到其对应 User-Session
	/** 
	 * 获取指定账号id的User-Session,如果Session尚未创建,则新建并返回 
	 * @param loginId 账号id 
	 * @return Session对象 
	 */
	public SaSession getSessionByLoginId(Object loginId) {
		return getSessionBySessionId(splicingKeySession(loginId), true);
	}

	/** 
	 * 获取当前User-Session, 如果Session尚未创建,isCreate=是否新建并返回 
	 * @param isCreate 是否新建 
	 * @return Session对象 
	 */
	public SaSession getSession(boolean isCreate) {
		return getSessionByLoginId(getLoginId(), isCreate);
	}
	
	/** 
	 * 获取当前User-Session,如果Session尚未创建,则新建并返回 
	 * @return Session对象 
	 */
	public SaSession getSession() {
		return getSession(true);
	}

	/** 
	 * 拼接key: Session 持久化  
	 * @param loginId 账号id
	 * @return key
	 */
	public String splicingKeySession(Object loginId) {
		return getConfig().getTokenName() + ":" + loginType + ":session:" + loginId;
	}

(四)Key 相关

无论是在浏览器 token 的获取,还是在服务端 session 的获取都是基于 key 进行匹配,通过梳理 Sa-token 框架中的相关 key,更有利于了解从客户端、服务端之间完成登录认证以及用户识别的处理方式。SptLogic 中定义了 8 种 key:

  • TokenName-key 获取认证凭证的键(session 域有效)
    /**
	 * 拼接key:客户端 tokenName 
	 * @return key
	 */
	public String splicingKeyTokenName() {
 		return getConfig().getTokenName();
 	}
  • TokenValue-key 获取凭证解析值(loginId)的键(session 域有效)
	/**  
	 * 拼接key: tokenValue 持久化 token-id
	 * @param tokenValue token值
	 * @return key
	 */
	public String splicingKeyTokenValue(String tokenValue) {
		return getConfig().getTokenName() + ":" + loginType + ":token:" + tokenValue;
	}
  • UserSession-key 获取 UserSession 信息的键(session+ 域有效
	/** 
	 * 拼接key: Session 持久化  
	 * @param loginId 账号id
	 * @return key
	 */
	public String splicingKeySession(Object loginId) {
		return getConfig().getTokenName() + ":" + loginType + ":session:" + loginId;
	}
  • TokenSession-key 获取 TokenSession 信息的键(session 域有效)
	/**  
	 * 拼接key: tokenValue的专属session 
	 * @param tokenValue token值
	 * @return key
	 */
	public String splicingKeyTokenSession(String tokenValue) {
		return getConfig().getTokenName() + ":" + loginType + ":token-session:" + tokenValue;
	}
  • LastActivityTime-key 获取会话最后活跃时间的键(session 域有效)
	/** 
	 * 拼接key: 指定token的最后操作时间 持久化 
	 * @param tokenValue token值
	 * @return key
	 */
	public String splicingKeyLastActivityTime(String tokenValue) {
		return getConfig().getTokenName() + ":" + loginType + ":last-activity:" + tokenValue;
	}
  • Switch-key 获临时切换身份信息(loginId)的键(request 域有效)
	/**
	 * 在进行身份切换时,使用的存储key 
	 * @return key
	 */
	public String splicingKeySwitch() {
		return SaTokenConsts.SWITCH_TO_SAVE_KEY + loginType;
	}
  • JustCreatedSave-key 获取新凭证的键(request 域有效)
	/**
	 * 如果token为本次请求新创建的,则以此字符串为key存储在当前request中 
	 * @return key
	 */
	public String splicingKeyJustCreatedSave() {
//		return SaTokenConsts.JUST_CREATED_SAVE_KEY + loginType;
		return SaTokenConsts.JUST_CREATED;
	}
  • Disable-key 获取账号封禁信息的键(session+ 域有效
	/**  
	 * 拼接key: 账号封禁
	 * @param loginId 账号id
	 * @return key 
	 */
	public String splicingKeyDisable(Object loginId) {
		return getConfig().getTokenName() + ":" + loginType + ":disable:" + loginId;
	}

 类似资料: