完善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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
2
sql/add_refresh_token.sql
Normal file
2
sql/add_refresh_token.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- 添加 refreshToken 字段
|
||||
ALTER TABLE sys_user ADD COLUMN refresh_token VARCHAR(512) DEFAULT NULL COMMENT '刷新Token' AFTER token;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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/",
|
||||
|
||||
80
src/main/java/com/it/rattan/monisuo/service/AuthService.java
Normal file
80
src/main/java/com/it/rattan/monisuo/service/AuthService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RefreshToken(30天有效期,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user