SaManager 负责 Sa-token 所有全局组件管理,其中包含如下:
1、负责全局配置的组件 SaTokenConfig;
2、负责持久化处理的组件 SaTokenDao;
3、负责权限认证的组件 StpInterface;
4、负责框架行为的组件 SaTokenAction;
5、负责上下文处理器的组件 SaTokenContext 与 SaTokenSecondContext;
6、负责认证活动监听的组件 SaTokenListener;
7、负责临时令牌验证的组件 SaTempInterface;
8、负责认证处理逻辑的组件 StpLogic;
接下来分别对以上8个组件逐个介绍。
目录
五、SaTokenContext与SaTokenSecondContext 上下文处理器
通过源码注释说明可以大致了解各项配置的含义(以下仅对关键配置重点说明)
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();
}
配置项 | 默认值 | 说明 | 配置方式 |
---|---|---|---|
tokenName | satoken | token键的前缀, 例如:Session、Id-Token、Sso-Ticket等的键值 | sa-token.token-name |
tokenPrefix | token值的前缀, 默认无前缀, 例如: 设置为 Bearer,那么生成的token值为:satoken: Bearer xxxx-xxxx-xxxx-xxxx | sa-token.token-prefix | |
timeout | 60*60*24*30 | token的长久有效期限(单位秒), 默认30天, -1代表不限制 | sa-token.timeout |
activityTimeout | -1 | token的临时有效期限(单位秒), 指定时间内无操作的过期时间, 默认 -1,代表不限制 | sa-token.activity-timeout |
idTokenTimeout | 60*60*24 | Id-Token的有效期(单位秒), 可用于微服务之间调用鉴权, 默认 1天 | sa-token.id-token-timeout |
autoRenew | true | 是否开启自动续签, 默认开启 | sa-token.auto-renew |
basic | "" | Http Basic 认证的默认账号和密码 格式为 user:password 可搭配使用 @SaCheckBasic 注解实现自定义认证配置 | sa-token.basic |
currDomain | 配置当前项目的网络访问地址, 影响采用SaToken默认提供的SaSsoHandle中客户端访问服务端获取授权以及校验Ticket, 默认从客户端请求中获取,当服务端与客户端访问地址不一致时,可通过配置服务端地址的方式进行覆盖。 | sa-token.curr-domain | |
cookie | SaCookieConfig配置 | sa-token.cookie. | |
sso | SaSsoConfig配置 | 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;
}
配置项 | 默认值 | 说明 | 配置方式 |
---|---|---|---|
domain | 写入Cookie时显式指定作用域 1、不可指定顶级域名, 2、指定顶级+二级域名,二级子域名都可获取Cookie 3、指定二级子域名,仅指定域名可获取Cookie | sa-token.cookie.domain | |
path | 写入Cookie时显示指定路径 非指定path的请求不会携带Cookie | sa-token.cookie.path | |
secure | false | 是否只在 https 协议下有效 默认不限制 | sa-token.cookie.secure |
httpOnly | false | 是否禁止 javascript 操作 Cookie | sa-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;
}
配置项 | 默认值 | 说明 | 配置方式 |
---|---|---|---|
isSlo | true | 是否开启单点注销功能 特别提示:客户端需要开启模式三(isHttp=true) 1、客户端在进行 ticket 校验请求时,将向服务端注册注销回调地址 2、服务端将在账号注销过程多播调用注销用户Session中所有已注册的客户端注销回调地址 | sa-token.sso.is-slo |
isHttp | false | 是否开启模式三(基于http请求校验ticket) | sa-token.sso.is-http |
secretkey | 接口调用密钥(用于SSO模式三单点注销的接口通信身份校验,服务端与客户端配置密钥需保持一致,否则校验失败) | sa-token.sso.secretkey | |
httpOnly | false | 是否禁止 javascript 操作 Cookie | sa-token.cookie.http-only |
sameSite | 第三方限制级别: Strict=完全禁止,只允许同站(顶级+二级域名相同)请求携Cookie; Lax=部分允许; None=不限制,必须配置 secure 为true; | sa-token.cookie.same-site |
与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;
}
}
}
提供了获取用户权限集以及用户角色标识接口定义,可自定义该接口的实现并结合 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 分别提供不同账户类型的权限、角色信息。
提供了框架内部的关键性处理逻辑定义,包括如下:
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 中封装定义如下:
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 {
}
封装定义了认证活动中的关键性操作行为监听,包括登录、注销、被踢下线、禁用等操作。可自定义各操作的实现,扩展功能支持,例如:用户认证日志、账号安全通知等。
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);
}
封装定义了临时令牌的生成、删除、解析、获取过期时间等接口定义,默认基于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 是 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 中接口功能:
/**
* 创建一个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";
});
/**
* 在当前会话写入当前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);
}
分优先级从以下途径获取
/**
* 获取当前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;
}
/**
* 会话登录
* @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);
}
/**
* 会话注销
*/
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();
}
}
/**
* 踢人下线,根据账号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();
}
}
/**
* 顶人下线,根据账号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);
}
Sa-Token 框架中,在 StpLogic 中有以下两类 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 框架的认证。
Token-Session 与 tokenValue 强相关:
/**
* 获取指定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;
}
User-Session 与 loginId 强相关:
/**
* 获取指定账号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;
}
无论是在浏览器 token 的获取,还是在服务端 session 的获取都是基于 key 进行匹配,通过梳理 Sa-token 框架中的相关 key,更有利于了解从客户端、服务端之间完成登录认证以及用户识别的处理方式。SptLogic 中定义了 8 种 key:
/**
* 拼接key:客户端 tokenName
* @return key
*/
public String splicingKeyTokenName() {
return getConfig().getTokenName();
}
/**
* 拼接key: tokenValue 持久化 token-id
* @param tokenValue token值
* @return key
*/
public String splicingKeyTokenValue(String tokenValue) {
return getConfig().getTokenName() + ":" + loginType + ":token:" + tokenValue;
}
/**
* 拼接key: Session 持久化
* @param loginId 账号id
* @return key
*/
public String splicingKeySession(Object loginId) {
return getConfig().getTokenName() + ":" + loginType + ":session:" + loginId;
}
/**
* 拼接key: tokenValue的专属session
* @param tokenValue token值
* @return key
*/
public String splicingKeyTokenSession(String tokenValue) {
return getConfig().getTokenName() + ":" + loginType + ":token-session:" + tokenValue;
}
/**
* 拼接key: 指定token的最后操作时间 持久化
* @param tokenValue token值
* @return key
*/
public String splicingKeyLastActivityTime(String tokenValue) {
return getConfig().getTokenName() + ":" + loginType + ":last-activity:" + tokenValue;
}
/**
* 在进行身份切换时,使用的存储key
* @return key
*/
public String splicingKeySwitch() {
return SaTokenConsts.SWITCH_TO_SAVE_KEY + loginType;
}
/**
* 如果token为本次请求新创建的,则以此字符串为key存储在当前request中
* @return key
*/
public String splicingKeyJustCreatedSave() {
// return SaTokenConsts.JUST_CREATED_SAVE_KEY + loginType;
return SaTokenConsts.JUST_CREATED;
}
/**
* 拼接key: 账号封禁
* @param loginId 账号id
* @return key
*/
public String splicingKeyDisable(Object loginId) {
return getConfig().getTokenName() + ":" + loginType + ":disable:" + loginId;
}