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

总结

参数校验

  • 确保 idloginModel 参数合法,避免空值导致运行时错误。

全局配置处理

  • 获取 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中

分类: Sa-Token 标签: Sa-Token

评论

暂无评论数据

暂无评论数据

目录