refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage
This commit is contained in:
+30
@@ -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();
|
||||
}
|
||||
}
|
||||
+207
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+244
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+227
@@ -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);
|
||||
}
|
||||
}
|
||||
+70
@@ -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);
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -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;
|
||||
}
|
||||
}
|
||||
+216
@@ -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;
|
||||
}
|
||||
}
|
||||
+14
@@ -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();
|
||||
}
|
||||
}
|
||||
+124
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
@@ -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 {
|
||||
}
|
||||
}
|
||||
+221
@@ -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;
|
||||
}
|
||||
}
|
||||
+71
@@ -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 {
|
||||
}
|
||||
}
|
||||
+125
@@ -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;
|
||||
}
|
||||
}
|
||||
+117
@@ -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;
|
||||
}
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
}
|
||||
+165
@@ -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
|
||||
}
|
||||
}
|
||||
+151
@@ -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();
|
||||
}
|
||||
}
|
||||
+112
@@ -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;
|
||||
}
|
||||
}
|
||||
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+212
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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();
|
||||
}
|
||||
+44
@@ -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();
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
+41
@@ -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();
|
||||
}
|
||||
+22
@@ -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);
|
||||
}
|
||||
+25
@@ -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();
|
||||
}
|
||||
+75
@@ -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();
|
||||
}
|
||||
+181
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
+290
@@ -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;
|
||||
}
|
||||
}
|
||||
+221
@@ -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");
|
||||
}
|
||||
}
|
||||
+182
@@ -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;
|
||||
}
|
||||
}
|
||||
+211
@@ -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();
|
||||
}
|
||||
}
|
||||
+104
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -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
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package cn.novalon.gym.manage.gateway.audit;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* AuditLogService单元测试
|
||||
*
|
||||
* 文件定义:测试审计日志服务的核心功能
|
||||
* 涉及业务:请求日志记录、响应日志记录、安全事件记录
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuditLogServiceTest {
|
||||
|
||||
private AuditLogService auditLogService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
auditLogService = new AuditLogService();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogRequest() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.header("X-Request-Id", "test-request-123")
|
||||
.header("User-Agent", "TestAgent")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 8080))
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> auditLogService.logRequest(request, "user123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogResponse() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.header("X-Request-Id", "test-request-456")
|
||||
.build();
|
||||
|
||||
auditLogService.logRequest(request, "user123");
|
||||
|
||||
assertDoesNotThrow(() -> auditLogService.logResponse("test-request-456", 200, 150));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogSecurityEvent() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/admin")
|
||||
.header("X-Request-Id", "test-request-789")
|
||||
.build();
|
||||
|
||||
auditLogService.logRequest(request, "user123");
|
||||
|
||||
assertDoesNotThrow(() ->
|
||||
auditLogService.logSecurityEvent("test-request-789", "UNAUTHORIZED_ACCESS", "User attempted to access admin resource"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogError() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.POST, "/api/data")
|
||||
.header("X-Request-Id", "test-request-error")
|
||||
.build();
|
||||
|
||||
auditLogService.logRequest(request, "user123");
|
||||
|
||||
assertDoesNotThrow(() ->
|
||||
auditLogService.logError("test-request-error", "INTERNAL_ERROR", "Database connection failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogRequestWithoutRequestId() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/test")
|
||||
.remoteAddress(new InetSocketAddress("10.0.0.1", 8080))
|
||||
.build();
|
||||
|
||||
assertDoesNotThrow(() -> auditLogService.logRequest(request, "user456"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogResponseWithNonExistentRequestId() {
|
||||
assertDoesNotThrow(() -> auditLogService.logResponse("non-existent-id", 404, 50));
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package cn.novalon.gym.manage.gateway.cache;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RequestCacheServiceTest {
|
||||
|
||||
private RequestCacheService cacheService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
cacheService = new RequestCacheService();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGet_CacheMiss() {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPutAndGet_CacheHit() {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
String response = "{\"data\":\"test\"}";
|
||||
cacheService.put(request, response);
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.expectNext(response)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEvict() {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
String response = "{\"data\":\"test\"}";
|
||||
cacheService.put(request, response);
|
||||
|
||||
cacheService.evict(request);
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEvictByPattern() {
|
||||
ServerHttpRequest request1 = MockServerHttpRequest
|
||||
.get("/api/test1")
|
||||
.build();
|
||||
ServerHttpRequest request2 = MockServerHttpRequest
|
||||
.get("/api/test2")
|
||||
.build();
|
||||
|
||||
cacheService.put(request1, "response1");
|
||||
cacheService.put(request2, "response2");
|
||||
|
||||
cacheService.evictByPattern("GET:/api/test.*");
|
||||
|
||||
assertEquals(0, cacheService.getCacheSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClear() {
|
||||
ServerHttpRequest request1 = MockServerHttpRequest
|
||||
.get("/api/test1")
|
||||
.build();
|
||||
ServerHttpRequest request2 = MockServerHttpRequest
|
||||
.get("/api/test2")
|
||||
.build();
|
||||
|
||||
cacheService.put(request1, "response1");
|
||||
cacheService.put(request2, "response2");
|
||||
|
||||
cacheService.clear();
|
||||
|
||||
assertEquals(0, cacheService.getCacheSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCacheDisabled() {
|
||||
cacheService.setCacheEnabled(false);
|
||||
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
cacheService.put(request, "response");
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(0, cacheService.getCacheSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCacheStatistics() {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
cacheService.put(request, "response");
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.expectNext("response")
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(1, cacheService.getHitCount());
|
||||
assertEquals(0, cacheService.getMissCount());
|
||||
assertEquals(1.0, cacheService.getHitRate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCacheMissStatistics() {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(0, cacheService.getHitCount());
|
||||
assertEquals(1, cacheService.getMissCount());
|
||||
assertEquals(0.0, cacheService.getHitRate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMaxCacheSize() {
|
||||
cacheService.setMaxCacheSize(5);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test" + i)
|
||||
.build();
|
||||
cacheService.put(request, "response" + i);
|
||||
}
|
||||
|
||||
assertTrue(cacheService.getCacheSize() <= 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCacheWithQueryParams() {
|
||||
ServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test?param=value")
|
||||
.build();
|
||||
|
||||
String response = "{\"data\":\"test\"}";
|
||||
cacheService.put(request, response);
|
||||
|
||||
StepVerifier.create(cacheService.get(request))
|
||||
.expectNext(response)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCacheWithDifferentMethods() {
|
||||
ServerHttpRequest getRequest = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
ServerHttpRequest postRequest = MockServerHttpRequest
|
||||
.post("/api/test")
|
||||
.build();
|
||||
|
||||
cacheService.put(getRequest, "getResponse");
|
||||
cacheService.put(postRequest, "postResponse");
|
||||
|
||||
StepVerifier.create(cacheService.get(getRequest))
|
||||
.expectNext("getResponse")
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(cacheService.get(postRequest))
|
||||
.expectNext("postResponse")
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package cn.novalon.gym.manage.gateway.config;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import io.github.resilience4j.retry.Retry;
|
||||
import io.github.resilience4j.retry.RetryRegistry;
|
||||
import io.github.resilience4j.timelimiter.TimeLimiter;
|
||||
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* ResilienceConfig单元测试
|
||||
*
|
||||
* 文件定义:测试Resilience4j配置类的核心功能
|
||||
* 涉及业务:断路器、重试、超时配置
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ResilienceConfigTest {
|
||||
|
||||
@InjectMocks
|
||||
private ResilienceConfig resilienceConfig;
|
||||
|
||||
@Test
|
||||
void testCircuitBreakerRegistry_ShouldCreateRegistry() {
|
||||
ReflectionTestUtils.setField(resilienceConfig, "circuitBreakerEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "failureRateThreshold", 50.0f);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slowCallRateThreshold", 100.0f);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slowCallDurationThreshold", java.time.Duration.ofSeconds(2));
|
||||
ReflectionTestUtils.setField(resilienceConfig, "permittedNumberOfCallsInHalfOpenState", 10);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowType", "COUNT_BASED");
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowSize", 100);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "minimumNumberOfCalls", 10);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "waitDurationInOpenState", java.time.Duration.ofSeconds(10));
|
||||
|
||||
CircuitBreakerRegistry registry = resilienceConfig.circuitBreakerRegistry();
|
||||
|
||||
assertNotNull(registry);
|
||||
assertNotNull(registry.getConfiguration("default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGatewayCircuitBreaker_ShouldCreateCircuitBreaker() {
|
||||
ReflectionTestUtils.setField(resilienceConfig, "circuitBreakerEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "failureRateThreshold", 50.0f);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slowCallRateThreshold", 100.0f);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slowCallDurationThreshold", java.time.Duration.ofSeconds(2));
|
||||
ReflectionTestUtils.setField(resilienceConfig, "permittedNumberOfCallsInHalfOpenState", 10);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowType", "COUNT_BASED");
|
||||
ReflectionTestUtils.setField(resilienceConfig, "slidingWindowSize", 100);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "minimumNumberOfCalls", 10);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "waitDurationInOpenState", java.time.Duration.ofSeconds(10));
|
||||
|
||||
CircuitBreakerRegistry registry = resilienceConfig.circuitBreakerRegistry();
|
||||
CircuitBreaker circuitBreaker = resilienceConfig.gatewayCircuitBreaker(registry);
|
||||
|
||||
assertNotNull(circuitBreaker);
|
||||
assertEquals("gateway", circuitBreaker.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetryRegistry_ShouldCreateRegistry() {
|
||||
ReflectionTestUtils.setField(resilienceConfig, "retryEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "retryMaxAttempts", 3);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "retryWaitDuration", java.time.Duration.ofMillis(500));
|
||||
|
||||
RetryRegistry registry = resilienceConfig.retryRegistry();
|
||||
|
||||
assertNotNull(registry);
|
||||
assertNotNull(registry.getConfiguration("default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGatewayRetry_ShouldCreateRetry() {
|
||||
ReflectionTestUtils.setField(resilienceConfig, "retryEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "retryMaxAttempts", 3);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "retryWaitDuration", java.time.Duration.ofMillis(500));
|
||||
|
||||
RetryRegistry registry = resilienceConfig.retryRegistry();
|
||||
Retry retry = resilienceConfig.gatewayRetry(registry);
|
||||
|
||||
assertNotNull(retry);
|
||||
assertEquals("gateway", retry.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimeLimiterRegistry_ShouldCreateRegistry() {
|
||||
ReflectionTestUtils.setField(resilienceConfig, "timeoutEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "timeoutDuration", java.time.Duration.ofSeconds(3));
|
||||
|
||||
TimeLimiterRegistry registry = resilienceConfig.timeLimiterRegistry();
|
||||
|
||||
assertNotNull(registry);
|
||||
assertNotNull(registry.getConfiguration("default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGatewayTimeLimiter_ShouldCreateTimeLimiter() {
|
||||
ReflectionTestUtils.setField(resilienceConfig, "timeoutEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceConfig, "timeoutDuration", java.time.Duration.ofSeconds(3));
|
||||
|
||||
TimeLimiterRegistry registry = resilienceConfig.timeLimiterRegistry();
|
||||
TimeLimiter timeLimiter = resilienceConfig.gatewayTimeLimiter(registry);
|
||||
|
||||
assertNotNull(timeLimiter);
|
||||
assertEquals("gateway", timeLimiter.getName());
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package cn.novalon.gym.manage.gateway.filter;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class CompressionFilterTest {
|
||||
|
||||
private CompressionFilter compressionFilter;
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
compressionFilter = new CompressionFilter();
|
||||
compressionFilter.setCompressionEnabled(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithGzipSupport() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("Accept-Encoding", "gzip, deflate")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
compressionFilter.filter(exchange, chain).block();
|
||||
|
||||
assertEquals("gzip", exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
|
||||
verify(chain).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithDeflateSupport() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("Accept-Encoding", "deflate")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
compressionFilter.filter(exchange, chain).block();
|
||||
|
||||
assertEquals("deflate", exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
|
||||
verify(chain).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_NoAcceptEncoding() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
compressionFilter.filter(exchange, chain).block();
|
||||
|
||||
assertNull(exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
|
||||
verify(chain).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_CompressionDisabled() {
|
||||
compressionFilter.setCompressionEnabled(false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
compressionFilter.filter(exchange, chain).block();
|
||||
|
||||
assertNull(exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
|
||||
verify(chain).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_OptionsRequest() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.OPTIONS, "/api/test")
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
compressionFilter.filter(exchange, chain).block();
|
||||
|
||||
assertNull(exchange.getResponse().getHeaders().getFirst("Content-Encoding"));
|
||||
verify(chain).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_VaryHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
compressionFilter.filter(exchange, chain).block();
|
||||
|
||||
assertTrue(exchange.getResponse().getHeaders().get("Vary").contains("Accept-Encoding"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOrder() {
|
||||
assertEquals(Integer.MAX_VALUE - 100, compressionFilter.getOrder());
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
package cn.novalon.gym.manage.gateway.filter;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.util.JwtUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.mockito.ArgumentCaptor.forClass;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GatewayJwtAuthenticationFilterTest {
|
||||
|
||||
@Mock
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
private JwtAuthenticationFilter filter;
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new JwtAuthenticationFilter(jwtUtil);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_AllowAccess() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_Register() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_ActuatorHealth() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/health").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_ActuatorInfo() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/info").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_NoAuthHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_InvalidAuthHeader() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "InvalidToken")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_WithBearerPrefix() {
|
||||
String validToken = "valid.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(validToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(jwtUtil).validateToken(validToken);
|
||||
verify(jwtUtil).isTokenExpired(validToken);
|
||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_InvalidToken() {
|
||||
String invalidToken = "invalid.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(jwtUtil.validateToken(invalidToken)).thenReturn(false);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
verify(jwtUtil).validateToken(invalidToken);
|
||||
verify(jwtUtil, never()).isTokenExpired(anyString());
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_ExpiredToken() {
|
||||
String expiredToken = "expired.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + expiredToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(jwtUtil.validateToken(expiredToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(expiredToken)).thenReturn(true);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
verify(jwtUtil).validateToken(expiredToken);
|
||||
verify(jwtUtil).isTokenExpired(expiredToken);
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_ValidToken() {
|
||||
String validToken = "valid.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users/1")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(validToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(jwtUtil).validateToken(validToken);
|
||||
verify(jwtUtil).isTokenExpired(validToken);
|
||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHeadersAdded_ValidToken() {
|
||||
String validToken = "valid.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(validToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
var exchangeCaptor = forClass(ServerWebExchange.class);
|
||||
verify(chain).filter(exchangeCaptor.capture());
|
||||
ServerHttpRequest modifiedRequest = exchangeCaptor.getValue().getRequest();
|
||||
assert modifiedRequest.getHeaders().getFirst("X-User-Id").equals("1");
|
||||
assert modifiedRequest.getHeaders().getFirst("X-Username").equals("testuser");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedPath_AuthPath() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/logout").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verify(jwtUtil, never()).validateToken(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActuatorPath_Metrics() {
|
||||
String validToken = "valid.jwt.token";
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/metrics")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
when(jwtUtil.validateToken(validToken)).thenReturn(true);
|
||||
when(jwtUtil.isTokenExpired(validToken)).thenReturn(false);
|
||||
when(jwtUtil.getUsernameFromToken(validToken)).thenReturn("testuser");
|
||||
when(jwtUtil.getUserIdFromToken(validToken)).thenReturn(1L);
|
||||
|
||||
Mono<Void> result = filter.apply(new JwtAuthenticationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(jwtUtil).validateToken(validToken);
|
||||
verify(jwtUtil).isTokenExpired(validToken);
|
||||
verify(jwtUtil).getUsernameFromToken(validToken);
|
||||
verify(jwtUtil).getUserIdFromToken(validToken);
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
package cn.novalon.gym.manage.gateway.filter;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.config.RateLimitConfig;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RateLimitFilterTest {
|
||||
|
||||
@Mock
|
||||
private RateLimiter globalRateLimiter;
|
||||
|
||||
@Mock
|
||||
private RateLimiter ipRateLimiter;
|
||||
|
||||
@Mock
|
||||
private RateLimiter userRateLimiter;
|
||||
|
||||
@Mock
|
||||
private RateLimitConfig rateLimitConfig;
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
private RateLimitFilter rateLimitFilter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(rateLimitConfig.isRateLimitEnabled()).thenReturn(true);
|
||||
|
||||
RateLimiterConfig config = RateLimiterConfig.custom()
|
||||
.limitForPeriod(100)
|
||||
.limitRefreshPeriod(Duration.ofSeconds(1))
|
||||
.timeoutDuration(Duration.ZERO)
|
||||
.build();
|
||||
|
||||
lenient().when(globalRateLimiter.getRateLimiterConfig()).thenReturn(config);
|
||||
lenient().when(ipRateLimiter.getRateLimiterConfig()).thenReturn(config);
|
||||
lenient().when(userRateLimiter.getRateLimiterConfig()).thenReturn(config);
|
||||
|
||||
rateLimitFilter = new RateLimitFilter(
|
||||
globalRateLimiter,
|
||||
ipRateLimiter,
|
||||
userRateLimiter,
|
||||
rateLimitConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenRateLimitDisabled_ShouldPassThrough() {
|
||||
when(rateLimitConfig.isRateLimitEnabled()).thenReturn(false);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(globalRateLimiter, never()).acquirePermission();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenGlobalRateLimitExceeded_ShouldReturn429() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(HttpStatus.TOO_MANY_REQUESTS, exchange.getResponse().getStatusCode());
|
||||
verify(chain, never()).filter(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenAllRateLimitsPass_ShouldContinueChain() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("X-User-Id", "user123")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(globalRateLimiter).acquirePermission();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WithoutUserId_ShouldSkipUserRateLimit() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientIp_FromXForwardedFor() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("X-Forwarded-For", "10.0.0.1, 192.168.1.1")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientIp_FromXRealIP() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.header("X-Real-IP", "10.0.0.2")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientIp_FromRemoteAddress() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.100", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRateLimitHeaders_WhenExceeded() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertTrue(headers.containsKey("X-RateLimit-Limit"));
|
||||
assertTrue(headers.containsKey("X-RateLimit-Remaining"));
|
||||
assertTrue(headers.containsKey("Retry-After"));
|
||||
assertTrue(headers.containsKey("X-RateLimit-Type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCounters_WhenRequestsProcessed() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(1, rateLimitFilter.getTotalRequests());
|
||||
assertEquals(0, rateLimitFilter.getBlockedRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCounters_WhenRequestsBlocked() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(1, rateLimitFilter.getTotalRequests());
|
||||
assertEquals(1, rateLimitFilter.getBlockedRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResetCounters() {
|
||||
when(globalRateLimiter.acquirePermission()).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("/api/test")
|
||||
.remoteAddress(new InetSocketAddress("192.168.1.1", 12345))
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
StepVerifier.create(rateLimitFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(1, rateLimitFilter.getTotalRequests());
|
||||
|
||||
rateLimitFilter.resetCounters();
|
||||
|
||||
assertEquals(0, rateLimitFilter.getTotalRequests());
|
||||
assertEquals(0, rateLimitFilter.getBlockedRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOrder() {
|
||||
int order = rateLimitFilter.getOrder();
|
||||
assertEquals(Ordered.HIGHEST_PRECEDENCE + 100, order);
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package cn.novalon.gym.manage.gateway.filter;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.service.PermissionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RbacAuthorizationFilterTest {
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
@Mock
|
||||
private PermissionService permissionService;
|
||||
|
||||
private RbacAuthorizationFilter filter;
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new RbacAuthorizationFilter(permissionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_AllowAccess() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_Register() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_ActuatorHealth() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/health").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPublicPath_ActuatorInfo() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/info").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_NoUserId() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_WithUserId() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-User-Id", "1")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(permissionService.hasPermission(eq(1L), eq("/api/users"), eq("GET"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_PostMethod() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/api/users")
|
||||
.header("X-User-Id", "1")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(permissionService.hasPermission(eq(1L), eq("/api/users"), eq("POST"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_PutMethod() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.put("/api/users/1")
|
||||
.header("X-User-Id", "1")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(permissionService.hasPermission(eq(1L), eq("/api/users/1"), eq("PUT"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_DeleteMethod() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.delete("/api/users/1")
|
||||
.header("X-User-Id", "1")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(permissionService.hasPermission(eq(1L), eq("/api/users/1"), eq("DELETE"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProtectedPath_EmptyUserId() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-User-Id", "")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
verify(chain, never()).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedPath_AuthPath() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/logout").build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedPath_UserPath() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users/profile")
|
||||
.header("X-User-Id", "1")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(permissionService.hasPermission(eq(1L), eq("/api/users/profile"), eq("GET"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActuatorPath_Metrics() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/metrics")
|
||||
.header("X-User-Id", "1")
|
||||
.build();
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(permissionService.hasPermission(eq(1L), eq("/actuator/metrics"), eq("GET"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package cn.novalon.gym.manage.gateway.filter;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
import io.github.resilience4j.retry.Retry;
|
||||
import io.github.resilience4j.retry.RetryConfig;
|
||||
import io.github.resilience4j.timelimiter.TimeLimiter;
|
||||
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* ResilienceFilter单元测试
|
||||
*
|
||||
* 文件定义:测试容错过滤器的核心功能
|
||||
* 涉及业务:断路器、重试、超时、降级
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ResilienceFilterTest {
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
private CircuitBreaker circuitBreaker;
|
||||
private Retry retry;
|
||||
private TimeLimiter timeLimiter;
|
||||
private ResilienceFilter resilienceFilter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
|
||||
.failureRateThreshold(50)
|
||||
.slidingWindowSize(100)
|
||||
.minimumNumberOfCalls(10)
|
||||
.waitDurationInOpenState(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
RetryConfig retryConfig = RetryConfig.custom()
|
||||
.maxAttempts(3)
|
||||
.waitDuration(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
TimeLimiterConfig tlConfig = TimeLimiterConfig.custom()
|
||||
.timeoutDuration(Duration.ofSeconds(3))
|
||||
.build();
|
||||
|
||||
circuitBreaker = CircuitBreaker.of("gateway", cbConfig);
|
||||
retry = Retry.of("gateway", retryConfig);
|
||||
timeLimiter = TimeLimiter.of("gateway", tlConfig);
|
||||
|
||||
resilienceFilter = new ResilienceFilter(circuitBreaker, retry, timeLimiter);
|
||||
|
||||
ReflectionTestUtils.setField(resilienceFilter, "resilienceEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceFilter, "circuitBreakerEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceFilter, "retryEnabled", true);
|
||||
ReflectionTestUtils.setField(resilienceFilter, "timeoutEnabled", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenResilienceDisabled_ShouldContinueChain() {
|
||||
ReflectionTestUtils.setField(resilienceFilter, "resilienceEnabled", false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(resilienceFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenAllPatternsEnabled_ShouldApplyResilience() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(resilienceFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenCircuitBreakerDisabled_ShouldSkipCircuitBreaker() {
|
||||
ReflectionTestUtils.setField(resilienceFilter, "circuitBreakerEnabled", false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(resilienceFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenRetryDisabled_ShouldSkipRetry() {
|
||||
ReflectionTestUtils.setField(resilienceFilter, "retryEnabled", false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(resilienceFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenTimeoutDisabled_ShouldSkipTimeout() {
|
||||
ReflectionTestUtils.setField(resilienceFilter, "timeoutEnabled", false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(resilienceFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenChainThrowsException_ShouldHandleFallback() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.error(new RuntimeException("Test error")));
|
||||
|
||||
StepVerifier.create(resilienceFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOrder_ShouldReturnCorrectOrder() {
|
||||
int order = resilienceFilter.getOrder();
|
||||
|
||||
assertEquals(-2147483448, order);
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package cn.novalon.gym.manage.gateway.filter;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.service.SignatureService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* SignatureFilter单元测试
|
||||
*
|
||||
* 文件定义:测试签名验证过滤器的核心功能
|
||||
* 涉及业务:签名验证、白名单过滤、错误响应
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SignatureFilterTest {
|
||||
|
||||
@Mock
|
||||
private SignatureService signatureService;
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
@InjectMocks
|
||||
private SignatureFilter signatureFilter;
|
||||
|
||||
private static final String TEST_SECRET = "TestSecretKey123";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(signatureFilter, "signatureEnabled", true);
|
||||
ReflectionTestUtils.setField(signatureFilter, "signatureSecret", TEST_SECRET);
|
||||
ReflectionTestUtils.setField(signatureFilter, "whitelistPaths", "/actuator/health,/actuator/info");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenSignatureDisabled_ShouldContinueChain() {
|
||||
ReflectionTestUtils.setField(signatureFilter, "signatureEnabled", false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(signatureService, never()).verifySignature(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenPathIsWhitelisted_ShouldContinueChain() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/actuator/health")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(signatureService, never()).verifySignature(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenSignatureValid_ShouldContinueChain() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.header("X-Signature", "valid-signature")
|
||||
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()))
|
||||
.header("X-Nonce", "test-nonce")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(signatureService.verifySignature(any(), any())).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(signatureService).verifySignature(request, TEST_SECRET);
|
||||
verify(chain).filter(exchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenSignatureInvalid_ShouldReturnUnauthorized() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.header("X-Signature", "invalid-signature")
|
||||
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()))
|
||||
.header("X-Nonce", "test-nonce")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(signatureService.verifySignature(any(), any())).thenReturn(false);
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(signatureService).verifySignature(request, TEST_SECRET);
|
||||
verify(chain, never()).filter(any());
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenMissingSignatureHeaders_ShouldReturnUnauthorized() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(signatureService.verifySignature(any(), any())).thenReturn(false);
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(signatureService).verifySignature(request, TEST_SECRET);
|
||||
verify(chain, never()).filter(any());
|
||||
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenMultipleWhitelistPaths_ShouldMatchAny() {
|
||||
MockServerHttpRequest request1 = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/actuator/health")
|
||||
.build();
|
||||
|
||||
MockServerHttpRequest request2 = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/actuator/info")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange1 = MockServerWebExchange.builder(request1).build();
|
||||
MockServerWebExchange exchange2 = MockServerWebExchange.builder(request2).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange1, chain))
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange2, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain, times(2)).filter(any());
|
||||
verify(signatureService, never()).verifySignature(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenPathStartsWithWhitelist_ShouldMatch() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/actuator/health/details")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verify(signatureService, never()).verifySignature(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetOrder_ShouldReturnCorrectOrder() {
|
||||
int order = signatureFilter.getOrder();
|
||||
|
||||
assertEquals(-2147483498, order);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_WhenSignatureEnabled_ShouldVerifySignature() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, "/api/users")
|
||||
.header("X-Signature", "test-signature")
|
||||
.header("X-Timestamp", String.valueOf(System.currentTimeMillis()))
|
||||
.header("X-Nonce", "test-nonce")
|
||||
.build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.builder(request).build();
|
||||
|
||||
when(signatureService.verifySignature(any(), any())).thenReturn(true);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(signatureFilter.filter(exchange, chain))
|
||||
.verifyComplete();
|
||||
|
||||
verify(signatureService).verifySignature(request, TEST_SECRET);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package cn.novalon.gym.manage.gateway.health;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.Status;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* GatewayHealthIndicator单元测试
|
||||
*
|
||||
* 文件定义:测试网关健康检查指示器的核心功能
|
||||
* 涉及业务:断路器健康检查、限流器健康检查
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GatewayHealthIndicatorTest {
|
||||
|
||||
private CircuitBreakerRegistry circuitBreakerRegistry;
|
||||
private RateLimiterRegistry rateLimiterRegistry;
|
||||
private GatewayHealthIndicator healthIndicator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
|
||||
.failureRateThreshold(50)
|
||||
.slidingWindowSize(100)
|
||||
.minimumNumberOfCalls(10)
|
||||
.waitDurationInOpenState(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
RateLimiterConfig rlConfig = RateLimiterConfig.custom()
|
||||
.limitForPeriod(100)
|
||||
.limitRefreshPeriod(Duration.ofSeconds(1))
|
||||
.build();
|
||||
|
||||
circuitBreakerRegistry = CircuitBreakerRegistry.of(cbConfig);
|
||||
rateLimiterRegistry = RateLimiterRegistry.of(rlConfig);
|
||||
|
||||
healthIndicator = new GatewayHealthIndicator(circuitBreakerRegistry, rateLimiterRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHealth_WhenAllComponentsHealthy_ShouldReturnUp() {
|
||||
circuitBreakerRegistry.circuitBreaker("test-cb");
|
||||
rateLimiterRegistry.rateLimiter("test-rl");
|
||||
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
assertTrue(health.getDetails().containsKey("circuitBreakers"));
|
||||
assertTrue(health.getDetails().containsKey("rateLimiters"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHealth_WhenNoComponents_ShouldReturnUp() {
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
assertEquals(Status.UP, health.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHealth_ShouldIncludeComponentDetails() {
|
||||
circuitBreakerRegistry.circuitBreaker("gateway");
|
||||
rateLimiterRegistry.rateLimiter("gateway");
|
||||
|
||||
Health health = healthIndicator.health();
|
||||
|
||||
assertTrue(health.getDetails().containsKey("circuitBreakers"));
|
||||
assertTrue(health.getDetails().containsKey("rateLimiters"));
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package cn.novalon.gym.manage.gateway.integration;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.filter.RbacAuthorizationFilter;
|
||||
import cn.novalon.gym.manage.gateway.service.PermissionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RbacIntegrationTest {
|
||||
|
||||
@Mock
|
||||
private PermissionService permissionService;
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
private RbacAuthorizationFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter = new RbacAuthorizationFilter(permissionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndToEnd_AdminUserFullAccess() {
|
||||
Long adminUserId = 1L;
|
||||
String adminPath = "/api/admin/users";
|
||||
String adminMethod = "GET";
|
||||
|
||||
when(permissionService.hasPermission(eq(adminUserId), eq(adminPath), eq(adminMethod))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get(adminPath)
|
||||
.header("X-User-Id", adminUserId.toString())
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == null || exchange.getResponse().getStatusCode() == HttpStatus.OK;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndToEnd_RegularUserLimitedAccess() {
|
||||
Long regularUserId = 2L;
|
||||
String adminPath = "/api/admin/users";
|
||||
String userPath = "/api/users/profile";
|
||||
|
||||
when(permissionService.hasPermission(eq(regularUserId), eq(adminPath), eq("GET"))).thenReturn(false);
|
||||
when(permissionService.hasPermission(eq(regularUserId), eq(userPath), eq("GET"))).thenReturn(true);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest adminRequest = MockServerHttpRequest.get(adminPath)
|
||||
.header("X-User-Id", regularUserId.toString())
|
||||
.build();
|
||||
ServerWebExchange adminExchange = MockServerWebExchange.from(adminRequest);
|
||||
|
||||
Mono<Void> adminResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(adminExchange, chain);
|
||||
|
||||
StepVerifier.create(adminResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert adminExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||
|
||||
MockServerHttpRequest userRequest = MockServerHttpRequest.get(userPath)
|
||||
.header("X-User-Id", regularUserId.toString())
|
||||
.build();
|
||||
ServerWebExchange userExchange = MockServerWebExchange.from(userRequest);
|
||||
|
||||
Mono<Void> userResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(userExchange, chain);
|
||||
|
||||
StepVerifier.create(userResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert userExchange.getResponse().getStatusCode() == null || userExchange.getResponse().getStatusCode() == HttpStatus.OK;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndToEnd_MultipleHttpMethods() {
|
||||
Long userId = 3L;
|
||||
String basePath = "/api/users";
|
||||
|
||||
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("GET"))).thenReturn(true);
|
||||
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("POST"))).thenReturn(true);
|
||||
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("PUT"))).thenReturn(false);
|
||||
when(permissionService.hasPermission(eq(userId), eq(basePath), eq("DELETE"))).thenReturn(false);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
MockServerHttpRequest getRequest = MockServerHttpRequest.get(basePath)
|
||||
.header("X-User-Id", userId.toString())
|
||||
.build();
|
||||
ServerWebExchange getExchange = MockServerWebExchange.from(getRequest);
|
||||
|
||||
Mono<Void> getResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(getExchange, chain);
|
||||
|
||||
StepVerifier.create(getResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert getExchange.getResponse().getStatusCode() == null || getExchange.getResponse().getStatusCode() == HttpStatus.OK;
|
||||
|
||||
MockServerHttpRequest postRequest = MockServerHttpRequest.post(basePath)
|
||||
.header("X-User-Id", userId.toString())
|
||||
.build();
|
||||
ServerWebExchange postExchange = MockServerWebExchange.from(postRequest);
|
||||
|
||||
Mono<Void> postResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(postExchange, chain);
|
||||
|
||||
StepVerifier.create(postResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert postExchange.getResponse().getStatusCode() == null || postExchange.getResponse().getStatusCode() == HttpStatus.OK;
|
||||
|
||||
MockServerHttpRequest putRequest = MockServerHttpRequest.put(basePath)
|
||||
.header("X-User-Id", userId.toString())
|
||||
.build();
|
||||
ServerWebExchange putExchange = MockServerWebExchange.from(putRequest);
|
||||
|
||||
Mono<Void> putResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(putExchange, chain);
|
||||
|
||||
StepVerifier.create(putResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert putExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||
|
||||
MockServerHttpRequest deleteRequest = MockServerHttpRequest.delete(basePath)
|
||||
.header("X-User-Id", userId.toString())
|
||||
.build();
|
||||
ServerWebExchange deleteExchange = MockServerWebExchange.from(deleteRequest);
|
||||
|
||||
Mono<Void> deleteResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(deleteExchange, chain);
|
||||
|
||||
StepVerifier.create(deleteResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert deleteExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndToEnd_PathMatchingScenarios() {
|
||||
Long userId = 4L;
|
||||
|
||||
when(permissionService.hasPermission(eq(userId), eq("/api/users"), eq("GET"))).thenReturn(true);
|
||||
when(permissionService.hasPermission(eq(userId), eq("/api/users/123"), eq("GET"))).thenReturn(true);
|
||||
when(permissionService.hasPermission(eq(userId), eq("/api/users/123/profile"), eq("GET"))).thenReturn(true);
|
||||
when(permissionService.hasPermission(eq(userId), eq("/api/admin"), eq("GET"))).thenReturn(false);
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
String[] allowedPaths = {"/api/users", "/api/users/123", "/api/users/123/profile"};
|
||||
for (String path : allowedPaths) {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get(path)
|
||||
.header("X-User-Id", userId.toString())
|
||||
.build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == null || exchange.getResponse().getStatusCode() == HttpStatus.OK;
|
||||
}
|
||||
|
||||
MockServerHttpRequest adminRequest = MockServerHttpRequest.get("/api/admin")
|
||||
.header("X-User-Id", userId.toString())
|
||||
.build();
|
||||
ServerWebExchange adminExchange = MockServerWebExchange.from(adminRequest);
|
||||
|
||||
Mono<Void> adminResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(adminExchange, chain);
|
||||
|
||||
StepVerifier.create(adminResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert adminExchange.getResponse().getStatusCode() == HttpStatus.FORBIDDEN;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndToEnd_PublicPathsBypass() {
|
||||
String[] publicPaths = {
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/actuator/health",
|
||||
"/actuator/info"
|
||||
};
|
||||
|
||||
for (String path : publicPaths) {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get(path).build();
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
assert exchange.getResponse().getStatusCode() == null || exchange.getResponse().getStatusCode() == HttpStatus.OK;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEndToEnd_ErrorScenarios() {
|
||||
MockServerHttpRequest noHeaderRequest = MockServerHttpRequest.get("/api/users").build();
|
||||
ServerWebExchange noHeaderExchange = MockServerWebExchange.from(noHeaderRequest);
|
||||
|
||||
Mono<Void> noHeaderResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(noHeaderExchange, chain);
|
||||
|
||||
StepVerifier.create(noHeaderResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert noHeaderExchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
|
||||
MockServerHttpRequest invalidIdRequest = MockServerHttpRequest.get("/api/users")
|
||||
.header("X-User-Id", "invalid")
|
||||
.build();
|
||||
ServerWebExchange invalidIdExchange = MockServerWebExchange.from(invalidIdRequest);
|
||||
|
||||
Mono<Void> invalidIdResult = filter.apply(new RbacAuthorizationFilter.Config())
|
||||
.filter(invalidIdExchange, chain);
|
||||
|
||||
StepVerifier.create(invalidIdResult)
|
||||
.verifyComplete();
|
||||
|
||||
assert invalidIdExchange.getResponse().getStatusCode() == HttpStatus.UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package cn.novalon.gym.manage.gateway.loadbalancer;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* CustomLoadBalancer单元测试
|
||||
*
|
||||
* 文件定义:测试自定义负载均衡器的核心功能
|
||||
* 涉及业务:轮询、随机、加权轮询、最少连接策略
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
class CustomLoadBalancerTest {
|
||||
|
||||
private CustomLoadBalancer loadBalancer;
|
||||
private List<ServiceInstance> instances;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
loadBalancer = new CustomLoadBalancer();
|
||||
|
||||
instances = Arrays.asList(
|
||||
createInstance("host1", 8080),
|
||||
createInstance("host2", 8080),
|
||||
createInstance("host3", 8080)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectByRoundRobin() {
|
||||
ServiceInstance instance1 = loadBalancer.selectInstance(
|
||||
instances,
|
||||
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
|
||||
|
||||
ServiceInstance instance2 = loadBalancer.selectInstance(
|
||||
instances,
|
||||
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
|
||||
|
||||
assertNotNull(instance1);
|
||||
assertNotNull(instance2);
|
||||
assertNotSame(instance1, instance2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectByRandom() {
|
||||
ServiceInstance instance = loadBalancer.selectInstance(
|
||||
instances,
|
||||
CustomLoadBalancer.LoadBalanceStrategy.RANDOM);
|
||||
|
||||
assertNotNull(instance);
|
||||
assertTrue(instances.contains(instance));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectByWeightedRoundRobin() {
|
||||
ServiceInstance instance = loadBalancer.selectInstance(
|
||||
instances,
|
||||
CustomLoadBalancer.LoadBalanceStrategy.WEIGHTED_ROUND_ROBIN);
|
||||
|
||||
assertNotNull(instance);
|
||||
assertTrue(instances.contains(instance));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectByLeastConnections() {
|
||||
ServiceInstance instance = loadBalancer.selectInstance(
|
||||
instances,
|
||||
CustomLoadBalancer.LoadBalanceStrategy.LEAST_CONNECTIONS);
|
||||
|
||||
assertNotNull(instance);
|
||||
assertTrue(instances.contains(instance));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectInstance_EmptyList() {
|
||||
ServiceInstance instance = loadBalancer.selectInstance(
|
||||
Collections.emptyList(),
|
||||
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
|
||||
|
||||
assertNull(instance);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectInstance_NullList() {
|
||||
ServiceInstance instance = loadBalancer.selectInstance(
|
||||
null,
|
||||
CustomLoadBalancer.LoadBalanceStrategy.ROUND_ROBIN);
|
||||
|
||||
assertNull(instance);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetWeight() {
|
||||
ServiceInstance instance = instances.get(0);
|
||||
|
||||
loadBalancer.setWeight(instance, 5);
|
||||
|
||||
assertNotNull(instance);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIncrementConnection() {
|
||||
ServiceInstance instance = instances.get(0);
|
||||
|
||||
loadBalancer.incrementConnection(instance);
|
||||
loadBalancer.incrementConnection(instance);
|
||||
|
||||
assertNotNull(instance);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecrementConnection() {
|
||||
ServiceInstance instance = instances.get(0);
|
||||
|
||||
loadBalancer.incrementConnection(instance);
|
||||
loadBalancer.incrementConnection(instance);
|
||||
loadBalancer.decrementConnection(instance);
|
||||
|
||||
assertNotNull(instance);
|
||||
}
|
||||
|
||||
private ServiceInstance createInstance(String host, int port) {
|
||||
return new DefaultServiceInstance(
|
||||
"service-" + host + "-" + port,
|
||||
"test-service",
|
||||
host,
|
||||
port,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package cn.novalon.gym.manage.gateway.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* GatewayMetrics单元测试
|
||||
*
|
||||
* 文件定义:测试网关指标收集器的核心功能
|
||||
* 涉及业务:请求统计、性能监控、活跃连接数统计
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
class GatewayMetricsTest {
|
||||
|
||||
private MeterRegistry meterRegistry;
|
||||
private GatewayMetrics gatewayMetrics;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
meterRegistry = new SimpleMeterRegistry();
|
||||
gatewayMetrics = new GatewayMetrics(meterRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIncrementTotalRequests() {
|
||||
gatewayMetrics.incrementTotalRequests();
|
||||
|
||||
assertEquals(1, gatewayMetrics.getTotalRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIncrementSuccessRequests() {
|
||||
gatewayMetrics.incrementSuccessRequests();
|
||||
|
||||
assertEquals(1, gatewayMetrics.getSuccessRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIncrementFailedRequests() {
|
||||
gatewayMetrics.incrementFailedRequests();
|
||||
|
||||
assertEquals(1, gatewayMetrics.getFailedRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIncrementActiveConnections() {
|
||||
gatewayMetrics.incrementActiveConnections();
|
||||
|
||||
assertEquals(1, gatewayMetrics.getActiveConnections());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecrementActiveConnections() {
|
||||
gatewayMetrics.incrementActiveConnections();
|
||||
gatewayMetrics.incrementActiveConnections();
|
||||
gatewayMetrics.decrementActiveConnections();
|
||||
|
||||
assertEquals(1, gatewayMetrics.getActiveConnections());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordRequestDuration() {
|
||||
gatewayMetrics.recordRequestDuration("/api/users", Duration.ofMillis(100));
|
||||
|
||||
assertNotNull(meterRegistry.find("gateway.request.duration").timer());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleIncrements() {
|
||||
gatewayMetrics.incrementTotalRequests();
|
||||
gatewayMetrics.incrementTotalRequests();
|
||||
gatewayMetrics.incrementTotalRequests();
|
||||
|
||||
assertEquals(3, gatewayMetrics.getTotalRequests());
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package cn.novalon.gym.manage.gateway.monitor;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PerformanceMonitorTest {
|
||||
|
||||
private PerformanceMonitor performanceMonitor;
|
||||
private MeterRegistry meterRegistry;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
meterRegistry = new SimpleMeterRegistry();
|
||||
performanceMonitor = new PerformanceMonitor(meterRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordRequest() {
|
||||
performanceMonitor.recordRequest("/api/test", 100);
|
||||
|
||||
assertEquals(1, performanceMonitor.getPathStats().size());
|
||||
assertTrue(performanceMonitor.getAverageProcessingTime() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSlowRequestDetection() {
|
||||
performanceMonitor.setSlowRequestThresholdMs(50);
|
||||
performanceMonitor.recordRequest("/api/test", 100);
|
||||
|
||||
assertEquals(1, performanceMonitor.getPathStats().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleRequests() {
|
||||
performanceMonitor.recordRequest("/api/test1", 100);
|
||||
performanceMonitor.recordRequest("/api/test2", 200);
|
||||
performanceMonitor.recordRequest("/api/test1", 150);
|
||||
|
||||
Map<String, PerformanceMonitor.PerformanceStats> stats = performanceMonitor.getPathStats();
|
||||
|
||||
assertEquals(2, stats.size());
|
||||
|
||||
PerformanceMonitor.PerformanceStats test1Stats = stats.get("/api/test1");
|
||||
assertNotNull(test1Stats);
|
||||
assertEquals(2, test1Stats.getRequestCount());
|
||||
assertEquals(125.0, test1Stats.getAverageTime());
|
||||
assertEquals(150, test1Stats.getMaxTime());
|
||||
assertEquals(100, test1Stats.getMinTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMemoryStats() {
|
||||
Map<String, Object> memoryStats = performanceMonitor.getMemoryStats();
|
||||
|
||||
assertNotNull(memoryStats);
|
||||
assertTrue(memoryStats.containsKey("totalMemory"));
|
||||
assertTrue(memoryStats.containsKey("freeMemory"));
|
||||
assertTrue(memoryStats.containsKey("usedMemory"));
|
||||
assertTrue(memoryStats.containsKey("maxMemory"));
|
||||
assertTrue(memoryStats.containsKey("memoryUsage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThreadStats() {
|
||||
Map<String, Object> threadStats = performanceMonitor.getThreadStats();
|
||||
|
||||
assertNotNull(threadStats);
|
||||
assertTrue(threadStats.containsKey("threadCount"));
|
||||
assertTrue(threadStats.containsKey("peakThreadCount"));
|
||||
assertTrue(threadStats.containsKey("daemonThreadCount"));
|
||||
assertTrue(threadStats.containsKey("totalStartedThreadCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMemoryUsage() {
|
||||
double memoryUsage = performanceMonitor.getMemoryUsage();
|
||||
|
||||
assertTrue(memoryUsage >= 0.0);
|
||||
assertTrue(memoryUsage <= 1.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAverageProcessingTime_NoRequests() {
|
||||
assertEquals(0.0, performanceMonitor.getAverageProcessingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAverageProcessingTime_WithRequests() {
|
||||
performanceMonitor.recordRequest("/api/test1", 100);
|
||||
performanceMonitor.recordRequest("/api/test2", 200);
|
||||
|
||||
assertEquals(150.0, performanceMonitor.getAverageProcessingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClearStats() {
|
||||
performanceMonitor.recordRequest("/api/test", 100);
|
||||
performanceMonitor.clearStats();
|
||||
|
||||
assertEquals(0, performanceMonitor.getPathStats().size());
|
||||
assertEquals(0.0, performanceMonitor.getAverageProcessingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetSlowRequestThreshold() {
|
||||
performanceMonitor.setSlowRequestThresholdMs(500);
|
||||
performanceMonitor.recordRequest("/api/test", 600);
|
||||
|
||||
assertEquals(1, performanceMonitor.getPathStats().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetMemoryWarningThreshold() {
|
||||
performanceMonitor.setMemoryWarningThreshold(0.9);
|
||||
performanceMonitor.recordRequest("/api/test", 100);
|
||||
|
||||
assertEquals(1, performanceMonitor.getPathStats().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPerformanceStats() {
|
||||
PerformanceMonitor.PerformanceStats stats = new PerformanceMonitor.PerformanceStats();
|
||||
|
||||
stats.recordRequest(100);
|
||||
stats.recordRequest(200);
|
||||
stats.recordRequest(150);
|
||||
|
||||
assertEquals(3, stats.getRequestCount());
|
||||
assertEquals(150.0, stats.getAverageTime());
|
||||
assertEquals(200, stats.getMaxTime());
|
||||
assertEquals(100, stats.getMinTime());
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package cn.novalon.gym.manage.gateway.route;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.service.impl.DynamicRouteService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* DynamicRouteService单元测试
|
||||
*
|
||||
* 文件定义:测试动态路由服务的核心功能
|
||||
* 涉及业务:路由增删改查、路由刷新
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-04-14
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DynamicRouteServiceTest {
|
||||
|
||||
@Mock
|
||||
private RouteDefinitionWriter routeDefinitionWriter;
|
||||
|
||||
@Mock
|
||||
private RouteDefinitionLocator routeDefinitionLocator;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
private DynamicRouteService dynamicRouteService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(routeDefinitionLocator.getRouteDefinitions())
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
dynamicRouteService = new DynamicRouteService(
|
||||
routeDefinitionWriter,
|
||||
routeDefinitionLocator,
|
||||
publisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddRoute_Success() {
|
||||
RouteDefinition routeDefinition = createRouteDefinition("test-route");
|
||||
|
||||
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(dynamicRouteService.addRoute(routeDefinition))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
verify(routeDefinitionWriter).save(any());
|
||||
verify(publisher).publishEvent(any(RefreshRoutesEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddRoute_NullRoute() {
|
||||
StepVerifier.create(dynamicRouteService.addRoute(null))
|
||||
.expectNext(false)
|
||||
.verifyComplete();
|
||||
|
||||
verify(routeDefinitionWriter, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRoute_Success() {
|
||||
String routeId = "test-route";
|
||||
RouteDefinition routeDefinition = createRouteDefinition(routeId);
|
||||
|
||||
// 先添加路由到缓存中
|
||||
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
|
||||
StepVerifier.create(dynamicRouteService.addRoute(routeDefinition))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
// 然后删除路由
|
||||
when(routeDefinitionWriter.delete(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(dynamicRouteService.deleteRoute(routeId))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
verify(routeDefinitionWriter).delete(any());
|
||||
verify(publisher, times(2)).publishEvent(any(RefreshRoutesEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRoute_NullId() {
|
||||
StepVerifier.create(dynamicRouteService.deleteRoute(null))
|
||||
.expectNext(false)
|
||||
.verifyComplete();
|
||||
|
||||
verify(routeDefinitionWriter, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRoutes() {
|
||||
RouteDefinition route1 = createRouteDefinition("route1");
|
||||
RouteDefinition route2 = createRouteDefinition("route2");
|
||||
|
||||
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(dynamicRouteService.addRoute(route1))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(dynamicRouteService.addRoute(route2))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(dynamicRouteService.getRoutes().collectList())
|
||||
.assertNext(routes -> {
|
||||
assertNotNull(routes);
|
||||
assertTrue(routes.size() >= 2);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRouteCount() {
|
||||
RouteDefinition route = createRouteDefinition("test-route");
|
||||
|
||||
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
|
||||
|
||||
StepVerifier.create(dynamicRouteService.addRoute(route))
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(dynamicRouteService.getRouteCount())
|
||||
.assertNext(count -> assertTrue(count >= 1))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
private RouteDefinition createRouteDefinition(String id) {
|
||||
RouteDefinition routeDefinition = new RouteDefinition();
|
||||
routeDefinition.setId(id);
|
||||
routeDefinition.setUri(java.net.URI.create("http://localhost:8080"));
|
||||
return routeDefinition;
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package cn.novalon.gym.manage.gateway.service.impl;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtKeyServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private JwtKeyServiceImpl jwtKeyService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(jwtKeyService, "configuredSecret", null);
|
||||
ReflectionTestUtils.setField(jwtKeyService, "encryptionPassword", "testEncryptionPassword");
|
||||
ReflectionTestUtils.setField(jwtKeyService, "rotationEnabled", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitializeKeys_GeneratesNewKey() {
|
||||
jwtKeyService.initializeKeys();
|
||||
|
||||
String version = jwtKeyService.getCurrentKeyVersion();
|
||||
SecretKey key = jwtKeyService.getCurrentSigningKey();
|
||||
|
||||
assertNotNull(version);
|
||||
assertNotNull(key);
|
||||
assertEquals("v1", version);
|
||||
assertEquals("AES", key.getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateSecureKey_GeneratesValidKey() {
|
||||
String key = jwtKeyService.generateSecureKey();
|
||||
|
||||
assertNotNull(key);
|
||||
assertFalse(key.isEmpty());
|
||||
assertTrue(jwtKeyService.validateKeyStrength(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateKeyStrength_ValidKey() {
|
||||
String validKey = "StrongPassword123ABC!@#XYZabcdefg";
|
||||
assertTrue(jwtKeyService.validateKeyStrength(validKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateKeyStrength_WeakKey() {
|
||||
String weakKey = "weak";
|
||||
assertFalse(jwtKeyService.validateKeyStrength(weakKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateKeyStrength_NullKey() {
|
||||
assertFalse(jwtKeyService.validateKeyStrength(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateKeyStrength_ShortKey() {
|
||||
String shortKey = "Short1!";
|
||||
assertFalse(jwtKeyService.validateKeyStrength(shortKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptKey_WithPassword() {
|
||||
String originalKey = "MySecretKey123!";
|
||||
String encryptedKey = jwtKeyService.encryptKey(originalKey);
|
||||
|
||||
assertNotNull(encryptedKey);
|
||||
assertNotEquals(originalKey, encryptedKey);
|
||||
assertTrue(encryptedKey.length() > originalKey.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptDecryptKey_RoundTrip() {
|
||||
String originalKey = "MySecretKey123!";
|
||||
String encryptedKey = jwtKeyService.encryptKey(originalKey);
|
||||
String decryptedKey = jwtKeyService.decryptKey(encryptedKey);
|
||||
|
||||
assertNotNull(encryptedKey);
|
||||
assertNotNull(decryptedKey);
|
||||
assertEquals(originalKey, decryptedKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRotateKey_CreatesNewVersion() {
|
||||
jwtKeyService.initializeKeys();
|
||||
String oldVersion = jwtKeyService.getCurrentKeyVersion();
|
||||
|
||||
jwtKeyService.rotateKey();
|
||||
|
||||
String newVersion = jwtKeyService.getCurrentKeyVersion();
|
||||
SecretKey newKey = jwtKeyService.getCurrentSigningKey();
|
||||
|
||||
assertNotEquals(oldVersion, newVersion);
|
||||
assertEquals("v2", newVersion);
|
||||
assertNotNull(newKey);
|
||||
assertEquals("AES", newKey.getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSigningKeyByVersion_ReturnsCorrectKey() {
|
||||
jwtKeyService.initializeKeys();
|
||||
SecretKey v1Key = jwtKeyService.getSigningKeyByVersion("v1");
|
||||
|
||||
assertNotNull(v1Key);
|
||||
assertEquals("AES", v1Key.getAlgorithm());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSigningKeyByVersion_InvalidVersion() {
|
||||
jwtKeyService.initializeKeys();
|
||||
SecretKey invalidKey = jwtKeyService.getSigningKeyByVersion("v999");
|
||||
|
||||
assertNull(invalidKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRotateKey_Disabled() {
|
||||
ReflectionTestUtils.setField(jwtKeyService, "rotationEnabled", false);
|
||||
jwtKeyService.initializeKeys();
|
||||
String oldVersion = jwtKeyService.getCurrentKeyVersion();
|
||||
|
||||
jwtKeyService.rotateKey();
|
||||
|
||||
String newVersion = jwtKeyService.getCurrentKeyVersion();
|
||||
assertEquals(oldVersion, newVersion);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldRotateKey_NewKey() {
|
||||
jwtKeyService.initializeKeys();
|
||||
|
||||
String currentVersion = jwtKeyService.getCurrentKeyVersion();
|
||||
SecretKey currentKey = jwtKeyService.getCurrentSigningKey();
|
||||
|
||||
assertNotNull(currentVersion, "Current version should not be null");
|
||||
assertNotNull(currentKey, "Current signing key should not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleRotations_CreatesMultipleVersions() {
|
||||
jwtKeyService.initializeKeys();
|
||||
|
||||
jwtKeyService.rotateKey();
|
||||
assertEquals("v2", jwtKeyService.getCurrentKeyVersion());
|
||||
|
||||
jwtKeyService.rotateKey();
|
||||
assertEquals("v3", jwtKeyService.getCurrentKeyVersion());
|
||||
|
||||
jwtKeyService.rotateKey();
|
||||
assertEquals("v4", jwtKeyService.getCurrentKeyVersion());
|
||||
|
||||
assertNotNull(jwtKeyService.getSigningKeyByVersion("v1"));
|
||||
assertNotNull(jwtKeyService.getSigningKeyByVersion("v2"));
|
||||
assertNotNull(jwtKeyService.getSigningKeyByVersion("v3"));
|
||||
assertNotNull(jwtKeyService.getSigningKeyByVersion("v4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptKey_WithoutPassword() {
|
||||
ReflectionTestUtils.setField(jwtKeyService, "encryptionPassword", "");
|
||||
String originalKey = "MySecretKey123!";
|
||||
String encryptedKey = jwtKeyService.encryptKey(originalKey);
|
||||
|
||||
assertEquals(originalKey, encryptedKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecryptKey_WithoutPassword() {
|
||||
ReflectionTestUtils.setField(jwtKeyService, "encryptionPassword", "");
|
||||
String originalKey = "MySecretKey123!";
|
||||
String decryptedKey = jwtKeyService.decryptKey(originalKey);
|
||||
|
||||
assertEquals(originalKey, decryptedKey);
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package cn.novalon.gym.manage.gateway.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.gateway.model.Permission;
|
||||
import cn.novalon.gym.manage.gateway.model.Role;
|
||||
import cn.novalon.gym.manage.gateway.model.User;
|
||||
import cn.novalon.gym.manage.gateway.service.PermissionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class PermissionServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private WebClient.Builder webClientBuilder;
|
||||
|
||||
@Mock
|
||||
private WebClient webClient;
|
||||
|
||||
@Mock
|
||||
private WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec;
|
||||
|
||||
@Mock
|
||||
private WebClient.RequestHeadersSpec<?> requestHeadersSpec;
|
||||
|
||||
@Mock
|
||||
private WebClient.ResponseSpec responseSpec;
|
||||
|
||||
private PermissionService permissionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
doReturn(webClient).when(webClientBuilder).build();
|
||||
doReturn(requestHeadersUriSpec).when(webClient).get();
|
||||
doReturn(requestHeadersSpec).when(requestHeadersUriSpec).uri(anyString());
|
||||
doReturn(responseSpec).when(requestHeadersSpec).retrieve();
|
||||
permissionService = new PermissionServiceImpl(webClientBuilder, "http://localhost:8084");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_Success() {
|
||||
User expectedUser = new User(1L, "testuser", "test@example.com", "1234567890", 1, System.currentTimeMillis(), System.currentTimeMillis());
|
||||
|
||||
doReturn(Mono.just(expectedUser)).when(responseSpec).bodyToMono(eq(User.class));
|
||||
|
||||
User user = permissionService.getUserById(1L);
|
||||
|
||||
assertNotNull(user);
|
||||
assertEquals("testuser", user.getUsername());
|
||||
verify(webClient).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_NullUserId() {
|
||||
User user = permissionService.getUserById(null);
|
||||
|
||||
assertNull(user);
|
||||
verify(webClient, never()).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserRoles_Success() {
|
||||
List<Role> expectedRoles = Arrays.asList(
|
||||
new Role(1L, "ADMIN", "Administrator", "Admin role", 1, System.currentTimeMillis(), System.currentTimeMillis()),
|
||||
new Role(2L, "USER", "User", "User role", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
);
|
||||
|
||||
doReturn(Mono.just(expectedRoles.toArray(new Role[0]))).when(responseSpec).bodyToMono(eq(Role[].class));
|
||||
|
||||
List<Role> roles = permissionService.getUserRoles(1L);
|
||||
|
||||
assertNotNull(roles);
|
||||
assertEquals(2, roles.size());
|
||||
verify(webClient).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserRoles_NullUserId() {
|
||||
List<Role> roles = permissionService.getUserRoles(null);
|
||||
|
||||
assertNotNull(roles);
|
||||
assertTrue(roles.isEmpty());
|
||||
verify(webClient, never()).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserPermissions_Success() {
|
||||
Set<Permission> expectedPermissions = new HashSet<>(Arrays.asList(
|
||||
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis()),
|
||||
new Permission(2L, "user:write", "Write User", "API", "/api/users/**", "POST", "Write user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
));
|
||||
|
||||
doReturn(Mono.just(expectedPermissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
|
||||
|
||||
Set<Permission> permissions = permissionService.getUserPermissions(1L);
|
||||
|
||||
assertNotNull(permissions);
|
||||
assertEquals(2, permissions.size());
|
||||
verify(webClient).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserPermissions_NullUserId() {
|
||||
Set<Permission> permissions = permissionService.getUserPermissions(null);
|
||||
|
||||
assertNotNull(permissions);
|
||||
assertTrue(permissions.isEmpty());
|
||||
verify(webClient, never()).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasPermission_True() {
|
||||
Set<Permission> permissions = new HashSet<>(Arrays.asList(
|
||||
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
));
|
||||
|
||||
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
|
||||
|
||||
boolean hasPermission = permissionService.hasPermission(1L, "/api/users/123", "GET");
|
||||
|
||||
assertTrue(hasPermission);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasPermission_False() {
|
||||
Set<Permission> permissions = new HashSet<>(Arrays.asList(
|
||||
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
));
|
||||
|
||||
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
|
||||
|
||||
boolean hasPermission = permissionService.hasPermission(1L, "/api/users/123", "POST");
|
||||
|
||||
assertFalse(hasPermission);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasPermission_NullUserId() {
|
||||
boolean hasPermission = permissionService.hasPermission(null, "/api/users/123", "GET");
|
||||
|
||||
assertFalse(hasPermission);
|
||||
verify(webClient, never()).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetPermissionPaths_Success() {
|
||||
Set<Permission> permissions = new HashSet<>(Arrays.asList(
|
||||
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis()),
|
||||
new Permission(2L, "user:write", "Write User", "API", "/api/users/**", "POST", "Write user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
));
|
||||
|
||||
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
|
||||
|
||||
Set<String> paths = permissionService.getPermissionPaths(1L, "GET");
|
||||
|
||||
assertNotNull(paths);
|
||||
assertEquals(1, paths.size());
|
||||
assertTrue(paths.contains("/api/users/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClearCache_Success() {
|
||||
User user = new User(1L, "testuser", "test@example.com", "1234567890", 1, System.currentTimeMillis(), System.currentTimeMillis());
|
||||
List<Role> roles = Arrays.asList(new Role(1L, "ADMIN", "Administrator", "Admin role", 1, System.currentTimeMillis(), System.currentTimeMillis()));
|
||||
Set<Permission> permissions = new HashSet<>(Arrays.asList(
|
||||
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
));
|
||||
|
||||
doReturn(Mono.just(user)).when(responseSpec).bodyToMono(eq(User.class));
|
||||
doReturn(Mono.just(roles.toArray(new Role[0]))).when(responseSpec).bodyToMono(eq(Role[].class));
|
||||
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
|
||||
|
||||
permissionService.getUserById(1L);
|
||||
permissionService.getUserRoles(1L);
|
||||
permissionService.getUserPermissions(1L);
|
||||
|
||||
permissionService.clearCache(1L);
|
||||
|
||||
verify(webClient, times(3)).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClearAllCache_Success() {
|
||||
User user = new User(1L, "testuser", "test@example.com", "1234567890", 1, System.currentTimeMillis(), System.currentTimeMillis());
|
||||
List<Role> roles = Arrays.asList(new Role(1L, "ADMIN", "Administrator", "Admin role", 1, System.currentTimeMillis(), System.currentTimeMillis()));
|
||||
Set<Permission> permissions = new HashSet<>(Arrays.asList(
|
||||
new Permission(1L, "user:read", "Read User", "API", "/api/users/**", "GET", "Read user permissions", 1, System.currentTimeMillis(), System.currentTimeMillis())
|
||||
));
|
||||
|
||||
doReturn(Mono.just(user)).when(responseSpec).bodyToMono(eq(User.class));
|
||||
doReturn(Mono.just(roles.toArray(new Role[0]))).when(responseSpec).bodyToMono(eq(Role[].class));
|
||||
doReturn(Mono.just(permissions.toArray(new Permission[0]))).when(responseSpec).bodyToMono(eq(Permission[].class));
|
||||
|
||||
permissionService.getUserById(1L);
|
||||
permissionService.getUserRoles(1L);
|
||||
permissionService.getUserPermissions(1L);
|
||||
|
||||
permissionService.clearAllCache();
|
||||
|
||||
permissionService.getUserById(1L);
|
||||
permissionService.getUserRoles(1L);
|
||||
permissionService.getUserPermissions(1L);
|
||||
|
||||
verify(webClient, times(6)).get();
|
||||
}
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package cn.novalon.gym.manage.gateway.service.impl;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* SignatureServiceImpl单元测试
|
||||
*
|
||||
* 文件定义:测试签名服务的核心功能
|
||||
* 涉及业务:签名生成、签名验证、时间戳验证、nonce防重放
|
||||
*
|
||||
* @author 张翔
|
||||
* @date 2026-03-26
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SignatureServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private SignatureServiceImpl signatureService;
|
||||
|
||||
private static final String TEST_SECRET = "TestSecretKey123";
|
||||
private static final String TEST_METHOD = "GET";
|
||||
private static final String TEST_PATH = "/api/users";
|
||||
private static final String TEST_QUERY = "page=1&size=10";
|
||||
private static final String TEST_BODY = "";
|
||||
private static final String TEST_NONCE = "test-nonce-12345";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(signatureService, "signatureEnabled", true);
|
||||
ReflectionTestUtils.setField(signatureService, "maxAgeMinutes", 5);
|
||||
ReflectionTestUtils.setField(signatureService, "nonceCacheSize", 10000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateSignature_ShouldGenerateValidSignature() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
String signature = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
|
||||
assertNotNull(signature);
|
||||
assertFalse(signature.isEmpty());
|
||||
assertTrue(signature.length() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateSignature_ShouldGenerateSameSignatureForSameInput() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
String signature1 = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
String signature2 = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
|
||||
assertEquals(signature1, signature2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateSignature_ShouldGenerateDifferentSignatureForDifferentInput() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
String signature1 = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
String signature2 = signatureService.generateSignature(
|
||||
"POST", TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
|
||||
assertNotEquals(signature1, signature2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifySignature_WithValidSignature_ShouldReturnTrue() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String signature = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, TEST_PATH + "?" + TEST_QUERY)
|
||||
.header("X-Signature", signature)
|
||||
.header("X-Timestamp", String.valueOf(timestamp))
|
||||
.header("X-Nonce", TEST_NONCE)
|
||||
.build();
|
||||
|
||||
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
|
||||
|
||||
assertTrue(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifySignature_WithInvalidSignature_ShouldReturnFalse() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, TEST_PATH)
|
||||
.header("X-Signature", "invalid-signature")
|
||||
.header("X-Timestamp", String.valueOf(timestamp))
|
||||
.header("X-Nonce", TEST_NONCE)
|
||||
.build();
|
||||
|
||||
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
|
||||
|
||||
assertFalse(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifySignature_WithMissingHeaders_ShouldReturnFalse() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, TEST_PATH)
|
||||
.build();
|
||||
|
||||
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
|
||||
|
||||
assertFalse(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifySignature_WithExpiredTimestamp_ShouldReturnFalse() {
|
||||
long expiredTimestamp = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10);
|
||||
String signature = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, expiredTimestamp, TEST_NONCE, TEST_SECRET);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, TEST_PATH)
|
||||
.header("X-Signature", signature)
|
||||
.header("X-Timestamp", String.valueOf(expiredTimestamp))
|
||||
.header("X-Nonce", TEST_NONCE)
|
||||
.build();
|
||||
|
||||
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
|
||||
|
||||
assertFalse(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifySignature_WithUsedNonce_ShouldReturnFalse() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String signature = signatureService.generateSignature(
|
||||
TEST_METHOD, TEST_PATH, TEST_QUERY, TEST_BODY, timestamp, TEST_NONCE, TEST_SECRET);
|
||||
|
||||
signatureService.recordNonce(TEST_NONCE);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, TEST_PATH)
|
||||
.header("X-Signature", signature)
|
||||
.header("X-Timestamp", String.valueOf(timestamp))
|
||||
.header("X-Nonce", TEST_NONCE)
|
||||
.build();
|
||||
|
||||
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
|
||||
|
||||
assertFalse(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsTimestampValid_WithValidTimestamp_ShouldReturnTrue() {
|
||||
long validTimestamp = System.currentTimeMillis();
|
||||
|
||||
boolean isValid = signatureService.isTimestampValid(validTimestamp, 5);
|
||||
|
||||
assertTrue(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsTimestampValid_WithExpiredTimestamp_ShouldReturnFalse() {
|
||||
long expiredTimestamp = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10);
|
||||
|
||||
boolean isValid = signatureService.isTimestampValid(expiredTimestamp, 5);
|
||||
|
||||
assertFalse(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsTimestampValid_WithFutureTimestamp_ShouldReturnFalse() {
|
||||
long futureTimestamp = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10);
|
||||
|
||||
boolean isValid = signatureService.isTimestampValid(futureTimestamp, 5);
|
||||
|
||||
assertFalse(isValid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsNonceUsed_WithNewNonce_ShouldReturnFalse() {
|
||||
boolean isUsed = signatureService.isNonceUsed("new-nonce-123");
|
||||
|
||||
assertFalse(isUsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsNonceUsed_WithUsedNonce_ShouldReturnTrue() {
|
||||
String nonce = "used-nonce-123";
|
||||
signatureService.recordNonce(nonce);
|
||||
|
||||
boolean isUsed = signatureService.isNonceUsed(nonce);
|
||||
|
||||
assertTrue(isUsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordNonce_ShouldIncreaseCacheSize() {
|
||||
int initialSize = signatureService.getNonceCacheSize();
|
||||
|
||||
signatureService.recordNonce("test-nonce-1");
|
||||
signatureService.recordNonce("test-nonce-2");
|
||||
signatureService.recordNonce("test-nonce-3");
|
||||
|
||||
int finalSize = signatureService.getNonceCacheSize();
|
||||
assertEquals(initialSize + 3, finalSize);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCleanupExpiredNonces_ShouldRemoveExpiredEntries() {
|
||||
ReflectionTestUtils.setField(signatureService, "nonceCacheSize", 5);
|
||||
|
||||
signatureService.recordNonce("nonce-1");
|
||||
signatureService.recordNonce("nonce-2");
|
||||
signatureService.recordNonce("nonce-3");
|
||||
signatureService.recordNonce("nonce-4");
|
||||
signatureService.recordNonce("nonce-5");
|
||||
signatureService.recordNonce("nonce-6");
|
||||
|
||||
int cacheSize = signatureService.getNonceCacheSize();
|
||||
assertTrue(cacheSize <= 6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testVerifySignature_WhenDisabled_ShouldReturnTrue() {
|
||||
ReflectionTestUtils.setField(signatureService, "signatureEnabled", false);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.method(HttpMethod.GET, TEST_PATH)
|
||||
.build();
|
||||
|
||||
boolean isValid = signatureService.verifySignature(request, TEST_SECRET);
|
||||
|
||||
assertTrue(isValid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
spring:
|
||||
application:
|
||||
name: manage-gateway
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: user-service
|
||||
uri: http://localhost:8084
|
||||
predicates:
|
||||
- Path=/api/users/**
|
||||
- id: auth-service
|
||||
uri: http://localhost:8083
|
||||
predicates:
|
||||
- Path=/api/auth/**
|
||||
|
||||
user:
|
||||
service:
|
||||
url: http://localhost:8084
|
||||
|
||||
permission:
|
||||
cache:
|
||||
expiry:
|
||||
minutes: 5
|
||||
|
||||
logging:
|
||||
level:
|
||||
cn.novalon.manage.gateway: DEBUG
|
||||
org.springframework.cloud.gateway: DEBUG
|
||||
org.springframework.web.reactive: DEBUG
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
Reference in New Issue
Block a user