Token架构优化:防重放攻击清除token、防重复forceLogout、refreshToken空值防御
This commit is contained in:
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"e4b8dca3f1b4ede4c30371002441c88c12187e
|
|||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
serviceWorkerSettings: {
|
||||||
serviceWorkerVersion: "112731293" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
serviceWorkerVersion: "3241221564" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29185,9 +29185,11 @@ _.c=c
|
|||||||
_.$ti=d},
|
_.$ti=d},
|
||||||
b47(a){return B.b.hY(B.a2v,new A.aS1(a))},
|
b47(a){return B.b.hY(B.a2v,new A.aS1(a))},
|
||||||
aS1:function aS1(a){this.a=a},
|
aS1:function aS1(a){this.a=a},
|
||||||
DU:function DU(a){this.a=$
|
DU:function DU(a){var _=this
|
||||||
this.b=a
|
_.a=$
|
||||||
this.c=null},
|
_.b=a
|
||||||
|
_.c=null
|
||||||
|
_.d=!1},
|
||||||
aji:function aji(){},
|
aji:function aji(){},
|
||||||
re:function re(a,b,c,d){var _=this
|
re:function re(a,b,c,d){var _=this
|
||||||
_.d=a
|
_.d=a
|
||||||
@@ -102824,11 +102826,13 @@ A.cR().$1("ResponseData: "+A.l(r?q:s.a))
|
|||||||
A.cR().$1("====================")
|
A.cR().$1("====================")
|
||||||
if((r?q:s.c)===401)return new A.cg(!1,"\u767b\u9304\u5df2\u904e\u671f\uff0c\u8acb\u91cd\u65b0\u767b\u9304",q,b.i("cg<0>"))
|
if((r?q:s.c)===401)return new A.cg(!1,"\u767b\u9304\u5df2\u904e\u671f\uff0c\u8acb\u91cd\u65b0\u767b\u9304",q,b.i("cg<0>"))
|
||||||
return new A.cg(!1,this.ajp(a),q,b.i("cg<0>"))},
|
return new A.cg(!1,this.ajp(a),q,b.i("cg<0>"))},
|
||||||
Eu(){var s,r,q
|
Eu(){var s,r,q,p=this
|
||||||
|
if(p.d)return
|
||||||
|
p.d=!0
|
||||||
A.yj()
|
A.yj()
|
||||||
s=this.c
|
s=p.c
|
||||||
if(s!=null)s.$0()
|
if(s!=null)s.$0()
|
||||||
r=$.a5.aq$.x.h(0,this.b)
|
r=$.a5.aq$.x.h(0,p.b)
|
||||||
if(r!=null){s=A.c_(r,!1)
|
if(r!=null){s=A.c_(r,!1)
|
||||||
q=s.xc("/login",null,t.X)
|
q=s.xc("/login",null,t.X)
|
||||||
q.toString
|
q.toString
|
||||||
@@ -102905,7 +102909,8 @@ g=t.z
|
|||||||
e=m.d
|
e=m.d
|
||||||
s=j!=null?15:17
|
s=j!=null?15:17
|
||||||
break
|
break
|
||||||
case 15:d=a.b
|
case 15:e.d=!1
|
||||||
|
d=a.b
|
||||||
d===$&&A.a()
|
d===$&&A.a()
|
||||||
d.m(0,"Authorization","Bearer "+j)
|
d.m(0,"Authorization","Bearer "+j)
|
||||||
e=e.a
|
e=e.a
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ApiEndpoints {
|
|||||||
/// 退出登錄
|
/// 退出登錄
|
||||||
static const String logout = '/api/user/logout';
|
static const String logout = '/api/user/logout';
|
||||||
|
|
||||||
/// 刷新 Token(後端尚未實現,預留接口)
|
/// 刷新 Token
|
||||||
static const String tokenRefresh = '/api/auth/refresh';
|
static const String tokenRefresh = '/api/auth/refresh';
|
||||||
|
|
||||||
// ==================== 行情模塊 ====================
|
// ==================== 行情模塊 ====================
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class DioClient {
|
|||||||
/// 未授權回調(token 徹底過期時觸發 AuthProvider.forceLogout)
|
/// 未授權回調(token 徹底過期時觸發 AuthProvider.forceLogout)
|
||||||
VoidCallback? onForceLogout;
|
VoidCallback? onForceLogout;
|
||||||
|
|
||||||
|
/// 防止重複 forceLogout
|
||||||
|
bool hasForceLoggedOut = false;
|
||||||
|
|
||||||
DioClient({this.navigatorKey}) {
|
DioClient({this.navigatorKey}) {
|
||||||
_dio = _createDio();
|
_dio = _createDio();
|
||||||
_setupInterceptors();
|
_setupInterceptors();
|
||||||
@@ -133,8 +136,10 @@ class DioClient {
|
|||||||
return ApiResponse.fail(message);
|
return ApiResponse.fail(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 徹底失效時清除數據並跳轉登錄頁
|
/// 徹底失效時清除數據並跳轉登錄頁(防重複調用)
|
||||||
void forceLogout() {
|
void forceLogout() {
|
||||||
|
if (hasForceLoggedOut) return;
|
||||||
|
hasForceLoggedOut = true;
|
||||||
LocalStorage.clearUserData();
|
LocalStorage.clearUserData();
|
||||||
onForceLogout?.call();
|
onForceLogout?.call();
|
||||||
final context = navigatorKey?.currentContext;
|
final context = navigatorKey?.currentContext;
|
||||||
@@ -253,6 +258,8 @@ class _TokenRefreshInterceptor extends QueuedInterceptor {
|
|||||||
try {
|
try {
|
||||||
final newToken = await _refreshToken();
|
final newToken = await _refreshToken();
|
||||||
if (newToken != null) {
|
if (newToken != null) {
|
||||||
|
// 刷新成功,重置 forceLogout 標記(用戶重新激活)
|
||||||
|
_client.hasForceLoggedOut = false;
|
||||||
// 刷新成功,更新 header 並重試原始請求
|
// 刷新成功,更新 header 並重試原始請求
|
||||||
requestOptions.headers['Authorization'] = 'Bearer $newToken';
|
requestOptions.headers['Authorization'] = 'Bearer $newToken';
|
||||||
final retryResponse = await _client._dio.fetch(requestOptions);
|
final retryResponse = await _client._dio.fetch(requestOptions);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ CREATE TABLE `sys_user` (
|
|||||||
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
|
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
|
||||||
`last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
|
`last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
|
||||||
`token` varchar(500) DEFAULT NULL COMMENT '当前Token',
|
`token` varchar(500) DEFAULT NULL COMMENT '当前Token',
|
||||||
|
`refresh_token` varchar(512) DEFAULT NULL COMMENT '刷新Token',
|
||||||
`referral_code` varchar(8) DEFAULT NULL COMMENT '推广码',
|
`referral_code` varchar(8) DEFAULT NULL COMMENT '推广码',
|
||||||
`referred_by` bigint(20) DEFAULT NULL COMMENT '推广人用户ID',
|
`referred_by` bigint(20) DEFAULT NULL COMMENT '推广人用户ID',
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
@@ -53,6 +54,7 @@ CREATE TABLE `sys_admin` (
|
|||||||
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
|
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
|
||||||
`last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
|
`last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
|
||||||
`token` varchar(500) DEFAULT NULL COMMENT '当前Token',
|
`token` varchar(500) DEFAULT NULL COMMENT '当前Token',
|
||||||
|
`refresh_token` varchar(512) DEFAULT NULL COMMENT '刷新Token',
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
|||||||
@@ -20,23 +20,27 @@ public class TokenFilter implements Filter {
|
|||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
/** 不需要验证的路径 */
|
/** 不需要验证的路径(精确匹配) */
|
||||||
private static final String[] EXCLUDE_PATHS = {
|
private static final String[] EXACT_PATHS = {
|
||||||
"/api/user/register",
|
"/api/user/register",
|
||||||
"/api/user/login",
|
"/api/user/login",
|
||||||
"/api/auth/refresh",
|
"/api/auth/refresh",
|
||||||
"/api/wallet/default",
|
"/api/wallet/default",
|
||||||
"/api/wallet/networks",
|
"/api/wallet/networks",
|
||||||
"/api/kline/",
|
|
||||||
"/ws/",
|
|
||||||
"/admin/login",
|
"/admin/login",
|
||||||
"/uploads/",
|
|
||||||
"/swagger-resources",
|
"/swagger-resources",
|
||||||
"/v2/api-docs",
|
"/v2/api-docs",
|
||||||
"/webjars/",
|
|
||||||
"/swagger-ui.html"
|
"/swagger-ui.html"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 不需要验证的路径前缀 */
|
||||||
|
private static final String[] PREFIX_PATHS = {
|
||||||
|
"/api/kline/",
|
||||||
|
"/ws/",
|
||||||
|
"/uploads/",
|
||||||
|
"/webjars/"
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
@@ -45,9 +49,17 @@ public class TokenFilter implements Filter {
|
|||||||
|
|
||||||
String uri = httpRequest.getRequestURI();
|
String uri = httpRequest.getRequestURI();
|
||||||
|
|
||||||
// 检查是否排除路径
|
// 检查是否排除路径(精确匹配)
|
||||||
for (String excludePath : EXCLUDE_PATHS) {
|
for (String path : EXACT_PATHS) {
|
||||||
if (uri.contains(excludePath)) {
|
if (uri.equals(path)) {
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否排除路径前缀
|
||||||
|
for (String prefix : PREFIX_PATHS) {
|
||||||
|
if (uri.startsWith(prefix)) {
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.it.rattan.monisuo.mapper.UserMapper;
|
|||||||
import com.it.rattan.monisuo.util.JwtUtil;
|
import com.it.rattan.monisuo.util.JwtUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ public class AuthService {
|
|||||||
* 5. 更新数据库中的 refreshToken
|
* 5. 更新数据库中的 refreshToken
|
||||||
* 6. 返回新 token 对
|
* 6. 返回新 token 对
|
||||||
*/
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Map<String, String> refreshToken(String refreshToken) {
|
public Map<String, String> refreshToken(String refreshToken) {
|
||||||
// 1. 验证 refreshToken
|
// 1. 验证 refreshToken
|
||||||
if (!JwtUtil.isValid(refreshToken)) {
|
if (!JwtUtil.isValid(refreshToken)) {
|
||||||
@@ -56,8 +58,14 @@ public class AuthService {
|
|||||||
throw new RuntimeException("账号已被禁用");
|
throw new RuntimeException("账号已被禁用");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 refreshToken 与数据库中存储的一致
|
// 验证 refreshToken 与数据库中存储的一致(防重放攻击)
|
||||||
if (!refreshToken.equals(user.getRefreshToken())) {
|
String storedRefreshToken = user.getRefreshToken();
|
||||||
|
if (storedRefreshToken == null || !refreshToken.equals(storedRefreshToken)) {
|
||||||
|
// refreshToken 不匹配说明可能被窃取,清除该用户所有 token
|
||||||
|
userMapper.update(null, new LambdaUpdateWrapper<User>()
|
||||||
|
.eq(User::getId, user.getId())
|
||||||
|
.set(User::getToken, null)
|
||||||
|
.set(User::getRefreshToken, null));
|
||||||
throw new RuntimeException("refreshToken不匹配,请重新登录");
|
throw new RuntimeException("refreshToken不匹配,请重新登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user