修改redis的缓存

This commit is contained in:
panxuejie 2026-01-09 10:21:28 +08:00
parent 70fe35510b
commit a174cb255f
27 changed files with 256 additions and 82 deletions

View File

@ -50,6 +50,13 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>

View File

@ -26,5 +26,9 @@ public class CommonConstant {
public static final String ROLE_USER = "user";
// Redis key
public static final String REDIS_KEY_TACIT = "tacit:";
public static final String REDIS_KEY_USER_TOKEN = "user:token:";
public static final String REDIS_KEY_TACIT_USER_TOKEN = "tacit:user:token:";
}

View File

@ -0,0 +1,89 @@
package com.tacit.common.utils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
/**
* Security工具类提供获取当前用户信息的方法
* @author lidongjin
* @date 2026-01-09
*/
public class SecurityUtils {
/**
* 从当前认证上下文获取用户ID
* @return 用户ID
*/
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
// 获取认证对象的details这里存储了userId等信息
Object details = authentication.getDetails();
if (details == null || !(details instanceof java.util.Map)) {
return null;
}
// 从map中获取userId
java.util.Map<?, ?> userDetails = (java.util.Map<?, ?>) details;
Object userIdObj = userDetails.get("userId");
if (userIdObj == null) {
return null;
}
if (userIdObj instanceof Long) {
return (Long) userIdObj;
}
if (userIdObj instanceof Integer) {
return ((Integer) userIdObj).longValue();
}
return Long.parseLong(userIdObj.toString());
}
/**
* 从当前认证上下文获取用户名
* @return 用户名
*/
public static String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal == null || !(principal instanceof User)) {
return null;
}
return ((User) principal).getUsername();
}
/**
* 从当前认证上下文获取用户角色
* @return 用户角色
*/
public static String getCurrentUserRole() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return null;
}
// 获取第一个角色
return authentication.getAuthorities().stream()
.findFirst()
.map(grantedAuthority -> {
String role = grantedAuthority.getAuthority();
// 移除ROLE_前缀
if (role.startsWith("ROLE_")) {
return role.substring(5);
}
return role;
})
.orElse(null);
}
}

View File

@ -1,7 +1,5 @@
package com.tacit.common.utils;
import com.tacit.common.constant.CommonConstant;
import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

View File

@ -22,6 +22,12 @@
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.tacit</groupId>
<artifactId>common-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Spring Cloud Alibaba Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>

View File

@ -1,6 +1,6 @@
package com.tacit.common.feign.config;
import com.tacit.common.feign.FeignAuthInterceptor;
import com.tacit.common.feign.interceptor.FeignAuthInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@ -1,13 +1,16 @@
package com.tacit.common.feign.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* Feign全局配置类设置默认Feign客户端配置
* 所有@FeignClient会自动应用此配置无需显式指定
*/
@Configuration
@EnableFeignClients(defaultConfiguration = FeignClientConfig.class)
@AutoConfiguration
@EnableFeignClients(
basePackages = "com.tacit.common.feign",
defaultConfiguration = FeignClientConfig.class
)
public class FeignGlobalConfig {
}

View File

@ -1,4 +1,4 @@
package com.tacit.common.feign;
package com.tacit.common.feign.interceptor;
import com.tacit.common.constant.CommonConstant;
import lombok.extern.slf4j.Slf4j;

View File

@ -0,0 +1 @@
com.tacit.common.feign.config.FeignGlobalConfig

View File

@ -20,6 +20,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Auto Configure -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Jedis -->
<dependency>

View File

@ -5,21 +5,25 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.tacit.common.redis.utils.RedisUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@ComponentScan(basePackages = "com.tacit.common.redis.utils")
@AutoConfiguration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
public RedisTemplate<String, Object> stringObjectRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
@ -51,9 +55,8 @@ public class RedisConfig {
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(factory);
return template;
@ConditionalOnMissingBean
public RedisUtils redisUtils() {
return new RedisUtils();
}
}

View File

@ -2,9 +2,9 @@ package com.tacit.common.redis.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@ -18,9 +18,10 @@ import java.util.concurrent.TimeUnit;
* @CreateTime: 2026-01-07 09:27
*/
@Slf4j
@Component
public class RedisUtils {
@Autowired
@Qualifier("stringObjectRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
@Autowired
@ -345,7 +346,7 @@ public class RedisUtils {
/**
* 删除键
* @param key 可以多个
* @param keys 可以多个
*/
public void delete(String... keys) {
if (keys != null && keys.length > 0) {

View File

@ -1 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.tacit.common.redis.config.RedisConfig

View File

@ -0,0 +1 @@
com.tacit.common.redis.config.RedisConfig

View File

@ -20,7 +20,12 @@ import java.util.stream.Collectors;
* @Description: com.tacit.starter.xxljob
* @version: 1.0
*/
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@AutoConfiguration
@EnableConfigurationProperties(XxlJobProperties.class)
@ConditionalOnClass(XxlJobSpringExecutor.class)
public class XxlJobAutoConfiguration {
private Logger log = LoggerFactory.getLogger(XxlJobAutoConfiguration.class);

View File

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tacit.common.xxljob.XxlJobAutoConfiguration

View File

@ -1,18 +1,14 @@
package com.tacit.admin;
import com.tacit.common.redis.config.RedisConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import(RedisConfig.class)
@MapperScan("com.tacit.admin.mapper")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.tacit.common.feign")
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);

View File

@ -19,6 +19,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
@ -27,6 +28,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
@Configuration
@EnableWebSecurity
@ -44,11 +46,10 @@ public class SecurityConfig {
.cors(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**","test/**", "/test/hello", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/auth/login", "/auth/register","test/**", "/test/hello", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/test/feign/**").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(authenticationFilter(), org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class);
.anyRequest().authenticated())
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@ -63,30 +64,55 @@ public class SecurityConfig {
if (authorization != null && authorization.startsWith(CommonConstant.JWT_PREFIX)) {
String token = authorization.substring(CommonConstant.JWT_PREFIX.length());
try {
// 验证JWT令牌和Redis中的令牌是否存在
if (JwtUtils.validateToken(token) && redisUtils.hasKey(token)) {
// 验证JWT令牌
if (JwtUtils.validateToken(token)) {
// 从令牌中获取用户信息
String username = JwtUtils.getUsernameFromToken(token);
String role = JwtUtils.getRoleFromToken(token);
Long userId = JwtUtils.getUserIdFromToken(token);
// 创建认证对象
User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 构建Redis keytacit:user:token:{userId}
String redisKey = CommonConstant.REDIS_KEY_TACIT_USER_TOKEN + userId;
// 验证Redis中的令牌是否存在且匹配
String redisToken = redisUtils.get(redisKey);
if (token.equals(redisToken)) {
String username = JwtUtils.getUsernameFromToken(token);
String role = JwtUtils.getRoleFromToken(token);
// 创建认证对象
User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
// 创建用户详情map存储userId等信息
HashMap<String, Object> userDetails = new HashMap<>();
userDetails.put("userId", userId);
userDetails.put("username", username);
userDetails.put("role", role);
var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
authentication.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
log.error("JWT令牌验证失败: {}", e.getMessage());
}
} else {
// 检查是否有用户上下文头网关转发时添加
String userId = request.getHeader("X-User-Id");
String userIdStr = request.getHeader("X-User-Id");
String username = request.getHeader("X-Username");
String role = request.getHeader("X-Role");
if (userId != null && username != null && role != null) {
if (userIdStr != null && username != null && role != null) {
// 创建认证对象
User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
// 创建用户详情map存储userId等信息
HashMap<String, Object> userDetails = new HashMap<>();
userDetails.put("userId", Long.parseLong(userIdStr));
userDetails.put("username", username);
userDetails.put("role", role);
var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
authentication.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

View File

@ -4,17 +4,16 @@ import com.tacit.admin.entity.dto.LoginRequest;
import com.tacit.admin.entity.dto.LoginResponse;
import com.tacit.admin.entity.dto.RegisterRequest;
import com.tacit.admin.service.UserService;
import com.tacit.common.constant.CommonConstant;
import com.tacit.common.entity.ResponseResult;
import com.tacit.common.utils.ResCode;
import com.tacit.common.utils.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
@ -39,10 +38,15 @@ public class AuthController {
}
@Operation(summary = "用户退出登录", description = "用户退出登录")
@PostMapping("/logout")
public ResponseResult<Void> logout(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
String token = authorization.substring(CommonConstant.JWT_PREFIX.length());
userService.logout(token);
public ResponseResult<Void> logout() {
// 从SecurityContext中获取当前用户id
Long userId = SecurityUtils.getCurrentUserId();
if (userId == null) {
return ResponseResult.<Void>fail(ResCode.NO_LOGIN.getResultCode(), ResCode.NO_LOGIN.getResultMsg());
}
// 调用服务层处理退出登录
userService.logoutByUserId(userId);
return ResponseResult.<Void>success(ResCode.LOGOUT.getResultCode(), ResCode.LOGOUT.getResultMsg(), null);
}
}

View File

@ -66,4 +66,10 @@ public interface UserService extends IService<User> {
* 用户退出登录
*/
void logout(String token);
/**
* 根据用户ID退出登录
* @param userId 用户ID
*/
void logoutByUserId(Long userId);
}

View File

@ -10,11 +10,11 @@ import com.tacit.admin.entity.dto.RegisterRequest;
import com.tacit.admin.mapper.UserMapper;
import com.tacit.admin.service.RoleService;
import com.tacit.admin.service.UserService;
import com.tacit.common.utils.JwtUtils;
import com.tacit.common.constant.CommonConstant;
import com.tacit.common.redis.utils.RedisUtils;
import com.tacit.common.utils.JwtUtils;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@ -103,8 +103,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
claims.put("username", user.getUsername());
claims.put("role", user.getRole());
String token = JwtUtils.generateToken(claims);
// 构建Redis keytacit:user:token:{userId}
String redisKey = CommonConstant.REDIS_KEY_TACIT_USER_TOKEN + user.getId();
// 将生成的 JWT 令牌存储到 Redis 缓存中设置过期时间为 7 7 * 24 * 60 * 60
redisUtils.setObject(token, user.getId(),7 * 24 * 60 * 60, TimeUnit.SECONDS);
redisUtils.set(redisKey, token ,7 * 24 * 60 * 60, TimeUnit.SECONDS);
Role roleMenu = roleService.getRoleByUserId(user.getId());
// 构建登录响应
LoginResponse loginResponse = new LoginResponse();
@ -119,8 +123,20 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
*/
@Override
public void logout(String token) {
// 从token中解析userId
Long userId = JwtUtils.getUserIdFromToken(token);
// 构建Redis keytacit:user:token:{userId}
String redisKey = CommonConstant.REDIS_KEY_TACIT_USER_TOKEN + userId;
//清除缓存
redisUtils.delete(token);
redisUtils.delete(redisKey);
}
@Override
public void logoutByUserId(Long userId) {
// 构建Redis keytacit:user:token:{userId}
String redisKey = CommonConstant.REDIS_KEY_TACIT_USER_TOKEN + userId;
//清除缓存
redisUtils.delete(redisKey);
}
@Override

View File

@ -10,7 +10,6 @@ import com.tacit.common.redis.config.RedisConfig;
@SpringBootApplication
@Import(RedisConfig.class)
@MapperScan("com.tacit.app.mapper")
@EnableFeignClients(basePackages = "com.tacit.common.feign")
public class AppApiApplication {
public static void main(String[] args) {
SpringApplication.run(AppApiApplication.class, args);

View File

@ -62,17 +62,25 @@ public class AppSecurityConfig {
if (authorization != null && authorization.startsWith(CommonConstant.JWT_PREFIX)) {
String token = authorization.substring(CommonConstant.JWT_PREFIX.length());
try {
// 验证JWT令牌和Redis中的令牌是否存在
if (JwtUtils.validateToken(token) && redisUtils.hasKey(token)) {
// 验证JWT令牌
if (JwtUtils.validateToken(token)) {
// 从令牌中获取用户信息
Long userId = JwtUtils.getUserIdFromToken(token);
String username = JwtUtils.getUsernameFromToken(token);
String role = JwtUtils.getRoleFromToken(token);
// 创建认证对象
User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 构建Redis keytacit:user:token:{userId}
String redisKey = CommonConstant.REDIS_KEY_TACIT_USER_TOKEN + userId;
// 验证Redis中的令牌是否存在且匹配
String redisToken = redisUtils.get(redisKey);
if (token.equals(redisToken)) {
String username = JwtUtils.getUsernameFromToken(token);
String role = JwtUtils.getRoleFromToken(token);
// 创建认证对象
User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
log.error("JWT令牌验证失败: {}", e.getMessage());

View File

@ -68,12 +68,6 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.tacit</groupId>
<artifactId>common-redis</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

View File

@ -7,7 +7,6 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
@Import(RedisConfig.class)
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

View File

@ -30,7 +30,6 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
private static final List<String> WHITE_LIST = List.of(
"/auth/login",
"/auth/register",
"/auth/login",
"/auth/register",
"/swagger-ui",
"/v3/api-docs"
@ -53,33 +52,40 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
return chain.filter(exchange);
}
// 获取Authorization头
// 获取 Authorization头
HttpHeaders headers = request.getHeaders();
String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);
// 检查Authorization头是否存在
// 检查 Authorization头是否存在
if (authorization == null || authorization.isEmpty()) {
return unauthorizedResponse(exchange, "缺少认证令牌");
}
// 检查Authorization头格式
// 检查 Authorization头格式
if (!authorization.startsWith(CommonConstant.JWT_PREFIX)) {
return unauthorizedResponse(exchange, "认证令牌格式错误");
}
// 提取JWT令牌
// 提取 JWT令牌
String token = authorization.substring(CommonConstant.JWT_PREFIX.length());
// 验证JWT令牌
// 验证 JWT令牌
try {
boolean isTokenValid = redisUtils.hasKey(token);
if (!isTokenValid) {
return unauthorizedResponse(exchange, "Token已被注销");
}
JwtUtils.validateToken(token);
// 从令牌中获取用户信息并添加到请求头
// 从令牌中获取用户信息
Long userId = JwtUtils.getUserIdFromToken(token);
// 构建Redis keytacit:user:token:{userId}
String redisKey = CommonConstant.REDIS_KEY_TACIT_USER_TOKEN + userId;
// 验证 Redis中的令牌是否存在且匹配
String redisToken = redisUtils.get(redisKey);
if (!token.equals(redisToken)) {
return unauthorizedResponse(exchange, "Token已被注销");
}
// 从令牌中获取用户信息并添加到请求头
String username = JwtUtils.getUsernameFromToken(token);
String role = JwtUtils.getRoleFromToken(token);