Sa-Token源码
Sa-Token源码
登陆模块
// 使用 Sa-Token 登录,并指定设备类型(如 web、app)
StpUtil.login(userId, device);
申请Token的核心代码逻辑,注释版:
/**
* 创建登录会话,为指定用户生成一个 Token 并绑定相关会话信息。
*
* @param id 用户的唯一标识(如用户 ID)。
* @param loginModel 登录模型,包含登录时的配置参数(如设备类型、超时时间等)。
* @return 生成的 Token 值,用于后续请求认证。
*/
public String createLoginSession(Object id, SaLoginModel loginModel) {
// 1. 校验登录参数是否合法(id 和 loginModel 是否为空等)
this.checkLoginArgs(id, loginModel);
// 2. 获取 Sa-Token 配置对象(全局或局部配置)
SaTokenConfig config = this.getConfigOrGlobal();
// 3. 使用全局配置填充 loginModel 的参数(如超时时间等)
loginModel.build(config);
// 4. 生成一个可用的 Token,如果已存在可能复用;否则新生成
String tokenValue = this.distUsableToken(id, loginModel);
// 5. 获取或创建用户的全局会话(Account-Session)
// - 如果会话不存在,则会新建
// - 会话的超时时间根据 loginModel 或全局配置设置
SaSession session = this.getSessionByLoginId(id, true, loginModel.getTimeoutOrGlobalConfig());
// 6. 更新会话的最小超时时间,确保会话不会比当前 Token 的超时时间更早失效
session.updateMinTimeout(loginModel.getTimeout());
// 7. 创建一个 Token 签名(TokenSign),包含 Token 值、设备类型和自定义标记
TokenSign tokenSign = new TokenSign(tokenValue, loginModel.getDeviceOrDefault(), loginModel.getTokenSignTag());
// 8. 将 Token 签名信息添加到当前用户的全局会话(Account-Session)中
session.addTokenSign(tokenSign);
// 9. 保存 Token 和用户 ID 的映射关系(方便通过 Token 快速找到对应用户)
// - 同时设置 Token 的超时时间
this.saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
// 10. 如果启用了活动时间检查机制(ActiveTimeout),更新最后活动时间
if (this.isOpenCheckActiveTimeout()) {
// 设置最后活动时间为当前时间,同时更新活动超时时间
this.setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig());
}
// 11. 触发登录事件,通知其他组件(如记录日志、触发扩展功能等)
SaTokenEventCenter.doLogin(this.loginType, id, tokenValue, loginModel);
// 12. 检查是否启用了最大登录限制(MaxLoginCount)
// - 如果超过限制,自动注销多余的登录(如剔除最早的登录会话)
if (config.getMaxLoginCount() != -1) {
this.logoutByMaxLoginCount(id, session, (String) null, config.getMaxLoginCount());
}
// 13. 返回生成的 Token 值,供前端或客户端使用
return tokenValue;
}
总结:
参数校验
- 确保
id
和loginModel
参数合法,避免空值导致运行时错误。
全局配置处理
- 获取 Sa-Token 的全局配置(如 Token 的超时时间、最大登录数等),并填充到
loginModel
。
Token 生成
- 调用
distUsableToken
方法生成或复用 Token,用于后续的请求认证。
会话管理
- 通过
getSessionByLoginId
获取或创建用户的Account-Session
,这是全局会话,用于存储用户的共享状态。
超时管理
- 确保会话的超时时间不短于当前 Token 的超时时间,以保证用户会话的一致性。
Token 签名
- 为每个生成的 Token 创建一个
TokenSign
对象,标记 Token 所属设备(如 Web、App),并存储到全局会话中。
Token 与用户的映射
- 将 Token 和用户 ID 关联起来,方便后续通过 Token 快速查找对应的用户。
活动时间检查
- 如果启用了
ActiveTimeout
,每次登录会记录最后活动时间,避免长时间未使用的 Token 被清除。
登录事件通知
- 使用事件中心(
SaTokenEventCenter
)触发登录事件,方便扩展功能(如日志记录、插件扩展等)。
最大登录限制
- 如果同一用户的登录数量超过限制(如最多允许 5 个 Token 存在),会注销多余的 Token。
返回 Token
- 将生成的 Token 返回给调用方(如前端或客户端),用于后续的认证。
创建Token的核心逻辑
/**
* 分发一个可用的 Token。
*
* 根据登录模型生成或复用一个可用的 Token,具体行为根据并发登录策略和共享策略决定。
*
* @param id 用户的唯一标识(如用户 ID)。
* @param loginModel 登录模型,包含登录相关的配置参数(如设备类型、超时时间、Token 策略等)。
* @return 可用的 Token 值(可能是新生成的,也可能是复用的)。
*/
protected String distUsableToken(Object id, SaLoginModel loginModel) {
// 1. 获取是否允许并发登录的配置
Boolean isConcurrent = this.getConfigOrGlobal().getIsConcurrent();
// 2. 如果不允许并发登录,则直接强制下线同一用户在同一设备上的旧会话
if (!isConcurrent) {
this.replaced(id, loginModel.getDevice()); // 替换旧的会话
}
// 3. 如果登录模型中指定了 Token 值,直接返回该 Token
if (SaFoxUtil.isNotEmpty(loginModel.getToken())) {
return loginModel.getToken(); // 登录使用指定的 Token 值
} else {
// 4. 如果允许并发登录且启用了 Token 共享模式
if (isConcurrent && this.getConfigOfIsShare()) {
// 检查当前用户在指定设备上的 Token 是否已存在
String tokenValue = this.getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
if (SaFoxUtil.isNotEmpty(tokenValue)) {
return tokenValue; // 如果已存在,直接复用
}
}
// 5. 如果没有复用的 Token,生成一个新的唯一 Token
return SaStrategy.instance.generateUniqueToken.execute(
"token",
this.getConfigOfMaxTryTimes(), // 最大尝试次数,防止冲突
() -> {
// Token 的生成逻辑
return this.createTokenValue(
id,
loginModel.getDeviceOrDefault(),
loginModel.getTimeout(),
loginModel.getExtraData()
);
},
(tokenValuex) -> {
// 检查生成的 Token 是否未被使用
return this.getLoginIdNotHandle(tokenValuex) == null;
}
);
}
}
getTokenSession源码
首先通过this.getTokenValue()
来获取到token
的值。
当toekn
不为空的时候,通过token
来获取到TokenSession
。
通过this.splicingKeyTokenSession(tokenValue)
来拼接出ID。
然后调用getSessionBySessionId
获取到SaSession
对象。
// 根据 SessionId 获取 SaSession 对象,如果不存在则根据参数决定是否创建新会话
public SaSession getSessionBySessionId(String sessionId, boolean isCreate, Long timeout, Consumer<SaSession> appendOperation) {
// 1. 如果 sessionId 为空,抛出异常
if (SaFoxUtil.isEmpty(sessionId)) {
throw (new SaTokenException("SessionId 不能为空")).setCode(11072); // 自定义异常,错误代码为 11072
} else {
// 2. 尝试从持久层获取对应的会话对象(SaSession)
SaSession session = this.getSaTokenDao().getSession(sessionId);
// 3. 如果会话为空并且 isCreate 参数为 true,则创建新会话
if (session == null && isCreate) {
// 3.1 创建一个新的 SaSession 对象,使用策略模式调用 createSession 方法
session = (SaSession) SaStrategy.instance.createSession.apply(sessionId);
// 3.2 如果 appendOperation 不为空,执行传入的操作(对会话进行附加处理)
if (appendOperation != null) {
appendOperation.accept(session); // 对新创建的会话执行自定义逻辑
}
// 3.3 计算会话的超时时间
if (timeout == null) {
// 如果是 Token-Session 类型,根据 Token 的超时时间设置
if ("Token-Session".equals(session.getType())) {
timeout = this.getTokenTimeout(session.getToken());
// 如果 Token 超时时间是 -2(表示未设置),则使用全局默认超时时间
if (timeout == -2L) {
timeout = this.getConfigOrGlobal().getTimeout();
}
} else {
// 如果不是 Token-Session 类型,直接使用全局默认超时时间
timeout = this.getConfigOrGlobal().getTimeout();
}
}
// 3.4 将新创建的会话对象保存到持久层,并设置超时时间
this.getSaTokenDao().setSession(session, timeout);
}
// 4. 返回获取到的(或新创建的)会话对象
return session;
}
}
SaSession的结构
public class SaSession implements SaSetValueInterface, Serializable {
private static final long serialVersionUID = 1L;
public static final String USER = "USER";
public static final String ROLE_LIST = "ROLE_LIST";
public static final String PERMISSION_LIST = "PERMISSION_LIST";
private String id;
private String type;
private String loginType;
private Object loginId;
private String token;
private long createTime;
private Map<String, Object> dataMap = new ConcurrentHashMap();
private List<TokenSign> tokenSignList = new Vector();
}
AccountSession
会在签名中记录自己的Token列表,也就知道该用户有哪些Token
TokenSession
会直接写在成员Token中
版权申明
本文系作者 @hayaizo 原创发布在Hello World站点。未经许可,禁止转载。
暂无评论数据