# 1 基本原理

1、登录阶段(蓝色区域):

  • 前端发送用户名密码到登录接口
  • 后端验证成功后生成 Token 信息返回

2、API请求阶段(绿色区域):

  • 前端在后续请求的Authorization头中携带 Token
  • 后端校验Token有效性后返回数据

# 2 Token 信息

登录成功后,会返回 Token 信息 :

{
  "code": 0,
  "data": {
    "userId": 140,
    "accessToken": "8c89a136079b46c8a5bb459b66b46b41",
    "refreshToken": "3148ab50da5d41b0a809aab92a11fa57",
    "expiresTime": 1753775164774
  },
  "msg": ""
}
1
2
3
4
5
6
7
8
9
10

1、访问令牌 accessToken

用于访问需要认证的 API 接口 ,前端发起请求时需要在请求头中携带:

### Authorization: Bearer 登录时返回的 accessToken
Authorization: Bearer 8c89a136079b46c8a5bb459b66b46b41
1
2

2、刷新令牌 refreshToken

用于解决访问令牌(accessToken) 的安全续期问题:

  • accessToken 通常有效期较短(如 半个小时),过期后用户需重新登录,体验差。
  • refreshToken 有效期较长(如 7 天),用于在 accessToken 过期后 静默获取新令牌,避免频繁登录。

整体流程:

# 3 Token 管理

magic-admin 项目里在 framework 里添加了 magic-spring-boot-starter-token 模块,用于 Token 的管理,所有的 Token 存储在 Redis 中。

public interface SecurityTokenService {

    /**
     * 创建访问令牌
     * @param securityCreateTokenDTO 令牌创建参数(包含用户ID、客户端信息等)
     * @return 生成的令牌信息(含accessToken和refreshToken)
     */
    SecurityAccessTokenDTO createAccessToken(SecurityCreateTokenDTO securityCreateTokenDTO);

    /**
     * 刷新访问令牌
     * @param refreshToken 有效的刷新令牌
     * @return 新的令牌信息(含新accessToken和新refreshToken)
     */
    SecurityAccessTokenDTO refreshAccessToken(String refreshToken);

    /**
     * 主动使令牌失效
     * @param accessToken 需要失效的令牌
     * @return 被移除的令牌信息
     */
    SecurityAccessTokenDTO removeAccessToken(String accessToken);

    /**
     * 获取令牌详细信息
     * @param accessToken 待查询的令牌
     * @return 令牌完整信息(含用户ID、过期时间等)
     */
    SecurityAccessTokenDTO getAccessToken(String accessToken);

    /**
     * 验证令牌有效性
     * @param accessToken 待验证的令牌
     * @return 令牌信息
     */
    SecurityAccessTokenDTO checkAccessToken(String accessToken);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

我们重点讲解:【创建访问令牌】和【刷新访问令牌】两个方法。

# 01 建访问令牌

public SecurityAccessTokenDTO createAccessToken(SecurityCreateTokenDTO securityCreateTokenDTO) {

        // step1 :获取 客户端信息
        SecurityClientDTO securityClientDTO = clientAdapter.getClient();

        // step2 :创建 refreshToken
        SecurityRefreshTokenDTO refreshTokenDTO = new SecurityRefreshTokenDTO().setRefreshToken(generateRefreshToken()).setUserId(securityCreateTokenDTO.getUserId()).setUserType(securityCreateTokenDTO.getUserType()).setClientId(securityClientDTO.getClientId()).setExpiresTime(LocalDateTime.now().plusSeconds(securityClientDTO.getRefreshTimeout()));

        // step3 : 保存 refreshToken 到 Redis
        String refreshTokenKey = String.format(SECURITY_REFRESH_TOKEN, securityClientDTO.getNamespace(), securityClientDTO.getClientId(), refreshTokenDTO.getRefreshToken());
        long timeDiff1 = LocalDateTimeUtil.between(LocalDateTime.now(), refreshTokenDTO.getExpiresTime(), ChronoUnit.SECONDS);
        if (timeDiff1 > 0) {
            stringRedisTemplate.opsForValue().set(refreshTokenKey, JsonUtils.toJsonString(refreshTokenDTO), timeDiff1, TimeUnit.SECONDS);
        }

        // step4 :创建 accessToken
        SecurityAccessTokenDTO accessTokenDTO = new SecurityAccessTokenDTO().setAccessToken(generateAccessToken()).setUserId(refreshTokenDTO.getUserId()).setUserType(refreshTokenDTO.getUserType()).setClientId(refreshTokenDTO.getClientId()).setRefreshToken(refreshTokenDTO.getRefreshToken()).setExpiresTime(LocalDateTime.now().plusSeconds(securityClientDTO.getAccessTimeout()));

        // step5 : 保存 accessToken 到 Redis
        String accessTokenKey = String.format(SECURITY_ACCESS_TOKEN, securityClientDTO.getNamespace(), securityClientDTO.getClientId(), accessTokenDTO.getAccessToken());
        long timeDiff2 = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDTO.getExpiresTime(), ChronoUnit.SECONDS);
        if (timeDiff2 > 0) {
            stringRedisTemplate.opsForValue().set(accessTokenKey, JsonUtils.toJsonString(accessTokenDTO), timeDiff2, TimeUnit.SECONDS);
        }

        return accessTokenDTO;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这段代码实现创建令牌(Token)的核心逻辑,主要包含以下处理流程:

1、客户端信息获取

  • 通过clientAdapter获取当前客户端配置信息(包括命名空间、超时时间等参数)

2、刷新令牌生成与存储

  • 生成唯一的刷新令牌字符串
  • 绑定用户ID、用户类型和客户端ID等关键信息
  • 根据配置计算刷新令牌的有效期
  • 按照固定格式构造Redis存储键名
  • 计算剩余有效时间并存入Redis(带自动过期)

3、访问令牌生成与存储

  • 生成唯一的访问令牌字符串
  • 继承刷新令牌中的用户和客户端信息
  • 关联对应的刷新令牌
  • 设置较短的有效期(相比刷新令牌)
  • 同样存入Redis并设置自动过期

关键设计特点:

  • 采用长时效刷新令牌+短时效访问令牌的双令牌机制
  • 通过命名空间和客户端ID实现系统隔离
  • 所有令牌信息都加密存储在Redis中
  • 精确的时间计算确保令牌自动过期
  • 令牌间通过 refreshToken 字段建立关联关系

# 02 刷新访问令牌

public SecurityAccessTokenDTO refreshAccessToken(String refreshToken) {
    // 第一步:获取客户端认证信息
    SecurityClientDTO securityClientDTO = clientAdapter.getClient();
    
    // 第二步:验证刷新令牌有效性
    String refreshTokenKey = String.format(
        SECURITY_REFRESH_TOKEN, 
        securityClientDTO.getNamespace(),  // 系统命名空间
        securityClientDTO.getClientId(),   // 客户端ID 
        refreshToken                       // 传入的刷新令牌
    );
    
    // 从Redis获取令牌数据(JSON格式)
    SecurityAccessTokenDTO refreshTokenDTO = JsonUtils.parseObject(
        stringRedisTemplate.opsForValue().get(refreshTokenKey),
        SecurityAccessTokenDTO.class
    );
    
    // 情况1:令牌不存在
    if (refreshTokenDTO == null) {
        throw exception0(
            GlobalErrorCodeConstants.BAD_REQUEST.getCode(), 
            "无效的刷新令牌"  // 可能已被撤销或伪造
        );
    }
    
    // 第三步:检查令牌有效期
    if (DateUtils.isExpired(refreshTokenDTO.getExpiresTime())) {
        // 主动清理过期令牌
        stringRedisTemplate.delete(refreshTokenKey); 
        throw exception0(
            GlobalErrorCodeConstants.UNAUTHORIZED.getCode(),
            "刷新令牌已过期"  // 需重新登录获取新令牌
        );
    }
    
    // 第四步:生成新访问令牌
    SecurityCreateTokenDTO securityCreateTokenDTO = new SecurityCreateTokenDTO()
        .setUserType(refreshTokenDTO.getUserType())  // 继承用户类型
        .setUserId(refreshTokenDTO.getUserId());     // 继承用户ID
    
    // 委托给专用方法创建令牌(复用创建逻辑)
    return createAccessToken(securityCreateTokenDTO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

1、客户端验证阶段

通过clientAdapter获取客户端配置信息(包含命名空间、客户端ID等)

2、刷新令牌验证阶段

  • 构造Redis键名:security:refresh_token:{namespace}:{clientId}:{token}
  • 从Redis查询令牌数据,如果不存在则抛出"无效令牌"异常
  • 检查令牌过期时间,如果已过期则清理Redis并抛出"令牌过期"异常

3、创建访问令牌:复用创建访问令牌方法,生成访问令牌。

上次更新: 2026/6/28