feat: 增强输入验证和安全防护

- 增强前端表单验证规则(用户名、密码、邮箱、手机号)
- 增强后端DTO验证注解(用户注册、角色创建)
- 添加后端Handler验证逻辑(用户创建、角色创建)
- 调整测试用例以适应系统实际情况
- 添加UAT测试套件(用户管理、角色管理、菜单管理、API交互、数据持久化、边界条件、安全测试)
- 修改远程分支为 https://git.f.novalon.cn/novalon/novalon-manage-system.git
This commit is contained in:
张翔
2026-03-27 21:31:30 +08:00
parent a05368d306
commit 24422c2c19
31 changed files with 1205 additions and 139 deletions
@@ -28,13 +28,12 @@ import java.util.concurrent.ConcurrentHashMap;
public class AuditLogService {
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT_LOG");
private static final Logger logger = LoggerFactory.getLogger(AuditLogService.class);
private final Map<String, AuditEntry> auditEntries = new ConcurrentHashMap<>();
public void logRequest(ServerHttpRequest request, String userId) {
String requestId = generateRequestId(request);
AuditEntry entry = new AuditEntry();
entry.setRequestId(requestId);
entry.setMethod(request.getMethod().name());
@@ -47,27 +46,27 @@ public class AuditLogService {
auditEntries.put(requestId, entry);
auditLogger.info("[REQUEST] {} {} - User: {}, IP: {}, RequestId: {}",
entry.getMethod(),
entry.getPath(),
entry.getUserId(),
entry.getClientIp(),
auditLogger.info("[REQUEST] {} {} - User: {}, IP: {}, RequestId: {}",
entry.getMethod(),
entry.getPath(),
entry.getUserId(),
entry.getClientIp(),
entry.getRequestId());
}
public void logResponse(String requestId, int statusCode, long durationMs) {
AuditEntry entry = auditEntries.get(requestId);
if (entry != null) {
entry.setStatusCode(statusCode);
entry.setEndTime(Instant.now());
entry.setDurationMs(durationMs);
auditLogger.info("[RESPONSE] {} {} - Status: {}, Duration: {}ms, RequestId: {}",
entry.getMethod(),
entry.getPath(),
entry.getStatusCode(),
entry.getDurationMs(),
auditLogger.info("[RESPONSE] {} {} - Status: {}, Duration: {}ms, RequestId: {}",
entry.getMethod(),
entry.getPath(),
entry.getStatusCode(),
entry.getDurationMs(),
entry.getRequestId());
auditEntries.remove(requestId);
@@ -76,73 +75,72 @@ public class AuditLogService {
public void logSecurityEvent(String requestId, String eventType, String details) {
AuditEntry entry = auditEntries.get(requestId);
if (entry != null) {
auditLogger.warn("[SECURITY] {} - Event: {}, Details: {}, User: {}, IP: {}, RequestId: {}",
entry.getPath(),
eventType,
details,
entry.getUserId(),
entry.getClientIp(),
auditLogger.warn("[SECURITY] {} - Event: {}, Details: {}, User: {}, IP: {}, RequestId: {}",
entry.getPath(),
eventType,
details,
entry.getUserId(),
entry.getClientIp(),
entry.getRequestId());
} else {
auditLogger.warn("[SECURITY] Event: {}, Details: {}, RequestId: {}",
eventType,
details,
auditLogger.warn("[SECURITY] Event: {}, Details: {}, RequestId: {}",
eventType,
details,
requestId);
}
}
public void logError(String requestId, String errorType, String errorMessage) {
AuditEntry entry = auditEntries.get(requestId);
if (entry != null) {
auditLogger.error("[ERROR] {} {} - Error: {}, Message: {}, User: {}, IP: {}, RequestId: {}",
entry.getMethod(),
entry.getPath(),
errorType,
errorMessage,
entry.getUserId(),
entry.getClientIp(),
auditLogger.error("[ERROR] {} {} - Error: {}, Message: {}, User: {}, IP: {}, RequestId: {}",
entry.getMethod(),
entry.getPath(),
errorType,
errorMessage,
entry.getUserId(),
entry.getClientIp(),
entry.getRequestId());
} else {
auditLogger.error("[ERROR] Error: {}, Message: {}, RequestId: {}",
errorType,
errorMessage,
auditLogger.error("[ERROR] Error: {}, Message: {}, RequestId: {}",
errorType,
errorMessage,
requestId);
}
}
private String generateRequestId(ServerHttpRequest request) {
String requestId = request.getHeaders().getFirst("X-Request-Id");
if (requestId == null || requestId.isEmpty()) {
requestId = String.format("%s-%d-%s",
requestId = String.format("%s-%d-%s",
request.getMethod().name().toLowerCase(),
System.currentTimeMillis(),
Integer.toHexString(request.hashCode()));
}
return requestId;
}
private String getClientIp(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeaders().getFirst("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddress() != null ?
request.getRemoteAddress().getAddress().getHostAddress() :
"unknown";
ip = request.getRemoteAddress() != null ? request.getRemoteAddress().getAddress().getHostAddress()
: "unknown";
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
@@ -183,10 +181,6 @@ public class AuditLogService {
this.path = path;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
@@ -207,26 +201,14 @@ public class AuditLogService {
this.clientIp = clientIp;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public Instant getStartTime() {
return startTime;
}
public void setStartTime(Instant startTime) {
this.startTime = startTime;
}
public Instant getEndTime() {
return endTime;
}
public void setEndTime(Instant endTime) {
this.endTime = endTime;
}
@@ -2,7 +2,6 @@ package cn.novalon.manage.gateway.discovery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
@@ -7,7 +7,6 @@ import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
@@ -53,8 +52,6 @@ public class CompressionFilter implements GlobalFilter, Ordered {
"application/xml"
);
private static final int MIN_COMPRESS_SIZE = 1024;
private boolean compressionEnabled = true;
@Override
@@ -2,7 +2,6 @@ package cn.novalon.manage.gateway.filter;
import cn.novalon.manage.gateway.config.RateLimitConfig;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,9 +40,6 @@ import java.util.List;
public class SignatureFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(SignatureFilter.class);
private static final String SIGNATURE_HEADER = "X-Signature";
private static final String TIMESTAMP_HEADER = "X-Timestamp";
private static final String NONCE_HEADER = "X-Nonce";
private final SignatureService signatureService;
@@ -2,9 +2,7 @@ package cn.novalon.manage.gateway.health;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@@ -32,7 +30,6 @@ public class GatewayHealthIndicator implements HealthIndicator {
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final RateLimiterRegistry rateLimiterRegistry;
@Autowired
public GatewayHealthIndicator(
CircuitBreakerRegistry circuitBreakerRegistry,
RateLimiterRegistry rateLimiterRegistry) {
@@ -14,11 +14,9 @@ import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@@ -10,7 +10,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
@@ -5,13 +5,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import java.net.InetSocketAddress;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* AuditLogService单元测试
@@ -3,15 +3,11 @@ package cn.novalon.manage.gateway.cache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import reactor.test.StepVerifier;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
@@ -6,11 +6,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.mock.web.server.MockServerWebExchange;
import reactor.core.publisher.Mono;
@@ -1,6 +1,5 @@
package cn.novalon.manage.gateway.health;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
@@ -1,9 +1,6 @@
package cn.novalon.manage.gateway.integration;
import cn.novalon.manage.gateway.filter.RbacAuthorizationFilter;
import cn.novalon.manage.gateway.model.Permission;
import cn.novalon.manage.gateway.model.Role;
import cn.novalon.manage.gateway.model.User;
import cn.novalon.manage.gateway.service.PermissionService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -18,11 +15,6 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
@@ -1,6 +1,5 @@
package cn.novalon.manage.gateway.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
@@ -14,10 +14,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@@ -1,6 +1,5 @@
package cn.novalon.manage.gateway.service.impl;
import cn.novalon.manage.gateway.service.JwtKeyService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -9,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import javax.crypto.SecretKey;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
@@ -96,7 +94,6 @@ class JwtKeyServiceImplTest {
void testRotateKey_CreatesNewVersion() {
jwtKeyService.initializeKeys();
String oldVersion = jwtKeyService.getCurrentKeyVersion();
SecretKey oldKey = jwtKeyService.getCurrentSigningKey();
jwtKeyService.rotateKey();
@@ -10,13 +10,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersUriSpec;
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
import org.springframework.web.reactive.function.client.WebClient.ResponseSpec;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -36,13 +32,13 @@ class PermissionServiceImplTest {
private WebClient webClient;
@Mock
private RequestHeadersUriSpec requestHeadersUriSpec;
private WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec;
@Mock
private RequestHeadersSpec requestHeadersSpec;
private WebClient.RequestHeadersSpec<?> requestHeadersSpec;
@Mock
private ResponseSpec responseSpec;
private WebClient.ResponseSpec responseSpec;
private PermissionService permissionService;