refactor: Token认证从JWT改为UUID+Redis方案
- fund-common: 新增TokenInfo和TokenService类 - fund-sys: AuthServiceImpl改用TokenService,移除JwtUtil - fund-gateway: 新增TokenAuthFilter和ReactiveTokenService - 移除JWT依赖,支持主动登出和强制踢下线功能
This commit is contained in:
parent
eeea69d512
commit
f3b7576bf1
@ -0,0 +1,95 @@
|
|||||||
|
package com.fundplatform.common.auth;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token信息实体
|
||||||
|
* 存储在Redis中的用户会话信息
|
||||||
|
*/
|
||||||
|
public class TokenInfo implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录时间戳
|
||||||
|
*/
|
||||||
|
private Long loginTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token过期时间戳
|
||||||
|
*/
|
||||||
|
private Long expireTime;
|
||||||
|
|
||||||
|
public TokenInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenInfo(Long userId, String username, Long tenantId, Long expireTime) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.username = username;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
this.loginTime = System.currentTimeMillis();
|
||||||
|
this.expireTime = expireTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTenantId(Long tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLoginTime() {
|
||||||
|
return loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoginTime(Long loginTime) {
|
||||||
|
this.loginTime = loginTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getExpireTime() {
|
||||||
|
return expireTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpireTime(Long expireTime) {
|
||||||
|
this.expireTime = expireTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Token是否过期
|
||||||
|
*/
|
||||||
|
public boolean isExpired() {
|
||||||
|
return System.currentTimeMillis() > expireTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,231 @@
|
|||||||
|
package com.fundplatform.common.auth;
|
||||||
|
|
||||||
|
import com.fundplatform.common.cache.RedisService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token服务 - 基于UUID + Redis实现
|
||||||
|
* 替代JWT方案,提供更好的token控制能力
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TokenService {
|
||||||
|
|
||||||
|
private final RedisService redisService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token缓存Key前缀
|
||||||
|
*/
|
||||||
|
private static final String TOKEN_KEY_PREFIX = "auth:token:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户Token列表Key前缀(用于支持单用户多设备登录控制)
|
||||||
|
*/
|
||||||
|
private static final String USER_TOKENS_PREFIX = "auth:user:tokens:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认Token过期时间:24小时
|
||||||
|
*/
|
||||||
|
private static final long DEFAULT_EXPIRE_SECONDS = 24 * 60 * 60;
|
||||||
|
|
||||||
|
public TokenService(RedisService redisService) {
|
||||||
|
this.redisService = redisService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成Token
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param username 用户名
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return Token字符串
|
||||||
|
*/
|
||||||
|
public String generateToken(Long userId, String username, Long tenantId) {
|
||||||
|
return generateToken(userId, username, tenantId, DEFAULT_EXPIRE_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成Token(指定过期时间)
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param username 用户名
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @param expireSeconds 过期时间(秒)
|
||||||
|
* @return Token字符串
|
||||||
|
*/
|
||||||
|
public String generateToken(Long userId, String username, Long tenantId, long expireSeconds) {
|
||||||
|
// 生成UUID作为Token
|
||||||
|
String token = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
|
||||||
|
// 计算过期时间戳
|
||||||
|
long expireTime = System.currentTimeMillis() + expireSeconds * 1000;
|
||||||
|
|
||||||
|
// 创建Token信息
|
||||||
|
TokenInfo tokenInfo = new TokenInfo(userId, username, tenantId, expireTime);
|
||||||
|
|
||||||
|
// 存储到Redis
|
||||||
|
String tokenKey = getTokenKey(token);
|
||||||
|
redisService.setEx(tokenKey, tokenInfo, expireSeconds);
|
||||||
|
|
||||||
|
// 同时存储用户Token列表(用于管理用户所有登录会话)
|
||||||
|
String userTokensKey = getUserTokensKey(userId);
|
||||||
|
redisService.hSet(userTokensKey, token, expireTime);
|
||||||
|
redisService.expire(userTokensKey, expireSeconds, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Token并获取用户信息
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return TokenInfo,如果无效返回null
|
||||||
|
*/
|
||||||
|
public TokenInfo validateToken(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tokenKey = getTokenKey(token);
|
||||||
|
TokenInfo tokenInfo = redisService.get(tokenKey, TokenInfo.class);
|
||||||
|
|
||||||
|
if (tokenInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (tokenInfo.isExpired()) {
|
||||||
|
// 清除过期Token
|
||||||
|
deleteToken(token);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新Token过期时间
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
public boolean refreshToken(String token) {
|
||||||
|
return refreshToken(token, DEFAULT_EXPIRE_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新Token过期时间(指定过期时间)
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @param expireSeconds 过期时间(秒)
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
public boolean refreshToken(String token, long expireSeconds) {
|
||||||
|
TokenInfo tokenInfo = validateToken(token);
|
||||||
|
if (tokenInfo == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新过期时间
|
||||||
|
long expireTime = System.currentTimeMillis() + expireSeconds * 1000;
|
||||||
|
tokenInfo.setExpireTime(expireTime);
|
||||||
|
|
||||||
|
// 更新Redis中的Token
|
||||||
|
String tokenKey = getTokenKey(token);
|
||||||
|
redisService.setEx(tokenKey, tokenInfo, expireSeconds);
|
||||||
|
|
||||||
|
// 更新用户Token列表
|
||||||
|
String userTokensKey = getUserTokensKey(tokenInfo.getUserId());
|
||||||
|
redisService.hSet(userTokensKey, token, expireTime);
|
||||||
|
redisService.expire(userTokensKey, expireSeconds, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除Token(登出)
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
*/
|
||||||
|
public void deleteToken(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Token信息以移除用户Token列表
|
||||||
|
String tokenKey = getTokenKey(token);
|
||||||
|
TokenInfo tokenInfo = redisService.get(tokenKey, TokenInfo.class);
|
||||||
|
|
||||||
|
// 删除Token
|
||||||
|
redisService.delete(tokenKey);
|
||||||
|
|
||||||
|
// 从用户Token列表中移除
|
||||||
|
if (tokenInfo != null) {
|
||||||
|
String userTokensKey = getUserTokensKey(tokenInfo.getUserId());
|
||||||
|
redisService.hDelete(userTokensKey, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户所有Token(强制登出所有设备)
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
public void deleteAllUserTokens(Long userId) {
|
||||||
|
String userTokensKey = getUserTokensKey(userId);
|
||||||
|
java.util.Map<Object, Object> tokens = redisService.hGetAll(userTokensKey);
|
||||||
|
|
||||||
|
if (tokens != null && !tokens.isEmpty()) {
|
||||||
|
// 删除所有Token
|
||||||
|
for (Object token : tokens.keySet()) {
|
||||||
|
redisService.delete(getTokenKey((String) token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户Token列表
|
||||||
|
redisService.delete(userTokensKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Token是否存在
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
|
public boolean existsToken(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return redisService.hasKey(getTokenKey(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Token剩余有效时间(秒)
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return 剩余秒数,-1表示不存在或已过期
|
||||||
|
*/
|
||||||
|
public long getTokenTTL(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Long ttl = redisService.getExpire(getTokenKey(token));
|
||||||
|
return ttl != null ? ttl : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建Token存储Key
|
||||||
|
*/
|
||||||
|
private String getTokenKey(String token) {
|
||||||
|
return TOKEN_KEY_PREFIX + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户Token列表Key
|
||||||
|
*/
|
||||||
|
private String getUserTokensKey(Long userId) {
|
||||||
|
return USER_TOKENS_PREFIX + userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,26 +35,7 @@
|
|||||||
<version>4.0.0</version>
|
<version>4.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- JWT -->
|
<!-- Redis for Rate Limiting & Token Storage -->
|
||||||
<dependency>
|
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
|
||||||
<artifactId>jjwt-api</artifactId>
|
|
||||||
<version>0.11.5</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
|
||||||
<artifactId>jjwt-impl</artifactId>
|
|
||||||
<version>0.11.5</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
|
||||||
<artifactId>jjwt-jackson</artifactId>
|
|
||||||
<version>0.11.5</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Redis for Rate Limiting -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||||
|
|||||||
@ -1,37 +1,30 @@
|
|||||||
package com.fundplatform.gateway.filter;
|
package com.fundplatform.gateway.filter;
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户信息全局过滤器
|
* 租户信息全局过滤器
|
||||||
*
|
*
|
||||||
* <p>从 JWT Token 中提取租户 ID,并写入请求头,供下游服务使用</p>
|
* <p>从请求头中获取用户信息(由TokenAuthFilter解析),构建租户组信息</p>
|
||||||
|
* <p>在TokenAuthFilter之后执行,用于多租户路由</p>
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class TenantGatewayFilter implements GlobalFilter, Ordered {
|
public class TenantGatewayFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TenantGatewayFilter.class);
|
private static final Logger logger = LoggerFactory.getLogger(TenantGatewayFilter.class);
|
||||||
private static final String SECRET_KEY = "fundplatform-secret-key-for-jwt-token-generation-min-256-bits";
|
|
||||||
private static final String TOKEN_PREFIX = "Bearer ";
|
|
||||||
|
|
||||||
// Header 名称
|
// Header 名称(由TokenAuthFilter设置)
|
||||||
public static final String HEADER_TENANT_ID = "X-Tenant-Id";
|
public static final String HEADER_TENANT_ID = "X-Tenant-Id";
|
||||||
public static final String HEADER_TENANT_GROUP = "X-Tenant-Group";
|
public static final String HEADER_TENANT_GROUP = "X-Tenant-Group";
|
||||||
public static final String HEADER_USER_ID = "X-User-Id";
|
public static final String HEADER_USER_ID = "X-User-Id";
|
||||||
@ -53,44 +46,23 @@ public class TenantGatewayFilter implements GlobalFilter, Ordered {
|
|||||||
return chain.filter(exchange);
|
return chain.filter(exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Token
|
// 从请求头获取用户信息(由TokenAuthFilter设置)
|
||||||
String token = getToken(request);
|
String tenantId = request.getHeaders().getFirst(HEADER_TENANT_ID);
|
||||||
if (token == null) {
|
String userId = request.getHeaders().getFirst(HEADER_USER_ID);
|
||||||
logger.warn("缺少 Token: {}", path);
|
String username = request.getHeaders().getFirst(HEADER_USERNAME);
|
||||||
return chain.filter(exchange);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// 构建租户组信息
|
||||||
// 解析 Token
|
String tenantGroup = buildTenantGroup(tenantId);
|
||||||
Claims claims = validateToken(token);
|
|
||||||
if (claims == null) {
|
// 将租户组信息写入请求头
|
||||||
logger.warn("Token 无效:{}", path);
|
ServerHttpRequest mutatedRequest = request.mutate()
|
||||||
return chain.filter(exchange);
|
.header(HEADER_TENANT_GROUP, tenantGroup)
|
||||||
}
|
.build();
|
||||||
|
|
||||||
// 提取租户信息和用户信息
|
logger.debug("[TenantGateway] 租户ID: {}, 租户组: {}, 用户: {}, 路径: {}",
|
||||||
String tenantId = claims.get("tenantId", String.class);
|
tenantId, tenantGroup, username, path);
|
||||||
Long userId = claims.get("userId", Long.class);
|
|
||||||
String username = claims.get("username", String.class);
|
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
||||||
|
|
||||||
// 将租户信息和用户信息写入请求头
|
|
||||||
ServerHttpRequest mutatedRequest = request.mutate()
|
|
||||||
.header(HEADER_TENANT_ID, tenantId != null ? tenantId : "1")
|
|
||||||
.header(HEADER_TENANT_GROUP, buildTenantGroup(tenantId != null ? tenantId : "1"))
|
|
||||||
.header(HEADER_USER_ID, userId != null ? String.valueOf(userId) : "")
|
|
||||||
.header(HEADER_USERNAME, username != null ? username : "")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
logger.debug("[TenantGateway] 租户ID: {}, 租户组:{}, 用户:{}, 路径:{}",
|
|
||||||
tenantId, buildTenantGroup(tenantId != null ? tenantId : "1"),
|
|
||||||
username, path);
|
|
||||||
|
|
||||||
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("租户信息提取失败:{}", e.getMessage());
|
|
||||||
return chain.filter(exchange);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,34 +72,6 @@ public class TenantGatewayFilter implements GlobalFilter, Ordered {
|
|||||||
return WHITE_LIST.stream().anyMatch(path::startsWith);
|
return WHITE_LIST.stream().anyMatch(path::startsWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从请求头获取 Token
|
|
||||||
*/
|
|
||||||
private String getToken(ServerHttpRequest request) {
|
|
||||||
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
|
||||||
if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) {
|
|
||||||
return authHeader.substring(TOKEN_PREFIX.length());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证 Token
|
|
||||||
*/
|
|
||||||
private Claims validateToken(String token) {
|
|
||||||
try {
|
|
||||||
SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(key)
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Token 解析失败:{}", e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建租户组名称
|
* 构建租户组名称
|
||||||
*/
|
*/
|
||||||
@ -140,6 +84,6 @@ public class TenantGatewayFilter implements GlobalFilter, Ordered {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getOrder() {
|
public int getOrder() {
|
||||||
return Ordered.HIGHEST_PRECEDENCE + 2; // 在 JwtAuthFilter 之后
|
return Ordered.HIGHEST_PRECEDENCE + 2; // 在TokenAuthFilter之后执行
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,11 @@ package com.fundplatform.gateway.filter;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fundplatform.common.auth.TokenInfo;
|
||||||
import com.fundplatform.common.core.Result;
|
import com.fundplatform.common.core.Result;
|
||||||
import io.jsonwebtoken.Claims;
|
import com.fundplatform.gateway.service.ReactiveTokenService;
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
@ -22,20 +20,19 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT鉴权过滤器
|
* Token鉴权过滤器
|
||||||
* 验证JWT Token,提取用户信息
|
* 基于UUID + Redis实现,替代JWT方案
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class JwtAuthFilter implements GlobalFilter, Ordered {
|
public class TokenAuthFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TokenAuthFilter.class);
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthFilter.class);
|
|
||||||
private static final String SECRET_KEY = "fundplatform-secret-key-for-jwt-token-generation-min-256-bits";
|
|
||||||
private static final String TOKEN_PREFIX = "Bearer ";
|
private static final String TOKEN_PREFIX = "Bearer ";
|
||||||
private static final String USER_ID_HEADER = "X-User-Id";
|
private static final String USER_ID_HEADER = "X-User-Id";
|
||||||
private static final String USERNAME_HEADER = "X-Username";
|
private static final String USERNAME_HEADER = "X-Username";
|
||||||
@ -49,7 +46,13 @@ public class JwtAuthFilter implements GlobalFilter, Ordered {
|
|||||||
"/proj/api/v1/proj/health"
|
"/proj/api/v1/proj/health"
|
||||||
);
|
);
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ReactiveTokenService reactiveTokenService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public TokenAuthFilter(ReactiveTokenService reactiveTokenService) {
|
||||||
|
this.reactiveTokenService = reactiveTokenService;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||||
@ -67,33 +70,26 @@ public class JwtAuthFilter implements GlobalFilter, Ordered {
|
|||||||
return unauthorized(exchange, "缺少认证Token");
|
return unauthorized(exchange, "缺少认证Token");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 使用响应式方式验证Token
|
||||||
// 验证Token
|
return reactiveTokenService.validateToken(token)
|
||||||
Claims claims = validateToken(token);
|
.flatMap(tokenInfo -> {
|
||||||
if (claims == null) {
|
if (tokenInfo == null) {
|
||||||
return unauthorized(exchange, "Token无效或已过期");
|
return unauthorized(exchange, "Token无效或已过期");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取用户信息
|
// 将用户信息写入请求头
|
||||||
Long userId = claims.get("userId", Long.class);
|
ServerHttpRequest mutatedRequest = request.mutate()
|
||||||
String username = claims.get("username", String.class);
|
.header(USER_ID_HEADER, String.valueOf(tokenInfo.getUserId()))
|
||||||
Long tenantId = claims.get("tenantId", Long.class);
|
.header(USERNAME_HEADER, tokenInfo.getUsername())
|
||||||
|
.header(TENANT_ID_HEADER, String.valueOf(tokenInfo.getTenantId()))
|
||||||
|
.build();
|
||||||
|
|
||||||
// 将用户信息写入请求头
|
logger.debug("Token验证通过: userId={}, username={}, tenantId={}",
|
||||||
ServerHttpRequest mutatedRequest = request.mutate()
|
tokenInfo.getUserId(), tokenInfo.getUsername(), tokenInfo.getTenantId());
|
||||||
.header(USER_ID_HEADER, String.valueOf(userId))
|
|
||||||
.header(USERNAME_HEADER, username)
|
|
||||||
.header(TENANT_ID_HEADER, String.valueOf(tenantId))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
logger.debug("Token验证通过: userId={}, username={}, tenantId={}", userId, username, tenantId);
|
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
||||||
|
})
|
||||||
return chain.filter(exchange.mutate().request(mutatedRequest).build());
|
.switchIfEmpty(unauthorized(exchange, "Token无效或已过期"));
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Token验证失败: {}", e.getMessage());
|
|
||||||
return unauthorized(exchange, "Token验证失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,23 +110,6 @@ public class JwtAuthFilter implements GlobalFilter, Ordered {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证Token
|
|
||||||
*/
|
|
||||||
private Claims validateToken(String token) {
|
|
||||||
try {
|
|
||||||
SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(key)
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Token解析失败: {}", e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回未授权响应
|
* 返回未授权响应
|
||||||
*/
|
*/
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
package com.fundplatform.gateway.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fundplatform.common.auth.TokenInfo;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应式Token服务
|
||||||
|
* 用于Gateway的Token验证
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ReactiveTokenService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ReactiveTokenService.class);
|
||||||
|
|
||||||
|
private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token缓存Key前缀
|
||||||
|
*/
|
||||||
|
private static final String TOKEN_KEY_PREFIX = "auth:token:";
|
||||||
|
|
||||||
|
public ReactiveTokenService(ReactiveRedisTemplate<String, Object> reactiveRedisTemplate) {
|
||||||
|
this.reactiveRedisTemplate = reactiveRedisTemplate;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Token并获取用户信息
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return Mono<TokenInfo>,如果无效返回Mono.empty()
|
||||||
|
*/
|
||||||
|
public Mono<TokenInfo> validateToken(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String tokenKey = getTokenKey(token);
|
||||||
|
|
||||||
|
return reactiveRedisTemplate.opsForValue()
|
||||||
|
.get(tokenKey)
|
||||||
|
.map(this::convertToTokenInfo)
|
||||||
|
.flatMap(tokenInfo -> {
|
||||||
|
if (tokenInfo == null || tokenInfo.isExpired()) {
|
||||||
|
// Token过期,删除
|
||||||
|
return reactiveRedisTemplate.delete(tokenKey)
|
||||||
|
.then(Mono.empty());
|
||||||
|
}
|
||||||
|
return Mono.just(tokenInfo);
|
||||||
|
})
|
||||||
|
.onErrorResume(e -> {
|
||||||
|
logger.error("Token验证失败: {}", e.getMessage());
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Token是否存在
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return Mono<Boolean>
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> existsToken(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return Mono.just(false);
|
||||||
|
}
|
||||||
|
return reactiveRedisTemplate.hasKey(getTokenKey(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除Token
|
||||||
|
*
|
||||||
|
* @param token Token字符串
|
||||||
|
* @return Mono<Boolean>
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> deleteToken(String token) {
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
return Mono.just(false);
|
||||||
|
}
|
||||||
|
return reactiveRedisTemplate.delete(getTokenKey(token))
|
||||||
|
.map(count -> count > 0)
|
||||||
|
.defaultIfEmpty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建Token存储Key
|
||||||
|
*/
|
||||||
|
private String getTokenKey(String token) {
|
||||||
|
return TOKEN_KEY_PREFIX + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Redis中的对象转换为TokenInfo
|
||||||
|
*/
|
||||||
|
private TokenInfo convertToTokenInfo(Object obj) {
|
||||||
|
try {
|
||||||
|
if (obj instanceof TokenInfo) {
|
||||||
|
return (TokenInfo) obj;
|
||||||
|
}
|
||||||
|
// 尝试Jackson转换
|
||||||
|
return objectMapper.convertValue(obj, TokenInfo.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("TokenInfo转换失败: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,25 +75,6 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- JWT -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
|
||||||
<artifactId>jjwt-api</artifactId>
|
|
||||||
<version>0.11.5</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
|
||||||
<artifactId>jjwt-impl</artifactId>
|
|
||||||
<version>0.11.5</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
|
||||||
<artifactId>jjwt-jackson</artifactId>
|
|
||||||
<version>0.11.5</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- BCrypt密码加密 -->
|
<!-- BCrypt密码加密 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package com.fundplatform.sys.service.impl;
|
package com.fundplatform.sys.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.fundplatform.common.auth.TokenService;
|
||||||
import com.fundplatform.sys.data.entity.SysUser;
|
import com.fundplatform.sys.data.entity.SysUser;
|
||||||
import com.fundplatform.sys.data.service.SysUserDataService;
|
import com.fundplatform.sys.data.service.SysUserDataService;
|
||||||
import com.fundplatform.sys.dto.LoginRequestDTO;
|
import com.fundplatform.sys.dto.LoginRequestDTO;
|
||||||
import com.fundplatform.sys.service.AuthService;
|
import com.fundplatform.sys.service.AuthService;
|
||||||
import com.fundplatform.sys.utils.JwtUtil;
|
|
||||||
import com.fundplatform.sys.vo.LoginVO;
|
import com.fundplatform.sys.vo.LoginVO;
|
||||||
import com.fundplatform.sys.vo.UserVO;
|
import com.fundplatform.sys.vo.UserVO;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
@ -18,10 +18,12 @@ import org.springframework.stereotype.Service;
|
|||||||
public class AuthServiceImpl implements AuthService {
|
public class AuthServiceImpl implements AuthService {
|
||||||
|
|
||||||
private final SysUserDataService userDataService;
|
private final SysUserDataService userDataService;
|
||||||
|
private final TokenService tokenService;
|
||||||
private final BCryptPasswordEncoder passwordEncoder;
|
private final BCryptPasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
public AuthServiceImpl(SysUserDataService userDataService) {
|
public AuthServiceImpl(SysUserDataService userDataService, TokenService tokenService) {
|
||||||
this.userDataService = userDataService;
|
this.userDataService = userDataService;
|
||||||
|
this.tokenService = tokenService;
|
||||||
this.passwordEncoder = new BCryptPasswordEncoder();
|
this.passwordEncoder = new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +53,8 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
throw new RuntimeException("用户已被禁用");
|
throw new RuntimeException("用户已被禁用");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成Token
|
// 使用UUID + Redis生成Token
|
||||||
String token = JwtUtil.generateToken(user.getId(), user.getUsername(), user.getTenantId());
|
String token = tokenService.generateToken(user.getId(), user.getUsername(), user.getTenantId());
|
||||||
|
|
||||||
// 返回登录信息
|
// 返回登录信息
|
||||||
return new LoginVO(user.getId(), user.getUsername(), token, user.getTenantId());
|
return new LoginVO(user.getId(), user.getUsername(), token, user.getTenantId());
|
||||||
@ -60,8 +62,9 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logout(Long userId) {
|
public void logout(Long userId) {
|
||||||
// TODO: 可以在此处清除用户的Token缓存或记录登出日志
|
// 清除用户所有Token(强制登出所有设备)
|
||||||
// 目前JWT是无状态的,登出只需前端清除Token即可
|
// 如果只需要登出当前设备,需要从前端传递token
|
||||||
|
tokenService.deleteAllUserTokens(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -77,7 +80,7 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成新Token
|
// 生成新Token
|
||||||
String token = JwtUtil.generateToken(user.getId(), user.getUsername(), user.getTenantId());
|
String token = tokenService.generateToken(user.getId(), user.getUsername(), user.getTenantId());
|
||||||
|
|
||||||
return new LoginVO(user.getId(), user.getUsername(), token, user.getTenantId());
|
return new LoginVO(user.getId(), user.getUsername(), token, user.getTenantId());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
package com.fundplatform.sys.utils;
|
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT工具类
|
|
||||||
*/
|
|
||||||
public class JwtUtil {
|
|
||||||
|
|
||||||
private static final String SECRET_KEY = "fundplatform-secret-key-for-jwt-token-generation-min-256-bits";
|
|
||||||
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000; // 24小时
|
|
||||||
|
|
||||||
private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成Token
|
|
||||||
*/
|
|
||||||
public static String generateToken(Long userId, String username, Long tenantId) {
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
|
||||||
claims.put("userId", userId);
|
|
||||||
claims.put("username", username);
|
|
||||||
claims.put("tenantId", tenantId);
|
|
||||||
|
|
||||||
return Jwts.builder()
|
|
||||||
.setClaims(claims)
|
|
||||||
.setSubject(username)
|
|
||||||
.setIssuedAt(new Date())
|
|
||||||
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
|
|
||||||
.signWith(KEY, SignatureAlgorithm.HS256)
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析Token
|
|
||||||
*/
|
|
||||||
public static Claims parseToken(String token) {
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(KEY)
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证Token
|
|
||||||
*/
|
|
||||||
public static boolean validateToken(String token) {
|
|
||||||
try {
|
|
||||||
Claims claims = parseToken(token);
|
|
||||||
return claims.getExpiration().after(new Date());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Token获取用户ID
|
|
||||||
*/
|
|
||||||
public static Long getUserId(String token) {
|
|
||||||
Claims claims = parseToken(token);
|
|
||||||
return claims.get("userId", Long.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Token获取用户名
|
|
||||||
*/
|
|
||||||
public static String getUsername(String token) {
|
|
||||||
Claims claims = parseToken(token);
|
|
||||||
return claims.get("username", String.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Token获取租户ID
|
|
||||||
*/
|
|
||||||
public static Long getTenantId(String token) {
|
|
||||||
Claims claims = parseToken(token);
|
|
||||||
return claims.get("tenantId", Long.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user