Compare commits
20
Commits
1fe871faa8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f97cab38f | ||
|
|
9e75d3b392 | ||
|
|
42228c63a5 | ||
|
|
411565f812 | ||
|
|
c0448ff6ab | ||
|
|
7b346802e0 | ||
|
|
f152b1e655 | ||
|
|
01d29e6ec3 | ||
|
|
06b5258824 | ||
|
|
3d4bec6e96 | ||
|
|
eb82090586 | ||
|
|
753a07f71d | ||
|
|
209769d024 | ||
|
|
88afa8b47e | ||
|
|
ab39c0f9b2 | ||
|
|
eeac5b430c | ||
|
|
a0cb0cb6b7 | ||
|
|
f679be8cee | ||
|
|
9d7a061305 | ||
|
|
5014411874 |
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-framework</artifactId>
|
<artifactId>zkh-framework</artifactId>
|
||||||
<version>1.5</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>
|
||||||
|
|||||||
+5
-1
@@ -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</version>
|
<version>1.5.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-common</artifactId>
|
<artifactId>zkh-common</artifactId>
|
||||||
@@ -38,6 +38,10 @@
|
|||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-jpa</artifactId>
|
<artifactId>spring-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-relational</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
|||||||
@@ -8,72 +8,73 @@ import org.springframework.data.annotation.LastModifiedBy;
|
|||||||
import org.springframework.data.annotation.LastModifiedDate;
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
public class BaseEntity {
|
public class BaseEntity implements Serializable {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@LastModifiedDate
|
@LastModifiedDate
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@CreatedBy
|
@CreatedBy
|
||||||
private String createBy;
|
private String createBy;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@LastModifiedBy
|
@LastModifiedBy
|
||||||
private String updateBy;
|
private String updateBy;
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(Long id) {
|
public void setId(Long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreateTime() {
|
public LocalDateTime getCreateTime() {
|
||||||
return createTime;
|
return createTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCreateTime(LocalDateTime createTime) {
|
public void setCreateTime(LocalDateTime createTime) {
|
||||||
this.createTime = createTime;
|
this.createTime = createTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalDateTime getUpdateTime() {
|
public LocalDateTime getUpdateTime() {
|
||||||
return updateTime;
|
return updateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUpdateTime(LocalDateTime updateTime) {
|
public void setUpdateTime(LocalDateTime updateTime) {
|
||||||
this.updateTime = updateTime;
|
this.updateTime = updateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCreateBy() {
|
public String getCreateBy() {
|
||||||
return createBy;
|
return createBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCreateBy(String createBy) {
|
public void setCreateBy(String createBy) {
|
||||||
this.createBy = createBy;
|
this.createBy = createBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUpdateBy() {
|
public String getUpdateBy() {
|
||||||
return updateBy;
|
return updateBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUpdateBy(String updateBy) {
|
public void setUpdateBy(String updateBy) {
|
||||||
this.updateBy = updateBy;
|
this.updateBy = updateBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package vip.jcfd.common.core;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
import org.springframework.data.annotation.*;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
|
||||||
|
@MappedSuperclass
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class R2dbcBaseEntity implements Serializable {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column("create_time")
|
||||||
|
@CreatedDate
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Column("update_time")
|
||||||
|
@LastModifiedDate
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
@Column("create_by")
|
||||||
|
@CreatedBy
|
||||||
|
private String createBy;
|
||||||
|
|
||||||
|
@Column("update_by")
|
||||||
|
@LastModifiedBy
|
||||||
|
private String updateBy;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreateTime() {
|
||||||
|
return createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateTime(LocalDateTime createTime) {
|
||||||
|
this.createTime = createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdateTime() {
|
||||||
|
return updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdateTime(LocalDateTime updateTime) {
|
||||||
|
this.updateTime = updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCreateBy() {
|
||||||
|
return createBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateBy(String createBy) {
|
||||||
|
this.createBy = createBy;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -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</version>
|
<version>1.5.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-data</artifactId>
|
<artifactId>zkh-data</artifactId>
|
||||||
|
|||||||
+1
-1
@@ -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</version>
|
<version>1.5.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-file</artifactId>
|
<artifactId>zkh-file</artifactId>
|
||||||
|
|||||||
+5
-1
@@ -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</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -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</version>
|
<version>1.5.12</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-web</artifactId>
|
<artifactId>zkh-web</artifactId>
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-log</artifactId>
|
<artifactId>zkh-log</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package vip.jcfd.web.config;
|
package vip.jcfd.web.config;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
|
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.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
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;
|
||||||
@@ -13,13 +17,13 @@ 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);
|
||||||
|
|
||||||
@ExceptionHandler(value = Exception.class)
|
@ExceptionHandler(value = {Exception.class, RuntimeException.class})
|
||||||
public R<String> handleException(Exception e) {
|
public R<String> handleException(Throwable e) {
|
||||||
log.error("服务异常", e);
|
log.error("服务异常", e);
|
||||||
return R.serverError("服务器繁忙,请稍候重试");
|
return R.serverError("服务器繁忙,请稍候重试");
|
||||||
}
|
}
|
||||||
@@ -37,6 +41,9 @@ public class GlobalExceptionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles bind exceptions; logs and returns formatted field errors
|
||||||
|
*/
|
||||||
@ExceptionHandler(value = BindException.class)
|
@ExceptionHandler(value = BindException.class)
|
||||||
public R<String> handleBindException(BindException e) {
|
public R<String> handleBindException(BindException e) {
|
||||||
log.error("接口入参校验失败", e);
|
log.error("接口入参校验失败", e);
|
||||||
@@ -44,4 +51,42 @@ public class GlobalExceptionHandler {
|
|||||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
||||||
return R.error(String.join("。\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
|
return R.error(String.join("。\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(value = ValidationException.class)
|
||||||
|
public R<String> handleValidationException(ValidationException e) {
|
||||||
|
log.error("接口入参校验失败", e);
|
||||||
|
return R.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 @RequestBody + @Valid 校验失败
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public R<?> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
|
||||||
|
log.error("接口入参校验失败", ex);
|
||||||
|
BindingResult bindingResult = ex.getBindingResult();
|
||||||
|
|
||||||
|
String msg = bindingResult.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.map(err -> err.getField() + ": " + err.getDefaultMessage())
|
||||||
|
.findFirst()
|
||||||
|
.orElse("参数错误");
|
||||||
|
|
||||||
|
return R.error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 @RequestParam / @PathVariable 校验失败
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
|
public R<?> handleConstraintViolation(ConstraintViolationException ex) {
|
||||||
|
log.error("接口入参校验失败", ex);
|
||||||
|
String msg = ex.getConstraintViolations()
|
||||||
|
.stream()
|
||||||
|
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
|
||||||
|
.findFirst()
|
||||||
|
.orElse("参数错误");
|
||||||
|
|
||||||
|
return R.error(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package vip.jcfd.web.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures Jackson mapper with locale, timezone, date format, serializer
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
|
||||||
|
return jacksonObjectMapperBuilder -> {
|
||||||
|
jacksonObjectMapperBuilder.locale(Locale.CHINA);
|
||||||
|
jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai")));
|
||||||
|
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
jacksonObjectMapperBuilder.serializers(new LongSerializer(), new LocalDateTimeSerializer(), new LocalDateSerializer());
|
||||||
|
jacksonObjectMapperBuilder.deserializers(new LongDeserializer(), new LocalDateTimeDeserializer(), new LocalDateDeserializer());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||||
|
|
||||||
|
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
|
if (value == null) {
|
||||||
|
gen.writeNull();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gen.writeString(value.format(formatter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<LocalDateTime> handledType() {
|
||||||
|
return LocalDateTime.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
|
||||||
|
|
||||||
|
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
if (p.getText() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocalDateTime.parse(p.getText(), formatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> handledType() {
|
||||||
|
return LocalDateTime.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
|
||||||
|
|
||||||
|
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
|
if (value == null) {
|
||||||
|
gen.writeNull();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gen.writeString(value.format(formatter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<LocalDate> handledType() {
|
||||||
|
return LocalDate.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
|
||||||
|
|
||||||
|
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
if (p.getText() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocalDate.parse(p.getText(), formatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> handledType() {
|
||||||
|
return LocalDate.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LongSerializer extends JsonSerializer<Long> {
|
||||||
|
@Override
|
||||||
|
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
|
if (value == null) {
|
||||||
|
gen.writeNull();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gen.writeString(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Long> handledType() {
|
||||||
|
return Long.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LongDeserializer extends JsonDeserializer<Long> {
|
||||||
|
@Override
|
||||||
|
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
if (p.getText() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Long.parseLong(p.getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> handledType() {
|
||||||
|
return Long.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,191 +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);
|
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) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
TokenRedisStorage tokenRedisStorage,
|
||||||
@Override
|
SecurityProps securityProps) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
||||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
@Override
|
||||||
log.warn("认证失败", authException);
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
log.warn("访问 {} ,但是认证失败", request.getRequestURI(), authException);
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
||||||
objectMapper.writeValue(response.getWriter(), data);
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
}
|
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_UNAUTHORIZED, "用户名或密码错误", 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",
|
||||||
1800, // 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user