# 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
2
3
4
5
6
7
8
9
10
1、访问令牌 accessToken
用于访问需要认证的 API 接口 ,前端发起请求时需要在请求头中携带:
### Authorization: Bearer 登录时返回的 accessToken
Authorization: Bearer 8c89a136079b46c8a5bb459b66b46b41
1
2
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
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
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
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、创建访问令牌:复用创建访问令牌方法,生成访问令牌。
