288 lines
7.6 KiB
Markdown
288 lines
7.6 KiB
Markdown
|
|
# Token滑动机制技术文档
|
|||
|
|
|
|||
|
|
## 一、概述
|
|||
|
|
|
|||
|
|
本系统采用基于Redis的Token滑动窗口机制,实现用户会话的自动续期功能。当用户在系统中持续活动时,Token会自动刷新,避免频繁登录,提升用户体验。
|
|||
|
|
|
|||
|
|
## 二、核心配置
|
|||
|
|
|
|||
|
|
### 2.1 配置文件(application.yml)
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# token配置
|
|||
|
|
token:
|
|||
|
|
# 令牌自定义标识
|
|||
|
|
header: Authorization
|
|||
|
|
# 令牌密钥
|
|||
|
|
secret: abcdefghijklmnopqrstuvwxyz
|
|||
|
|
# 令牌有效期(默认30分钟,单位:分钟)
|
|||
|
|
expireTime: 10080
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**配置说明:**
|
|||
|
|
- `header`: HTTP请求头中Token的字段名
|
|||
|
|
- `secret`: JWT签名密钥
|
|||
|
|
- `expireTime`: Token有效期,当前配置为10080分钟(7天)
|
|||
|
|
|
|||
|
|
## 三、核心实现
|
|||
|
|
|
|||
|
|
### 3.1 数据模型(LoginUser)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public class LoginUser implements UserDetails {
|
|||
|
|
/**
|
|||
|
|
* 用户唯一标识
|
|||
|
|
*/
|
|||
|
|
private String token;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 登录时间
|
|||
|
|
*/
|
|||
|
|
private Long loginTime;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 过期时间
|
|||
|
|
*/
|
|||
|
|
private Long expireTime;
|
|||
|
|
|
|||
|
|
// ... 其他字段
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 Token服务(TokenService)
|
|||
|
|
|
|||
|
|
#### 3.2.1 关键常量
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 令牌有效期(从配置文件读取)
|
|||
|
|
@Value("${token.expireTime}")
|
|||
|
|
private int expireTime;
|
|||
|
|
|
|||
|
|
// 毫秒常量
|
|||
|
|
protected static final long MILLIS_SECOND = 1000;
|
|||
|
|
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
|
|||
|
|
|
|||
|
|
// 滑动窗口触发阈值:20分钟
|
|||
|
|
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2.2 创建Token
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public String createToken(LoginUser loginUser) {
|
|||
|
|
String token = IdUtils.fastUUID();
|
|||
|
|
loginUser.setToken(token);
|
|||
|
|
setUserAgent(loginUser);
|
|||
|
|
refreshToken(loginUser); // 初始化Token过期时间
|
|||
|
|
|
|||
|
|
Map<String, Object> claims = new HashMap<>();
|
|||
|
|
claims.put(Constants.LOGIN_USER_KEY, token);
|
|||
|
|
return createToken(claims);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2.3 验证Token(滑动机制核心)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
/**
|
|||
|
|
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
|
|||
|
|
*
|
|||
|
|
* @param loginUser
|
|||
|
|
*/
|
|||
|
|
public void verifyToken(LoginUser loginUser) {
|
|||
|
|
long expireTime = loginUser.getExpireTime();
|
|||
|
|
long currentTime = System.currentTimeMillis();
|
|||
|
|
|
|||
|
|
// 如果距离过期时间不足20分钟,触发刷新
|
|||
|
|
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
|
|||
|
|
refreshToken(loginUser);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2.4 刷新Token
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
/**
|
|||
|
|
* 刷新令牌有效期
|
|||
|
|
*
|
|||
|
|
* @param loginUser 登录信息
|
|||
|
|
*/
|
|||
|
|
public void refreshToken(LoginUser loginUser) {
|
|||
|
|
// 更新登录时间为当前时间
|
|||
|
|
loginUser.setLoginTime(System.currentTimeMillis());
|
|||
|
|
|
|||
|
|
// 重新计算过期时间 = 当前时间 + 配置的有效期
|
|||
|
|
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
|
|||
|
|
|
|||
|
|
// 将更新后的用户信息存入Redis,并设置过期时间
|
|||
|
|
String userKey = getTokenKey(loginUser.getToken());
|
|||
|
|
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2.5 获取登录用户
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public LoginUser getLoginUser(HttpServletRequest request) {
|
|||
|
|
// 获取请求携带的令牌
|
|||
|
|
String token = getToken(request);
|
|||
|
|
if (StringUtils.isNotEmpty(token)) {
|
|||
|
|
try {
|
|||
|
|
Claims claims = parseToken(token);
|
|||
|
|
// 解析对应的权限以及用户信息
|
|||
|
|
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
|||
|
|
String userKey = getTokenKey(uuid);
|
|||
|
|
LoginUser user = redisCache.getCacheObject(userKey);
|
|||
|
|
return user;
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
// Token解析失败
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 四、滑动机制工作流程
|
|||
|
|
|
|||
|
|
### 4.1 流程图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户请求 → 获取Token → 从Redis获取LoginUser → 验证Token有效期
|
|||
|
|
↓
|
|||
|
|
距离过期 ≤ 20分钟?
|
|||
|
|
↓ ↓
|
|||
|
|
是 否
|
|||
|
|
↓ ↓
|
|||
|
|
刷新Token 继续使用
|
|||
|
|
↓
|
|||
|
|
更新过期时间
|
|||
|
|
↓
|
|||
|
|
更新Redis缓存
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 详细说明
|
|||
|
|
|
|||
|
|
1. **用户登录**
|
|||
|
|
- 创建Token(UUID)
|
|||
|
|
- 设置初始过期时间 = 当前时间 + expireTime
|
|||
|
|
- 存入Redis,设置过期时间
|
|||
|
|
|
|||
|
|
2. **用户请求**
|
|||
|
|
- 从请求头获取Token
|
|||
|
|
- 从Redis获取LoginUser对象
|
|||
|
|
- 调用`verifyToken()`验证
|
|||
|
|
|
|||
|
|
3. **滑动窗口判断**
|
|||
|
|
- 计算剩余有效时间 = expireTime - currentTime
|
|||
|
|
- 如果剩余时间 ≤ 20分钟,触发刷新
|
|||
|
|
- 否则,继续使用当前Token
|
|||
|
|
|
|||
|
|
4. **Token刷新**
|
|||
|
|
- 更新loginTime为当前时间
|
|||
|
|
- 重新计算expireTime
|
|||
|
|
- 更新Redis中的LoginUser对象
|
|||
|
|
|
|||
|
|
## 五、关键特性
|
|||
|
|
|
|||
|
|
### 5.1 滑动窗口策略
|
|||
|
|
|
|||
|
|
- **触发条件**:距离过期时间不足20分钟
|
|||
|
|
- **刷新动作**:重置过期时间为当前时间 + expireTime
|
|||
|
|
- **优势**:用户持续活动时,Token自动续期,无需重新登录
|
|||
|
|
|
|||
|
|
### 5.2 Redis存储
|
|||
|
|
|
|||
|
|
- **Key格式**:`login_tokens:{uuid}`
|
|||
|
|
- **Value**:LoginUser对象(序列化)
|
|||
|
|
- **过期时间**:与Token过期时间一致
|
|||
|
|
- **优势**:分布式环境下共享会话,自动清理过期数据
|
|||
|
|
|
|||
|
|
### 5.3 双重过期机制
|
|||
|
|
|
|||
|
|
1. **LoginUser.expireTime**:业务层面的过期时间判断
|
|||
|
|
2. **Redis TTL**:存储层面的自动清理机制
|
|||
|
|
|
|||
|
|
## 六、配置建议
|
|||
|
|
|
|||
|
|
### 6.1 生产环境配置
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
token:
|
|||
|
|
expireTime: 120 # 2小时
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**说明**:
|
|||
|
|
- 滑动窗口触发阈值固定为20分钟
|
|||
|
|
- 如果用户在2小时内有任何操作,且距离过期不足20分钟,会自动续期2小时
|
|||
|
|
- 如果用户超过2小时无操作,Token过期,需要重新登录
|
|||
|
|
|
|||
|
|
### 6.2 开发环境配置
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
token:
|
|||
|
|
expireTime: 10080 # 7天
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**说明**:
|
|||
|
|
- 方便开发调试,减少频繁登录
|
|||
|
|
- 生产环境不建议设置过长
|
|||
|
|
|
|||
|
|
## 七、安全考虑
|
|||
|
|
|
|||
|
|
### 7.1 Token安全
|
|||
|
|
|
|||
|
|
- 使用JWT签名,防止Token篡改
|
|||
|
|
- Token存储在Redis中,支持主动失效
|
|||
|
|
- 支持单点登录控制
|
|||
|
|
|
|||
|
|
### 7.2 滑动窗口安全
|
|||
|
|
|
|||
|
|
- 20分钟的滑动窗口阈值,平衡用户体验和安全性
|
|||
|
|
- 即使Token被盗用,最长有效期仍受expireTime限制
|
|||
|
|
- 可通过删除Redis中的Token实现强制下线
|
|||
|
|
|
|||
|
|
## 八、扩展功能
|
|||
|
|
|
|||
|
|
### 8.1 积木报表Token验证
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// JimuReportTokenService.java
|
|||
|
|
public boolean isTokenValid(String token) {
|
|||
|
|
LoginUser loginUser = tokenService.getLoginUser(request);
|
|||
|
|
if (loginUser != null) {
|
|||
|
|
// 检查token是否过期
|
|||
|
|
long expireTime = loginUser.getExpireTime();
|
|||
|
|
long currentTime = System.currentTimeMillis();
|
|||
|
|
return currentTime < expireTime;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 用户代理信息记录
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public void setUserAgent(LoginUser loginUser) {
|
|||
|
|
UserAgent userAgent = UserAgent.parseUserAgentString(
|
|||
|
|
ServletUtils.getRequest().getHeader("User-Agent")
|
|||
|
|
);
|
|||
|
|
String ip = IpUtils.getIpAddr();
|
|||
|
|
loginUser.setIpaddr(ip);
|
|||
|
|
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
|||
|
|
loginUser.setBrowser(userAgent.getBrowser().getName());
|
|||
|
|
loginUser.setOs(userAgent.getOperatingSystem().getName());
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 九、总结
|
|||
|
|
|
|||
|
|
本系统的Token滑动机制通过以下方式实现了高效的会话管理:
|
|||
|
|
|
|||
|
|
1. **自动续期**:用户活跃时自动延长会话,提升体验
|
|||
|
|
2. **灵活配置**:通过配置文件调整有效期和滑动窗口
|
|||
|
|
3. **分布式支持**:基于Redis实现,支持集群部署
|
|||
|
|
4. **安全可控**:双重过期机制,支持主动失效
|
|||
|
|
|
|||
|
|
**核心优势**:在保证安全性的前提下,最大化提升用户体验,避免频繁登录带来的困扰。
|