Compare commits

..
7 Commits
Author SHA1 Message Date
zkh 9f97cab38f feat(log): 扩展日志服务支持操作日志上下文
- 在 ConsoleLogService 中新增 log(LogContext) 方法实现
- 更新 ILogService 接口添加 LogContext 参数的日志方法并提供默认实现
- 在 LogConfig 中添加 LogAspect 切面的异常处理和日志上下文构建功能
- 新增 LogContext 记录类用于封装完整的操作日志信息
- 在 zkh-log 模块中添加 jakarta.servlet-api 依赖以获取请求信息
- 更新所有模块的版本号从 1.5.11 到 1.5.12
2026-04-16 12:19:51 +08:00
zkh 9e75d3b392 chore(version): 更新框架版本号
- 将 zkh-framework 父项目版本从 1.5.10 升级到 1.5.11
- 同步更新 zkh-common 模块依赖版本至 1.5.11
- 同步更新 zkh-data 模块依赖版本至 1.5.11
- 同步更新 zkh-file 模块依赖版本至 1.5.11
- 同步更新 zkh-log 模块依赖版本至 1.5.11
- 同步更新 zkh-web 模块依赖版本至 1.5.11
2026-02-24 11:25:41 +08:00
zkh 42228c63a5 refactor(error): 优化全局异常处理器配置
- 移除未使用的 HttpStatus 和 ResponseStatus 注解导入
- 简化 RestControllerAdvice 注解配置,移除不必要的 bean 名称参数
- 保持原有的全局异常处理功能不变
- 提高代码简洁性和可读性
2026-02-24 11:23:55 +08:00
zkh 411565f812 refactor(error): 优化自定义错误控制器实现
- 添加 SLF4J 日志记录功能
- 实现详细的错误信息提取和处理逻辑
- 增加对不同 HTTP 状态码的友好错误消息映射
- 添加错误日志记录,区分服务器错误和客户端错误
- 实现 getErrorPath 方法以符合 ErrorController 接口要求
- 添加详细的 JavaDoc 注释文档
2026-02-14 12:39:40 +08:00
zkh c0448ff6ab feat(web): 添加自定义错误控制器处理HTTP错误
- 实现了ErrorController接口来处理错误请求
- 添加了/error端点的错误处理逻辑
- 返回统一格式的错误响应对象R
- 根据请求状态码动态设置错误状态
- 提供了默认500错误码的异常处理机制
2026-02-14 12:37:02 +08:00
zkh 7b346802e0 feat(security): 更新安全配置并升级框架版本
- 将父项目版本从 1.5.7 升级到 1.5.8
- 在 WebSecurityConfig 中注入 SecurityProps 配置
- 修改 CustomAuthenticationEntryPoint 构造函数以接受 securityProps 参数
- 将硬编码的访问令牌持续时间替换为 securityProps 配置值
- 将认证失败响应状态码从 401 更改为 40
- 为 zkh-web 模块添加 spring-boot-starter-aop 依赖
2026-02-04 09:48:51 +08:00
zkh f152b1e655 feat(security): 配置安全过滤器链支持异步请求
- 添加 DispatcherType.ASYNC 类型匹配以允许异步请求通过
- 添加 Order 注解确保安全过滤器链正确排序
- 导入必要的 jakarta.servlet.DispatcherType 和 org.springframework.core.annotation.Order 依赖
2026-02-04 01:15:08 +08:00
13 changed files with 415 additions and 206 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.5.8</version> <version>1.5.12</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>ZKH Framework</name> <name>ZKH Framework</name>
<description>A Java framework for ZKH applications</description> <description>A Java framework for ZKH applications</description>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.5.8</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-common</artifactId> <artifactId>zkh-common</artifactId>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.5.8</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-data</artifactId> <artifactId>zkh-data</artifactId>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.5.8</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-file</artifactId> <artifactId>zkh-file</artifactId>
+5 -1
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.5.8</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-log</artifactId> <artifactId>zkh-log</artifactId>
@@ -14,6 +14,10 @@
<description>Logging utilities for ZKH framework</description> <description>Logging utilities for ZKH framework</description>
<dependencies> <dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>
@@ -5,10 +5,22 @@ import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
public class ConsoleLogService implements ILogService { public class ConsoleLogService implements ILogService {
private final Logger logger = LoggerFactory.getLogger(ConsoleLogService.class); private final Logger logger = LoggerFactory.getLogger(ConsoleLogService.class);
@Override @Override
public void log(String message, Authentication authentication) { public void log(String message, Authentication authentication) {
logger.debug("{} {}", authentication.getName(), message); String operator = authentication != null ? authentication.getName() : "anonymous";
} logger.debug("{} {}", operator, message);
}
@Override
public void log(LogContext context) {
if (context.status() == LogContext.SUCCESS) {
logger.debug("[操作日志] {} {} {} {} {}", context.operator(), context.httpMethod(),
context.requestUrl(), context.ip(), context.message());
} else {
logger.warn("[操作日志] {} {} {} {} {} 错误: {}", context.operator(), context.httpMethod(),
context.requestUrl(), context.ip(), context.message(), context.errorMessage());
}
}
} }
@@ -4,5 +4,16 @@ import org.springframework.security.core.Authentication;
public interface ILogService { public interface ILogService {
void log(String message, Authentication authentication); /**
* 记录日志(旧接口,保持向后兼容)
*/
void log(String message, Authentication authentication);
/**
* 记录日志(新接口,带完整上下文)
*/
default void log(LogContext context) {
// 默认实现:降级到旧接口
log(context.message(), null);
}
} }
@@ -0,0 +1,27 @@
package vip.jcfd.log;
/**
* 日志上下文,包含操作日志的完整信息
*/
public record LogContext(
String message,
String operator,
String requestUrl,
String httpMethod,
String ip,
int status,
String errorMessage
) {
/** 操作状态:成功 */
public static final int SUCCESS = 0;
/** 操作状态:失败 */
public static final int FAIL = 1;
public static LogContext success(String message, String operator, String requestUrl, String httpMethod, String ip) {
return new LogContext(message, operator, requestUrl, httpMethod, ip, SUCCESS, null);
}
public static LogContext fail(String message, String operator, String requestUrl, String httpMethod, String ip, String errorMessage) {
return new LogContext(message, operator, requestUrl, httpMethod, ip, FAIL, errorMessage);
}
}
@@ -1,7 +1,9 @@
package vip.jcfd.log.config; package vip.jcfd.log.config;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
@@ -12,39 +14,90 @@ import org.springframework.core.annotation.Order;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import vip.jcfd.log.ConsoleLogService; import vip.jcfd.log.ConsoleLogService;
import vip.jcfd.log.ILogService; import vip.jcfd.log.ILogService;
import vip.jcfd.log.LogContext;
import vip.jcfd.log.annotation.Log; import vip.jcfd.log.annotation.Log;
@Configuration("_logConfiguration") @Configuration("_logConfiguration")
public class LogConfig { public class LogConfig {
@Bean("_defaultLogService") @Bean("_defaultLogService")
@ConditionalOnMissingBean @ConditionalOnMissingBean
@Order @Order
public ILogService defaultLogService() { public ILogService defaultLogService() {
return new ConsoleLogService(); return new ConsoleLogService();
} }
@Aspect @Aspect
@Component @Component
public static class LogAspect { public static class LogAspect {
private final ILogService logService; private final ILogService logService;
public LogAspect(ILogService logService) { public LogAspect(ILogService logService) {
this.logService = logService; this.logService = logService;
} }
@Pointcut("@annotation(vip.jcfd.log.annotation.Log)") @Pointcut("@annotation(vip.jcfd.log.annotation.Log)")
public void logAspect() { public void logAspect() {
} }
@AfterReturning(value = "logAspect()") @AfterReturning(value = "logAspect()")
public void afterReturning(JoinPoint joinPoint) { public void afterReturning(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); LogContext context = buildLogContext(joinPoint, null);
Log log = signature.getMethod().getAnnotation(Log.class); logService.log(context);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); }
logService.log(log.value(), authentication);
} @AfterThrowing(value = "logAspect()", throwing = "ex")
} public void afterThrowing(JoinPoint joinPoint, Exception ex) {
LogContext context = buildLogContext(joinPoint, ex.getMessage());
logService.log(context);
}
private LogContext buildLogContext(JoinPoint joinPoint, String errorMessage) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Log log = signature.getMethod().getAnnotation(Log.class);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String operator = authentication != null ? authentication.getName() : "anonymous";
String requestUrl = "";
String httpMethod = "";
String ip = "";
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
requestUrl = request.getRequestURI();
httpMethod = request.getMethod();
ip = getClientIp(request);
}
if (errorMessage != null) {
return LogContext.fail(log.value(), operator, requestUrl, httpMethod, ip, errorMessage);
}
return LogContext.success(log.value(), operator, requestUrl, httpMethod, ip);
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// X-Forwarded-For 可能包含多个 IP,取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
} }
+1 -1
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.5.8</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-web</artifactId> <artifactId>zkh-web</artifactId>
@@ -4,13 +4,11 @@ import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException;
import vip.jcfd.common.core.BizException; import vip.jcfd.common.core.BizException;
@@ -19,7 +17,7 @@ import vip.jcfd.common.core.R;
import java.util.List; import java.util.List;
@RestControllerAdvice("_globalExceptionHandler") @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@@ -1,6 +1,7 @@
package vip.jcfd.web.config; package vip.jcfd.web.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@@ -10,6 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.domain.AuditorAware; import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -60,192 +62,194 @@ import java.util.UUID;
@EnableScheduling @EnableScheduling
public class WebSecurityConfig { public class WebSecurityConfig {
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class); private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
private final SecurityProps securityProps; private final SecurityProps securityProps;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final TokenRedisStorage tokenRedisStorage; private final TokenRedisStorage tokenRedisStorage;
public WebSecurityConfig(SecurityProps securityProps, public WebSecurityConfig(SecurityProps securityProps,
ObjectMapper objectMapper, ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage, TokenRedisStorage tokenRedisStorage,
AuthenticationManagerBuilder builder, AuthenticationManagerBuilder builder,
UserDetailsService userDetailsService) { UserDetailsService userDetailsService) {
this.securityProps = securityProps; this.securityProps = securityProps;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.tokenRedisStorage = tokenRedisStorage; this.tokenRedisStorage = tokenRedisStorage;
builder.authenticationProvider(new RefreshTokenAuthProvider(userDetailsService)); builder.authenticationProvider(new RefreshTokenAuthProvider(userDetailsService));
DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(userDetailsService); DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(userDetailsService);
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
builder.authenticationProvider(authenticationProvider); builder.authenticationProvider(authenticationProvider);
} }
@Scheduled(cron = "0 */30 * * * *") @Scheduled(cron = "0 */30 * * * *")
@Async @Async
public void scheduleClearExpiredTokens() { public void scheduleClearExpiredTokens() {
tokenRedisStorage.clearExpiredTokens(); tokenRedisStorage.clearExpiredTokens();
} }
@Bean @Bean
public AuditorAware<String> auditorAware() { public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext()) return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication) .map(SecurityContext::getAuthentication)
.map(Authentication::getName) .map(Authentication::getName)
.or(() -> Optional.of("system")); .or(() -> Optional.of("system"));
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public TokenFilter tokenFilter() { public TokenFilter tokenFilter() {
return new TokenFilter(tokenRedisStorage); return new TokenFilter(tokenRedisStorage);
} }
@Bean @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager(); return configuration.getAuthenticationManager();
} }
@Bean @Bean
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception { @Order
http.authorizeHttpRequests(config -> { public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll(); http.authorizeHttpRequests(config -> {
config.anyRequest().authenticated(); config.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
}); config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage, securityProps); config.anyRequest().authenticated();
http.formLogin(config -> { });
config.loginProcessingUrl("/login"); CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage, securityProps);
}); http.formLogin(config -> {
http.csrf(AbstractHttpConfigurer::disable); config.loginProcessingUrl("/login");
http.logout(config -> { });
config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage)); http.csrf(AbstractHttpConfigurer::disable);
}); http.logout(config -> {
http.rememberMe(AbstractHttpConfigurer::disable); config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
http.sessionManagement(AbstractHttpConfigurer::disable); });
http.exceptionHandling(config -> { http.rememberMe(AbstractHttpConfigurer::disable);
config.authenticationEntryPoint(authenticationEntryPoint); http.sessionManagement(AbstractHttpConfigurer::disable);
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper)); http.exceptionHandling(config -> {
}); config.authenticationEntryPoint(authenticationEntryPoint);
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
});
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager); JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
filter.setAuthenticationSuccessHandler(authenticationEntryPoint); filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
filter.setAuthenticationFailureHandler(authenticationEntryPoint); filter.setAuthenticationFailureHandler(authenticationEntryPoint);
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class); http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }
private record CustomAuthenticationEntryPoint( private record CustomAuthenticationEntryPoint(
ObjectMapper objectMapper, ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage, TokenRedisStorage tokenRedisStorage,
SecurityProps securityProps) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler { SecurityProps securityProps) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
@Override @Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.warn("访问 {} ,但是认证失败", request.getRequestURI(), authException); log.warn("访问 {} ,但是认证失败", request.getRequestURI(), authException);
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null); R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
} }
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("登录失败", exception); log.warn("登录失败", exception);
R<Object> data = new R<>(HttpServletResponse.SC_BAD_REQUEST, "用户名或密码错误", false, null); R<Object> data = new R<>(HttpServletResponse.SC_BAD_REQUEST, "用户名或密码错误", false, null);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
} }
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("用户「{}」登录成功", authentication.getName()); log.info("用户「{}」登录成功", authentication.getName());
// 生成双重Token // 生成双重Token
String accessToken = UUID.randomUUID().toString(); String accessToken = UUID.randomUUID().toString();
String refreshToken = UUID.randomUUID().toString(); String refreshToken = UUID.randomUUID().toString();
// 存储Access Token // 存储Access Token
tokenRedisStorage.putAccessToken(accessToken, authentication); tokenRedisStorage.putAccessToken(accessToken, authentication);
// 存储Refresh Token // 存储Refresh Token
String deviceId = extractDeviceId(request); String deviceId = extractDeviceId(request);
tokenRedisStorage.putRefreshToken(refreshToken, authentication.getName(), deviceId); tokenRedisStorage.putRefreshToken(refreshToken, authentication.getName(), deviceId);
// 构造登录响应 // 构造登录响应
LoginResponse loginResponse = new LoginResponse( LoginResponse loginResponse = new LoginResponse(
accessToken, accessToken,
refreshToken, refreshToken,
"Bearer", "Bearer",
securityProps.getDuration().getSeconds(), // 30分钟,秒数 securityProps.getDuration().getSeconds(), // 30分钟,秒数
authentication.getName() authentication.getName()
); );
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
R<LoginResponse> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse); R<LoginResponse> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse);
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
} }
private String extractDeviceId(HttpServletRequest request) { private String extractDeviceId(HttpServletRequest request) {
// 尝试从User-Agent提取设备信息 // 尝试从User-Agent提取设备信息
String userAgent = request.getHeader("User-Agent"); String userAgent = request.getHeader("User-Agent");
if (userAgent != null) { if (userAgent != null) {
// 简单的设备识别逻辑,生产环境可以使用更复杂的识别算法 // 简单的设备识别逻辑,生产环境可以使用更复杂的识别算法
if (userAgent.contains("Mobile") || userAgent.contains("Android") || userAgent.contains("iPhone")) { if (userAgent.contains("Mobile") || userAgent.contains("Android") || userAgent.contains("iPhone")) {
return "mobile-" + request.getRemoteAddr(); return "mobile-" + request.getRemoteAddr();
} else if (userAgent.contains("Tablet") || userAgent.contains("iPad")) { } else if (userAgent.contains("Tablet") || userAgent.contains("iPad")) {
return "tablet-" + request.getRemoteAddr(); return "tablet-" + request.getRemoteAddr();
} else { } else {
return "desktop-" + request.getRemoteAddr(); return "desktop-" + request.getRemoteAddr();
} }
} }
return "unknown-" + request.getRemoteAddr(); return "unknown-" + request.getRemoteAddr();
} }
} }
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler { private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
@Override @Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.warn("访问被拒绝", accessDeniedException); log.warn("访问被拒绝", accessDeniedException);
if (authentication.isAuthenticated()) { if (authentication.isAuthenticated()) {
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage()); log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
} else { } else {
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage()); log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
} }
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null); R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
} }
} }
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper, private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage) implements LogoutHandler { TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
@Override @Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION); String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null && header.startsWith("Bearer ")) { if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7); String token = header.substring(7);
authentication = tokenRedisStorage.get(token); authentication = tokenRedisStorage.get(token);
tokenRedisStorage.remove(token); tokenRedisStorage.remove(token);
} }
if (authentication != null) { if (authentication != null) {
log.info("用户「{}」退出成功", authentication.getName()); log.info("用户「{}」退出成功", authentication.getName());
String all = request.getParameter("all"); String all = request.getParameter("all");
if ("true".equals(all)) { if ("true".equals(all)) {
tokenRedisStorage.removeByUserName(authentication.getName()); tokenRedisStorage.removeByUserName(authentication.getName());
} }
} }
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null); R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
try { try {
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }
} }
@@ -0,0 +1,100 @@
package vip.jcfd.web.controller;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.jcfd.common.core.R;
import java.util.Optional;
/**
* 自定义错误控制器
* 重写 Spring MVC 默认的错误视图,返回 JSON 格式的错误响应
*/
@RestController
public class CustomErrorController implements ErrorController {
private static final Logger log = LoggerFactory.getLogger(CustomErrorController.class);
/**
* 处理错误请求
* 根据 HTTP 状态码返回相应的错误信息
*/
@RequestMapping("/error")
public R<?> handleError(HttpServletRequest request) {
// 获取状态码
int status = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE))
.map(Object::toString)
.map(Integer::parseInt)
.orElse(500);
// 获取请求 URI
String requestUri = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI))
.map(Object::toString)
.orElse("unknown");
// 获取异常信息
String exceptionMessage = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_MESSAGE))
.map(Object::toString)
.orElse(null);
// 获取异常类型
String exceptionType = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE))
.map(Object::toString)
.orElse(null);
// 获取异常对象
Throwable throwable = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_EXCEPTION))
.filter(Throwable.class::isInstance)
.map(Throwable.class::cast)
.orElse(null);
// 记录错误日志
if (status >= 500) {
log.error("服务器错误 - 状态码: {}, 请求路径: {}, 异常类型: {}, 异常信息: {}",
status, requestUri, exceptionType, exceptionMessage, throwable);
} else if (status >= 400) {
log.warn("客户端错误 - 状态码: {}, 请求路径: {}, 异常信息: {}",
status, requestUri, exceptionMessage);
}
// 根据状态码获取友好的错误消息
String errorMessage = getErrorMessage(status, exceptionMessage);
return new R<>(status, errorMessage, false, null);
}
/**
* 根据状态码获取友好的错误消息
*/
private String getErrorMessage(int status, String originalMessage) {
if (originalMessage != null && !originalMessage.isEmpty()) {
return originalMessage;
}
return switch (status) {
case 400 -> "请求参数错误";
case 401 -> "未授权,请先登录";
case 403 -> "无权访问";
case 404 -> "您访问的地址不存在";
case 405 -> "请求方法不支持";
case 500 -> "服务器内部错误";
case 502 -> "网关错误";
case 503 -> "服务暂时不可用";
case 504 -> "网关超时";
default -> "请求失败";
};
}
/**
* 返回错误路径
* 实现 ErrorController 接口要求
*/
public String getErrorPath() {
return "/error";
}
}