refactor: Token认证从JWT改为UUID+Redis方案

- fund-common: 新增TokenInfo和TokenService类
- fund-sys: AuthServiceImpl改用TokenService,移除JwtUtil
- fund-gateway: 新增TokenAuthFilter和ReactiveTokenService
- 移除JWT依赖,支持主动登出和强制踢下线功能
This commit is contained in:
zhangjf 2026-02-20 11:23:32 +08:00
parent eeea69d512
commit f3b7576bf1
9 changed files with 503 additions and 262 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
return chain.filter(exchange);
}
// 提取租户信息和用户信息 // 将租户组信息写入请求头
String tenantId = claims.get("tenantId", String.class); ServerHttpRequest mutatedRequest = request.mutate()
Long userId = claims.get("userId", Long.class); .header(HEADER_TENANT_GROUP, tenantGroup)
String username = claims.get("username", String.class); .build();
// 将租户信息和用户信息写入请求头 logger.debug("[TenantGateway] 租户ID: {}, 租户组: {}, 用户: {}, 路径: {}",
ServerHttpRequest mutatedRequest = request.mutate() tenantId, tenantGroup, username, path);
.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: {}, 租户组:{}, 用户:{}, 路径:{}", return chain.filter(exchange.mutate().request(mutatedRequest).build());
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之后执行
} }
} }

View File

@ -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;
}
}
/** /**
* 返回未授权响应 * 返回未授权响应
*/ */

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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());
} }

View File

@ -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);
}
}