feat(gateway): Implement gateway module foundation
This commit is contained in:
@@ -24,6 +24,12 @@
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Alibaba Nacos Config -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Alibaba Sentinel -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
@@ -41,5 +47,55 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons IO -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.15.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JJWT (JSON Web Token) -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.nuaa.video.gateway.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.reactive.CorsWebFilter;
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public CorsWebFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.addAllowedOriginPattern("*"); // 允许所有来源
|
||||
config.addAllowedHeader("*"); // 允许所有请求头
|
||||
config.addAllowedMethod("*"); // 允许所有方法
|
||||
config.setAllowCredentials(true); // 允许携带凭证
|
||||
config.setMaxAge(3600L); // 预检请求的有效期,单位为秒
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
return new CorsWebFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.nuaa.video.gateway.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
|
||||
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 动态路由配置
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DynamicRouteConfig implements ApplicationEventPublisherAware {
|
||||
|
||||
@Resource
|
||||
private RouteDefinitionWriter routeDefinitionWriter;
|
||||
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.publisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加路由
|
||||
*/
|
||||
public void addRoute(RouteDefinition definition) {
|
||||
try {
|
||||
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
|
||||
this.publisher.publishEvent(new RefreshRoutesEvent(this));
|
||||
log.info("成功添加路由: {}", definition.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("添加路由失败: {}", definition.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新路由
|
||||
*/
|
||||
public void updateRoute(RouteDefinition definition) {
|
||||
try {
|
||||
this.deleteRoute(definition.getId());
|
||||
this.addRoute(definition);
|
||||
log.info("成功更新路由: {}", definition.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("更新路由失败: {}", definition.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除路由
|
||||
*/
|
||||
public void deleteRoute(String id) {
|
||||
try {
|
||||
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
|
||||
this.publisher.publishEvent(new RefreshRoutesEvent(this));
|
||||
log.info("成功删除路由: {}", id);
|
||||
} catch (Exception e) {
|
||||
log.error("删除路由失败: {}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.nuaa.video.gateway.config;
|
||||
|
||||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
|
||||
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
|
||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
|
||||
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 网关降级配置
|
||||
*/
|
||||
@Configuration
|
||||
public class GatewayFallbackConfig {
|
||||
|
||||
/**
|
||||
* 初始化自定义限流响应和规则
|
||||
*/
|
||||
@PostConstruct
|
||||
public void doInit() {
|
||||
// 自定义限流响应
|
||||
initBlockHandlers();
|
||||
// 初始化QPS限流规则
|
||||
initQpsLimitRule();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义限流响应
|
||||
*/
|
||||
private void initBlockHandlers() {
|
||||
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable) -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
response.put("message", "请求过于频繁,请稍后再试");
|
||||
|
||||
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue(response));
|
||||
};
|
||||
|
||||
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化QPS限流规则
|
||||
*/
|
||||
private void initQpsLimitRule() {
|
||||
Set<GatewayFlowRule> rules = new HashSet<>();
|
||||
|
||||
// 对每个API路径设置限流
|
||||
// 用户服务限流
|
||||
rules.add(new GatewayFlowRule("video-user")
|
||||
.setCount(100)
|
||||
.setIntervalSec(1));
|
||||
|
||||
// 内容服务限流
|
||||
rules.add(new GatewayFlowRule("video-content")
|
||||
.setCount(200)
|
||||
.setIntervalSec(1));
|
||||
|
||||
// 认证服务限流
|
||||
rules.add(new GatewayFlowRule("video-auth")
|
||||
.setCount(50)
|
||||
.setIntervalSec(1));
|
||||
|
||||
// 加载限流规则
|
||||
GatewayRuleManager.loadRules(rules);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.nuaa.video.gateway.filter;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.core.io.buffer.DataBuffer;
|
||||
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.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthGlobalFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.token-header:Authorization}")
|
||||
private String tokenHeader;
|
||||
|
||||
@Value("${gateway.ignore.urls:/auth/login,/auth/register,/auth/captcha}")
|
||||
private List<String> ignoreUrls;
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String path = request.getURI().getPath();
|
||||
|
||||
// 跳过不需要验证的路径
|
||||
if (isIgnorePath(path)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 获取token
|
||||
String token = getToken(request);
|
||||
if (!StringUtils.hasText(token)) {
|
||||
return unauthorized(exchange, "缺少token");
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证token
|
||||
Claims claims = parseToken(token);
|
||||
|
||||
// 将用户信息传递到下游服务
|
||||
ServerHttpRequest.Builder mutate = request.mutate();
|
||||
mutate.header("userId", claims.getSubject());
|
||||
mutate.header("username", claims.get("username", String.class));
|
||||
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
||||
} catch (Exception e) {
|
||||
log.error("Token验证失败", e);
|
||||
return unauthorized(exchange, "Token验证失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为忽略路径
|
||||
*/
|
||||
private boolean isIgnorePath(String path) {
|
||||
return ignoreUrls.stream().anyMatch(ignore -> pathMatcher.match(ignore, path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取token
|
||||
*/
|
||||
private String getToken(ServerHttpRequest request) {
|
||||
String token = request.getHeaders().getFirst(tokenHeader);
|
||||
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
|
||||
return token.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析token
|
||||
*/
|
||||
private Claims parseToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回未授权响应
|
||||
*/
|
||||
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", HttpStatus.UNAUTHORIZED.value());
|
||||
result.put("message", message);
|
||||
|
||||
try {
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(result);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Mono.just(buffer));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("响应JSON转换异常", e);
|
||||
return response.setComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.nuaa.video.gateway.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 请求日志过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RequestLogFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
URI uri = request.getURI();
|
||||
String method = request.getMethod().name();
|
||||
String host = request.getRemoteAddress().getHostString();
|
||||
|
||||
// 记录请求日志
|
||||
log.info("Request: {} {} from {}", method, uri, host);
|
||||
|
||||
// 记录请求头信息(生产环境中可以根据需要调整日志级别)
|
||||
if (log.isDebugEnabled()) {
|
||||
Map<String, List<String>> headers = request.getHeaders();
|
||||
headers.forEach((name, values) ->
|
||||
log.debug("Header: {} = {}", name, String.join(",", values)));
|
||||
}
|
||||
|
||||
// 执行后续过滤器
|
||||
return chain.filter(exchange).then(Mono.fromRunnable(() ->
|
||||
log.info("Response for: {} {}", method, uri)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE + 10;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.nuaa.video.gateway.handler;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode; // 导入HttpStatusCode
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(-1)
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
// 设置响应的状态码
|
||||
if (response.isCommitted()) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
|
||||
HttpStatusCode status; // 修改为HttpStatusCode类型
|
||||
String message;
|
||||
int statusCode; // 添加一个int类型的状态码变量
|
||||
|
||||
// 根据异常类型设置响应状态和消息
|
||||
if (ex instanceof ResponseStatusException) {
|
||||
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
|
||||
status = responseStatusException.getStatusCode();
|
||||
message = responseStatusException.getReason();
|
||||
statusCode = status.value(); // 获取状态码的数值
|
||||
} else {
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
message = "服务器内部错误";
|
||||
statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
|
||||
}
|
||||
|
||||
log.error("网关异常: {}", message, ex);
|
||||
|
||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
response.setStatusCode(status);
|
||||
|
||||
// 构建响应体
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("code", statusCode); // 使用状态码数值
|
||||
result.put("message", message);
|
||||
|
||||
try {
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(result);
|
||||
DataBuffer buffer = response.bufferFactory().wrap(bytes);
|
||||
return response.writeWith(Mono.just(buffer));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("响应JSON转换异常", e);
|
||||
return response.setComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
spring:
|
||||
cloud:
|
||||
sentinel:
|
||||
log:
|
||||
dir: ./logs/sentinel/
|
||||
switch-pid: true
|
||||
gateway:
|
||||
httpclient:
|
||||
connect-timeout: 60000
|
||||
response-timeout: 60000
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.gateway: debug
|
||||
reactor.netty.http.client: debug
|
||||
@@ -4,12 +4,26 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: video-gateway
|
||||
profiles:
|
||||
active: dev
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:${spring.application.name}.yaml
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 5.180.253.115:8848
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
server-addr: ${NACOS_HOST:5.180.253.115}:${NACOS_PORT:8848}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
namespace: ${spring.cloud.nacos.discovery.namespace}
|
||||
group: ${spring.cloud.nacos.discovery.group}
|
||||
file-extension: yaml
|
||||
extension-configs:
|
||||
- data-id: video-gateway.yaml
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
refresh: true
|
||||
gateway:
|
||||
discovery:
|
||||
locator:
|
||||
@@ -54,7 +68,7 @@ spring:
|
||||
- StripPrefix=1
|
||||
sentinel:
|
||||
transport:
|
||||
dashboard: 5.180.253.115:8080
|
||||
dashboard: ${SENTINEL_DASHBOARD_HOST:5.180.253.115}:${SENTINEL_DASHBOARD_PORT:8080}
|
||||
port: 8719
|
||||
eager: true
|
||||
scg:
|
||||
@@ -67,3 +81,21 @@ management:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
gateway:
|
||||
enabled: true
|
||||
|
||||
# 不需要鉴权的URL列表
|
||||
gateway:
|
||||
ignore:
|
||||
urls:
|
||||
- /auth/login
|
||||
- /auth/register
|
||||
- /auth/captcha
|
||||
- /*/v3/api-docs
|
||||
- /swagger-resources/**
|
||||
- /swagger-ui/**
|
||||
- /actuator/**
|
||||
- /favicon.ico
|
||||
Reference in New Issue
Block a user