refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage

This commit is contained in:
张翔
2026-04-17 18:35:50 +08:00
parent 666189b676
commit deb961c427
916 changed files with 108360 additions and 38328 deletions
@@ -0,0 +1,30 @@
package cn.novalon.gym.manage.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
/**
* 网关应用启动类
*
* @author 张翔
* @date 2026-03-14
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("manage-app", r -> r
.path("/api/**")
.uri("http://localhost:8084"))
.build();
}
}
@@ -0,0 +1,207 @@
package cn.novalon.gym.manage.gateway.audit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 审计日志服务
*
* 文件定义:记录网关请求的审计日志
* 涉及业务:安全审计、访问追踪、问题排查
*
* 审计内容:
* 1. 请求信息:方法、路径、查询参数、请求头
* 2. 响应信息:状态码、响应时间
* 3. 安全事件:认证失败、授权失败、限流触发等
* 4. 错误信息:异常类型、错误消息
*
* @author 张翔
* @date 2026-03-26
*/
@Service
public class AuditLogService {
private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT_LOG");
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());
entry.setPath(request.getPath().value());
entry.setUserId(userId);
entry.setClientIp(getClientIp(request));
auditEntries.put(requestId, entry);
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.setDurationMs(durationMs);
auditLogger.info("[RESPONSE] {} {} - Status: {}, Duration: {}ms, RequestId: {}",
entry.getMethod(),
entry.getPath(),
entry.getStatusCode(),
entry.getDurationMs(),
entry.getRequestId());
auditEntries.remove(requestId);
}
}
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(),
entry.getRequestId());
} else {
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(),
entry.getRequestId());
} else {
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",
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";
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
private static class AuditEntry {
private String requestId;
private String method;
private String path;
private String userId;
private String clientIp;
private int statusCode;
private long durationMs;
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getClientIp() {
return clientIp;
}
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public long getDurationMs() {
return durationMs;
}
public void setDurationMs(long durationMs) {
this.durationMs = durationMs;
}
}
}
@@ -0,0 +1,244 @@
package cn.novalon.gym.manage.gateway.cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 请求缓存服务
*
* 文件定义:实现网关请求的缓存机制
* 涉及业务:响应缓存、缓存失效、缓存统计
*
* 核心功能:
* 1. 请求响应缓存
* 2. 缓存键生成
* 3. 缓存失效管理
* 4. 缓存统计
*
* @author 张翔
* @date 2026-03-26
*/
@Service
public class RequestCacheService {
private static final Logger logger = LoggerFactory.getLogger(RequestCacheService.class);
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
private final Map<String, CacheStats> stats = new ConcurrentHashMap<>();
private boolean cacheEnabled = true;
private Duration defaultTtl = Duration.ofMinutes(5);
private int maxCacheSize = 10000;
public Mono<String> get(ServerHttpRequest request) {
if (!cacheEnabled) {
return Mono.empty();
}
String cacheKey = generateCacheKey(request);
CacheEntry entry = cache.get(cacheKey);
if (entry == null) {
recordMiss(cacheKey);
return Mono.empty();
}
if (isExpired(entry)) {
cache.remove(cacheKey);
recordMiss(cacheKey);
return Mono.empty();
}
recordHit(cacheKey);
logger.debug("Cache hit for key: {}", cacheKey);
return Mono.just(entry.getValue());
}
public void put(ServerHttpRequest request, String response) {
if (!cacheEnabled || response == null) {
return;
}
String cacheKey = generateCacheKey(request);
if (cache.size() >= maxCacheSize) {
evictOldestEntries();
}
CacheEntry entry = new CacheEntry(
response,
System.currentTimeMillis(),
defaultTtl.toMillis()
);
cache.put(cacheKey, entry);
logger.debug("Cached response for key: {}", cacheKey);
}
public void evict(ServerHttpRequest request) {
String cacheKey = generateCacheKey(request);
cache.remove(cacheKey);
logger.debug("Evicted cache for key: {}", cacheKey);
}
public void evictByPattern(String pattern) {
cache.keySet().removeIf(key -> key.matches(pattern));
logger.info("Evicted cache entries matching pattern: {}", pattern);
}
public void clear() {
int size = cache.size();
cache.clear();
stats.clear();
logger.info("Cleared all cache entries. Removed {} entries", size);
}
private String generateCacheKey(ServerHttpRequest request) {
String method = request.getMethod().name();
String path = request.getPath().value();
String query = request.getURI().getQuery();
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(method).append(":").append(path);
if (query != null && !query.isEmpty()) {
keyBuilder.append("?").append(query);
}
return keyBuilder.toString();
}
private boolean isExpired(CacheEntry entry) {
long currentTime = System.currentTimeMillis();
return (currentTime - entry.getCreatedAt()) > entry.getTtl();
}
private void evictOldestEntries() {
int entriesToRemove = maxCacheSize / 10;
cache.entrySet().stream()
.sorted((e1, e2) ->
Long.compare(e1.getValue().getCreatedAt(),
e2.getValue().getCreatedAt()))
.limit(entriesToRemove)
.map(Map.Entry::getKey)
.forEach(cache::remove);
logger.info("Evicted {} oldest cache entries", entriesToRemove);
}
private void recordHit(String cacheKey) {
stats.compute(cacheKey, (key, stat) -> {
if (stat == null) {
stat = new CacheStats();
}
stat.incrementHits();
return stat;
});
}
private void recordMiss(String cacheKey) {
stats.compute(cacheKey, (key, stat) -> {
if (stat == null) {
stat = new CacheStats();
}
stat.incrementMisses();
return stat;
});
}
public int getCacheSize() {
return cache.size();
}
public long getHitCount() {
return stats.values().stream()
.mapToLong(CacheStats::getHits)
.sum();
}
public long getMissCount() {
return stats.values().stream()
.mapToLong(CacheStats::getMisses)
.sum();
}
public double getHitRate() {
long hits = getHitCount();
long misses = getMissCount();
long total = hits + misses;
if (total == 0) {
return 0.0;
}
return (double) hits / total;
}
public void setCacheEnabled(boolean enabled) {
this.cacheEnabled = enabled;
logger.info("Cache enabled: {}", enabled);
}
public void setDefaultTtl(Duration ttl) {
this.defaultTtl = ttl;
logger.info("Default TTL set to: {}", ttl);
}
public void setMaxCacheSize(int maxSize) {
this.maxCacheSize = maxSize;
logger.info("Max cache size set to: {}", maxSize);
}
private static class CacheEntry {
private final String value;
private final long createdAt;
private final long ttl;
public CacheEntry(String value, long createdAt, long ttl) {
this.value = value;
this.createdAt = createdAt;
this.ttl = ttl;
}
public String getValue() {
return value;
}
public long getCreatedAt() {
return createdAt;
}
public long getTtl() {
return ttl;
}
}
private static class CacheStats {
private long hits = 0;
private long misses = 0;
public void incrementHits() {
hits++;
}
public void incrementMisses() {
misses++;
}
public long getHits() {
return hits;
}
public long getMisses() {
return misses;
}
}
}
@@ -0,0 +1,227 @@
package cn.novalon.gym.manage.gateway.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 配置热更新服务
*
* 文件定义:实现配置的动态更新和管理
* 涉及业务:配置刷新、配置监听、配置版本管理
*
* 核心功能:
* 1. 配置热更新
* 2. 配置版本管理
* 3. 配置变更监听
* 4. 配置回滚
*
* @author 张翔
* @date 2026-03-26
*/
@Service
@RefreshScope
public class ConfigRefreshService {
private static final Logger logger = LoggerFactory.getLogger(ConfigRefreshService.class);
private final ContextRefresher contextRefresher;
private final Environment environment;
private final ConfigurableEnvironment configurableEnvironment;
private final Map<String, String> configHistory = new ConcurrentHashMap<>();
private final Map<String, Long> configUpdateTime = new ConcurrentHashMap<>();
private final Map<String, ConfigChangeListener> listeners = new ConcurrentHashMap<>();
private long currentVersion = System.currentTimeMillis();
public ConfigRefreshService(
ContextRefresher contextRefresher,
Environment environment,
ConfigurableEnvironment configurableEnvironment) {
this.contextRefresher = contextRefresher;
this.environment = environment;
this.configurableEnvironment = configurableEnvironment;
logger.info("ConfigRefreshService initialized");
}
public void refreshConfig() {
logger.info("Refreshing configuration");
try {
Set<String> refreshedKeys = contextRefresher.refresh();
if (!refreshedKeys.isEmpty()) {
currentVersion = System.currentTimeMillis();
logger.info("Configuration refreshed. Version: {}, Updated keys: {}",
currentVersion, refreshedKeys);
notifyListeners(refreshedKeys);
} else {
logger.info("No configuration changes detected");
}
} catch (Exception e) {
logger.error("Failed to refresh configuration", e);
}
}
public void updateConfig(String key, String value) {
if (key == null || key.isEmpty()) {
logger.warn("Config key is null or empty");
return;
}
String oldValue = environment.getProperty(key);
logger.info("Updating config - Key: {}, Old Value: {}, New Value: {}",
key, oldValue, value);
configHistory.put(key, oldValue);
configUpdateTime.put(key, System.currentTimeMillis());
try {
Map<String, Object> newConfig = new HashMap<>();
newConfig.put(key, value);
MapPropertySource propertySource = new MapPropertySource(
"dynamicConfig",
newConfig);
configurableEnvironment.getPropertySources()
.addFirst(propertySource);
logger.info("Config updated successfully: {}", key);
notifyListeners(Set.of(key));
} catch (Exception e) {
logger.error("Failed to update config: {}", key, e);
}
}
public void batchUpdateConfig(Map<String, String> configs) {
if (configs == null || configs.isEmpty()) {
logger.warn("No configs to update");
return;
}
logger.info("Batch updating {} configs", configs.size());
configs.forEach((key, value) -> {
String oldValue = environment.getProperty(key);
configHistory.put(key, oldValue);
configUpdateTime.put(key, System.currentTimeMillis());
});
try {
Map<String, Object> newConfigs = new HashMap<>(configs);
MapPropertySource propertySource = new MapPropertySource(
"batchDynamicConfig",
newConfigs);
configurableEnvironment.getPropertySources()
.addFirst(propertySource);
logger.info("Batch config update completed");
notifyListeners(configs.keySet());
} catch (Exception e) {
logger.error("Failed to batch update configs", e);
}
}
public String getConfig(String key) {
if (key == null || key.isEmpty()) {
logger.warn("Config key is null or empty");
return null;
}
return environment.getProperty(key);
}
public String getConfigWithDefault(String key, String defaultValue) {
return environment.getProperty(key, defaultValue);
}
public void rollbackConfig(String key) {
if (key == null || key.isEmpty()) {
logger.warn("Config key is null or empty");
return;
}
String oldValue = configHistory.get(key);
if (oldValue != null) {
logger.info("Rolling back config: {} to value: {}", key, oldValue);
updateConfig(key, oldValue);
} else {
logger.warn("No history found for config: {}", key);
}
}
public void registerListener(String key, ConfigChangeListener listener) {
if (key == null || key.isEmpty() || listener == null) {
logger.warn("Invalid listener registration");
return;
}
listeners.put(key, listener);
logger.info("Registered listener for config: {}", key);
}
public void unregisterListener(String key) {
if (key != null && !key.isEmpty()) {
listeners.remove(key);
logger.info("Unregistered listener for config: {}", key);
}
}
private void notifyListeners(Set<String> changedKeys) {
changedKeys.forEach(key -> {
ConfigChangeListener listener = listeners.get(key);
if (listener != null) {
try {
String newValue = environment.getProperty(key);
listener.onConfigChange(key, newValue);
logger.debug("Notified listener for config: {}", key);
} catch (Exception e) {
logger.error("Failed to notify listener for config: {}", key, e);
}
}
});
}
public long getCurrentVersion() {
return currentVersion;
}
public Map<String, String> getConfigHistory() {
return new HashMap<>(configHistory);
}
public Map<String, Long> getConfigUpdateTime() {
return new HashMap<>(configUpdateTime);
}
public void clearHistory() {
logger.info("Clearing config history");
configHistory.clear();
configUpdateTime.clear();
}
@FunctionalInterface
public interface ConfigChangeListener {
void onConfigChange(String key, String newValue);
}
}
@@ -0,0 +1,70 @@
package cn.novalon.gym.manage.gateway.config;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* 连接池配置
*
* 文件定义:配置HTTP连接池参数
* 涉及业务:连接池管理、超时控制、性能优化
*
* 配置内容:
* 1. 连接池大小
* 2. 连接超时
* 3. 读写超时
* 4. 连接空闲时间
*
* @author 张翔
* @date 2026-03-26
*/
@Configuration
public class ConnectionPoolConfig {
private static final Logger logger = LoggerFactory.getLogger(ConnectionPoolConfig.class);
@Bean
public HttpClient httpClient() {
ConnectionProvider connectionProvider = ConnectionProvider.builder("gateway-pool")
.maxConnections(500)
.maxIdleTime(Duration.ofSeconds(20))
.maxLifeTime(Duration.ofSeconds(60))
.pendingAcquireTimeout(Duration.ofSeconds(45))
.pendingAcquireMaxCount(1000)
.evictInBackground(Duration.ofSeconds(120))
.build();
HttpClient httpClient = HttpClient.create(connectionProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.doOnConnected(conn -> {
conn.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS));
conn.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS));
})
.responseTimeout(Duration.ofSeconds(10));
logger.info("HTTP client configured with connection pool");
logger.info("Max connections: 500");
logger.info("Connect timeout: 5000ms");
logger.info("Read/Write timeout: 10s");
return httpClient;
}
@Bean
public ReactorClientHttpConnector reactorClientHttpConnector(HttpClient httpClient) {
return new ReactorClientHttpConnector(httpClient);
}
}
@@ -0,0 +1,43 @@
package cn.novalon.gym.manage.gateway.config;
import cn.novalon.gym.manage.gateway.service.impl.JwtKeyServiceImpl;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@Configuration
@EnableScheduling
public class JwtKeyManagementConfig {
private static final Logger logger = LoggerFactory.getLogger(JwtKeyManagementConfig.class);
@Autowired
private JwtKeyServiceImpl jwtKeyService;
@PostConstruct
public void initialize() {
jwtKeyService.initializeKeys();
logger.info("JWT key management service initialized");
}
@Scheduled(fixedRate = 24 * 60 * 60 * 1000, initialDelay = 60 * 1000)
public void scheduledKeyRotationCheck() {
try {
logger.debug("Checking JWT key rotation status");
if (jwtKeyService.shouldRotateKey()) {
logger.info("JWT key rotation triggered");
jwtKeyService.rotateKey();
} else {
logger.debug("JWT key rotation not needed at this time");
}
} catch (Exception e) {
logger.error("Error during scheduled JWT key rotation check", e);
}
}
}
@@ -0,0 +1,119 @@
package cn.novalon.gym.manage.gateway.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* 限流配置类
*
* 文件定义:配置API限流策略,使用Resilience4j实现
* 涉及业务:API访问频率控制,防止滥用和DDoS攻击
* 算法:使用Resilience4j的RateLimiter实现令牌桶算法
*
* 支持多种限流策略:
* 1. 全局限流:对所有API请求进行统一限流
* 2. IP限流:基于客户端IP地址进行限流
* 3. 用户限流:基于用户ID进行限流
* 4. API路径限流:基于API路径进行差异化限流
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
public class RateLimitConfig {
private static final Logger logger = LoggerFactory.getLogger(RateLimitConfig.class);
@Value("${rate.limit.global.limit-for-period:1000}")
private int globalLimitForPeriod;
@Value("${rate.limit.global.limit-refresh-period:1s}")
private Duration globalLimitRefreshPeriod;
@Value("${rate.limit.global.timeout-duration:0}")
private Duration globalTimeoutDuration;
@Value("${rate.limit.ip.limit-for-period:100}")
private int ipLimitForPeriod;
@Value("${rate.limit.ip.limit-refresh-period:1s}")
private Duration ipLimitRefreshPeriod;
@Value("${rate.limit.ip.timeout-duration:0}")
private Duration ipTimeoutDuration;
@Value("${rate.limit.user.limit-for-period:200}")
private int userLimitForPeriod;
@Value("${rate.limit.user.limit-refresh-period:1s}")
private Duration userLimitRefreshPeriod;
@Value("${rate.limit.user.timeout-duration:0}")
private Duration userTimeoutDuration;
@Value("${rate.limit.enabled:true}")
private boolean rateLimitEnabled;
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
Map<String, RateLimiterConfig> configs = new HashMap<>();
configs.put("globalRateLimiter", createRateLimiterConfig(
globalLimitForPeriod, globalLimitRefreshPeriod, globalTimeoutDuration));
configs.put("ipRateLimiter", createRateLimiterConfig(
ipLimitForPeriod, ipLimitRefreshPeriod, ipTimeoutDuration));
configs.put("userRateLimiter", createRateLimiterConfig(
userLimitForPeriod, userLimitRefreshPeriod, userTimeoutDuration));
RateLimiterRegistry registry = RateLimiterRegistry.of(configs);
logger.info("Rate limiter registry initialized with {} configurations", configs.size());
logger.info("Global limit: {}/{}", globalLimitForPeriod, globalLimitRefreshPeriod);
logger.info("IP limit: {}/{}", ipLimitForPeriod, ipLimitRefreshPeriod);
logger.info("User limit: {}/{}", userLimitForPeriod, userLimitRefreshPeriod);
return registry;
}
@Bean
public RateLimiter globalRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("globalRateLimiter");
}
@Bean
public RateLimiter ipRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("ipRateLimiter");
}
@Bean
public RateLimiter userRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("userRateLimiter");
}
private RateLimiterConfig createRateLimiterConfig(
int limitForPeriod,
Duration limitRefreshPeriod,
Duration timeoutDuration) {
return RateLimiterConfig.custom()
.limitForPeriod(limitForPeriod)
.limitRefreshPeriod(limitRefreshPeriod)
.timeoutDuration(timeoutDuration)
.build();
}
public boolean isRateLimitEnabled() {
return rateLimitEnabled;
}
}
@@ -0,0 +1,216 @@
package cn.novalon.gym.manage.gateway.config;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* Resilience4j配置类
*
* 文件定义:配置断路器、重试、超时等容错机制
* 涉及业务:网关容错增强,提高系统稳定性和可用性
*
* 配置内容:
* 1. CircuitBreaker:断路器模式,防止级联故障
* 2. Retry:重试机制,处理临时故障
* 3. TimeLimiter:超时控制,防止长时间阻塞
* 4. Fallback:降级策略,提供备用响应
*
* @author 张翔
* @date 2026-03-26
*/
@Configuration
public class ResilienceConfig {
private static final Logger logger = LoggerFactory.getLogger(ResilienceConfig.class);
@Value("${resilience.circuit-breaker.enabled:true}")
private boolean circuitBreakerEnabled;
@Value("${resilience.circuit-breaker.failure-rate-threshold:50}")
private float failureRateThreshold;
@Value("${resilience.circuit-breaker.slow-call-rate-threshold:100}")
private float slowCallRateThreshold;
@Value("${resilience.circuit-breaker.slow-call-duration-threshold:2s}")
private Duration slowCallDurationThreshold;
@Value("${resilience.circuit-breaker.permitted-number-of-calls-in-half-open-state:10}")
private int permittedNumberOfCallsInHalfOpenState;
@Value("${resilience.circuit-breaker.sliding-window-type:COUNT_BASED}")
private String slidingWindowType;
@Value("${resilience.circuit-breaker.sliding-window-size:100}")
private int slidingWindowSize;
@Value("${resilience.circuit-breaker.minimum-number-of-calls:10}")
private int minimumNumberOfCalls;
@Value("${resilience.circuit-breaker.wait-duration-in-open-state:10s}")
private Duration waitDurationInOpenState;
@Value("${resilience.retry.enabled:true}")
private boolean retryEnabled;
@Value("${resilience.retry.max-attempts:3}")
private int retryMaxAttempts;
@Value("${resilience.retry.wait-duration:500ms}")
private Duration retryWaitDuration;
@Value("${resilience.retry.exponential-backoff-multiplier:2}")
private double exponentialBackoffMultiplier;
@Value("${resilience.timeout.enabled:true}")
private boolean timeoutEnabled;
@Value("${resilience.timeout.duration:3s}")
private Duration timeoutDuration;
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(failureRateThreshold)
.slowCallRateThreshold(slowCallRateThreshold)
.slowCallDurationThreshold(slowCallDurationThreshold)
.permittedNumberOfCallsInHalfOpenState(permittedNumberOfCallsInHalfOpenState)
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.valueOf(slidingWindowType))
.slidingWindowSize(slidingWindowSize)
.minimumNumberOfCalls(minimumNumberOfCalls)
.waitDurationInOpenState(waitDurationInOpenState)
.recordExceptions(Exception.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
Map<String, CircuitBreakerConfig> configs = new HashMap<>();
configs.put("default", config);
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(configs);
logger.info("CircuitBreaker registry initialized with {} configurations", configs.size());
logger.info("Failure rate threshold: {}%", failureRateThreshold);
logger.info("Slow call duration threshold: {}", slowCallDurationThreshold);
logger.info("Sliding window size: {}", slidingWindowSize);
logger.info("Wait duration in open state: {}", waitDurationInOpenState);
return registry;
}
@Bean
public CircuitBreaker gatewayCircuitBreaker(CircuitBreakerRegistry registry) {
CircuitBreaker circuitBreaker = registry.circuitBreaker("gateway", "default");
circuitBreaker.getEventPublisher()
.onStateTransition(event ->
logger.warn("CircuitBreaker state transition: {} -> {} for {}",
event.getStateTransition().getFromState(),
event.getStateTransition().getToState(),
event.getCircuitBreakerName()))
.onError(event ->
logger.error("CircuitBreaker error: {} - {}",
event.getCircuitBreakerName(),
event.getThrowable().getMessage()))
.onSuccess(event ->
logger.debug("CircuitBreaker success: {} - Duration: {}ms",
event.getCircuitBreakerName(),
event.getElapsedDuration().toMillis()));
logger.info("Gateway CircuitBreaker created: {}", circuitBreaker.getName());
return circuitBreaker;
}
@Bean
public RetryRegistry retryRegistry() {
RetryConfig config = RetryConfig.custom()
.maxAttempts(retryMaxAttempts)
.waitDuration(retryWaitDuration)
.retryExceptions(Exception.class)
.ignoreExceptions(IllegalArgumentException.class)
.build();
Map<String, RetryConfig> configs = new HashMap<>();
configs.put("default", config);
RetryRegistry registry = RetryRegistry.of(configs);
logger.info("Retry registry initialized with {} configurations", configs.size());
logger.info("Max attempts: {}", retryMaxAttempts);
logger.info("Wait duration: {}", retryWaitDuration);
return registry;
}
@Bean
public Retry gatewayRetry(RetryRegistry registry) {
Retry retry = registry.retry("gateway", "default");
retry.getEventPublisher()
.onRetry(event ->
logger.warn("Retry attempt {} of {} for {}",
event.getNumberOfRetryAttempts(),
retryMaxAttempts,
event.getName()))
.onError(event ->
logger.error("Retry failed after {} attempts for {}",
event.getNumberOfRetryAttempts(),
event.getName()))
.onSuccess(event ->
logger.debug("Retry succeeded after {} attempts for {}",
event.getNumberOfRetryAttempts(),
event.getName()));
logger.info("Gateway Retry created: {}", retry.getName());
return retry;
}
@Bean
public TimeLimiterRegistry timeLimiterRegistry() {
TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(timeoutDuration)
.cancelRunningFuture(true)
.build();
Map<String, TimeLimiterConfig> configs = new HashMap<>();
configs.put("default", config);
TimeLimiterRegistry registry = TimeLimiterRegistry.of(configs);
logger.info("TimeLimiter registry initialized with {} configurations", configs.size());
logger.info("Timeout duration: {}", timeoutDuration);
return registry;
}
@Bean
public TimeLimiter gatewayTimeLimiter(TimeLimiterRegistry registry) {
TimeLimiter timeLimiter = registry.timeLimiter("gateway", "default");
timeLimiter.getEventPublisher()
.onTimeout(event ->
logger.warn("Timeout occurred for {}",
event.getTimeLimiterName()))
.onSuccess(event ->
logger.debug("TimeLimiter success for {}",
event.getTimeLimiterName()));
logger.info("Gateway TimeLimiter created: {}", timeLimiter.getName());
return timeLimiter;
}
}
@@ -0,0 +1,14 @@
package cn.novalon.gym.manage.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
@@ -0,0 +1,124 @@
package cn.novalon.gym.manage.gateway.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
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.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* 响应压缩过滤器
*
* 文件定义:实现网关响应的压缩功能
* 涉及业务:响应压缩、性能优化、带宽节省
*
* 核心功能:
* 1. 检测客户端支持的压缩算法
* 2. 对响应进行压缩
* 3. 设置压缩相关响应头
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class CompressionFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(CompressionFilter.class);
private static final String ACCEPT_ENCODING = "Accept-Encoding";
private static final String CONTENT_ENCODING = "Content-Encoding";
private static final String GZIP = "gzip";
private static final String DEFLATE = "deflate";
private static final String VARY = "Vary";
private static final List<String> COMPRESSIBLE_TYPES = Arrays.asList(
"text/html",
"text/xml",
"text/plain",
"text/css",
"text/javascript",
"application/javascript",
"application/json",
"application/xml"
);
private boolean compressionEnabled = false;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (!compressionEnabled || !shouldCompress(request)) {
return chain.filter(exchange);
}
String acceptEncoding = request.getHeaders().getFirst(ACCEPT_ENCODING);
if (acceptEncoding == null || acceptEncoding.isEmpty()) {
return chain.filter(exchange);
}
String compressionType = determineCompressionType(acceptEncoding);
if (compressionType == null) {
return chain.filter(exchange);
}
logger.debug("Applying {} compression for request: {}",
compressionType, request.getPath());
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set(CONTENT_ENCODING, compressionType);
response.getHeaders().add(VARY, ACCEPT_ENCODING);
return chain.filter(exchange);
}
private boolean shouldCompress(ServerHttpRequest request) {
if (request.getMethod() == HttpMethod.OPTIONS) {
return false;
}
String contentType = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
if (contentType != null) {
return COMPRESSIBLE_TYPES.stream()
.anyMatch(type -> contentType.contains(type));
}
return true;
}
private String determineCompressionType(String acceptEncoding) {
if (acceptEncoding.contains(GZIP)) {
return GZIP;
}
if (acceptEncoding.contains(DEFLATE)) {
return DEFLATE;
}
return null;
}
public void setCompressionEnabled(boolean enabled) {
this.compressionEnabled = enabled;
logger.info("Compression enabled: {}", enabled);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
}
@@ -0,0 +1,65 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.util.JwtUtil;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
super(Config.class);
this.jwtUtil = jwtUtil;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (isPublicPath(path)) {
return chain.filter(exchange);
}
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String token = authHeader.substring(7);
if (!jwtUtil.validateToken(token) || jwtUtil.isTokenExpired(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String username = jwtUtil.getUsernameFromToken(token);
Long userId = jwtUtil.getUserIdFromToken(token);
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", String.valueOf(userId))
.header("X-Username", username)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
};
}
private boolean isPublicPath(String path) {
return path.startsWith("/api/auth/") ||
path.equals("/actuator/health") ||
path.startsWith("/actuator/info");
}
public static class Config {
}
}
@@ -0,0 +1,221 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.config.RateLimitConfig;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 网关限流过滤器
*
* 文件定义:实现多维度限流策略的全局过滤器
* 涉及业务:API访问频率控制,防止滥用和DDoS攻击
* 算法:使用Resilience4j的RateLimiter实现令牌桶算法
*
* 限流维度:
* 1. 全局限流:保护系统整体稳定性
* 2. IP限流:防止单个IP过度访问
* 3. 用户限流:防止单个用户过度访问
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class RateLimitFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(RateLimitFilter.class);
private static final String USER_ID_HEADER = "X-User-Id";
private static final String RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining";
private static final String RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit";
private static final String RETRY_AFTER_HEADER = "Retry-After";
private final RateLimiter globalRateLimiter;
private final RateLimiter ipRateLimiter;
private final RateLimiter userRateLimiter;
private final RateLimitConfig rateLimitConfig;
private final ConcurrentHashMap<String, RateLimiter> ipRateLimiterMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, RateLimiter> userRateLimiterMap = new ConcurrentHashMap<>();
private final AtomicInteger totalRequests = new AtomicInteger(0);
private final AtomicInteger blockedRequests = new AtomicInteger(0);
public RateLimitFilter(
RateLimiter globalRateLimiter,
RateLimiter ipRateLimiter,
RateLimiter userRateLimiter,
RateLimitConfig rateLimitConfig) {
this.globalRateLimiter = globalRateLimiter;
this.ipRateLimiter = ipRateLimiter;
this.userRateLimiter = userRateLimiter;
this.rateLimitConfig = rateLimitConfig;
logger.info("RateLimitFilter initialized with enabled: {}", rateLimitConfig.isRateLimitEnabled());
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!rateLimitConfig.isRateLimitEnabled()) {
return chain.filter(exchange);
}
totalRequests.incrementAndGet();
ServerHttpRequest request = exchange.getRequest();
String clientIp = getClientIp(request);
String userId = getUserId(request);
String requestPath = request.getPath().value();
logger.debug("Processing request - IP: {}, UserId: {}, Path: {}", clientIp, userId, requestPath);
return checkGlobalRateLimit(exchange, chain, clientIp, userId);
}
private Mono<Void> checkGlobalRateLimit(
ServerWebExchange exchange,
GatewayFilterChain chain,
String clientIp,
String userId) {
return Mono.fromCallable(() -> globalRateLimiter.acquirePermission())
.flatMap(permitted -> {
if (permitted) {
return checkIpRateLimit(exchange, chain, clientIp, userId);
} else {
return handleRateLimitExceeded(exchange, "Global", clientIp, userId);
}
})
.onErrorResume(RequestNotPermitted.class,
e -> handleRateLimitExceeded(exchange, "Global", clientIp, userId));
}
private Mono<Void> checkIpRateLimit(
ServerWebExchange exchange,
GatewayFilterChain chain,
String clientIp,
String userId) {
RateLimiter ipLimiter = ipRateLimiterMap.computeIfAbsent(
clientIp,
k -> createIpRateLimiter(clientIp));
return Mono.fromCallable(() -> ipLimiter.acquirePermission())
.flatMap(permitted -> {
if (permitted) {
if (userId != null && !userId.isEmpty()) {
return checkUserRateLimit(exchange, chain, userId);
} else {
return chain.filter(exchange);
}
} else {
return handleRateLimitExceeded(exchange, "IP", clientIp, userId);
}
})
.onErrorResume(RequestNotPermitted.class,
e -> handleRateLimitExceeded(exchange, "IP", clientIp, userId));
}
private Mono<Void> checkUserRateLimit(
ServerWebExchange exchange,
GatewayFilterChain chain,
String userId) {
RateLimiter userLimiter = userRateLimiterMap.computeIfAbsent(
userId,
k -> createUserRateLimiter(userId));
return Mono.fromCallable(() -> userLimiter.acquirePermission())
.flatMap(permitted -> {
if (permitted) {
return chain.filter(exchange);
} else {
return handleRateLimitExceeded(exchange, "User", null, userId);
}
})
.onErrorResume(RequestNotPermitted.class, e -> handleRateLimitExceeded(exchange, "User", null, userId));
}
private Mono<Void> handleRateLimitExceeded(
ServerWebExchange exchange,
String limitType,
String clientIp,
String userId) {
blockedRequests.incrementAndGet();
logger.warn("Rate limit exceeded - Type: {}, IP: {}, UserId: {}", limitType, clientIp, userId);
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
HttpHeaders headers = response.getHeaders();
headers.add(RATE_LIMIT_LIMIT_HEADER, "0");
headers.add(RATE_LIMIT_REMAINING_HEADER, "0");
headers.add(RETRY_AFTER_HEADER, "1");
headers.add("X-RateLimit-Type", limitType);
String errorMessage = String.format(
"{\"error\":\"Rate limit exceeded\",\"type\":\"%s\",\"message\":\"Too many requests. Please try again later.\"}",
limitType);
return response.writeWith(Mono.just(response.bufferFactory().wrap(errorMessage.getBytes())));
}
private String getClientIp(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeaders().getFirst("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddress() != null
? request.getRemoteAddress().getAddress().getHostAddress()
: "unknown";
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
private String getUserId(ServerHttpRequest request) {
return request.getHeaders().getFirst(USER_ID_HEADER);
}
private RateLimiter createIpRateLimiter(String ip) {
logger.debug("Creating rate limiter for IP: {}", ip);
return RateLimiter.of("ip-" + ip, ipRateLimiter.getRateLimiterConfig());
}
private RateLimiter createUserRateLimiter(String userId) {
logger.debug("Creating rate limiter for user: {}", userId);
return RateLimiter.of("user-" + userId, userRateLimiter.getRateLimiterConfig());
}
public int getTotalRequests() {
return totalRequests.get();
}
public int getBlockedRequests() {
return blockedRequests.get();
}
public void resetCounters() {
totalRequests.set(0);
blockedRequests.set(0);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 100;
}
}
@@ -0,0 +1,71 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.service.PermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
@Component
public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory<RbacAuthorizationFilter.Config> {
private static final Logger logger = LoggerFactory.getLogger(RbacAuthorizationFilter.class);
private final PermissionService permissionService;
public RbacAuthorizationFilter(PermissionService permissionService) {
super(Config.class);
this.permissionService = permissionService;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String method = request.getMethod().name();
if (isPublicPath(path)) {
logger.debug("Public path access: {}", path);
return chain.filter(exchange);
}
String userIdHeader = request.getHeaders().getFirst("X-User-Id");
if (userIdHeader == null || userIdHeader.isEmpty()) {
logger.warn("Missing X-User-Id header for path: {}", path);
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
Long userId;
try {
userId = Long.parseLong(userIdHeader);
} catch (NumberFormatException e) {
logger.error("Invalid X-User-Id header: {}", userIdHeader, e);
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
if (!permissionService.hasPermission(userId, path, method)) {
logger.warn("Permission denied for userId: {}, path: {}, method: {}", userId, path, method);
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
logger.debug("Permission granted for userId: {}, path: {}, method: {}", userId, path, method);
return chain.filter(exchange);
};
}
private boolean isPublicPath(String path) {
return path.startsWith("/api/auth/") ||
path.equals("/actuator/health") ||
path.startsWith("/actuator/info");
}
public static class Config {
}
}
@@ -0,0 +1,125 @@
package cn.novalon.gym.manage.gateway.filter;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator;
import io.github.resilience4j.reactor.retry.RetryOperator;
import io.github.resilience4j.reactor.timelimiter.TimeLimiterOperator;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.timelimiter.TimeLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 容错过滤器
*
* 文件定义:实现断路器、重试、超时等容错机制的全局过滤器
* 涉及业务:网关容错增强,提高系统稳定性和可用性
*
* 容错机制:
* 1. CircuitBreaker:断路器模式,防止级联故障
* 2. Retry:重试机制,处理临时故障
* 3. TimeLimiter:超时控制,防止长时间阻塞
* 4. Fallback:降级策略,提供备用响应
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class ResilienceFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(ResilienceFilter.class);
private final CircuitBreaker circuitBreaker;
private final Retry retry;
private final TimeLimiter timeLimiter;
@Value("${resilience.enabled:true}")
private boolean resilienceEnabled;
@Value("${resilience.circuit-breaker.enabled:true}")
private boolean circuitBreakerEnabled;
@Value("${resilience.retry.enabled:true}")
private boolean retryEnabled;
@Value("${resilience.timeout.enabled:true}")
private boolean timeoutEnabled;
public ResilienceFilter(CircuitBreaker circuitBreaker, Retry retry, TimeLimiter timeLimiter) {
this.circuitBreaker = circuitBreaker;
this.retry = retry;
this.timeLimiter = timeLimiter;
logger.info("ResilienceFilter initialized - CircuitBreaker: {}, Retry: {}, TimeLimiter: {}",
circuitBreaker.getName(), retry.getName(), timeLimiter.getName());
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!resilienceEnabled) {
logger.debug("Resilience is disabled");
return chain.filter(exchange);
}
logger.debug("Applying resilience patterns for request: {} {}",
exchange.getRequest().getMethod(),
exchange.getRequest().getPath());
Mono<Void> chainMono = chain.filter(exchange);
if (timeoutEnabled) {
chainMono = chainMono.transform(TimeLimiterOperator.of(timeLimiter));
}
if (retryEnabled) {
chainMono = chainMono.transform(RetryOperator.of(retry));
}
if (circuitBreakerEnabled) {
chainMono = chainMono.transform(CircuitBreakerOperator.of(circuitBreaker));
}
return chainMono
.onErrorResume(Exception.class, e -> handleFallback(exchange, e));
}
private Mono<Void> handleFallback(ServerWebExchange exchange, Throwable throwable) {
logger.error("Fallback triggered for request: {} {} - Error: {}",
exchange.getRequest().getMethod(),
exchange.getRequest().getPath(),
throwable.getMessage());
ServerHttpResponse response = exchange.getResponse();
if (throwable instanceof io.github.resilience4j.circuitbreaker.CallNotPermittedException) {
response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
String errorMessage = "{\"error\":\"Service Unavailable\",\"code\":\"CIRCUIT_BREAKER_OPEN\"," +
"\"message\":\"Service is temporarily unavailable due to circuit breaker being open. " +
"Please try again later.\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(errorMessage.getBytes())));
} else if (throwable instanceof java.util.concurrent.TimeoutException) {
response.setStatusCode(HttpStatus.GATEWAY_TIMEOUT);
String errorMessage = "{\"error\":\"Gateway Timeout\",\"code\":\"TIMEOUT\"," +
"\"message\":\"Request timed out. Please try again.\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(errorMessage.getBytes())));
} else {
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
String errorMessage = "{\"error\":\"Internal Server Error\",\"code\":\"INTERNAL_ERROR\"," +
"\"message\":\"An unexpected error occurred. Please try again later.\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(errorMessage.getBytes())));
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 200;
}
}
@@ -0,0 +1,117 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.service.SignatureService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* 请求签名验证过滤器
*
* 文件定义:实现API请求签名验证的全局过滤器
* 涉及业务:API安全防护,防止请求篡改和重放攻击
* 算法:HMAC-SHA256签名验证
*
* 验证流程:
* 1. 检查请求是否在白名单路径中
* 2. 提取签名相关头部(X-Signature, X-Timestamp, X-Nonce
* 3. 验证时间戳是否在有效期内
* 4. 验证nonce是否已使用(防重放攻击)
* 5. 重新计算签名并比对
* 6. 记录nonce防止重放
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class SignatureFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(SignatureFilter.class);
private final SignatureService signatureService;
@Value("${signature.enabled:true}")
private boolean signatureEnabled;
@Value("${signature.secret:${SIGNATURE_SECRET:NovalonManageSystemSecretKey2026}}")
private String signatureSecret;
@Value("${signature.whitelist.paths:/actuator/health,/actuator/info}")
private String whitelistPaths;
public SignatureFilter(SignatureService signatureService) {
this.signatureService = signatureService;
logger.info("SignatureFilter initialized with enabled: {}", signatureEnabled);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!signatureEnabled) {
logger.debug("Signature verification is disabled");
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
if (isWhitelisted(path)) {
logger.debug("Path {} is whitelisted, skipping signature verification", path);
return chain.filter(exchange);
}
logger.debug("Verifying signature for request: {} {}", request.getMethod(), path);
boolean isValid = signatureService.verifySignature(request, signatureSecret);
if (isValid) {
logger.debug("Signature verification passed for request: {}", path);
return chain.filter(exchange);
} else {
logger.warn("Signature verification failed for request: {} {}", request.getMethod(), path);
return handleSignatureFailure(exchange);
}
}
private boolean isWhitelisted(String path) {
if (whitelistPaths == null || whitelistPaths.isEmpty()) {
return false;
}
List<String> whitelistedPaths = Arrays.asList(whitelistPaths.split(","));
return whitelistedPaths.stream()
.anyMatch(whitelisted -> path.startsWith(whitelisted.trim()));
}
private Mono<Void> handleSignatureFailure(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
HttpHeaders headers = response.getHeaders();
headers.add("X-Error-Code", "INVALID_SIGNATURE");
headers.add("X-Error-Message", "Request signature verification failed");
String errorMessage = "{\"error\":\"Unauthorized\",\"code\":\"INVALID_SIGNATURE\"," +
"\"message\":\"Request signature verification failed. " +
"Please ensure you have included valid X-Signature, X-Timestamp, and X-Nonce headers.\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(errorMessage.getBytes())));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 150;
}
}
@@ -0,0 +1,100 @@
package cn.novalon.gym.manage.gateway.health;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 网关健康检查指示器
*
* 文件定义:实现自定义健康检查逻辑,监控网关核心组件状态
* 涉及业务:网关健康状态监控,包括断路器、限流器等关键组件
*
* 健康检查内容:
* 1. 断路器状态:检查所有断路器是否处于健康状态
* 2. 限流器状态:检查限流器是否正常工作
* 3. 自定义指标:检查网关特定的健康指标
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class GatewayHealthIndicator implements HealthIndicator {
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final RateLimiterRegistry rateLimiterRegistry;
public GatewayHealthIndicator(
CircuitBreakerRegistry circuitBreakerRegistry,
RateLimiterRegistry rateLimiterRegistry) {
this.circuitBreakerRegistry = circuitBreakerRegistry;
this.rateLimiterRegistry = rateLimiterRegistry;
}
@Override
public Health health() {
Health.Builder builder = Health.up();
Map<String, Object> details = new HashMap<>();
checkCircuitBreakers(details);
checkRateLimiters(details);
boolean hasUnhealthyComponents = details.values().stream()
.filter(value -> value instanceof Map)
.map(value -> (Map<?, ?>) value)
.flatMap(map -> map.values().stream())
.filter(value -> value instanceof Map)
.map(value -> (Map<?, ?>) value)
.anyMatch(componentDetails ->
componentDetails.containsKey("status") &&
"DOWN".equals(componentDetails.get("status")));
if (hasUnhealthyComponents) {
builder = Health.down();
}
builder.withDetails(details);
return builder.build();
}
private void checkCircuitBreakers(Map<String, Object> details) {
Map<String, Object> circuitBreakerDetails = new HashMap<>();
circuitBreakerRegistry.getAllCircuitBreakers().forEach(circuitBreaker -> {
String name = circuitBreaker.getName();
CircuitBreaker.State state = circuitBreaker.getState();
Map<String, Object> cbDetails = new HashMap<>();
cbDetails.put("state", state.name());
cbDetails.put("status", state == CircuitBreaker.State.OPEN ? "DOWN" : "UP");
circuitBreakerDetails.put(name, cbDetails);
});
details.put("circuitBreakers", circuitBreakerDetails);
}
private void checkRateLimiters(Map<String, Object> details) {
Map<String, Object> rateLimiterDetails = new HashMap<>();
rateLimiterRegistry.getAllRateLimiters().forEach(rateLimiter -> {
String name = rateLimiter.getName();
Map<String, Object> rlDetails = new HashMap<>();
rlDetails.put("status", "UP");
rlDetails.put("availablePermissions",
rateLimiter.getRateLimiterConfig().getLimitForPeriod());
rateLimiterDetails.put(name, rlDetails);
});
details.put("rateLimiters", rateLimiterDetails);
}
}
@@ -0,0 +1,165 @@
package cn.novalon.gym.manage.gateway.loadbalancer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义负载均衡器
*
* 文件定义:实现多种负载均衡策略
* 涉及业务:请求分发、服务实例选择、负载均衡策略
*
* 负载均衡策略:
* 1. 轮询
* 2. 随机
* 3. 加权轮询
* 4. 最少连接
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class CustomLoadBalancer {
private static final Logger logger = LoggerFactory.getLogger(CustomLoadBalancer.class);
private final AtomicInteger position = new AtomicInteger(new Random().nextInt(1000));
private final Map<String, AtomicInteger> connectionCounts = new ConcurrentHashMap<>();
private final Map<String, Integer> weights = new ConcurrentHashMap<>();
public ServiceInstance selectInstance(
List<ServiceInstance> instances,
LoadBalanceStrategy strategy) {
if (instances == null || instances.isEmpty()) {
logger.warn("No instances available");
return null;
}
ServiceInstance selectedInstance;
switch (strategy) {
case ROUND_ROBIN:
selectedInstance = selectByRoundRobin(instances);
break;
case RANDOM:
selectedInstance = selectByRandom(instances);
break;
case WEIGHTED_ROUND_ROBIN:
selectedInstance = selectByWeightedRoundRobin(instances);
break;
case LEAST_CONNECTIONS:
selectedInstance = selectByLeastConnections(instances);
break;
default:
selectedInstance = selectByRoundRobin(instances);
}
if (selectedInstance != null) {
logger.debug("Selected instance {}:{} using {} strategy",
selectedInstance.getHost(),
selectedInstance.getPort(),
strategy);
}
return selectedInstance;
}
private ServiceInstance selectByRoundRobin(List<ServiceInstance> instances) {
int pos = Math.abs(position.incrementAndGet());
return instances.get(pos % instances.size());
}
private ServiceInstance selectByRandom(List<ServiceInstance> instances) {
int index = new Random().nextInt(instances.size());
return instances.get(index);
}
private ServiceInstance selectByWeightedRoundRobin(List<ServiceInstance> instances) {
int totalWeight = instances.stream()
.mapToInt(this::getWeight)
.sum();
if (totalWeight == 0) {
return selectByRoundRobin(instances);
}
int randomWeight = new Random().nextInt(totalWeight);
int currentWeight = 0;
for (ServiceInstance instance : instances) {
currentWeight += getWeight(instance);
if (randomWeight < currentWeight) {
return instance;
}
}
return instances.get(0);
}
private ServiceInstance selectByLeastConnections(List<ServiceInstance> instances) {
ServiceInstance selectedInstance = null;
int minConnections = Integer.MAX_VALUE;
for (ServiceInstance instance : instances) {
int connections = getConnectionCount(instance);
if (connections < minConnections) {
minConnections = connections;
selectedInstance = instance;
}
}
return selectedInstance != null ? selectedInstance : instances.get(0);
}
private int getWeight(ServiceInstance instance) {
String instanceKey = getInstanceKey(instance);
return weights.getOrDefault(instanceKey, 1);
}
public void setWeight(ServiceInstance instance, int weight) {
String instanceKey = getInstanceKey(instance);
weights.put(instanceKey, weight);
logger.debug("Set weight {} for instance {}", weight, instanceKey);
}
private int getConnectionCount(ServiceInstance instance) {
String instanceKey = getInstanceKey(instance);
AtomicInteger count = connectionCounts.get(instanceKey);
return count != null ? count.get() : 0;
}
public void incrementConnection(ServiceInstance instance) {
String instanceKey = getInstanceKey(instance);
connectionCounts.computeIfAbsent(instanceKey, k -> new AtomicInteger(0)).incrementAndGet();
logger.debug("Incremented connection count for instance {}", instanceKey);
}
public void decrementConnection(ServiceInstance instance) {
String instanceKey = getInstanceKey(instance);
AtomicInteger count = connectionCounts.get(instanceKey);
if (count != null && count.get() > 0) {
count.decrementAndGet();
logger.debug("Decremented connection count for instance {}", instanceKey);
}
}
private String getInstanceKey(ServiceInstance instance) {
return instance.getHost() + ":" + instance.getPort();
}
public enum LoadBalanceStrategy {
ROUND_ROBIN,
RANDOM,
WEIGHTED_ROUND_ROBIN,
LEAST_CONNECTIONS
}
}
@@ -0,0 +1,151 @@
package cn.novalon.gym.manage.gateway.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 网关指标收集器
*
* 文件定义:收集和暴露网关自定义指标
* 涉及业务:请求统计、错误统计、性能监控
*
* 指标类型:
* 1. Counter:计数器,用于统计请求总数、错误总数等
* 2. Gauge:仪表盘,用于统计当前值,如活跃连接数
* 3. Timer:计时器,用于统计请求耗时
*
* @author 张翔
* @date 2026-03-26
*/
@Component
public class GatewayMetrics {
private static final Logger logger = LoggerFactory.getLogger(GatewayMetrics.class);
private final MeterRegistry meterRegistry;
private final Counter totalRequestsCounter;
private final Counter successRequestsCounter;
private final Counter failedRequestsCounter;
private final Counter rateLimitedRequestsCounter;
private final Counter circuitBreakerOpenCounter;
private final Counter unauthorizedRequestsCounter;
private final AtomicLong activeConnections = new AtomicLong(0);
private final ConcurrentHashMap<String, AtomicLong> pathRequestCounts = new ConcurrentHashMap<>();
public GatewayMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.totalRequestsCounter = Counter.builder("gateway.requests.total")
.description("Total number of gateway requests")
.register(meterRegistry);
this.successRequestsCounter = Counter.builder("gateway.requests.success")
.description("Number of successful gateway requests")
.register(meterRegistry);
this.failedRequestsCounter = Counter.builder("gateway.requests.failed")
.description("Number of failed gateway requests")
.register(meterRegistry);
this.rateLimitedRequestsCounter = Counter.builder("gateway.requests.rate_limited")
.description("Number of rate limited requests")
.register(meterRegistry);
this.circuitBreakerOpenCounter = Counter.builder("gateway.circuit_breaker.open")
.description("Number of circuit breaker open events")
.register(meterRegistry);
this.unauthorizedRequestsCounter = Counter.builder("gateway.requests.unauthorized")
.description("Number of unauthorized requests")
.register(meterRegistry);
Gauge.builder("gateway.connections.active", activeConnections, AtomicLong::get)
.description("Number of active connections")
.register(meterRegistry);
logger.info("Gateway metrics initialized");
}
public void incrementTotalRequests() {
totalRequestsCounter.increment();
}
public void incrementSuccessRequests() {
successRequestsCounter.increment();
}
public void incrementFailedRequests() {
failedRequestsCounter.increment();
}
public void incrementRateLimitedRequests() {
rateLimitedRequestsCounter.increment();
}
public void incrementCircuitBreakerOpen() {
circuitBreakerOpenCounter.increment();
}
public void incrementUnauthorizedRequests() {
unauthorizedRequestsCounter.increment();
}
public void incrementActiveConnections() {
activeConnections.incrementAndGet();
}
public void decrementActiveConnections() {
activeConnections.decrementAndGet();
}
public void recordRequestDuration(String path, Duration duration) {
Timer.builder("gateway.request.duration")
.description("Request duration")
.tag("path", path)
.register(meterRegistry)
.record(duration);
pathRequestCounts.computeIfAbsent(path, k -> {
AtomicLong counter = new AtomicLong(0);
Gauge.builder("gateway.path.requests", counter, AtomicLong::get)
.description("Number of requests per path")
.tag("path", path)
.register(meterRegistry);
return counter;
}).incrementAndGet();
}
public void recordCustomMetric(String name, double value, String... tags) {
Counter.builder(name)
.tags(tags)
.register(meterRegistry)
.increment(value);
}
public long getTotalRequests() {
return (long) totalRequestsCounter.count();
}
public long getSuccessRequests() {
return (long) successRequestsCounter.count();
}
public long getFailedRequests() {
return (long) failedRequestsCounter.count();
}
public long getActiveConnections() {
return activeConnections.get();
}
}
@@ -0,0 +1,112 @@
package cn.novalon.gym.manage.gateway.model;
public class Permission {
private Long id;
private String permissionCode;
private String permissionName;
private String resourceType;
private String resourcePath;
private String httpMethod;
private String description;
private Integer status;
private Long createTime;
private Long updateTime;
public Permission() {
}
public Permission(Long id, String permissionCode, String permissionName, String resourceType,
String resourcePath, String httpMethod, String description,
Integer status, Long createTime, Long updateTime) {
this.id = id;
this.permissionCode = permissionCode;
this.permissionName = permissionName;
this.resourceType = resourceType;
this.resourcePath = resourcePath;
this.httpMethod = httpMethod;
this.description = description;
this.status = status;
this.createTime = createTime;
this.updateTime = updateTime;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPermissionCode() {
return permissionCode;
}
public void setPermissionCode(String permissionCode) {
this.permissionCode = permissionCode;
}
public String getPermissionName() {
return permissionName;
}
public void setPermissionName(String permissionName) {
this.permissionName = permissionName;
}
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getResourcePath() {
return resourcePath;
}
public void setResourcePath(String resourcePath) {
this.resourcePath = resourcePath;
}
public String getHttpMethod() {
return httpMethod;
}
public void setHttpMethod(String httpMethod) {
this.httpMethod = httpMethod;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
}
@@ -0,0 +1,80 @@
package cn.novalon.gym.manage.gateway.model;
public class Role {
private Long id;
private String roleCode;
private String roleName;
private String description;
private Integer status;
private Long createTime;
private Long updateTime;
public Role() {
}
public Role(Long id, String roleCode, String roleName, String description, Integer status, Long createTime, Long updateTime) {
this.id = id;
this.roleCode = roleCode;
this.roleName = roleName;
this.description = description;
this.status = status;
this.createTime = createTime;
this.updateTime = updateTime;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
}
@@ -0,0 +1,50 @@
package cn.novalon.gym.manage.gateway.model;
public class RolePermission {
private Long id;
private Long roleId;
private Long permissionId;
private Long createTime;
public RolePermission() {
}
public RolePermission(Long id, Long roleId, Long permissionId, Long createTime) {
this.id = id;
this.roleId = roleId;
this.permissionId = permissionId;
this.createTime = createTime;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getPermissionId() {
return permissionId;
}
public void setPermissionId(Long permissionId) {
this.permissionId = permissionId;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
}
@@ -0,0 +1,80 @@
package cn.novalon.gym.manage.gateway.model;
public class User {
private Long id;
private String username;
private String email;
private String phone;
private Integer status;
private Long createTime;
private Long updateTime;
public User() {
}
public User(Long id, String username, String email, String phone, Integer status, Long createTime, Long updateTime) {
this.id = id;
this.username = username;
this.email = email;
this.phone = phone;
this.status = status;
this.createTime = createTime;
this.updateTime = updateTime;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
}
@@ -0,0 +1,50 @@
package cn.novalon.gym.manage.gateway.model;
public class UserRole {
private Long id;
private Long userId;
private Long roleId;
private Long createTime;
public UserRole() {
}
public UserRole(Long id, Long userId, Long roleId, Long createTime) {
this.id = id;
this.userId = userId;
this.roleId = roleId;
this.createTime = createTime;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
}
@@ -0,0 +1,212 @@
package cn.novalon.gym.manage.gateway.monitor;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 性能监控服务
*
* 文件定义:监控网关性能指标
* 涉及业务:性能统计、瓶颈识别、性能优化
*
* 监控指标:
* 1. 请求处理时间
* 2. 内存使用情况
* 3. 线程池状态
* 4. 连接池状态
*
* @author 张翔
* @date 2026-03-26
*/
@Service
public class PerformanceMonitor {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitor.class);
private final MeterRegistry meterRegistry;
private final Counter slowRequestsCounter;
private final Counter memoryWarningCounter;
private final AtomicLong totalProcessingTime = new AtomicLong(0);
private final AtomicLong requestCount = new AtomicLong(0);
private final Map<String, PerformanceStats> pathStats = new ConcurrentHashMap<>();
private long slowRequestThresholdMs = 2000;
private double memoryWarningThreshold = 0.85;
public PerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.slowRequestsCounter = Counter.builder("gateway.performance.slow_requests")
.description("Number of slow requests")
.register(meterRegistry);
this.memoryWarningCounter = Counter.builder("gateway.performance.memory_warnings")
.description("Number of memory warnings")
.register(meterRegistry);
Gauge.builder("gateway.performance.avg_processing_time",
this, PerformanceMonitor::getAverageProcessingTime)
.description("Average request processing time in ms")
.register(meterRegistry);
Gauge.builder("gateway.performance.memory_usage",
this, PerformanceMonitor::getMemoryUsage)
.description("Current memory usage ratio")
.register(meterRegistry);
logger.info("Performance monitor initialized");
}
public void recordRequest(String path, long durationMs) {
totalProcessingTime.addAndGet(durationMs);
requestCount.incrementAndGet();
pathStats.compute(path, (key, stats) -> {
if (stats == null) {
stats = new PerformanceStats();
}
stats.recordRequest(durationMs);
return stats;
});
if (durationMs > slowRequestThresholdMs) {
slowRequestsCounter.increment();
logger.warn("Slow request detected - Path: {}, Duration: {}ms", path, durationMs);
}
Timer.builder("gateway.performance.request_duration")
.description("Request processing duration")
.tag("path", path)
.register(meterRegistry)
.record(Duration.ofMillis(durationMs));
checkMemoryUsage();
}
private void checkMemoryUsage() {
double memoryUsage = getMemoryUsage();
if (memoryUsage > memoryWarningThreshold) {
memoryWarningCounter.increment();
logger.warn("High memory usage detected: {}%", String.format("%.2f", memoryUsage * 100));
}
}
public double getAverageProcessingTime() {
long count = requestCount.get();
if (count == 0) {
return 0.0;
}
return (double) totalProcessingTime.get() / count;
}
public double getMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
return (double) usedMemory / totalMemory;
}
public Map<String, Object> getMemoryStats() {
Runtime runtime = Runtime.getRuntime();
Map<String, Object> stats = new ConcurrentHashMap<>();
stats.put("totalMemory", runtime.totalMemory());
stats.put("freeMemory", runtime.freeMemory());
stats.put("usedMemory", runtime.totalMemory() - runtime.freeMemory());
stats.put("maxMemory", runtime.maxMemory());
stats.put("memoryUsage", getMemoryUsage());
return stats;
}
public Map<String, Object> getThreadStats() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
Map<String, Object> stats = new ConcurrentHashMap<>();
stats.put("threadCount", threadBean.getThreadCount());
stats.put("peakThreadCount", threadBean.getPeakThreadCount());
stats.put("daemonThreadCount", threadBean.getDaemonThreadCount());
stats.put("totalStartedThreadCount", threadBean.getTotalStartedThreadCount());
return stats;
}
public Map<String, PerformanceStats> getPathStats() {
return new ConcurrentHashMap<>(pathStats);
}
public void clearStats() {
totalProcessingTime.set(0);
requestCount.set(0);
pathStats.clear();
logger.info("Performance stats cleared");
}
public void setSlowRequestThresholdMs(long threshold) {
this.slowRequestThresholdMs = threshold;
logger.info("Slow request threshold set to: {}ms", threshold);
}
public void setMemoryWarningThreshold(double threshold) {
this.memoryWarningThreshold = threshold;
logger.info("Memory warning threshold set to: {}", threshold);
}
public static class PerformanceStats {
private final AtomicLong requestCount = new AtomicLong(0);
private final AtomicLong totalTime = new AtomicLong(0);
private final AtomicLong maxTime = new AtomicLong(0);
private final AtomicLong minTime = new AtomicLong(Long.MAX_VALUE);
public void recordRequest(long durationMs) {
requestCount.incrementAndGet();
totalTime.addAndGet(durationMs);
long currentMax = maxTime.get();
if (durationMs > currentMax) {
maxTime.compareAndSet(currentMax, durationMs);
}
long currentMin = minTime.get();
if (durationMs < currentMin) {
minTime.compareAndSet(currentMin, durationMs);
}
}
public long getRequestCount() {
return requestCount.get();
}
public double getAverageTime() {
long count = requestCount.get();
return count == 0 ? 0.0 : (double) totalTime.get() / count;
}
public long getMaxTime() {
return maxTime.get();
}
public long getMinTime() {
long min = minTime.get();
return min == Long.MAX_VALUE ? 0 : min;
}
}
}
@@ -0,0 +1,25 @@
package cn.novalon.gym.manage.gateway.service;
import reactor.core.publisher.Mono;
/**
* 配置刷新服务接口
*
* 文件定义:定义网关配置动态刷新接口
* 涉及业务:配置热更新、配置版本管理
*
* @author 张翔
* @date 2026-04-14
*/
public interface IConfigRefreshService {
Mono<Void> refreshGatewayConfig();
Mono<Void> refreshRouteConfig();
Mono<Void> refreshFilterConfig();
Mono<String> getCurrentConfigVersion();
Mono<Boolean> isConfigChanged();
}
@@ -0,0 +1,44 @@
package cn.novalon.gym.manage.gateway.service;
import org.springframework.cloud.gateway.route.RouteDefinition;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 动态路由服务接口
*
* 文件定义:定义网关路由的动态配置和管理接口
* 涉及业务:路由增删改查、路由刷新、路由缓存管理
*
* 核心功能:
* 1. 动态添加路由
* 2. 动态删除路由
* 3. 动态更新路由
* 4. 路由列表查询
* 5. 路由刷新
*
* @author 张翔
* @date 2026-04-14
*/
public interface IDynamicRouteService {
Mono<Boolean> addRoute(RouteDefinition routeDefinition);
Mono<Boolean> updateRoute(RouteDefinition routeDefinition);
Mono<Boolean> deleteRoute(String routeId);
Flux<RouteDefinition> getRoutes();
Mono<RouteDefinition> getRoute(String routeId);
Mono<Void> refreshRoutes();
Mono<Long> getRouteCount();
Mono<Boolean> routeExists(String routeId);
Mono<Void> clearRouteCache();
}
@@ -0,0 +1,27 @@
package cn.novalon.gym.manage.gateway.service;
import reactor.core.publisher.Mono;
/**
* 请求缓存服务接口
*
* 文件定义:定义请求缓存管理接口
* 涉及业务:请求缓存、缓存清理、缓存统计
*
* @author 张翔
* @date 2026-04-14
*/
public interface IRequestCacheService {
Mono<Void> cacheRequest(String requestId, Object requestData);
Mono<Object> getCachedRequest(String requestId);
Mono<Boolean> removeCachedRequest(String requestId);
Mono<Void> clearExpiredCache();
Mono<Long> getCacheSize();
Mono<Boolean> isRequestCached(String requestId);
}
@@ -0,0 +1,41 @@
package cn.novalon.gym.manage.gateway.service;
import org.springframework.cloud.client.ServiceInstance;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 服务发现服务接口
*
* 文件定义:定义服务实例的发现、监控和管理接口
* 涉及业务:服务实例查询、健康检查、服务状态监控
*
* 核心功能:
* 1. 服务实例查询
* 2. 服务健康检查
* 3. 服务状态监控
* 4. 服务实例缓存
*
* @author 张翔
* @date 2026-04-14
*/
public interface IServiceDiscoveryService {
Flux<ServiceInstance> getInstances(String serviceId);
Flux<String> getServices();
Mono<Boolean> isServiceHealthy(String serviceId);
Mono<Long> getInstanceCount(String serviceId);
Mono<Void> refreshServiceCache(String serviceId);
Mono<Void> refreshAllServiceCache();
Mono<Long> getServiceCount();
Mono<Boolean> serviceExists(String serviceId);
Mono<Void> clearServiceCache();
}
@@ -0,0 +1,22 @@
package cn.novalon.gym.manage.gateway.service;
import javax.crypto.SecretKey;
public interface JwtKeyService {
SecretKey getCurrentSigningKey();
SecretKey getSigningKeyByVersion(String version);
String getCurrentKeyVersion();
void rotateKey();
boolean validateKeyStrength(String key);
String generateSecureKey();
String encryptKey(String key);
String decryptKey(String encryptedKey);
}
@@ -0,0 +1,25 @@
package cn.novalon.gym.manage.gateway.service;
import cn.novalon.gym.manage.gateway.model.Permission;
import cn.novalon.gym.manage.gateway.model.Role;
import cn.novalon.gym.manage.gateway.model.User;
import java.util.List;
import java.util.Set;
public interface PermissionService {
User getUserById(Long userId);
List<Role> getUserRoles(Long userId);
Set<Permission> getUserPermissions(Long userId);
boolean hasPermission(Long userId, String path, String method);
Set<String> getPermissionPaths(Long userId, String method);
void clearCache(Long userId);
void clearAllCache();
}
@@ -0,0 +1,75 @@
package cn.novalon.gym.manage.gateway.service;
import org.springframework.http.server.reactive.ServerHttpRequest;
/**
* 请求签名服务接口
*
* 文件定义:提供API请求签名生成和验证功能
* 涉及业务:API安全防护,防止请求篡改和重放攻击
* 算法:HMAC-SHA256签名算法
*
* @author 张翔
* @date 2026-03-26
*/
public interface SignatureService {
/**
* 生成请求签名
*
* @param method HTTP方法
* @param path 请求路径
* @param query 查询参数
* @param body 请求体
* @param timestamp 时间戳
* @param nonce 随机数
* @param secret 密钥
* @return 签名字符串
*/
String generateSignature(
String method,
String path,
String query,
String body,
long timestamp,
String nonce,
String secret);
/**
* 验证请求签名
*
* @param request HTTP请求
* @param secret 密钥
* @return 验证结果
*/
boolean verifySignature(ServerHttpRequest request, String secret);
/**
* 检查时间戳是否有效
*
* @param timestamp 时间戳(毫秒)
* @param maxAgeMinutes 最大有效期(分钟)
* @return 是否有效
*/
boolean isTimestampValid(long timestamp, int maxAgeMinutes);
/**
* 检查nonce是否已使用(防重放攻击)
*
* @param nonce 随机数
* @return 是否已使用
*/
boolean isNonceUsed(String nonce);
/**
* 记录nonce为已使用
*
* @param nonce 随机数
*/
void recordNonce(String nonce);
/**
* 清理过期的nonce记录
*/
void cleanupExpiredNonces();
}
@@ -0,0 +1,181 @@
package cn.novalon.gym.manage.gateway.service.impl;
import cn.novalon.gym.manage.gateway.service.IDynamicRouteService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 动态路由服务实现类
*
* 文件定义:实现网关路由的动态配置和管理
* 涉及业务:路由增删改查、路由刷新、路由缓存管理
*
* 核心功能:
* 1. 动态添加路由
* 2. 动态删除路由
* 3. 动态更新路由
* 4. 路由列表查询
* 5. 路由刷新
*
* @author 张翔
* @date 2026-04-14
*/
@Service
public class DynamicRouteService implements IDynamicRouteService {
private static final Logger logger = LoggerFactory.getLogger(DynamicRouteService.class);
private final RouteDefinitionWriter routeDefinitionWriter;
private final RouteDefinitionLocator routeDefinitionLocator;
private final ApplicationEventPublisher publisher;
private final Map<String, RouteDefinition> routeCache = new ConcurrentHashMap<>();
public DynamicRouteService(
RouteDefinitionWriter routeDefinitionWriter,
RouteDefinitionLocator routeDefinitionLocator,
ApplicationEventPublisher publisher) {
this.routeDefinitionWriter = routeDefinitionWriter;
this.routeDefinitionLocator = routeDefinitionLocator;
this.publisher = publisher;
initializeRouteCache();
}
private void initializeRouteCache() {
routeDefinitionLocator.getRouteDefinitions()
.doOnNext(route -> routeCache.put(route.getId(), route))
.subscribe(
route -> logger.debug("Cached route: {}", route.getId()),
error -> logger.error("Failed to initialize route cache", error),
() -> logger.info("Route cache initialized with {} routes", routeCache.size())
);
}
@Override
public Mono<Boolean> addRoute(RouteDefinition routeDefinition) {
if (routeDefinition == null || routeDefinition.getId() == null) {
logger.error("Invalid route definition: route or route ID is null");
return Mono.just(false);
}
String routeId = routeDefinition.getId();
logger.info("Adding route: {}", routeId);
return routeDefinitionWriter.save(Mono.just(routeDefinition))
.then(Mono.fromRunnable(() -> routeCache.put(routeId, routeDefinition)))
.then(refreshRoutes())
.then(Mono.fromRunnable(() -> logger.info("Route added successfully: {}", routeId)))
.thenReturn(true)
.onErrorResume(error -> {
logger.error("Failed to add route: {}", routeId, error);
return Mono.just(false);
});
}
@Override
public Mono<Boolean> updateRoute(RouteDefinition routeDefinition) {
if (routeDefinition == null || routeDefinition.getId() == null) {
logger.error("Invalid route definition: route or route ID is null");
return Mono.just(false);
}
String routeId = routeDefinition.getId();
if (!routeCache.containsKey(routeId)) {
logger.warn("Route not found for update: {}", routeId);
return Mono.just(false);
}
logger.info("Updating route: {}", routeId);
return routeDefinitionWriter.delete(Mono.just(routeId))
.then(routeDefinitionWriter.save(Mono.just(routeDefinition)))
.then(Mono.fromRunnable(() -> routeCache.put(routeId, routeDefinition)))
.then(refreshRoutes())
.then(Mono.fromRunnable(() -> logger.info("Route updated successfully: {}", routeId)))
.thenReturn(true)
.onErrorResume(error -> {
logger.error("Failed to update route: {}", routeId, error);
return Mono.just(false);
});
}
@Override
public Mono<Boolean> deleteRoute(String routeId) {
if (routeId == null) {
logger.error("Invalid route ID: null");
return Mono.just(false);
}
if (!routeCache.containsKey(routeId)) {
logger.warn("Route not found for deletion: {}", routeId);
return Mono.just(false);
}
logger.info("Deleting route: {}", routeId);
return routeDefinitionWriter.delete(Mono.just(routeId))
.then(Mono.fromRunnable(() -> routeCache.remove(routeId)))
.then(refreshRoutes())
.then(Mono.fromRunnable(() -> logger.info("Route deleted successfully: {}", routeId)))
.thenReturn(true)
.onErrorResume(error -> {
logger.error("Failed to delete route: {}", routeId, error);
return Mono.just(false);
});
}
@Override
public Flux<RouteDefinition> getRoutes() {
return Flux.fromIterable(routeCache.values());
}
@Override
public Mono<RouteDefinition> getRoute(String routeId) {
if (routeId == null) {
return Mono.empty();
}
return Mono.justOrEmpty(routeCache.get(routeId));
}
@Override
public Mono<Void> refreshRoutes() {
return Mono.fromRunnable(() -> {
publisher.publishEvent(new RefreshRoutesEvent(this));
logger.info("Routes refreshed");
});
}
@Override
public Mono<Long> getRouteCount() {
return Mono.just((long) routeCache.size());
}
@Override
public Mono<Boolean> routeExists(String routeId) {
if (routeId == null) {
return Mono.just(false);
}
return Mono.just(routeCache.containsKey(routeId));
}
@Override
public Mono<Void> clearRouteCache() {
return Mono.fromRunnable(() -> {
routeCache.clear();
logger.info("Route cache cleared");
});
}
}
@@ -0,0 +1,290 @@
package cn.novalon.gym.manage.gateway.service.impl;
import cn.novalon.gym.manage.gateway.service.JwtKeyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@Service
public class JwtKeyServiceImpl implements JwtKeyService {
private static final Logger logger = LoggerFactory.getLogger(JwtKeyServiceImpl.class);
private static final String KEY_ALGORITHM = "AES";
private static final String KEY_ENCRYPTION_ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;
private static final int KEY_SIZE_BITS = 256;
private static final int MIN_KEY_LENGTH = 32;
private static final int KEY_ROTATION_INTERVAL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
@Value("${jwt.secret:}")
private String configuredSecret;
@Value("${jwt.key.encryption.password:}")
private String encryptionPassword;
@Value("${jwt.key.rotation.enabled:true}")
private boolean rotationEnabled;
private final AtomicReference<String> currentKeyVersion = new AtomicReference<>("v1");
private final Map<String, SecretKey> keyVersionMap = new ConcurrentHashMap<>();
private final Map<String, Long> keyCreationTimeMap = new ConcurrentHashMap<>();
private final SecureRandom secureRandom = new SecureRandom();
@Override
public SecretKey getCurrentSigningKey() {
String version = getCurrentKeyVersion();
return getSigningKeyByVersion(version);
}
@Override
public SecretKey getSigningKeyByVersion(String version) {
return keyVersionMap.get(version);
}
@Override
public String getCurrentKeyVersion() {
return currentKeyVersion.get();
}
@Override
public void rotateKey() {
if (!rotationEnabled) {
logger.info("Key rotation is disabled");
return;
}
logger.info("Starting JWT key rotation");
try {
String newVersion = generateNextVersion();
String newKey = generateSecureKey();
SecretKey signingKey = new SecretKeySpec(
newKey.getBytes(StandardCharsets.UTF_8),
KEY_ALGORITHM
);
keyVersionMap.put(newVersion, signingKey);
keyCreationTimeMap.put(newVersion, System.currentTimeMillis());
currentKeyVersion.set(newVersion);
logger.info("JWT key rotated successfully. New version: {}", newVersion);
cleanupOldKeys();
} catch (Exception e) {
logger.error("Failed to rotate JWT key", e);
throw new RuntimeException("Key rotation failed", e);
}
}
@Override
public boolean validateKeyStrength(String key) {
if (key == null || key.length() < MIN_KEY_LENGTH) {
logger.warn("Key validation failed: key is null or too short");
return false;
}
boolean hasUpperCase = !key.equals(key.toLowerCase());
boolean hasLowerCase = !key.equals(key.toUpperCase());
boolean hasDigit = key.matches(".*\\d.*");
boolean hasSpecialChar = !key.matches("[a-zA-Z0-9]*");
int strengthScore = (hasUpperCase ? 1 : 0) +
(hasLowerCase ? 1 : 0) +
(hasDigit ? 1 : 0) +
(hasSpecialChar ? 1 : 0);
boolean isValid = strengthScore >= 3 && key.length() >= MIN_KEY_LENGTH;
if (!isValid) {
logger.warn("Key validation failed: strength score = {}, length = {}", strengthScore, key.length());
}
return isValid;
}
@Override
public String generateSecureKey() {
byte[] keyBytes = new byte[KEY_SIZE_BITS / 8];
secureRandom.nextBytes(keyBytes);
return Base64.getEncoder().encodeToString(keyBytes);
}
@Override
public String encryptKey(String key) {
if (encryptionPassword == null || encryptionPassword.isEmpty()) {
logger.warn("Encryption password not configured, returning plain key");
return key;
}
try {
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
SecretKey encryptionKey = deriveEncryptionKey(encryptionPassword);
Cipher cipher = Cipher.getInstance(KEY_ENCRYPTION_ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, spec);
byte[] encryptedBytes = cipher.doFinal(key.getBytes(StandardCharsets.UTF_8));
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedBytes.length);
byteBuffer.put(iv);
byteBuffer.put(encryptedBytes);
String result = Base64.getEncoder().encodeToString(byteBuffer.array());
logger.debug("Key encrypted successfully");
return result;
} catch (Exception e) {
logger.error("Failed to encrypt key", e);
throw new RuntimeException("Key encryption failed", e);
}
}
@Override
public String decryptKey(String encryptedKey) {
if (encryptionPassword == null || encryptionPassword.isEmpty()) {
logger.warn("Encryption password not configured, returning key as is");
return encryptedKey;
}
try {
byte[] decodedBytes = Base64.getDecoder().decode(encryptedKey);
ByteBuffer byteBuffer = ByteBuffer.wrap(decodedBytes);
byte[] iv = new byte[GCM_IV_LENGTH];
byteBuffer.get(iv);
byte[] encryptedBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(encryptedBytes);
SecretKey encryptionKey = deriveEncryptionKey(encryptionPassword);
Cipher cipher = Cipher.getInstance(KEY_ENCRYPTION_ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, spec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
String result = new String(decryptedBytes, StandardCharsets.UTF_8);
logger.debug("Key decrypted successfully");
return result;
} catch (Exception e) {
logger.error("Failed to decrypt key", e);
throw new RuntimeException("Key decryption failed", e);
}
}
public void initializeKeys() {
try {
String initialKey;
if (configuredSecret != null && !configuredSecret.isEmpty()) {
if (configuredSecret.startsWith("enc:")) {
initialKey = decryptKey(configuredSecret.substring(4));
logger.info("Decrypted JWT key from configuration");
} else {
initialKey = configuredSecret;
logger.warn("Using plain JWT key from configuration (not recommended)");
if (!validateKeyStrength(initialKey)) {
logger.error("Configured JWT key does not meet strength requirements");
throw new IllegalArgumentException("Weak JWT key configuration");
}
}
} else {
initialKey = generateSecureKey();
logger.info("Generated new secure JWT key");
}
SecretKey signingKey = new SecretKeySpec(
initialKey.getBytes(StandardCharsets.UTF_8),
KEY_ALGORITHM
);
keyVersionMap.put("v1", signingKey);
keyCreationTimeMap.put("v1", System.currentTimeMillis());
currentKeyVersion.set("v1");
logger.info("JWT key service initialized with version v1");
} catch (Exception e) {
logger.error("Failed to initialize JWT keys", e);
throw new RuntimeException("JWT key initialization failed", e);
}
}
private String generateNextVersion() {
String currentVersion = getCurrentKeyVersion();
int versionNumber = Integer.parseInt(currentVersion.substring(1));
return "v" + (versionNumber + 1);
}
private SecretKey deriveEncryptionKey(String password) throws Exception {
byte[] salt = "NovalonManageSystemSalt".getBytes(StandardCharsets.UTF_8);
KeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
65536,
256
);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, KEY_ALGORITHM);
}
private void cleanupOldKeys() {
long currentTime = System.currentTimeMillis();
long rotationThreshold = KEY_ROTATION_INTERVAL_MS * 2; // Keep keys for 2 rotation cycles
keyVersionMap.keySet().stream()
.filter(version -> !version.equals(getCurrentKeyVersion()))
.filter(version -> {
Long creationTime = keyCreationTimeMap.get(version);
return creationTime != null && (currentTime - creationTime) > rotationThreshold;
})
.forEach(version -> {
keyVersionMap.remove(version);
keyCreationTimeMap.remove(version);
logger.info("Removed old JWT key version: {}", version);
});
}
public boolean shouldRotateKey() {
if (!rotationEnabled) {
return false;
}
String currentVersion = getCurrentKeyVersion();
Long creationTime = keyCreationTimeMap.get(currentVersion);
if (creationTime == null) {
return true;
}
long keyAge = System.currentTimeMillis() - creationTime;
return keyAge >= KEY_ROTATION_INTERVAL_MS;
}
}
@@ -0,0 +1,221 @@
package cn.novalon.gym.manage.gateway.service.impl;
import cn.novalon.gym.manage.gateway.model.Permission;
import cn.novalon.gym.manage.gateway.model.Role;
import cn.novalon.gym.manage.gateway.model.User;
import cn.novalon.gym.manage.gateway.service.PermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class PermissionServiceImpl implements PermissionService {
private static final Logger logger = LoggerFactory.getLogger(PermissionServiceImpl.class);
private final WebClient webClient;
private final String userServiceUrl;
private final Map<Long, User> userCache = new ConcurrentHashMap<>();
private final Map<Long, List<Role>> userRolesCache = new ConcurrentHashMap<>();
private final Map<Long, Set<Permission>> userPermissionsCache = new ConcurrentHashMap<>();
private final Map<Long, Long> userCacheTimestamp = new ConcurrentHashMap<>();
private final Map<Long, Long> rolesCacheTimestamp = new ConcurrentHashMap<>();
private final Map<Long, Long> permissionsCacheTimestamp = new ConcurrentHashMap<>();
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
public PermissionServiceImpl(WebClient.Builder webClientBuilder,
@Value("${user.service.url:http://localhost:8084}") String userServiceUrl) {
this.webClient = webClientBuilder.build();
this.userServiceUrl = userServiceUrl;
}
@Override
public User getUserById(Long userId) {
if (userId == null) {
return null;
}
Long cacheTime = userCacheTimestamp.get(userId);
long currentTime = System.currentTimeMillis();
if (cacheTime != null && (currentTime - cacheTime) < CACHE_EXPIRY_MS) {
logger.debug("Returning cached user for userId: {}", userId);
return userCache.get(userId);
}
try {
logger.debug("Fetching user from service for userId: {}", userId);
User user = webClient.get()
.uri(userServiceUrl + "/api/users/" + userId)
.retrieve()
.bodyToMono(User.class)
.block();
if (user != null) {
userCache.put(userId, user);
userCacheTimestamp.put(userId, currentTime);
logger.debug("Cached user for userId: {}", userId);
}
return user;
} catch (Exception e) {
logger.error("Error fetching user for userId: {}", userId, e);
return userCache.get(userId);
}
}
@Override
public List<Role> getUserRoles(Long userId) {
if (userId == null) {
return Collections.emptyList();
}
Long cacheTime = rolesCacheTimestamp.get(userId);
long currentTime = System.currentTimeMillis();
if (cacheTime != null && (currentTime - cacheTime) < CACHE_EXPIRY_MS) {
logger.debug("Returning cached roles for userId: {}", userId);
return userRolesCache.getOrDefault(userId, Collections.emptyList());
}
try {
logger.debug("Fetching roles from service for userId: {}", userId);
Role[] roles = webClient.get()
.uri(userServiceUrl + "/api/users/" + userId + "/roles")
.retrieve()
.bodyToMono(Role[].class)
.block();
List<Role> roleList = roles != null ? Arrays.asList(roles) : Collections.emptyList();
userRolesCache.put(userId, roleList);
rolesCacheTimestamp.put(userId, currentTime);
logger.debug("Cached roles for userId: {}, count: {}", userId, roleList.size());
return roleList;
} catch (Exception e) {
logger.error("Error fetching roles for userId: {}", userId, e);
return userRolesCache.getOrDefault(userId, Collections.emptyList());
}
}
@Override
public Set<Permission> getUserPermissions(Long userId) {
if (userId == null) {
return Collections.emptySet();
}
Long cacheTime = permissionsCacheTimestamp.get(userId);
long currentTime = System.currentTimeMillis();
if (cacheTime != null && (currentTime - cacheTime) < CACHE_EXPIRY_MS) {
logger.debug("Returning cached permissions for userId: {}", userId);
return userPermissionsCache.getOrDefault(userId, Collections.emptySet());
}
try {
logger.debug("Fetching permissions from service for userId: {}", userId);
Permission[] permissions = webClient.get()
.uri(userServiceUrl + "/api/users/" + userId + "/permissions")
.retrieve()
.bodyToMono(Permission[].class)
.block();
Set<Permission> permissionSet = permissions != null ?
new HashSet<>(Arrays.asList(permissions)) : Collections.emptySet();
userPermissionsCache.put(userId, permissionSet);
permissionsCacheTimestamp.put(userId, currentTime);
logger.debug("Cached permissions for userId: {}, count: {}", userId, permissionSet.size());
return permissionSet;
} catch (Exception e) {
logger.error("Error fetching permissions for userId: {}", userId, e);
return userPermissionsCache.getOrDefault(userId, Collections.emptySet());
}
}
@Override
public boolean hasPermission(Long userId, String path, String method) {
if (userId == null) {
logger.warn("UserId is null, denying access");
return false;
}
Set<String> permissionPaths = getPermissionPaths(userId, method);
for (String permissionPath : permissionPaths) {
if (matchPath(permissionPath, path)) {
logger.debug("Permission granted for userId: {}, path: {}, method: {}, matched permission: {}",
userId, path, method, permissionPath);
return true;
}
}
logger.warn("Permission denied for userId: {}, path: {}, method: {}", userId, path, method);
return false;
}
@Override
public Set<String> getPermissionPaths(Long userId, String method) {
Set<Permission> permissions = getUserPermissions(userId);
return permissions.stream()
.filter(p -> method.equalsIgnoreCase(p.getHttpMethod()))
.map(Permission::getResourcePath)
.collect(Collectors.toSet());
}
private boolean matchPath(String permissionPath, String requestPath) {
if (permissionPath.equals(requestPath)) {
return true;
}
if (permissionPath.endsWith("/**")) {
String basePath = permissionPath.substring(0, permissionPath.length() - 3);
return requestPath.startsWith(basePath);
}
if (permissionPath.endsWith("/*")) {
String basePath = permissionPath.substring(0, permissionPath.length() - 2);
return requestPath.startsWith(basePath) &&
!requestPath.substring(basePath.length() + 1).contains("/");
}
if (permissionPath.contains("*")) {
String regex = permissionPath.replace("*", ".*");
return requestPath.matches(regex);
}
return false;
}
public void clearCache(Long userId) {
if (userId != null) {
userCache.remove(userId);
userRolesCache.remove(userId);
userPermissionsCache.remove(userId);
userCacheTimestamp.remove(userId);
rolesCacheTimestamp.remove(userId);
permissionsCacheTimestamp.remove(userId);
logger.info("Cleared cache for userId: {}", userId);
}
}
public void clearAllCache() {
userCache.clear();
userRolesCache.clear();
userPermissionsCache.clear();
userCacheTimestamp.clear();
rolesCacheTimestamp.clear();
permissionsCacheTimestamp.clear();
logger.info("Cleared all permission cache");
}
}
@@ -0,0 +1,182 @@
package cn.novalon.gym.manage.gateway.service.impl;
import cn.novalon.gym.manage.gateway.service.IServiceDiscoveryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 服务发现服务实现类
*
* 文件定义:实现服务实例的发现、监控和管理
* 涉及业务:服务实例查询、健康检查、服务状态监控
*
* 核心功能:
* 1. 服务实例查询
* 2. 服务健康检查
* 3. 服务状态监控
* 4. 服务实例缓存
*
* @author 张翔
* @date 2026-04-14
*/
@Service
public class ServiceDiscoveryService implements IServiceDiscoveryService {
private static final Logger logger = LoggerFactory.getLogger(ServiceDiscoveryService.class);
private final ReactiveDiscoveryClient reactiveDiscoveryClient;
private final DiscoveryClient discoveryClient;
private final Map<String, List<ServiceInstance>> serviceCache = new ConcurrentHashMap<>();
private final Map<String, Long> lastUpdateTime = new ConcurrentHashMap<>();
private static final long CACHE_TTL_MS = 30000;
public ServiceDiscoveryService(
ReactiveDiscoveryClient reactiveDiscoveryClient,
DiscoveryClient discoveryClient) {
this.reactiveDiscoveryClient = reactiveDiscoveryClient;
this.discoveryClient = discoveryClient;
initializeServiceCache();
}
private void initializeServiceCache() {
logger.info("Initializing service cache");
discoveryClient.getServices().forEach(serviceId -> {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if (!instances.isEmpty()) {
serviceCache.put(serviceId, instances);
lastUpdateTime.put(serviceId, System.currentTimeMillis());
logger.debug("Cached {} instances for service: {}", instances.size(), serviceId);
}
});
logger.info("Service cache initialized with {} services", serviceCache.size());
}
@Override
public Flux<ServiceInstance> getInstances(String serviceId) {
if (serviceId == null || serviceId.isEmpty()) {
logger.warn("Service ID is null or empty");
return Flux.empty();
}
if (isCacheValid(serviceId)) {
List<ServiceInstance> cachedInstances = serviceCache.get(serviceId);
if (cachedInstances != null && !cachedInstances.isEmpty()) {
logger.debug("Returning {} cached instances for service: {}",
cachedInstances.size(), serviceId);
return Flux.fromIterable(cachedInstances);
}
}
logger.debug("Fetching instances for service: {}", serviceId);
return reactiveDiscoveryClient.getInstances(serviceId)
.doOnNext(instance -> logger.debug("Found instance: {}:{} for service: {}",
instance.getHost(), instance.getPort(), serviceId))
.collectList()
.doOnNext(instances -> {
serviceCache.put(serviceId, instances);
lastUpdateTime.put(serviceId, System.currentTimeMillis());
logger.info("Updated cache with {} instances for service: {}",
instances.size(), serviceId);
})
.flatMapMany(Flux::fromIterable);
}
@Override
public Flux<String> getServices() {
return reactiveDiscoveryClient.getServices()
.doOnNext(serviceId -> logger.debug("Found service: {}", serviceId));
}
@Override
public Mono<Boolean> isServiceHealthy(String serviceId) {
return getInstances(serviceId)
.hasElements()
.map(hasInstances -> {
if (hasInstances) {
logger.debug("Service {} is healthy - has instances", serviceId);
return true;
} else {
logger.warn("Service {} is unhealthy - no instances found", serviceId);
return false;
}
});
}
@Override
public Mono<Long> getInstanceCount(String serviceId) {
return getInstances(serviceId)
.count()
.doOnNext(count -> logger.debug("Service {} has {} instances", serviceId, count));
}
@Override
public Mono<Void> refreshServiceCache(String serviceId) {
return Mono.fromRunnable(() -> {
if (serviceId != null) {
serviceCache.remove(serviceId);
lastUpdateTime.remove(serviceId);
logger.info("Refreshed cache for service: {}", serviceId);
}
});
}
@Override
public Mono<Void> refreshAllServiceCache() {
return Mono.fromRunnable(() -> {
serviceCache.clear();
lastUpdateTime.clear();
initializeServiceCache();
logger.info("Refreshed all service cache");
});
}
@Override
public Mono<Long> getServiceCount() {
return getServices()
.count()
.doOnNext(count -> logger.debug("Found {} services", count));
}
@Override
public Mono<Boolean> serviceExists(String serviceId) {
if (serviceId == null || serviceId.isEmpty()) {
return Mono.just(false);
}
return getServices()
.any(s -> s.equals(serviceId))
.doOnNext(exists -> logger.debug("Service {} exists: {}", serviceId, exists));
}
@Override
public Mono<Void> clearServiceCache() {
return Mono.fromRunnable(() -> {
serviceCache.clear();
lastUpdateTime.clear();
logger.info("Cleared service cache");
});
}
private boolean isCacheValid(String serviceId) {
Long lastUpdate = lastUpdateTime.get(serviceId);
if (lastUpdate == null) {
return false;
}
return System.currentTimeMillis() - lastUpdate < CACHE_TTL_MS;
}
}
@@ -0,0 +1,211 @@
package cn.novalon.gym.manage.gateway.service.impl;
import cn.novalon.gym.manage.gateway.service.SignatureService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 请求签名服务实现
*
* 文件定义:实现API请求签名生成和验证功能
* 涉及业务:API安全防护,防止请求篡改和重放攻击
* 算法:HMAC-SHA256签名算法
*
* 签名算法:
* 1. 构造签名字符串:METHOD + "\n" + PATH + "\n" + QUERY + "\n" + BODY + "\n" + TIMESTAMP + "\n" + NONCE
* 2. 使用HMAC-SHA256算法对签名字符串进行签名
* 3. 将签名结果进行Base64编码
*
* 防重放攻击:
* 1. 检查时间戳是否在有效期内(默认5分钟)
* 2. 检查nonce是否已使用(使用ConcurrentHashMap存储)
* 3. 定期清理过期的nonce记录
*
* @author 张翔
* @date 2026-03-26
*/
@Service
public class SignatureServiceImpl implements SignatureService {
private static final Logger logger = LoggerFactory.getLogger(SignatureServiceImpl.class);
private static final String HMAC_SHA256 = "HmacSHA256";
private static final String SIGNATURE_HEADER = "X-Signature";
private static final String TIMESTAMP_HEADER = "X-Timestamp";
private static final String NONCE_HEADER = "X-Nonce";
@Value("${signature.enabled:true}")
private boolean signatureEnabled;
@Value("${signature.max-age-minutes:5}")
private int maxAgeMinutes;
@Value("${signature.nonce-cache-size:10000}")
private int nonceCacheSize;
private final ConcurrentHashMap<String, Long> nonceCache = new ConcurrentHashMap<>();
@Override
public String generateSignature(
String method,
String path,
String query,
String body,
long timestamp,
String nonce,
String secret) {
try {
String stringToSign = buildStringToSign(method, path, query, body, timestamp, nonce);
logger.debug("String to sign: {}", stringToSign);
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
mac.init(secretKeySpec);
byte[] signatureBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
String signature = Base64.getEncoder().encodeToString(signatureBytes);
logger.debug("Generated signature: {}", signature);
return signature;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
logger.error("Failed to generate signature", e);
throw new RuntimeException("Signature generation failed", e);
}
}
@Override
public boolean verifySignature(ServerHttpRequest request, String secret) {
if (!signatureEnabled) {
logger.debug("Signature verification is disabled");
return true;
}
String signature = request.getHeaders().getFirst(SIGNATURE_HEADER);
String timestampStr = request.getHeaders().getFirst(TIMESTAMP_HEADER);
String nonce = request.getHeaders().getFirst(NONCE_HEADER);
if (signature == null || timestampStr == null || nonce == null) {
logger.warn("Missing signature headers - Signature: {}, Timestamp: {}, Nonce: {}",
signature, timestampStr, nonce);
return false;
}
try {
long timestamp = Long.parseLong(timestampStr);
if (!isTimestampValid(timestamp, maxAgeMinutes)) {
logger.warn("Timestamp is invalid or expired: {}", timestamp);
return false;
}
if (isNonceUsed(nonce)) {
logger.warn("Nonce has been used: {}", nonce);
return false;
}
String method = request.getMethod().name();
String path = request.getPath().value();
String query = request.getURI().getQuery() != null ? request.getURI().getQuery() : "";
String body = ""; // 在WebFlux中,请求体需要特殊处理
String expectedSignature = generateSignature(method, path, query, body, timestamp, nonce, secret);
boolean isValid = signature.equals(expectedSignature);
if (isValid) {
recordNonce(nonce);
logger.debug("Signature verification passed for request: {} {}", method, path);
} else {
logger.warn("Signature verification failed - Expected: {}, Actual: {}", expectedSignature, signature);
}
return isValid;
} catch (NumberFormatException e) {
logger.error("Invalid timestamp format: {}", timestampStr, e);
return false;
}
}
@Override
public boolean isTimestampValid(long timestamp, int maxAgeMinutes) {
long currentTime = System.currentTimeMillis();
long timeDifference = Math.abs(currentTime - timestamp);
long maxAgeMillis = TimeUnit.MINUTES.toMillis(maxAgeMinutes);
boolean isValid = timeDifference <= maxAgeMillis;
if (!isValid) {
logger.debug("Timestamp validation failed - Current: {}, Request: {}, Difference: {}ms, Max: {}ms",
currentTime, timestamp, timeDifference, maxAgeMillis);
}
return isValid;
}
@Override
public boolean isNonceUsed(String nonce) {
return nonceCache.containsKey(nonce);
}
@Override
public void recordNonce(String nonce) {
nonceCache.put(nonce, System.currentTimeMillis());
logger.debug("Recorded nonce: {}", nonce);
if (nonceCache.size() > nonceCacheSize) {
cleanupExpiredNonces();
}
}
@Override
public void cleanupExpiredNonces() {
long currentTime = System.currentTimeMillis();
long expirationTime = TimeUnit.MINUTES.toMillis(maxAgeMinutes * 2);
int initialSize = nonceCache.size();
nonceCache.entrySet().removeIf(entry ->
(currentTime - entry.getValue()) > expirationTime);
int removedCount = initialSize - nonceCache.size();
if (removedCount > 0) {
logger.info("Cleaned up {} expired nonces. Current cache size: {}",
removedCount, nonceCache.size());
}
}
private String buildStringToSign(
String method,
String path,
String query,
String body,
long timestamp,
String nonce) {
StringBuilder sb = new StringBuilder();
sb.append(method).append("\n");
sb.append(path).append("\n");
sb.append(query != null ? query : "").append("\n");
sb.append(body != null ? body : "").append("\n");
sb.append(timestamp).append("\n");
sb.append(nonce);
return sb.toString();
}
public int getNonceCacheSize() {
return nonceCache.size();
}
}
@@ -0,0 +1,104 @@
package cn.novalon.gym.manage.gateway.util;
import cn.novalon.gym.manage.gateway.service.JwtKeyService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
@Value("${jwt.expiration:86400000}")
private Long expiration;
@Autowired
private JwtKeyService jwtKeyService;
private SecretKey getSigningKey() {
return jwtKeyService.getCurrentSigningKey();
}
public String generateToken(String username, Long userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
try {
String token = Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.claim("keyVersion", jwtKeyService.getCurrentKeyVersion())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey())
.compact();
logger.debug("Generated JWT token for user: {}, userId: {}", username, userId);
return token;
} catch (Exception e) {
logger.error("Failed to generate JWT token for user: {}", username, e);
throw new RuntimeException("Token generation failed", e);
}
}
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.error("Failed to parse JWT token", e);
throw new RuntimeException("Invalid token", e);
}
}
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
public boolean validateToken(String token) {
try {
parseToken(token);
logger.debug("JWT token validation successful");
return true;
} catch (Exception e) {
logger.warn("JWT token validation failed: {}", e.getMessage());
return false;
}
}
public boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
boolean expired = claims.getExpiration().before(new Date());
if (expired) {
logger.warn("JWT token is expired");
}
return expired;
} catch (Exception e) {
logger.error("Failed to check token expiration", e);
return true;
}
}
}
@@ -0,0 +1 @@
cn.novalon.manage.gateway.config.RateLimitConfig
@@ -0,0 +1,13 @@
spring:
cloud:
gateway:
routes:
- id: manage-app
uri: http://localhost:8084
predicates:
- Path=/api/**
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.web.reactive: TRACE
@@ -0,0 +1,38 @@
# 本地开发环境配置
spring:
config:
activate:
on-profile: local
cloud:
gateway:
routes:
- id: manage-app
uri: http://localhost:8084
predicates:
- Path=/api/**
default-filters:
- name: JwtAuthentication
- name: RbacAuthorization
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
- name: DedupeResponseHeader
args:
name: Content-Encoding
strategy: RETAIN_FIRST
jwt:
secret: U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4
expiration: 86400000
logging:
level:
cn.novalon.manage.gateway: DEBUG
org.springframework.cloud.gateway: DEBUG
@@ -0,0 +1,13 @@
spring:
cloud:
gateway:
routes:
- id: manage-app
uri: http://app:8084
predicates:
- Path=/api/**
logging:
level:
cn.novalon.manage: INFO
org.springframework.cloud.gateway: INFO
@@ -0,0 +1,99 @@
server:
port: 8080
spring:
codec:
max-in-memory-size: 10MB
application:
name: manage-gateway
cloud:
gateway:
routes:
- id: manage-app
uri: http://localhost:8084
predicates:
- Path=/api/**
jwt:
secret: test-secret-key-for-e2e-testing-novalon-manage-system-2026
expiration: 86400000
key:
encryption:
password: test-encryption-password
rotation:
enabled: false
interval:
days: 30
rate:
limit:
enabled: false
global:
limit-for-period: 10000
limit-refresh-period: 1s
timeout-duration: 0
ip:
limit-for-period: 1000
limit-refresh-period: 1s
timeout-duration: 0
user:
limit-for-period: 2000
limit-refresh-period: 1s
timeout-duration: 0
signature:
enabled: false
secret: TestSecretKey2026
max-age-minutes: 30
nonce-cache-size: 10000
whitelist:
paths: /actuator/health,/actuator/info,/api/auth/login,/api/auth/register
resilience:
enabled: true
circuit-breaker:
enabled: true
failure-rate-threshold: 50
slow-call-rate-threshold: 100
slow-call-duration-threshold: 2s
permitted-number-of-calls-in-half-open-state: 10
sliding-window-type: COUNT_BASED
sliding-window-size: 100
minimum-number-of-calls: 10
wait-duration-in-open-state: 10s
retry:
enabled: true
max-attempts: 3
wait-duration: 500ms
timeout:
enabled: true
duration: 5s
user:
service:
url: http://localhost:8084
permission:
cache:
expiry:
minutes: 1
management:
endpoints:
web:
exposure:
include: health,info,metrics
base-path: /actuator
endpoint:
health:
show-details: always
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.cloud.gateway: DEBUG
@@ -0,0 +1,149 @@
server:
port: 8080
spring:
codec:
max-in-memory-size: 10MB
application:
name: gym-manage-gateway
cloud:
gateway:
routes:
- id: manage-app
uri: http://localhost:8084
predicates:
- Path=/api/**
default-filters:
- name: JwtAuthentication
- name: RbacAuthorization
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
- name: DedupeResponseHeader
args:
name: Content-Encoding
strategy: RETAIN_FIRST
jwt:
secret: ${JWT_SECRET:enc:U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4}
expiration: ${JWT_EXPIRATION:86400000}
key:
encryption:
password: ${JWT_KEY_ENCRYPTION_PASSWORD:}
rotation:
enabled: ${JWT_KEY_ROTATION_ENABLED:true}
interval:
days: ${JWT_KEY_ROTATION_INTERVAL_DAYS:30}
rate:
limit:
enabled: ${RATE_LIMIT_ENABLED:true}
global:
limit-for-period: ${RATE_LIMIT_GLOBAL_LIMIT:1000}
limit-refresh-period: ${RATE_LIMIT_GLOBAL_PERIOD:1s}
timeout-duration: ${RATE_LIMIT_GLOBAL_TIMEOUT:0}
ip:
limit-for-period: ${RATE_LIMIT_IP_LIMIT:100}
limit-refresh-period: ${RATE_LIMIT_IP_PERIOD:1s}
timeout-duration: ${RATE_LIMIT_IP_TIMEOUT:0}
user:
limit-for-period: ${RATE_LIMIT_USER_LIMIT:200}
limit-refresh-period: ${RATE_LIMIT_USER_PERIOD:1s}
timeout-duration: ${RATE_LIMIT_USER_TIMEOUT:0}
signature:
enabled: ${SIGNATURE_ENABLED:true}
secret: ${SIGNATURE_SECRET:NovalonManageSystemSecretKey2026}
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
whitelist:
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login}
resilience:
enabled: ${RESILIENCE_ENABLED:true}
circuit-breaker:
enabled: ${RESILIENCE_CIRCUIT_BREAKER_ENABLED:true}
failure-rate-threshold: ${RESILIENCE_CB_FAILURE_RATE:50}
slow-call-rate-threshold: ${RESILIENCE_CB_SLOW_CALL_RATE:100}
slow-call-duration-threshold: ${RESILIENCE_CB_SLOW_CALL_DURATION:2s}
permitted-number-of-calls-in-half-open-state: ${RESILIENCE_CB_HALF_OPEN_CALLS:10}
sliding-window-type: ${RESILIENCE_CB_SLIDING_WINDOW_TYPE:COUNT_BASED}
sliding-window-size: ${RESILIENCE_CB_SLIDING_WINDOW_SIZE:100}
minimum-number-of-calls: ${RESILIENCE_CB_MIN_CALLS:10}
wait-duration-in-open-state: ${RESILIENCE_CB_WAIT_DURATION:10s}
retry:
enabled: ${RESILIENCE_RETRY_ENABLED:true}
max-attempts: ${RESILIENCE_RETRY_MAX_ATTEMPTS:3}
wait-duration: ${RESILIENCE_RETRY_WAIT_DURATION:500ms}
timeout:
enabled: ${RESILIENCE_TIMEOUT_ENABLED:true}
duration: ${RESILIENCE_TIMEOUT_DURATION:3s}
user:
service:
url: ${USER_SERVICE_URL:http://localhost:8084}
permission:
cache:
expiry:
minutes: 5
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers,httptrace,threaddump,heapdump
base-path: /actuator
endpoint:
health:
show-details: always
probes:
enabled: true
group:
liveness:
include: ping,livenessState
readiness:
include: ping,readinessState
metrics:
enabled: true
env:
enabled: true
loggers:
enabled: true
httptrace:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
circuitbreakers:
enabled: true
ratelimiters:
enabled: true
metrics:
tags:
application: ${spring.application.name}
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5,0.95,0.99
web:
server:
request:
autotime:
enabled: true
percentiles: 0.5,0.95,0.99
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.cloud.gateway: DEBUG
@@ -0,0 +1,97 @@
package cn.novalon.gym.manage.gateway.audit;
import org.junit.jupiter.api.BeforeEach;
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.mock.http.server.reactive.MockServerHttpRequest;
import java.net.InetSocketAddress;
import static org.junit.jupiter.api.Assertions.*;
/**
* AuditLogService单元测试
*
* 文件定义:测试审计日志服务的核心功能
* 涉及业务:请求日志记录、响应日志记录、安全事件记录
*
* @author 张翔
* @date 2026-03-26
*/
@ExtendWith(MockitoExtension.class)
class AuditLogServiceTest {
private AuditLogService auditLogService;
@BeforeEach
void setUp() {
auditLogService = new AuditLogService();
}
@Test
void testLogRequest() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.header("X-Request-Id", "test-request-123")
.header("User-Agent", "TestAgent")
.remoteAddress(new InetSocketAddress("192.168.1.1", 8080))
.build();
assertDoesNotThrow(() -> auditLogService.logRequest(request, "user123"));
}
@Test
void testLogResponse() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.header("X-Request-Id", "test-request-456")
.build();
auditLogService.logRequest(request, "user123");
assertDoesNotThrow(() -> auditLogService.logResponse("test-request-456", 200, 150));
}
@Test
void testLogSecurityEvent() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/admin")
.header("X-Request-Id", "test-request-789")
.build();
auditLogService.logRequest(request, "user123");
assertDoesNotThrow(() ->
auditLogService.logSecurityEvent("test-request-789", "UNAUTHORIZED_ACCESS", "User attempted to access admin resource"));
}
@Test
void testLogError() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.POST, "/api/data")
.header("X-Request-Id", "test-request-error")
.build();
auditLogService.logRequest(request, "user123");
assertDoesNotThrow(() ->
auditLogService.logError("test-request-error", "INTERNAL_ERROR", "Database connection failed"));
}
@Test
void testLogRequestWithoutRequestId() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/test")
.remoteAddress(new InetSocketAddress("10.0.0.1", 8080))
.build();
assertDoesNotThrow(() -> auditLogService.logRequest(request, "user456"));
}
@Test
void testLogResponseWithNonExistentRequestId() {
assertDoesNotThrow(() -> auditLogService.logResponse("non-existent-id", 404, 50));
}
}
@@ -0,0 +1,191 @@
package cn.novalon.gym.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.junit.jupiter.MockitoExtension;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import reactor.test.StepVerifier;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class RequestCacheServiceTest {
private RequestCacheService cacheService;
@BeforeEach
void setUp() {
cacheService = new RequestCacheService();
}
@Test
void testGet_CacheMiss() {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
StepVerifier.create(cacheService.get(request))
.verifyComplete();
}
@Test
void testPutAndGet_CacheHit() {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
String response = "{\"data\":\"test\"}";
cacheService.put(request, response);
StepVerifier.create(cacheService.get(request))
.expectNext(response)
.verifyComplete();
}
@Test
void testEvict() {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
String response = "{\"data\":\"test\"}";
cacheService.put(request, response);
cacheService.evict(request);
StepVerifier.create(cacheService.get(request))
.verifyComplete();
}
@Test
void testEvictByPattern() {
ServerHttpRequest request1 = MockServerHttpRequest
.get("/api/test1")
.build();
ServerHttpRequest request2 = MockServerHttpRequest
.get("/api/test2")
.build();
cacheService.put(request1, "response1");
cacheService.put(request2, "response2");
cacheService.evictByPattern("GET:/api/test.*");
assertEquals(0, cacheService.getCacheSize());
}
@Test
void testClear() {
ServerHttpRequest request1 = MockServerHttpRequest
.get("/api/test1")
.build();
ServerHttpRequest request2 = MockServerHttpRequest
.get("/api/test2")
.build();
cacheService.put(request1, "response1");
cacheService.put(request2, "response2");
cacheService.clear();
assertEquals(0, cacheService.getCacheSize());
}
@Test
void testCacheDisabled() {
cacheService.setCacheEnabled(false);
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
cacheService.put(request, "response");
StepVerifier.create(cacheService.get(request))
.verifyComplete();
assertEquals(0, cacheService.getCacheSize());
}
@Test
void testCacheStatistics() {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
cacheService.put(request, "response");
StepVerifier.create(cacheService.get(request))
.expectNext("response")
.verifyComplete();
assertEquals(1, cacheService.getHitCount());
assertEquals(0, cacheService.getMissCount());
assertEquals(1.0, cacheService.getHitRate());
}
@Test
void testCacheMissStatistics() {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
StepVerifier.create(cacheService.get(request))
.verifyComplete();
assertEquals(0, cacheService.getHitCount());
assertEquals(1, cacheService.getMissCount());
assertEquals(0.0, cacheService.getHitRate());
}
@Test
void testMaxCacheSize() {
cacheService.setMaxCacheSize(5);
for (int i = 0; i < 10; i++) {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test" + i)
.build();
cacheService.put(request, "response" + i);
}
assertTrue(cacheService.getCacheSize() <= 10);
}
@Test
void testCacheWithQueryParams() {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test?param=value")
.build();
String response = "{\"data\":\"test\"}";
cacheService.put(request, response);
StepVerifier.create(cacheService.get(request))
.expectNext(response)
.verifyComplete();
}
@Test
void testCacheWithDifferentMethods() {
ServerHttpRequest getRequest = MockServerHttpRequest
.get("/api/test")
.build();
ServerHttpRequest postRequest = MockServerHttpRequest
.post("/api/test")
.build();
cacheService.put(getRequest, "getResponse");
cacheService.put(postRequest, "postResponse");
StepVerifier.create(cacheService.get(getRequest))
.expectNext("getResponse")
.verifyComplete();
StepVerifier.create(cacheService.get(postRequest))
.expectNext("postResponse")
.verifyComplete();
}
}
@@ -0,0 +1,116 @@
package cn.novalon.gym.manage.gateway.config;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.*;
/**
* ResilienceConfig单元测试
*
* 文件定义:测试Resilience4j配置类的核心功能
* 涉及业务:断路器、重试、超时配置
*
* @author 张翔
* @date 2026-03-26
*/
@ExtendWith(MockitoExtension.class)
class ResilienceConfigTest {
@InjectMocks
private ResilienceConfig resilienceConfig;
@Test
void testCircuitBreakerRegistry_ShouldCreateRegistry() {
ReflectionTestUtils.setField(resilienceConfig, "circuitBreakerEnabled", true);
ReflectionTestUtils.setField(resilienceConfig, "failureRateThreshold", 50.0f);
ReflectionTestUtils.setField(resilienceConfig, "slowCallRateThreshold", 100.0f);
ReflectionTestUtils.setField(resilienceConfig, "slowCallDurationThreshold", java.time.Duration.ofSeconds(2));
ReflectionTestUtils.setField(resilienceConfig, "permittedNumberOfCallsInHalfOpenState", 10);
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowType", "COUNT_BASED");
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowSize", 100);
ReflectionTestUtils.setField(resilienceConfig, "minimumNumberOfCalls", 10);
ReflectionTestUtils.setField(resilienceConfig, "waitDurationInOpenState", java.time.Duration.ofSeconds(10));
CircuitBreakerRegistry registry = resilienceConfig.circuitBreakerRegistry();
assertNotNull(registry);
assertNotNull(registry.getConfiguration("default"));
}
@Test
void testGatewayCircuitBreaker_ShouldCreateCircuitBreaker() {
ReflectionTestUtils.setField(resilienceConfig, "circuitBreakerEnabled", true);
ReflectionTestUtils.setField(resilienceConfig, "failureRateThreshold", 50.0f);
ReflectionTestUtils.setField(resilienceConfig, "slowCallRateThreshold", 100.0f);
ReflectionTestUtils.setField(resilienceConfig, "slowCallDurationThreshold", java.time.Duration.ofSeconds(2));
ReflectionTestUtils.setField(resilienceConfig, "permittedNumberOfCallsInHalfOpenState", 10);
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowType", "COUNT_BASED");
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowSize", 100);
ReflectionTestUtils.setField(resilienceConfig, "minimumNumberOfCalls", 10);
ReflectionTestUtils.setField(resilienceConfig, "waitDurationInOpenState", java.time.Duration.ofSeconds(10));
CircuitBreakerRegistry registry = resilienceConfig.circuitBreakerRegistry();
CircuitBreaker circuitBreaker = resilienceConfig.gatewayCircuitBreaker(registry);
assertNotNull(circuitBreaker);
assertEquals("gateway", circuitBreaker.getName());
}
@Test
void testRetryRegistry_ShouldCreateRegistry() {
ReflectionTestUtils.setField(resilienceConfig, "retryEnabled", true);
ReflectionTestUtils.setField(resilienceConfig, "retryMaxAttempts", 3);
ReflectionTestUtils.setField(resilienceConfig, "retryWaitDuration", java.time.Duration.ofMillis(500));
RetryRegistry registry = resilienceConfig.retryRegistry();
assertNotNull(registry);
assertNotNull(registry.getConfiguration("default"));
}
@Test
void testGatewayRetry_ShouldCreateRetry() {
ReflectionTestUtils.setField(resilienceConfig, "retryEnabled", true);
ReflectionTestUtils.setField(resilienceConfig, "retryMaxAttempts", 3);
ReflectionTestUtils.setField(resilienceConfig, "retryWaitDuration", java.time.Duration.ofMillis(500));
RetryRegistry registry = resilienceConfig.retryRegistry();
Retry retry = resilienceConfig.gatewayRetry(registry);
assertNotNull(retry);
assertEquals("gateway", retry.getName());
}
@Test
void testTimeLimiterRegistry_ShouldCreateRegistry() {
ReflectionTestUtils.setField(resilienceConfig, "timeoutEnabled", true);
ReflectionTestUtils.setField(resilienceConfig, "timeoutDuration", java.time.Duration.ofSeconds(3));
TimeLimiterRegistry registry = resilienceConfig.timeLimiterRegistry();
assertNotNull(registry);
assertNotNull(registry.getConfiguration("default"));
}
@Test
void testGatewayTimeLimiter_ShouldCreateTimeLimiter() {
ReflectionTestUtils.setField(resilienceConfig, "timeoutEnabled", true);
ReflectionTestUtils.setField(resilienceConfig, "timeoutDuration", java.time.Duration.ofSeconds(3));
TimeLimiterRegistry registry = resilienceConfig.timeLimiterRegistry();
TimeLimiter timeLimiter = resilienceConfig.gatewayTimeLimiter(registry);
assertNotNull(timeLimiter);
assertEquals("gateway", timeLimiter.getName());
}
}
@@ -0,0 +1,131 @@
package cn.novalon.gym.manage.gateway.filter;
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.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import reactor.core.publisher.Mono;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CompressionFilterTest {
private CompressionFilter compressionFilter;
@Mock
private GatewayFilterChain chain;
@BeforeEach
void setUp() {
compressionFilter = new CompressionFilter();
compressionFilter.setCompressionEnabled(true);
when(chain.filter(any())).thenReturn(Mono.empty());
}
@Test
void testFilter_WithGzipSupport() {
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("Accept-Encoding", "gzip, deflate")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
compressionFilter.filter(exchange, chain).block();
assertEquals("gzip", exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
verify(chain).filter(any());
}
@Test
void testFilter_WithDeflateSupport() {
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("Accept-Encoding", "deflate")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
compressionFilter.filter(exchange, chain).block();
assertEquals("deflate", exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
verify(chain).filter(any());
}
@Test
void testFilter_NoAcceptEncoding() {
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
compressionFilter.filter(exchange, chain).block();
assertNull(exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
verify(chain).filter(any());
}
@Test
void testFilter_CompressionDisabled() {
compressionFilter.setCompressionEnabled(false);
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("Accept-Encoding", "gzip")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
compressionFilter.filter(exchange, chain).block();
assertNull(exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
verify(chain).filter(any());
}
@Test
void testFilter_OptionsRequest() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.OPTIONS, "/api/test")
.header("Accept-Encoding", "gzip")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
compressionFilter.filter(exchange, chain).block();
assertNull(exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
verify(chain).filter(any());
}
@Test
void testFilter_VaryHeader() {
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("Accept-Encoding", "gzip")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
compressionFilter.filter(exchange, chain).block();
assertTrue(exchange.getResponse().getHeaders().get("Vary").contains("Accept-Encoding"));
}
@Test
void testGetOrder() {
assertEquals(Integer.MAX_VALUE - 100, compressionFilter.getOrder());
}
}
@@ -0,0 +1,311 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.util.JwtUtil;
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.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GatewayJwtAuthenticationFilterTest {
@Mock
private JwtUtil jwtUtil;
@Mock
private GatewayFilterChain chain;
private JwtAuthenticationFilter filter;
private ServerWebExchange exchange;
@BeforeEach
void setUp() {
filter = new JwtAuthenticationFilter(jwtUtil);
}
@Test
void testPublicPath_AllowAccess() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testPublicPath_Register() {
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testPublicPath_ActuatorHealth() {
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/health").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testPublicPath_ActuatorInfo() {
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/info").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testProtectedPath_NoAuthHeader() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
exchange = MockServerWebExchange.from(request);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
verify(chain, never()).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testProtectedPath_InvalidAuthHeader() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header(HttpHeaders.AUTHORIZATION, "InvalidToken")
.build();
exchange = MockServerWebExchange.from(request);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
verify(chain, never()).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testProtectedPath_WithBearerPrefix() {
String validToken = "valid.jwt.token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
.build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
when(jwtUtil.validateToken(validToken)).thenReturn(true);
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(jwtUtil).validateToken(validToken);
verify(jwtUtil).isTokenExpired(validToken);
verify(jwtUtil).getUsernameFromToken(validToken);
verify(jwtUtil).getUserIdFromToken(validToken);
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_InvalidToken() {
String invalidToken = "invalid.jwt.token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken)
.build();
exchange = MockServerWebExchange.from(request);
when(jwtUtil.validateToken(invalidToken)).thenReturn(false);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
verify(jwtUtil).validateToken(invalidToken);
verify(jwtUtil, never()).isTokenExpired(anyString());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_ExpiredToken() {
String expiredToken = "expired.jwt.token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + expiredToken)
.build();
exchange = MockServerWebExchange.from(request);
when(jwtUtil.validateToken(expiredToken)).thenReturn(true);
when(jwtUtil.isTokenExpired(expiredToken)).thenReturn(true);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
verify(jwtUtil).validateToken(expiredToken);
verify(jwtUtil).isTokenExpired(expiredToken);
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_ValidToken() {
String validToken = "valid.jwt.token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users/1")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
.build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
when(jwtUtil.validateToken(validToken)).thenReturn(true);
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(jwtUtil).validateToken(validToken);
verify(jwtUtil).isTokenExpired(validToken);
verify(jwtUtil).getUsernameFromToken(validToken);
verify(jwtUtil).getUserIdFromToken(validToken);
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testHeadersAdded_ValidToken() {
String validToken = "valid.jwt.token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
.build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
when(jwtUtil.validateToken(validToken)).thenReturn(true);
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
var exchangeCaptor = forClass(ServerWebExchange.class);
verify(chain).filter(exchangeCaptor.capture());
ServerHttpRequest modifiedRequest = exchangeCaptor.getValue().getRequest();
assert modifiedRequest.getHeaders().getFirst("X-User-Id").equals("1");
assert modifiedRequest.getHeaders().getFirst("X-Username").equals("testuser");
}
@Test
void testMixedPath_AuthPath() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/logout").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
verify(jwtUtil, never()).validateToken(anyString());
}
@Test
void testActuatorPath_Metrics() {
String validToken = "valid.jwt.token";
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/metrics")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
.build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
when(jwtUtil.validateToken(validToken)).thenReturn(true);
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(jwtUtil).validateToken(validToken);
verify(jwtUtil).isTokenExpired(validToken);
verify(jwtUtil).getUsernameFromToken(validToken);
verify(jwtUtil).getUserIdFromToken(validToken);
verify(chain).filter(any(ServerWebExchange.class));
}
}
@@ -0,0 +1,285 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.config.RateLimitConfig;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
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.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.net.InetSocketAddress;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RateLimitFilterTest {
@Mock
private RateLimiter globalRateLimiter;
@Mock
private RateLimiter ipRateLimiter;
@Mock
private RateLimiter userRateLimiter;
@Mock
private RateLimitConfig rateLimitConfig;
@Mock
private GatewayFilterChain chain;
private RateLimitFilter rateLimitFilter;
@BeforeEach
void setUp() {
lenient().when(rateLimitConfig.isRateLimitEnabled()).thenReturn(true);
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(100)
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ZERO)
.build();
lenient().when(globalRateLimiter.getRateLimiterConfig()).thenReturn(config);
lenient().when(ipRateLimiter.getRateLimiterConfig()).thenReturn(config);
lenient().when(userRateLimiter.getRateLimiterConfig()).thenReturn(config);
rateLimitFilter = new RateLimitFilter(
globalRateLimiter,
ipRateLimiter,
userRateLimiter,
rateLimitConfig);
}
@Test
void testFilter_WhenRateLimitDisabled_ShouldPassThrough() {
when(rateLimitConfig.isRateLimitEnabled()).thenReturn(false);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(globalRateLimiter, never()).acquirePermission();
}
@Test
void testFilter_WhenGlobalRateLimitExceeded_ShouldReturn429() {
when(globalRateLimiter.acquirePermission()).thenReturn(false);
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
assertEquals(HttpStatus.TOO_MANY_REQUESTS, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any());
}
@Test
void testFilter_WhenAllRateLimitsPass_ShouldContinueChain() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("X-User-Id", "user123")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(globalRateLimiter).acquirePermission();
}
@Test
void testFilter_WithoutUserId_ShouldSkipUserRateLimit() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testGetClientIp_FromXForwardedFor() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("X-Forwarded-For", "10.0.0.1, 192.168.1.1")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testGetClientIp_FromXRealIP() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.header("X-Real-IP", "10.0.0.2")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testGetClientIp_FromRemoteAddress() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.100", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testRateLimitHeaders_WhenExceeded() {
when(globalRateLimiter.acquirePermission()).thenReturn(false);
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
assertTrue(headers.containsKey("X-RateLimit-Limit"));
assertTrue(headers.containsKey("X-RateLimit-Remaining"));
assertTrue(headers.containsKey("Retry-After"));
assertTrue(headers.containsKey("X-RateLimit-Type"));
}
@Test
void testCounters_WhenRequestsProcessed() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
assertEquals(1, rateLimitFilter.getTotalRequests());
assertEquals(0, rateLimitFilter.getBlockedRequests());
}
@Test
void testCounters_WhenRequestsBlocked() {
when(globalRateLimiter.acquirePermission()).thenReturn(false);
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
assertEquals(1, rateLimitFilter.getTotalRequests());
assertEquals(1, rateLimitFilter.getBlockedRequests());
}
@Test
void testResetCounters() {
when(globalRateLimiter.acquirePermission()).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest
.get("/api/test")
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
.verifyComplete();
assertEquals(1, rateLimitFilter.getTotalRequests());
rateLimitFilter.resetCounters();
assertEquals(0, rateLimitFilter.getTotalRequests());
assertEquals(0, rateLimitFilter.getBlockedRequests());
}
@Test
void testGetOrder() {
int order = rateLimitFilter.getOrder();
assertEquals(Ordered.HIGHEST_PRECEDENCE + 100, order);
}
}
@@ -0,0 +1,262 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.service.PermissionService;
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.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RbacAuthorizationFilterTest {
@Mock
private GatewayFilterChain chain;
@Mock
private PermissionService permissionService;
private RbacAuthorizationFilter filter;
private ServerWebExchange exchange;
@BeforeEach
void setUp() {
filter = new RbacAuthorizationFilter(permissionService);
}
@Test
void testPublicPath_AllowAccess() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testPublicPath_Register() {
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testPublicPath_ActuatorHealth() {
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/health").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testPublicPath_ActuatorInfo() {
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/info").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_NoUserId() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
exchange = MockServerWebExchange.from(request);
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_WithUserId() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-User-Id", "1")
.build();
exchange = MockServerWebExchange.from(request);
when(permissionService.hasPermission(eq(1L), eq("/api/users"), eq("GET"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_PostMethod() {
MockServerHttpRequest request = MockServerHttpRequest.post("/api/users")
.header("X-User-Id", "1")
.build();
exchange = MockServerWebExchange.from(request);
when(permissionService.hasPermission(eq(1L), eq("/api/users"), eq("POST"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_PutMethod() {
MockServerHttpRequest request = MockServerHttpRequest.put("/api/users/1")
.header("X-User-Id", "1")
.build();
exchange = MockServerWebExchange.from(request);
when(permissionService.hasPermission(eq(1L), eq("/api/users/1"), eq("PUT"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_DeleteMethod() {
MockServerHttpRequest request = MockServerHttpRequest.delete("/api/users/1")
.header("X-User-Id", "1")
.build();
exchange = MockServerWebExchange.from(request);
when(permissionService.hasPermission(eq(1L), eq("/api/users/1"), eq("DELETE"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testProtectedPath_EmptyUserId() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-User-Id", "")
.build();
exchange = MockServerWebExchange.from(request);
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testMixedPath_AuthPath() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/logout").build();
exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testMixedPath_UserPath() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users/profile")
.header("X-User-Id", "1")
.build();
exchange = MockServerWebExchange.from(request);
when(permissionService.hasPermission(eq(1L), eq("/api/users/profile"), eq("GET"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
@Test
void testActuatorPath_Metrics() {
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/metrics")
.header("X-User-Id", "1")
.build();
exchange = MockServerWebExchange.from(request);
when(permissionService.hasPermission(eq(1L), eq("/actuator/metrics"), eq("GET"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
verify(chain).filter(any(ServerWebExchange.class));
}
}
@@ -0,0 +1,189 @@
package cn.novalon.gym.manage.gateway.filter;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
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.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.test.util.ReflectionTestUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* ResilienceFilter单元测试
*
* 文件定义:测试容错过滤器的核心功能
* 涉及业务:断路器、重试、超时、降级
*
* @author 张翔
* @date 2026-03-26
*/
@ExtendWith(MockitoExtension.class)
class ResilienceFilterTest {
@Mock
private GatewayFilterChain chain;
private CircuitBreaker circuitBreaker;
private Retry retry;
private TimeLimiter timeLimiter;
private ResilienceFilter resilienceFilter;
@BeforeEach
void setUp() {
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slidingWindowSize(100)
.minimumNumberOfCalls(10)
.waitDurationInOpenState(Duration.ofSeconds(10))
.build();
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.build();
TimeLimiterConfig tlConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(3))
.build();
circuitBreaker = CircuitBreaker.of("gateway", cbConfig);
retry = Retry.of("gateway", retryConfig);
timeLimiter = TimeLimiter.of("gateway", tlConfig);
resilienceFilter = new ResilienceFilter(circuitBreaker, retry, timeLimiter);
ReflectionTestUtils.setField(resilienceFilter, "resilienceEnabled", true);
ReflectionTestUtils.setField(resilienceFilter, "circuitBreakerEnabled", true);
ReflectionTestUtils.setField(resilienceFilter, "retryEnabled", true);
ReflectionTestUtils.setField(resilienceFilter, "timeoutEnabled", true);
}
@Test
void testFilter_WhenResilienceDisabled_ShouldContinueChain() {
ReflectionTestUtils.setField(resilienceFilter, "resilienceEnabled", false);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(resilienceFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testFilter_WhenAllPatternsEnabled_ShouldApplyResilience() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(resilienceFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testFilter_WhenCircuitBreakerDisabled_ShouldSkipCircuitBreaker() {
ReflectionTestUtils.setField(resilienceFilter, "circuitBreakerEnabled", false);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(resilienceFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testFilter_WhenRetryDisabled_ShouldSkipRetry() {
ReflectionTestUtils.setField(resilienceFilter, "retryEnabled", false);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(resilienceFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testFilter_WhenTimeoutDisabled_ShouldSkipTimeout() {
ReflectionTestUtils.setField(resilienceFilter, "timeoutEnabled", false);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(resilienceFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
}
@Test
void testFilter_WhenChainThrowsException_ShouldHandleFallback() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.error(new RuntimeException("Test error")));
StepVerifier.create(resilienceFilter.filter(exchange, chain))
.verifyComplete();
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, exchange.getResponse().getStatusCode());
}
@Test
void testGetOrder_ShouldReturnCorrectOrder() {
int order = resilienceFilter.getOrder();
assertEquals(-2147483448, order);
}
}
@@ -0,0 +1,219 @@
package cn.novalon.gym.manage.gateway.filter;
import cn.novalon.gym.manage.gateway.service.SignatureService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.test.util.ReflectionTestUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* SignatureFilter单元测试
*
* 文件定义:测试签名验证过滤器的核心功能
* 涉及业务:签名验证、白名单过滤、错误响应
*
* @author 张翔
* @date 2026-03-26
*/
@ExtendWith(MockitoExtension.class)
class SignatureFilterTest {
@Mock
private SignatureService signatureService;
@Mock
private GatewayFilterChain chain;
@InjectMocks
private SignatureFilter signatureFilter;
private static final String TEST_SECRET = "TestSecretKey123";
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(signatureFilter, "signatureEnabled", true);
ReflectionTestUtils.setField(signatureFilter, "signatureSecret", TEST_SECRET);
ReflectionTestUtils.setField(signatureFilter, "whitelistPaths", "/actuator/health,/actuator/info");
}
@Test
void testFilter_WhenSignatureDisabled_ShouldContinueChain() {
ReflectionTestUtils.setField(signatureFilter, "signatureEnabled", false);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(signatureService, never()).verifySignature(any(), any());
}
@Test
void testFilter_WhenPathIsWhitelisted_ShouldContinueChain() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/actuator/health")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(signatureService, never()).verifySignature(any(), any());
}
@Test
void testFilter_WhenSignatureValid_ShouldContinueChain() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.header("X-Signature", "valid-signature")
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()))
.header("X-Nonce", "test-nonce")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(signatureService.verifySignature(any(), any())).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(signatureService).verifySignature(request, TEST_SECRET);
verify(chain).filter(exchange);
}
@Test
void testFilter_WhenSignatureInvalid_ShouldReturnUnauthorized() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.header("X-Signature", "invalid-signature")
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()))
.header("X-Nonce", "test-nonce")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(signatureService.verifySignature(any(), any())).thenReturn(false);
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(signatureService).verifySignature(request, TEST_SECRET);
verify(chain, never()).filter(any());
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
}
@Test
void testFilter_WhenMissingSignatureHeaders_ShouldReturnUnauthorized() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(signatureService.verifySignature(any(), any())).thenReturn(false);
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(signatureService).verifySignature(request, TEST_SECRET);
verify(chain, never()).filter(any());
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
}
@Test
void testFilter_WhenMultipleWhitelistPaths_ShouldMatchAny() {
MockServerHttpRequest request1 = MockServerHttpRequest
.method(HttpMethod.GET, "/actuator/health")
.build();
MockServerHttpRequest request2 = MockServerHttpRequest
.method(HttpMethod.GET, "/actuator/info")
.build();
MockServerWebExchange exchange1 = MockServerWebExchange.builder(request1).build();
MockServerWebExchange exchange2 = MockServerWebExchange.builder(request2).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(signatureFilter.filter(exchange1, chain))
.verifyComplete();
StepVerifier.create(signatureFilter.filter(exchange2, chain))
.verifyComplete();
verify(chain, times(2)).filter(any());
verify(signatureService, never()).verifySignature(any(), any());
}
@Test
void testFilter_WhenPathStartsWithWhitelist_ShouldMatch() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/actuator/health/details")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(chain).filter(exchange);
verify(signatureService, never()).verifySignature(any(), any());
}
@Test
void testGetOrder_ShouldReturnCorrectOrder() {
int order = signatureFilter.getOrder();
assertEquals(-2147483498, order);
}
@Test
void testFilter_WhenSignatureEnabled_ShouldVerifySignature() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, "/api/users")
.header("X-Signature", "test-signature")
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()))
.header("X-Nonce", "test-nonce")
.build();
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
when(signatureService.verifySignature(any(), any())).thenReturn(true);
when(chain.filter(any())).thenReturn(Mono.empty());
StepVerifier.create(signatureFilter.filter(exchange, chain))
.verifyComplete();
verify(signatureService).verifySignature(request, TEST_SECRET);
}
}
@@ -0,0 +1,83 @@
package cn.novalon.gym.manage.gateway.health;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
/**
* GatewayHealthIndicator单元测试
*
* 文件定义:测试网关健康检查指示器的核心功能
* 涉及业务:断路器健康检查、限流器健康检查
*
* @author 张翔
* @date 2026-03-26
*/
@ExtendWith(MockitoExtension.class)
class GatewayHealthIndicatorTest {
private CircuitBreakerRegistry circuitBreakerRegistry;
private RateLimiterRegistry rateLimiterRegistry;
private GatewayHealthIndicator healthIndicator;
@BeforeEach
void setUp() {
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slidingWindowSize(100)
.minimumNumberOfCalls(10)
.waitDurationInOpenState(Duration.ofSeconds(10))
.build();
RateLimiterConfig rlConfig = RateLimiterConfig.custom()
.limitForPeriod(100)
.limitRefreshPeriod(Duration.ofSeconds(1))
.build();
circuitBreakerRegistry = CircuitBreakerRegistry.of(cbConfig);
rateLimiterRegistry = RateLimiterRegistry.of(rlConfig);
healthIndicator = new GatewayHealthIndicator(circuitBreakerRegistry, rateLimiterRegistry);
}
@Test
void testHealth_WhenAllComponentsHealthy_ShouldReturnUp() {
circuitBreakerRegistry.circuitBreaker("test-cb");
rateLimiterRegistry.rateLimiter("test-rl");
Health health = healthIndicator.health();
assertEquals(Status.UP, health.getStatus());
assertTrue(health.getDetails().containsKey("circuitBreakers"));
assertTrue(health.getDetails().containsKey("rateLimiters"));
}
@Test
void testHealth_WhenNoComponents_ShouldReturnUp() {
Health health = healthIndicator.health();
assertEquals(Status.UP, health.getStatus());
}
@Test
void testHealth_ShouldIncludeComponentDetails() {
circuitBreakerRegistry.circuitBreaker("gateway");
rateLimiterRegistry.rateLimiter("gateway");
Health health = healthIndicator.health();
assertTrue(health.getDetails().containsKey("circuitBreakers"));
assertTrue(health.getDetails().containsKey("rateLimiters"));
}
}
@@ -0,0 +1,252 @@
package cn.novalon.gym.manage.gateway.integration;
import cn.novalon.gym.manage.gateway.filter.RbacAuthorizationFilter;
import cn.novalon.gym.manage.gateway.service.PermissionService;
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.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RbacIntegrationTest {
@Mock
private PermissionService permissionService;
@Mock
private GatewayFilterChain chain;
private RbacAuthorizationFilter filter;
@BeforeEach
void setUp() {
filter = new RbacAuthorizationFilter(permissionService);
}
@Test
void testEndToEnd_AdminUserFullAccess() {
Long adminUserId = 1L;
String adminPath = "/api/admin/users";
String adminMethod = "GET";
when(permissionService.hasPermission(eq(adminUserId), eq(adminPath), eq(adminMethod))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest.get(adminPath)
.header("X-User-Id", adminUserId.toString())
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == null || exchange.getResponse().getStatusCode() == HttpStatus.OK;
}
@Test
void testEndToEnd_RegularUserLimitedAccess() {
Long regularUserId = 2L;
String adminPath = "/api/admin/users";
String userPath = "/api/users/profile";
when(permissionService.hasPermission(eq(regularUserId), eq(adminPath), eq("GET"))).thenReturn(false);
when(permissionService.hasPermission(eq(regularUserId), eq(userPath), eq("GET"))).thenReturn(true);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
MockServerHttpRequest adminRequest = MockServerHttpRequest.get(adminPath)
.header("X-User-Id", regularUserId.toString())
.build();
ServerWebExchange adminExchange = MockServerWebExchange.from(adminRequest);
Mono<Void> adminResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(adminExchange, chain);
StepVerifier.create(adminResult)
.verifyComplete();
assert adminExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
MockServerHttpRequest userRequest = MockServerHttpRequest.get(userPath)
.header("X-User-Id", regularUserId.toString())
.build();
ServerWebExchange userExchange = MockServerWebExchange.from(userRequest);
Mono<Void> userResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(userExchange, chain);
StepVerifier.create(userResult)
.verifyComplete();
assert userExchange.getResponse().getStatusCode() == null || userExchange.getResponse().getStatusCode() == HttpStatus.OK;
}
@Test
void testEndToEnd_MultipleHttpMethods() {
Long userId = 3L;
String basePath = "/api/users";
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("GET"))).thenReturn(true);
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("POST"))).thenReturn(true);
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("PUT"))).thenReturn(false);
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("DELETE"))).thenReturn(false);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
MockServerHttpRequest getRequest = MockServerHttpRequest.get(basePath)
.header("X-User-Id", userId.toString())
.build();
ServerWebExchange getExchange = MockServerWebExchange.from(getRequest);
Mono<Void> getResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(getExchange, chain);
StepVerifier.create(getResult)
.verifyComplete();
assert getExchange.getResponse().getStatusCode() == null || getExchange.getResponse().getStatusCode() == HttpStatus.OK;
MockServerHttpRequest postRequest = MockServerHttpRequest.post(basePath)
.header("X-User-Id", userId.toString())
.build();
ServerWebExchange postExchange = MockServerWebExchange.from(postRequest);
Mono<Void> postResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(postExchange, chain);
StepVerifier.create(postResult)
.verifyComplete();
assert postExchange.getResponse().getStatusCode() == null || postExchange.getResponse().getStatusCode() == HttpStatus.OK;
MockServerHttpRequest putRequest = MockServerHttpRequest.put(basePath)
.header("X-User-Id", userId.toString())
.build();
ServerWebExchange putExchange = MockServerWebExchange.from(putRequest);
Mono<Void> putResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(putExchange, chain);
StepVerifier.create(putResult)
.verifyComplete();
assert putExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
MockServerHttpRequest deleteRequest = MockServerHttpRequest.delete(basePath)
.header("X-User-Id", userId.toString())
.build();
ServerWebExchange deleteExchange = MockServerWebExchange.from(deleteRequest);
Mono<Void> deleteResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(deleteExchange, chain);
StepVerifier.create(deleteResult)
.verifyComplete();
assert deleteExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
}
@Test
void testEndToEnd_PathMatchingScenarios() {
Long userId = 4L;
when(permissionService.hasPermission(eq(userId), eq("/api/users"), eq("GET"))).thenReturn(true);
when(permissionService.hasPermission(eq(userId), eq("/api/users/123"), eq("GET"))).thenReturn(true);
when(permissionService.hasPermission(eq(userId), eq("/api/users/123/profile"), eq("GET"))).thenReturn(true);
when(permissionService.hasPermission(eq(userId), eq("/api/admin"), eq("GET"))).thenReturn(false);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
String[] allowedPaths = {"/api/users", "/api/users/123", "/api/users/123/profile"};
for (String path : allowedPaths) {
MockServerHttpRequest request = MockServerHttpRequest.get(path)
.header("X-User-Id", userId.toString())
.build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == null || exchange.getResponse().getStatusCode() == HttpStatus.OK;
}
MockServerHttpRequest adminRequest = MockServerHttpRequest.get("/api/admin")
.header("X-User-Id", userId.toString())
.build();
ServerWebExchange adminExchange = MockServerWebExchange.from(adminRequest);
Mono<Void> adminResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(adminExchange, chain);
StepVerifier.create(adminResult)
.verifyComplete();
assert adminExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
}
@Test
void testEndToEnd_PublicPathsBypass() {
String[] publicPaths = {
"/api/auth/login",
"/api/auth/register",
"/actuator/health",
"/actuator/info"
};
for (String path : publicPaths) {
MockServerHttpRequest request = MockServerHttpRequest.get(path).build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
.filter(exchange, chain);
StepVerifier.create(result)
.verifyComplete();
assert exchange.getResponse().getStatusCode() == null || exchange.getResponse().getStatusCode() == HttpStatus.OK;
}
}
@Test
void testEndToEnd_ErrorScenarios() {
MockServerHttpRequest noHeaderRequest = MockServerHttpRequest.get("/api/users").build();
ServerWebExchange noHeaderExchange = MockServerWebExchange.from(noHeaderRequest);
Mono<Void> noHeaderResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(noHeaderExchange, chain);
StepVerifier.create(noHeaderResult)
.verifyComplete();
assert noHeaderExchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
MockServerHttpRequest invalidIdRequest = MockServerHttpRequest.get("/api/users")
.header("X-User-Id", "invalid")
.build();
ServerWebExchange invalidIdExchange = MockServerWebExchange.from(invalidIdRequest);
Mono<Void> invalidIdResult = filter.apply(new RbacAuthorizationFilter.Config())
.filter(invalidIdExchange, chain);
StepVerifier.create(invalidIdResult)
.verifyComplete();
assert invalidIdExchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
}
}
@@ -0,0 +1,141 @@
package cn.novalon.gym.manage.gateway.loadbalancer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* CustomLoadBalancer单元测试
*
* 文件定义:测试自定义负载均衡器的核心功能
* 涉及业务:轮询、随机、加权轮询、最少连接策略
*
* @author 张翔
* @date 2026-03-26
*/
class CustomLoadBalancerTest {
private CustomLoadBalancer loadBalancer;
private List<ServiceInstance> instances;
@BeforeEach
void setUp() {
loadBalancer = new CustomLoadBalancer();
instances = Arrays.asList(
createInstance("host1", 8080),
createInstance("host2", 8080),
createInstance("host3", 8080)
);
}
@Test
void testSelectByRoundRobin() {
ServiceInstance instance1 = loadBalancer.selectInstance(
instances,
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
ServiceInstance instance2 = loadBalancer.selectInstance(
instances,
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
assertNotNull(instance1);
assertNotNull(instance2);
assertNotSame(instance1, instance2);
}
@Test
void testSelectByRandom() {
ServiceInstance instance = loadBalancer.selectInstance(
instances,
CustomLoadBalancer.LoadBalanceStrategy.RANDOM);
assertNotNull(instance);
assertTrue(instances.contains(instance));
}
@Test
void testSelectByWeightedRoundRobin() {
ServiceInstance instance = loadBalancer.selectInstance(
instances,
CustomLoadBalancer.LoadBalanceStrategy.WEIGHTED_ROUND_ROBIN);
assertNotNull(instance);
assertTrue(instances.contains(instance));
}
@Test
void testSelectByLeastConnections() {
ServiceInstance instance = loadBalancer.selectInstance(
instances,
CustomLoadBalancer.LoadBalanceStrategy.LEAST_CONNECTIONS);
assertNotNull(instance);
assertTrue(instances.contains(instance));
}
@Test
void testSelectInstance_EmptyList() {
ServiceInstance instance = loadBalancer.selectInstance(
Collections.emptyList(),
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
assertNull(instance);
}
@Test
void testSelectInstance_NullList() {
ServiceInstance instance = loadBalancer.selectInstance(
null,
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
assertNull(instance);
}
@Test
void testSetWeight() {
ServiceInstance instance = instances.get(0);
loadBalancer.setWeight(instance, 5);
assertNotNull(instance);
}
@Test
void testIncrementConnection() {
ServiceInstance instance = instances.get(0);
loadBalancer.incrementConnection(instance);
loadBalancer.incrementConnection(instance);
assertNotNull(instance);
}
@Test
void testDecrementConnection() {
ServiceInstance instance = instances.get(0);
loadBalancer.incrementConnection(instance);
loadBalancer.incrementConnection(instance);
loadBalancer.decrementConnection(instance);
assertNotNull(instance);
}
private ServiceInstance createInstance(String host, int port) {
return new DefaultServiceInstance(
"service-" + host + "-" + port,
"test-service",
host,
port,
false
);
}
}
@@ -0,0 +1,84 @@
package cn.novalon.gym.manage.gateway.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
/**
* GatewayMetrics单元测试
*
* 文件定义:测试网关指标收集器的核心功能
* 涉及业务:请求统计、性能监控、活跃连接数统计
*
* @author 张翔
* @date 2026-03-26
*/
class GatewayMetricsTest {
private MeterRegistry meterRegistry;
private GatewayMetrics gatewayMetrics;
@BeforeEach
void setUp() {
meterRegistry = new SimpleMeterRegistry();
gatewayMetrics = new GatewayMetrics(meterRegistry);
}
@Test
void testIncrementTotalRequests() {
gatewayMetrics.incrementTotalRequests();
assertEquals(1, gatewayMetrics.getTotalRequests());
}
@Test
void testIncrementSuccessRequests() {
gatewayMetrics.incrementSuccessRequests();
assertEquals(1, gatewayMetrics.getSuccessRequests());
}
@Test
void testIncrementFailedRequests() {
gatewayMetrics.incrementFailedRequests();
assertEquals(1, gatewayMetrics.getFailedRequests());
}
@Test
void testIncrementActiveConnections() {
gatewayMetrics.incrementActiveConnections();
assertEquals(1, gatewayMetrics.getActiveConnections());
}
@Test
void testDecrementActiveConnections() {
gatewayMetrics.incrementActiveConnections();
gatewayMetrics.incrementActiveConnections();
gatewayMetrics.decrementActiveConnections();
assertEquals(1, gatewayMetrics.getActiveConnections());
}
@Test
void testRecordRequestDuration() {
gatewayMetrics.recordRequestDuration("/api/users", Duration.ofMillis(100));
assertNotNull(meterRegistry.find("gateway.request.duration").timer());
}
@Test
void testMultipleIncrements() {
gatewayMetrics.incrementTotalRequests();
gatewayMetrics.incrementTotalRequests();
gatewayMetrics.incrementTotalRequests();
assertEquals(3, gatewayMetrics.getTotalRequests());
}
}
@@ -0,0 +1,139 @@
package cn.novalon.gym.manage.gateway.monitor;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class PerformanceMonitorTest {
private PerformanceMonitor performanceMonitor;
private MeterRegistry meterRegistry;
@BeforeEach
void setUp() {
meterRegistry = new SimpleMeterRegistry();
performanceMonitor = new PerformanceMonitor(meterRegistry);
}
@Test
void testRecordRequest() {
performanceMonitor.recordRequest("/api/test", 100);
assertEquals(1, performanceMonitor.getPathStats().size());
assertTrue(performanceMonitor.getAverageProcessingTime() > 0);
}
@Test
void testSlowRequestDetection() {
performanceMonitor.setSlowRequestThresholdMs(50);
performanceMonitor.recordRequest("/api/test", 100);
assertEquals(1, performanceMonitor.getPathStats().size());
}
@Test
void testMultipleRequests() {
performanceMonitor.recordRequest("/api/test1", 100);
performanceMonitor.recordRequest("/api/test2", 200);
performanceMonitor.recordRequest("/api/test1", 150);
Map<String, PerformanceMonitor.PerformanceStats> stats = performanceMonitor.getPathStats();
assertEquals(2, stats.size());
PerformanceMonitor.PerformanceStats test1Stats = stats.get("/api/test1");
assertNotNull(test1Stats);
assertEquals(2, test1Stats.getRequestCount());
assertEquals(125.0, test1Stats.getAverageTime());
assertEquals(150, test1Stats.getMaxTime());
assertEquals(100, test1Stats.getMinTime());
}
@Test
void testMemoryStats() {
Map<String, Object> memoryStats = performanceMonitor.getMemoryStats();
assertNotNull(memoryStats);
assertTrue(memoryStats.containsKey("totalMemory"));
assertTrue(memoryStats.containsKey("freeMemory"));
assertTrue(memoryStats.containsKey("usedMemory"));
assertTrue(memoryStats.containsKey("maxMemory"));
assertTrue(memoryStats.containsKey("memoryUsage"));
}
@Test
void testThreadStats() {
Map<String, Object> threadStats = performanceMonitor.getThreadStats();
assertNotNull(threadStats);
assertTrue(threadStats.containsKey("threadCount"));
assertTrue(threadStats.containsKey("peakThreadCount"));
assertTrue(threadStats.containsKey("daemonThreadCount"));
assertTrue(threadStats.containsKey("totalStartedThreadCount"));
}
@Test
void testMemoryUsage() {
double memoryUsage = performanceMonitor.getMemoryUsage();
assertTrue(memoryUsage >= 0.0);
assertTrue(memoryUsage <= 1.0);
}
@Test
void testAverageProcessingTime_NoRequests() {
assertEquals(0.0, performanceMonitor.getAverageProcessingTime());
}
@Test
void testAverageProcessingTime_WithRequests() {
performanceMonitor.recordRequest("/api/test1", 100);
performanceMonitor.recordRequest("/api/test2", 200);
assertEquals(150.0, performanceMonitor.getAverageProcessingTime());
}
@Test
void testClearStats() {
performanceMonitor.recordRequest("/api/test", 100);
performanceMonitor.clearStats();
assertEquals(0, performanceMonitor.getPathStats().size());
assertEquals(0.0, performanceMonitor.getAverageProcessingTime());
}
@Test
void testSetSlowRequestThreshold() {
performanceMonitor.setSlowRequestThresholdMs(500);
performanceMonitor.recordRequest("/api/test", 600);
assertEquals(1, performanceMonitor.getPathStats().size());
}
@Test
void testSetMemoryWarningThreshold() {
performanceMonitor.setMemoryWarningThreshold(0.9);
performanceMonitor.recordRequest("/api/test", 100);
assertEquals(1, performanceMonitor.getPathStats().size());
}
@Test
void testPerformanceStats() {
PerformanceMonitor.PerformanceStats stats = new PerformanceMonitor.PerformanceStats();
stats.recordRequest(100);
stats.recordRequest(200);
stats.recordRequest(150);
assertEquals(3, stats.getRequestCount());
assertEquals(150.0, stats.getAverageTime());
assertEquals(200, stats.getMaxTime());
assertEquals(100, stats.getMinTime());
}
}
@@ -0,0 +1,154 @@
package cn.novalon.gym.manage.gateway.route;
import cn.novalon.gym.manage.gateway.service.impl.DynamicRouteService;
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.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* DynamicRouteService单元测试
*
* 文件定义:测试动态路由服务的核心功能
* 涉及业务:路由增删改查、路由刷新
*
* @author 张翔
* @date 2026-04-14
*/
@ExtendWith(MockitoExtension.class)
class DynamicRouteServiceTest {
@Mock
private RouteDefinitionWriter routeDefinitionWriter;
@Mock
private RouteDefinitionLocator routeDefinitionLocator;
@Mock
private ApplicationEventPublisher publisher;
private DynamicRouteService dynamicRouteService;
@BeforeEach
void setUp() {
when(routeDefinitionLocator.getRouteDefinitions())
.thenReturn(Flux.empty());
dynamicRouteService = new DynamicRouteService(
routeDefinitionWriter,
routeDefinitionLocator,
publisher);
}
@Test
void testAddRoute_Success() {
RouteDefinition routeDefinition = createRouteDefinition("test-route");
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.addRoute(routeDefinition))
.expectNext(true)
.verifyComplete();
verify(routeDefinitionWriter).save(any());
verify(publisher).publishEvent(any(RefreshRoutesEvent.class));
}
@Test
void testAddRoute_NullRoute() {
StepVerifier.create(dynamicRouteService.addRoute(null))
.expectNext(false)
.verifyComplete();
verify(routeDefinitionWriter, never()).save(any());
}
@Test
void testDeleteRoute_Success() {
String routeId = "test-route";
RouteDefinition routeDefinition = createRouteDefinition(routeId);
// 先添加路由到缓存中
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.addRoute(routeDefinition))
.expectNext(true)
.verifyComplete();
// 然后删除路由
when(routeDefinitionWriter.delete(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.deleteRoute(routeId))
.expectNext(true)
.verifyComplete();
verify(routeDefinitionWriter).delete(any());
verify(publisher, times(2)).publishEvent(any(RefreshRoutesEvent.class));
}
@Test
void testDeleteRoute_NullId() {
StepVerifier.create(dynamicRouteService.deleteRoute(null))
.expectNext(false)
.verifyComplete();
verify(routeDefinitionWriter, never()).delete(any());
}
@Test
void testGetAllRoutes() {
RouteDefinition route1 = createRouteDefinition("route1");
RouteDefinition route2 = createRouteDefinition("route2");
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.addRoute(route1))
.expectNext(true)
.verifyComplete();
StepVerifier.create(dynamicRouteService.addRoute(route2))
.expectNext(true)
.verifyComplete();
StepVerifier.create(dynamicRouteService.getRoutes().collectList())
.assertNext(routes -> {
assertNotNull(routes);
assertTrue(routes.size() >= 2);
})
.verifyComplete();
}
@Test
void testGetRouteCount() {
RouteDefinition route = createRouteDefinition("test-route");
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.addRoute(route))
.expectNext(true)
.verifyComplete();
StepVerifier.create(dynamicRouteService.getRouteCount())
.assertNext(count -> assertTrue(count >= 1))
.verifyComplete();
}
private RouteDefinition createRouteDefinition(String id) {
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(id);
routeDefinition.setUri(java.net.URI.create("http://localhost:8080"));
return routeDefinition;
}
}
@@ -0,0 +1,185 @@
package cn.novalon.gym.manage.gateway.service.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import javax.crypto.SecretKey;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class JwtKeyServiceImplTest {
@InjectMocks
private JwtKeyServiceImpl jwtKeyService;
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(jwtKeyService, "configuredSecret", null);
ReflectionTestUtils.setField(jwtKeyService, "encryptionPassword", "testEncryptionPassword");
ReflectionTestUtils.setField(jwtKeyService, "rotationEnabled", true);
}
@Test
void testInitializeKeys_GeneratesNewKey() {
jwtKeyService.initializeKeys();
String version = jwtKeyService.getCurrentKeyVersion();
SecretKey key = jwtKeyService.getCurrentSigningKey();
assertNotNull(version);
assertNotNull(key);
assertEquals("v1", version);
assertEquals("AES", key.getAlgorithm());
}
@Test
void testGenerateSecureKey_GeneratesValidKey() {
String key = jwtKeyService.generateSecureKey();
assertNotNull(key);
assertFalse(key.isEmpty());
assertTrue(jwtKeyService.validateKeyStrength(key));
}
@Test
void testValidateKeyStrength_ValidKey() {
String validKey = "StrongPassword123ABC!@#XYZabcdefg";
assertTrue(jwtKeyService.validateKeyStrength(validKey));
}
@Test
void testValidateKeyStrength_WeakKey() {
String weakKey = "weak";
assertFalse(jwtKeyService.validateKeyStrength(weakKey));
}
@Test
void testValidateKeyStrength_NullKey() {
assertFalse(jwtKeyService.validateKeyStrength(null));
}
@Test
void testValidateKeyStrength_ShortKey() {
String shortKey = "Short1!";
assertFalse(jwtKeyService.validateKeyStrength(shortKey));
}
@Test
void testEncryptKey_WithPassword() {
String originalKey = "MySecretKey123!";
String encryptedKey = jwtKeyService.encryptKey(originalKey);
assertNotNull(encryptedKey);
assertNotEquals(originalKey, encryptedKey);
assertTrue(encryptedKey.length() > originalKey.length());
}
@Test
void testEncryptDecryptKey_RoundTrip() {
String originalKey = "MySecretKey123!";
String encryptedKey = jwtKeyService.encryptKey(originalKey);
String decryptedKey = jwtKeyService.decryptKey(encryptedKey);
assertNotNull(encryptedKey);
assertNotNull(decryptedKey);
assertEquals(originalKey, decryptedKey);
}
@Test
void testRotateKey_CreatesNewVersion() {
jwtKeyService.initializeKeys();
String oldVersion = jwtKeyService.getCurrentKeyVersion();
jwtKeyService.rotateKey();
String newVersion = jwtKeyService.getCurrentKeyVersion();
SecretKey newKey = jwtKeyService.getCurrentSigningKey();
assertNotEquals(oldVersion, newVersion);
assertEquals("v2", newVersion);
assertNotNull(newKey);
assertEquals("AES", newKey.getAlgorithm());
}
@Test
void testGetSigningKeyByVersion_ReturnsCorrectKey() {
jwtKeyService.initializeKeys();
SecretKey v1Key = jwtKeyService.getSigningKeyByVersion("v1");
assertNotNull(v1Key);
assertEquals("AES", v1Key.getAlgorithm());
}
@Test
void testGetSigningKeyByVersion_InvalidVersion() {
jwtKeyService.initializeKeys();
SecretKey invalidKey = jwtKeyService.getSigningKeyByVersion("v999");
assertNull(invalidKey);
}
@Test
void testRotateKey_Disabled() {
ReflectionTestUtils.setField(jwtKeyService, "rotationEnabled", false);
jwtKeyService.initializeKeys();
String oldVersion = jwtKeyService.getCurrentKeyVersion();
jwtKeyService.rotateKey();
String newVersion = jwtKeyService.getCurrentKeyVersion();
assertEquals(oldVersion, newVersion);
}
@Test
void testShouldRotateKey_NewKey() {
jwtKeyService.initializeKeys();
String currentVersion = jwtKeyService.getCurrentKeyVersion();
SecretKey currentKey = jwtKeyService.getCurrentSigningKey();
assertNotNull(currentVersion, "Current version should not be null");
assertNotNull(currentKey, "Current signing key should not be null");
}
@Test
void testMultipleRotations_CreatesMultipleVersions() {
jwtKeyService.initializeKeys();
jwtKeyService.rotateKey();
assertEquals("v2", jwtKeyService.getCurrentKeyVersion());
jwtKeyService.rotateKey();
assertEquals("v3", jwtKeyService.getCurrentKeyVersion());
jwtKeyService.rotateKey();
assertEquals("v4", jwtKeyService.getCurrentKeyVersion());
assertNotNull(jwtKeyService.getSigningKeyByVersion("v1"));
assertNotNull(jwtKeyService.getSigningKeyByVersion("v2"));
assertNotNull(jwtKeyService.getSigningKeyByVersion("v3"));
assertNotNull(jwtKeyService.getSigningKeyByVersion("v4"));
}
@Test
void testEncryptKey_WithoutPassword() {
ReflectionTestUtils.setField(jwtKeyService, "encryptionPassword", "");
String originalKey = "MySecretKey123!";
String encryptedKey = jwtKeyService.encryptKey(originalKey);
assertEquals(originalKey, encryptedKey);
}
@Test
void testDecryptKey_WithoutPassword() {
ReflectionTestUtils.setField(jwtKeyService, "encryptionPassword", "");
String originalKey = "MySecretKey123!";
String decryptedKey = jwtKeyService.decryptKey(originalKey);
assertEquals(originalKey, decryptedKey);
}
}
@@ -0,0 +1,223 @@
package cn.novalon.gym.manage.gateway.service.impl;
import cn.novalon.gym.manage.gateway.model.Permission;
import cn.novalon.gym.manage.gateway.model.Role;
import cn.novalon.gym.manage.gateway.model.User;
import cn.novalon.gym.manage.gateway.service.PermissionService;
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.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class PermissionServiceImplTest {
@Mock
private WebClient.Builder webClientBuilder;
@Mock
private WebClient webClient;
@Mock
private WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec;
@Mock
private WebClient.RequestHeadersSpec<?> requestHeadersSpec;
@Mock
private WebClient.ResponseSpec responseSpec;
private PermissionService permissionService;
@BeforeEach
void setUp() {
doReturn(webClient).when(webClientBuilder).build();
doReturn(requestHeadersUriSpec).when(webClient).get();
doReturn(requestHeadersSpec).when(requestHeadersUriSpec).uri(anyString());
doReturn(responseSpec).when(requestHeadersSpec).retrieve();
permissionService = new PermissionServiceImpl(webClientBuilder, "http://localhost:8084");
}
@Test
void testGetUserById_Success() {
User expectedUser = new User(1L, "testuser", "test@example.com", "1234567890", 1, System.currentTimeMillis(), System.currentTimeMillis());
doReturn(Mono.just(expectedUser)).when(responseSpec).bodyToMono(eq(User.class));
User user = permissionService.getUserById(1L);
assertNotNull(user);
assertEquals("testuser", user.getUsername());
verify(webClient).get();
}
@Test
void testGetUserById_NullUserId() {
User user = permissionService.getUserById(null);
assertNull(user);
verify(webClient, never()).get();
}
@Test
void testGetUserRoles_Success() {
List<Role> expectedRoles = Arrays.asList(
new Role(1L, "ADMIN", "Administrator", "Admin role", 1, System.currentTimeMillis(), System.currentTimeMillis()),
new Role(2L, "USER", "User", "User role", 1, System.currentTimeMillis(), System.currentTimeMillis())
);
doReturn(Mono.just(expectedRoles.toArray(new Role[0]))).when(responseSpec).bodyToMono(eq(Role[].class));
List<Role> roles = permissionService.getUserRoles(1L);
assertNotNull(roles);
assertEquals(2, roles.size());
verify(webClient).get();
}
@Test
void testGetUserRoles_NullUserId() {
List<Role> roles = permissionService.getUserRoles(null);
assertNotNull(roles);
assertTrue(roles.isEmpty());
verify(webClient, never()).get();
}
@Test
void testGetUserPermissions_Success() {
Set<Permission> expectedPermissions = new HashSet<>(Arrays.asList(
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis()),
new Permission(2L, "user:write", "Write User", "API", "/api/users/**", "POST", "Write user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
));
doReturn(Mono.just(expectedPermissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
Set<Permission> permissions = permissionService.getUserPermissions(1L);
assertNotNull(permissions);
assertEquals(2, permissions.size());
verify(webClient).get();
}
@Test
void testGetUserPermissions_NullUserId() {
Set<Permission> permissions = permissionService.getUserPermissions(null);
assertNotNull(permissions);
assertTrue(permissions.isEmpty());
verify(webClient, never()).get();
}
@Test
void testHasPermission_True() {
Set<Permission> permissions = new HashSet<>(Arrays.asList(
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
));
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
boolean hasPermission = permissionService.hasPermission(1L, "/api/users/123", "GET");
assertTrue(hasPermission);
}
@Test
void testHasPermission_False() {
Set<Permission> permissions = new HashSet<>(Arrays.asList(
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
));
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
boolean hasPermission = permissionService.hasPermission(1L, "/api/users/123", "POST");
assertFalse(hasPermission);
}
@Test
void testHasPermission_NullUserId() {
boolean hasPermission = permissionService.hasPermission(null, "/api/users/123", "GET");
assertFalse(hasPermission);
verify(webClient, never()).get();
}
@Test
void testGetPermissionPaths_Success() {
Set<Permission> permissions = new HashSet<>(Arrays.asList(
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis()),
new Permission(2L, "user:write", "Write User", "API", "/api/users/**", "POST", "Write user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
));
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
Set<String> paths = permissionService.getPermissionPaths(1L, "GET");
assertNotNull(paths);
assertEquals(1, paths.size());
assertTrue(paths.contains("/api/users/**"));
}
@Test
void testClearCache_Success() {
User user = new User(1L, "testuser", "test@example.com", "1234567890", 1, System.currentTimeMillis(), System.currentTimeMillis());
List<Role> roles = Arrays.asList(new Role(1L, "ADMIN", "Administrator", "Admin role", 1, System.currentTimeMillis(), System.currentTimeMillis()));
Set<Permission> permissions = new HashSet<>(Arrays.asList(
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
));
doReturn(Mono.just(user)).when(responseSpec).bodyToMono(eq(User.class));
doReturn(Mono.just(roles.toArray(new Role[0]))).when(responseSpec).bodyToMono(eq(Role[].class));
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
permissionService.getUserById(1L);
permissionService.getUserRoles(1L);
permissionService.getUserPermissions(1L);
permissionService.clearCache(1L);
verify(webClient, times(3)).get();
}
@Test
void testClearAllCache_Success() {
User user = new User(1L, "testuser", "test@example.com", "1234567890", 1, System.currentTimeMillis(), System.currentTimeMillis());
List<Role> roles = Arrays.asList(new Role(1L, "ADMIN", "Administrator", "Admin role", 1, System.currentTimeMillis(), System.currentTimeMillis()));
Set<Permission> permissions = new HashSet<>(Arrays.asList(
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
));
doReturn(Mono.just(user)).when(responseSpec).bodyToMono(eq(User.class));
doReturn(Mono.just(roles.toArray(new Role[0]))).when(responseSpec).bodyToMono(eq(Role[].class));
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
permissionService.getUserById(1L);
permissionService.getUserRoles(1L);
permissionService.getUserPermissions(1L);
permissionService.clearAllCache();
permissionService.getUserById(1L);
permissionService.getUserRoles(1L);
permissionService.getUserPermissions(1L);
verify(webClient, times(6)).get();
}
}
@@ -0,0 +1,247 @@
package cn.novalon.gym.manage.gateway.service.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
/**
* SignatureServiceImpl单元测试
*
* 文件定义:测试签名服务的核心功能
* 涉及业务:签名生成、签名验证、时间戳验证、nonce防重放
*
* @author 张翔
* @date 2026-03-26
*/
@ExtendWith(MockitoExtension.class)
class SignatureServiceImplTest {
@InjectMocks
private SignatureServiceImpl signatureService;
private static final String TEST_SECRET = "TestSecretKey123";
private static final String TEST_METHOD = "GET";
private static final String TEST_PATH = "/api/users";
private static final String TEST_QUERY = "page=1&size=10";
private static final String TEST_BODY = "";
private static final String TEST_NONCE = "test-nonce-12345";
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(signatureService, "signatureEnabled", true);
ReflectionTestUtils.setField(signatureService, "maxAgeMinutes", 5);
ReflectionTestUtils.setField(signatureService, "nonceCacheSize", 10000);
}
@Test
void testGenerateSignature_ShouldGenerateValidSignature() {
long timestamp = System.currentTimeMillis();
String signature = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
assertNotNull(signature);
assertFalse(signature.isEmpty());
assertTrue(signature.length() > 0);
}
@Test
void testGenerateSignature_ShouldGenerateSameSignatureForSameInput() {
long timestamp = System.currentTimeMillis();
String signature1 = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
String signature2 = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
assertEquals(signature1, signature2);
}
@Test
void testGenerateSignature_ShouldGenerateDifferentSignatureForDifferentInput() {
long timestamp = System.currentTimeMillis();
String signature1 = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
String signature2 = signatureService.generateSignature(
"POST", TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
assertNotEquals(signature1, signature2);
}
@Test
void testVerifySignature_WithValidSignature_ShouldReturnTrue() {
long timestamp = System.currentTimeMillis();
String signature = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, TEST_PATH + "?" + TEST_QUERY)
.header("X-Signature", signature)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Nonce", TEST_NONCE)
.build();
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
assertTrue(isValid);
}
@Test
void testVerifySignature_WithInvalidSignature_ShouldReturnFalse() {
long timestamp = System.currentTimeMillis();
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, TEST_PATH)
.header("X-Signature", "invalid-signature")
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Nonce", TEST_NONCE)
.build();
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
assertFalse(isValid);
}
@Test
void testVerifySignature_WithMissingHeaders_ShouldReturnFalse() {
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, TEST_PATH)
.build();
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
assertFalse(isValid);
}
@Test
void testVerifySignature_WithExpiredTimestamp_ShouldReturnFalse() {
long expiredTimestamp = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10);
String signature = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, expiredTimestamp, TEST_NONCE, TEST_SECRET);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, TEST_PATH)
.header("X-Signature", signature)
.header("X-Timestamp", String.valueOf(expiredTimestamp))
.header("X-Nonce", TEST_NONCE)
.build();
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
assertFalse(isValid);
}
@Test
void testVerifySignature_WithUsedNonce_ShouldReturnFalse() {
long timestamp = System.currentTimeMillis();
String signature = signatureService.generateSignature(
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
signatureService.recordNonce(TEST_NONCE);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, TEST_PATH)
.header("X-Signature", signature)
.header("X-Timestamp", String.valueOf(timestamp))
.header("X-Nonce", TEST_NONCE)
.build();
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
assertFalse(isValid);
}
@Test
void testIsTimestampValid_WithValidTimestamp_ShouldReturnTrue() {
long validTimestamp = System.currentTimeMillis();
boolean isValid = signatureService.isTimestampValid(validTimestamp, 5);
assertTrue(isValid);
}
@Test
void testIsTimestampValid_WithExpiredTimestamp_ShouldReturnFalse() {
long expiredTimestamp = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10);
boolean isValid = signatureService.isTimestampValid(expiredTimestamp, 5);
assertFalse(isValid);
}
@Test
void testIsTimestampValid_WithFutureTimestamp_ShouldReturnFalse() {
long futureTimestamp = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10);
boolean isValid = signatureService.isTimestampValid(futureTimestamp, 5);
assertFalse(isValid);
}
@Test
void testIsNonceUsed_WithNewNonce_ShouldReturnFalse() {
boolean isUsed = signatureService.isNonceUsed("new-nonce-123");
assertFalse(isUsed);
}
@Test
void testIsNonceUsed_WithUsedNonce_ShouldReturnTrue() {
String nonce = "used-nonce-123";
signatureService.recordNonce(nonce);
boolean isUsed = signatureService.isNonceUsed(nonce);
assertTrue(isUsed);
}
@Test
void testRecordNonce_ShouldIncreaseCacheSize() {
int initialSize = signatureService.getNonceCacheSize();
signatureService.recordNonce("test-nonce-1");
signatureService.recordNonce("test-nonce-2");
signatureService.recordNonce("test-nonce-3");
int finalSize = signatureService.getNonceCacheSize();
assertEquals(initialSize + 3, finalSize);
}
@Test
void testCleanupExpiredNonces_ShouldRemoveExpiredEntries() {
ReflectionTestUtils.setField(signatureService, "nonceCacheSize", 5);
signatureService.recordNonce("nonce-1");
signatureService.recordNonce("nonce-2");
signatureService.recordNonce("nonce-3");
signatureService.recordNonce("nonce-4");
signatureService.recordNonce("nonce-5");
signatureService.recordNonce("nonce-6");
int cacheSize = signatureService.getNonceCacheSize();
assertTrue(cacheSize <= 6);
}
@Test
void testVerifySignature_WhenDisabled_ShouldReturnTrue() {
ReflectionTestUtils.setField(signatureService, "signatureEnabled", false);
MockServerHttpRequest request = MockServerHttpRequest
.method(HttpMethod.GET, TEST_PATH)
.build();
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
assertTrue(isValid);
}
}
@@ -0,0 +1,32 @@
spring:
application:
name: manage-gateway
cloud:
gateway:
routes:
- id: user-service
uri: http://localhost:8084
predicates:
- Path=/api/users/**
- id: auth-service
uri: http://localhost:8083
predicates:
- Path=/api/auth/**
user:
service:
url: http://localhost:8084
permission:
cache:
expiry:
minutes: 5
logging:
level:
cn.novalon.manage.gateway: DEBUG
org.springframework.cloud.gateway: DEBUG
org.springframework.web.reactive: DEBUG
server:
port: 8080