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