完善Token刷新架构:后端实现refreshToken轮转机制

后端:
- JwtUtil: accessToken 2h + refreshToken 30d,区分type
- AuthController: POST /api/auth/refresh 接口
- AuthService: 验证refreshToken → 轮转生成新token对
- UserService: 登录/注册返回refreshToken
- User entity: 添加refreshToken字段
- TokenFilter: 排除/api/auth/refresh路径
- SQL: sys_user添加refresh_token列

前端:
- DioClient: 刷新成功后同时保存新的refreshToken
This commit is contained in:
2026-04-16 13:02:16 +08:00
parent 5c7aa09207
commit 8d0ce203ef
10 changed files with 8398 additions and 8221 deletions

View File

@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"e4b8dca3f1b4ede4c30371002441c88c12187e
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "1911676977" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
serviceWorkerVersion: "112731293" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
}
});

File diff suppressed because one or more lines are too long

View File

@@ -305,8 +305,12 @@ class _TokenRefreshInterceptor extends QueuedInterceptor {
data['code'] == '0000' &&
data['data'] != null) {
final newToken = data['data']['token'] as String?;
final newRefreshToken = data['data']['refreshToken'] as String?;
if (newToken != null) {
await LocalStorage.saveToken(newToken);
if (newRefreshToken != null) {
await LocalStorage.saveRefreshToken(newRefreshToken);
}
debugPrint('Token 刷新成功');
return newToken;
}

View File

@@ -0,0 +1,2 @@
-- 添加 refreshToken 字段
ALTER TABLE sys_user ADD COLUMN refresh_token VARCHAR(512) DEFAULT NULL COMMENT '刷新Token' AFTER token;

View File

@@ -0,0 +1,39 @@
package com.it.rattan.monisuo.controller;
import com.it.rattan.monisuo.common.Result;
import com.it.rattan.monisuo.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 认证接口 - Token 刷新
*/
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthService authService;
/**
* 刷新 Token
* 请求体: { "refreshToken": "xxx" }
* 返回: { "token": "新accessToken", "refreshToken": "新refreshToken" }
*/
@PostMapping("/refresh")
public Result<Map<String, String>> refresh(@RequestBody Map<String, String> params) {
String refreshToken = params.get("refreshToken");
if (refreshToken == null || refreshToken.isEmpty()) {
return Result.fail("refreshToken不能为空");
}
try {
Map<String, String> result = authService.refreshToken(refreshToken);
return Result.success("刷新成功", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}

View File

@@ -69,6 +69,10 @@ public class User implements Serializable {
@JsonIgnore
private String token;
/** 刷新Token */
@JsonIgnore
private String refreshToken;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

View File

@@ -24,6 +24,7 @@ public class TokenFilter implements Filter {
private static final String[] EXCLUDE_PATHS = {
"/api/user/register",
"/api/user/login",
"/api/auth/refresh",
"/api/wallet/default",
"/api/wallet/networks",
"/api/kline/",

View File

@@ -0,0 +1,80 @@
package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.mapper.UserMapper;
import com.it.rattan.monisuo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 认证服务 - Token 刷新逻辑
*/
@Service
public class AuthService {
@Autowired
private UserMapper userMapper;
/**
* 刷新 Token
*
* 流程:
* 1. 验证 refreshToken 格式和签名
* 2. 确认是 refresh 类型(不是 access token
* 3. 从数据库查找用户,验证 refreshToken 匹配
* 4. 生成新的 accessToken 和 refreshToken轮转
* 5. 更新数据库中的 refreshToken
* 6. 返回新 token 对
*/
public Map<String, String> refreshToken(String refreshToken) {
// 1. 验证 refreshToken
if (!JwtUtil.isValid(refreshToken)) {
throw new RuntimeException("refreshToken已过期请重新登录");
}
// 2. 确认是 refresh 类型
if (!JwtUtil.isRefreshToken(refreshToken)) {
throw new RuntimeException("无效的refreshToken");
}
// 3. 提取 userId查找用户
Long userId = JwtUtil.getUserId(refreshToken);
if (userId == null) {
throw new RuntimeException("无效的refreshToken");
}
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getStatus() == 0) {
throw new RuntimeException("账号已被禁用");
}
// 验证 refreshToken 与数据库中存储的一致
if (!refreshToken.equals(user.getRefreshToken())) {
throw new RuntimeException("refreshToken不匹配请重新登录");
}
// 4. 生成新的 token 对(轮转机制:每次刷新都生成新的 refreshToken
String newAccessToken = JwtUtil.createToken(user.getId(), user.getUsername(), "user");
String newRefreshToken = JwtUtil.createRefreshToken(user.getId(), user.getUsername());
// 5. 更新数据库
userMapper.update(null, new LambdaUpdateWrapper<User>()
.eq(User::getId, user.getId())
.set(User::getToken, newAccessToken)
.set(User::getRefreshToken, newRefreshToken));
// 6. 返回
Map<String, String> result = new HashMap<>();
result.put("token", newAccessToken);
result.put("refreshToken", newRefreshToken);
return result;
}
}

View File

@@ -104,11 +104,14 @@ public class UserService extends ServiceImpl<UserMapper, User> {
// 生成Token
String token = JwtUtil.createToken(user.getId(), username, "user");
String refreshToken = JwtUtil.createRefreshToken(user.getId(), username);
user.setToken(token);
user.setRefreshToken(refreshToken);
userMapper.updateById(user);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("refreshToken", refreshToken);
result.put("userInfo", buildUserInfo(user));
return result;
}
@@ -132,14 +135,17 @@ public class UserService extends ServiceImpl<UserMapper, User> {
}
String token = JwtUtil.createToken(user.getId(), username, "user");
// 只更新 token 和最后登录时间,避免 updateById 全字段更新
String refreshToken = JwtUtil.createRefreshToken(user.getId(), username);
// 更新 token、refreshToken 和最后登录时间
userMapper.update(null, new LambdaUpdateWrapper<User>()
.eq(User::getId, user.getId())
.set(User::getToken, token)
.set(User::getRefreshToken, refreshToken)
.set(User::getLastLoginTime, LocalDateTime.now()));
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("refreshToken", refreshToken);
result.put("userInfo", buildUserInfo(user));
return result;
}

View File

@@ -16,8 +16,10 @@ public class JwtUtil {
/** 密钥 */
private static final String SECRET = "monisuo_jwt_secret_key_2024";
/** 过期时间(7天) */
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L;
/** Access Token 过期时间(2小时) */
private static final long EXPIRE_TIME = 2 * 60 * 60 * 1000L;
/** Refresh Token 过期时间(30天) */
private static final long REFRESH_EXPIRE_TIME = 30 * 24 * 60 * 60 * 1000L;
/** 签发者 */
private static final String ISSUER = "monisuo";
@@ -122,4 +124,38 @@ public class JwtUtil {
return false;
}
}
/**
* 生成RefreshToken30天有效期type=refresh
*/
public static String createRefreshToken(Long userId, String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + REFRESH_EXPIRE_TIME);
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
return JWT.create()
.withHeader(header)
.withIssuer(ISSUER)
.withIssuedAt(now)
.withExpiresAt(expireDate)
.withClaim("userId", userId)
.withClaim("username", username)
.withClaim("type", "refresh")
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 判断Token是否为refresh类型
*/
public static boolean isRefreshToken(String token) {
try {
DecodedJWT jwt = verifyToken(token);
return "refresh".equals(jwt.getClaim("type").asString());
} catch (JWTVerificationException e) {
return false;
}
}
}