网关问题

This commit is contained in:
2025-12-02 18:46:03 +08:00
parent 90ddcf7af3
commit 9cb4844be4
13 changed files with 1226 additions and 5 deletions

396
docs/网关认证方案.md Normal file
View File

@@ -0,0 +1,396 @@
# Gateway 认证方案说明
## 问题背景
在微服务架构中,如果同时使用 Gateway 和 common-auth 模块,会出现**重复认证**的问题:
```
浏览器 → Gateway (AuthGlobalFilter 验证 JWT)
→ 微服务 (JwtAuthenticationFilter 再次验证 JWT) ❌ 重复!
```
## 解决方案
提供两种认证模式,通过配置选择:
### **模式一Gateway 统一认证(推荐)**
Gateway 负责认证,微服务信任 Gateway 传递的用户信息。
#### 架构流程
```
浏览器
↓ (带 JWT Token)
Nginx (80端口)
Gateway (8080端口)
├─ AuthGlobalFilter: 验证 JWT ✓
├─ 提取用户信息 (userId, username)
├─ 添加到请求头传递给下游
└─ 路由到微服务
微服务
└─ GatewayTrustFilter: 从请求头获取用户信息 ✓
```
#### 配置方式
**1. Gateway 服务配置** (`gateway/application.yml`)
```yaml
auth:
enabled: true
token-header: Authorization
token-prefix: "Bearer "
auth-paths:
- /auth/login
- /auth/logout
whitelist:
- /actuator/**
- /v3/api-docs/**
```
**2. 微服务配置** (`system/application.yml`, `log/application.yml` 等)
```yaml
auth:
enabled: true
gateway-mode: true # 关键:启用 Gateway 模式
```
#### 优点
- ✅ 避免重复认证,性能更好
- ✅ 统一认证逻辑,易维护
- ✅ 微服务之间调用不需要传递 JWT
---
### **模式二:微服务独立认证**
每个微服务独立验证 JWTGateway 不做认证。
#### 架构流程
```
浏览器
↓ (带 JWT Token)
Nginx
Gateway
└─ 直接路由(不验证)
微服务
└─ JwtAuthenticationFilter: 验证 JWT ✓
```
#### 配置方式
**1. Gateway 服务配置**
```yaml
auth:
enabled: false # 关键:关闭 Gateway 认证
```
**2. 微服务配置**
```yaml
auth:
enabled: true
gateway-mode: false # 或不配置(默认 false
token-header: Authorization
token-prefix: "Bearer "
```
#### 适用场景
- 微服务需要直接对外暴露(不经过 Gateway
- 对安全性要求极高,需要每层都验证
---
## 配置文件对比
### Gateway 服务 (`gateway/application.yml`)
```yaml
server:
port: 8080
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: system-service
uri: lb://system-service
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 认证配置
auth:
enabled: true # 是否启用认证
gateway-mode: false # Gateway 本身不需要此配置
token-header: Authorization
token-prefix: "Bearer "
auth-paths:
- /auth/login
- /auth/logout
- /auth/captcha
- /auth/refresh
whitelist:
- /actuator/**
- /v3/api-docs/**
```
### 微服务配置 (Gateway 模式)
**auth-service/application.yml**
```yaml
server:
port: 8081
spring:
application:
name: auth-service
# 认证配置
auth:
enabled: true
gateway-mode: true # 关键:信任 Gateway
token-header: Authorization
token-prefix: "Bearer "
whitelist:
- /v3/api-docs/**
- /actuator/**
```
**system-service/application.yml**
```yaml
server:
port: 8082
spring:
application:
name: system-service
auth:
enabled: true
gateway-mode: true # 关键:信任 Gateway
```
---
## 工作原理
### Gateway 认证流程
**AuthGlobalFilter (Gateway 层)**
```java
1. 检查请求路径是否在白名单
2. 提取 Authorization 请求头中的 JWT Token
3. 验证 Token 是否过期
4. 验证 Token 签名是否有效
5. 提取用户信息 (userId, username)
6. 将用户信息添加到请求头
- X-User-Id: {userId}
- X-Username: {username}
7. 路由到下游微服务
```
### 微服务信任流程
**GatewayTrustFilter (微服务层)**
```java
1. 从请求头获取 Gateway 传递的用户信息
- X-User-Id
- X-Username
2. 构造 Spring Security 认证对象
3. 设置到 SecurityContext
4. 设置到 request attributes供业务代码使用
```
---
## 安全考虑
### 内网安全
采用 Gateway 模式时,需确保:
1. **微服务不对外暴露**
- 只能通过 Gateway 访问
- 使用 Kubernetes Network Policy 或防火墙隔离
2. **请求头保护**
- Gateway 在转发前清除任何客户端传递的 `X-User-Id` 等头
- 防止伪造用户身份
3. **Gateway 过滤器增强**(可选)
```java
// 在 Gateway 中清除客户端可能伪造的请求头
ServerHttpRequest mutatedRequest = request.mutate()
.headers(headers -> {
headers.remove("X-User-Id");
headers.remove("X-Username");
})
.header(AuthContants.USER_ID_ATTRIBUTE, userId)
.header(AuthContants.TOKEN_ATTRIBUTE, token)
.build();
```
---
## 测试验证
### 测试 Gateway 模式
**1. 登录获取 Token**
```bash
curl -X POST http://localhost/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 响应
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userId": "1001"
}
}
```
**2. 使用 Token 访问受保护接口**
```bash
curl -X GET http://localhost/api/system/user/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**3. 查看日志验证单次认证**
```
Gateway 日志:
[Gateway] Token 验证成功: userId=1001, path=/system/user/profile
System-Service 日志:
[GatewayTrustFilter] 从 Gateway 获取用户信息: userId=1001, username=admin
```
---
## 迁移指南
### 从独立认证迁移到 Gateway 统一认证
**步骤 1**: 更新所有微服务配置
```yaml
auth:
gateway-mode: true # 添加这一行
```
**步骤 2**: 重启服务(先重启微服务,后重启 Gateway
```bash
# 重启微服务
docker-compose restart auth-service system-service log-service
# 重启 Gateway
docker-compose restart gateway
```
**步骤 3**: 验证功能
- 测试登录
- 测试受保护接口访问
- 检查日志确认只认证一次
---
## 常见问题
### Q1: Gateway 模式下,微服务之间如何调用?
**A**: 微服务间调用不需要传递 JWT Token直接调用即可。Gateway已经验证过身份。
```java
// 微服务 A 调用微服务 B
@Autowired
private RestTemplate restTemplate;
public void callServiceB() {
// 直接调用,不需要添加 Authorization 头
String result = restTemplate.getForObject(
"http://service-b/api/xxx",
String.class
);
}
```
### Q2: 如何获取当前登录用户信息?
**A**: 使用 `@HttpLogin` 注解或从 SecurityContext 获取。
```java
// 方式一:使用注解(推荐)
@GetMapping("/profile")
public ResultDomain<UserDTO> getProfile(@HttpLogin LoginDomain loginDomain) {
String userId = loginDomain.getUser().getUserId();
// ...
}
// 方式二:从 SecurityContext 获取
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = (String) auth.getPrincipal();
```
### Q3: Gateway 模式更安全还是独立认证更安全?
**A**: 取决于网络拓扑:
- **内网隔离良好**: Gateway 模式更优(性能好,维护简单)
- **微服务直接对外**: 独立认证更安全(每层验证)
---
## 推荐配置
### 生产环境推荐
```yaml
# Gateway
auth:
enabled: true
gateway-mode: false
# 所有微服务
auth:
enabled: true
gateway-mode: true
```
### 开发环境(快速调试)
可以临时关闭认证:
```yaml
auth:
enabled: false
```
---
## 总结
| 对比项 | Gateway 统一认证 | 微服务独立认证 |
|--------|-----------------|---------------|
| 认证次数 | 1次仅 Gateway | N次每个服务 |
| 性能 | ⭐⭐⭐⭐⭐ 最优 | ⭐⭐⭐ 一般 |
| 维护性 | ⭐⭐⭐⭐⭐ 统一管理 | ⭐⭐⭐ 分散管理 |
| 安全性 | ⭐⭐⭐⭐ 需内网隔离 | ⭐⭐⭐⭐⭐ 多层防护 |
| 推荐场景 | 内网微服务架构 | 微服务对外暴露 |
**推荐使用 Gateway 统一认证模式!**

View File

@@ -0,0 +1,82 @@
package org.xyzh.common.auth.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.xyzh.common.auth.contants.AuthContants;
import java.io.IOException;
import java.util.Collections;
/**
* @description Gateway 认证模式配置 - 信任 Gateway 传递的用户信息
* @author yslg
* @since 2025-12-02
*
* 当启用 gateway-mode 时,微服务不再独立验证 JWT而是信任 Gateway 传递的用户信息
* 配置项auth.gateway-mode=true
*/
@Configuration
@ConditionalOnProperty(name = "auth.gateway-mode", havingValue = "true")
public class GatewayAuthConfig {
private static final Logger log = LoggerFactory.getLogger(GatewayAuthConfig.class);
/**
* Gateway 信任过滤器 - 从请求头获取用户信息
* 替代 JwtAuthenticationFilter
*/
@Bean
public GatewayTrustFilter gatewayTrustFilter() {
log.info("启用 Gateway 认证模式,微服务将信任 Gateway 传递的用户信息");
return new GatewayTrustFilter();
}
/**
* Gateway 信任过滤器实现
*/
public static class GatewayTrustFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(GatewayTrustFilter.class);
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
// 从 Gateway 传递的请求头获取用户信息
String userId = request.getHeader(AuthContants.USER_ID_ATTRIBUTE);
String username = request.getHeader(AuthContants.USERNAME_ATTRIBUTE);
if (StringUtils.hasText(userId)) {
// 构造简化的 Principal使用 userId 作为身份标识)
// 注意:完整的 LoginDomain 应该从 Redis 加载,这里只是基本身份验证
// 设置到 Spring Security 上下文
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 同时将用户信息设置到 request attributes 中,供业务代码使用
request.setAttribute(AuthContants.USER_ID_ATTRIBUTE, userId);
if (StringUtils.hasText(username)) {
request.setAttribute(AuthContants.USERNAME_ATTRIBUTE, username);
}
log.debug("从 Gateway 获取用户信息: userId={}, username={}", userId, username);
}
filterChain.doFilter(request, response);
}
}
}

View File

@@ -1,5 +1,7 @@
package org.xyzh.common.auth.config; package org.xyzh.common.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -15,17 +17,29 @@ import org.xyzh.redis.service.RedisService;
@EnableMethodSecurity @EnableMethodSecurity
public class SecurityConfig { public class SecurityConfig {
/**
* JWT 认证过滤器 - 仅在非 Gateway 模式下启用
* 当 auth.gateway-mode=false 或未配置时使用
*/
@Bean @Bean
@ConditionalOnProperty(name = "auth.gateway-mode", havingValue = "false", matchIfMissing = true)
public JwtAuthenticationFilter jwtAuthenticationFilter(TokenParser tokenParser, public JwtAuthenticationFilter jwtAuthenticationFilter(TokenParser tokenParser,
AuthProperties authProperties, AuthProperties authProperties,
RedisService redisService) { RedisService redisService) {
return new JwtAuthenticationFilter(tokenParser, authProperties, redisService); return new JwtAuthenticationFilter(tokenParser, authProperties, redisService);
} }
/**
* Security 过滤器链配置
* 根据是否启用 Gateway 模式决定是否添加 JWT 过滤器
*/
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, public SecurityFilterChain securityFilterChain(
AuthProperties authProperties, HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { AuthProperties authProperties,
@Autowired(required = false) JwtAuthenticationFilter jwtAuthenticationFilter,
@Autowired(required = false) GatewayAuthConfig.GatewayTrustFilter gatewayTrustFilter) throws Exception {
http http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable()) .formLogin(form -> form.disable())
@@ -45,8 +59,16 @@ public class SecurityConfig {
authz authz
.requestMatchers("/error", "/favicon.ico").permitAll() .requestMatchers("/error", "/favicon.ico").permitAll()
.anyRequest().authenticated(); .anyRequest().authenticated();
}) });
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 根据模式添加对应的过滤器
if (jwtAuthenticationFilter != null) {
// 非 Gateway 模式:使用 JWT 过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
} else if (gatewayTrustFilter != null) {
// Gateway 模式:使用信任过滤器
http.addFilterBefore(gatewayTrustFilter, UsernamePasswordAuthenticationFilter.class);
}
return http.build(); return http.build();
} }

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xyzh</groupId>
<artifactId>urban-lifeline</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh</groupId>
<artifactId>gateway</artifactId>
<version>${urban-lifeline.version}</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 认证模块JWT验证 -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-auth</artifactId>
</dependency>
<!-- Redis用于Token验证、限流等 -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-redis</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-utils</artifactId>
</dependency>
<!-- 核心模块 -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
</dependency>
<!-- Spring Boot Actuator健康检查 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Log4j2 日志 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- WebFluxGateway依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 配置处理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- FastJson -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>org.xyzh.gateway.GatewayApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,21 @@
package org.xyzh.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @description Gateway 网关启动类
* @author yslg
* @since 2025-12-02
*/
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
System.out.println("========================================");
System.out.println(" Gateway 网关服务启动成功!");
System.out.println("========================================");
}
}

View File

@@ -0,0 +1,78 @@
package org.xyzh.gateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @description Gateway 动态路由配置Java 代码方式)
* @author yslg
* @since 2025-12-02
*
* 这是路由配置的第二种方式,与 application.yml 中的配置二选一或配合使用
* 优点:灵活性更高,可以动态添加/修改路由,支持更复杂的路由逻辑
*/
@Configuration
public class GatewayRouteConfig {
/**
* 通过代码方式配置路由(示例,可根据需要启用)
*/
// @Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 认证服务路由
.route("auth-service", r -> r
.path("/auth/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "gateway-service")
)
.uri("lb://auth-service")
)
// 系统服务路由
.route("system-service", r -> r
.path("/system/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "gateway-service")
)
.uri("lb://system-service")
)
// 日志服务路由
.route("log-service", r -> r
.path("/log/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "gateway-service")
)
.uri("lb://log-service")
)
// 文件服务路由
.route("file-service", r -> r
.path("/file/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "gateway-service")
)
.uri("lb://file-service")
)
// WebSocket 路由示例
.route("websocket-route", r -> r
.path("/ws/**")
.uri("lb:ws://system-service")
)
.build();
}
/**
* 动态路由刷新端点(如需要)
* 可以配合 Nacos 配置中心实现动态路由更新
*/
}

View File

@@ -0,0 +1,47 @@
package org.xyzh.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* @description 限流配置 - 基于 Redis 的令牌桶算法
* @author yslg
* @since 2025-12-02
*/
@Configuration
public class RateLimiterConfig {
/**
* 基于 IP 地址的限流
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> {
String ip = exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "unknown";
return Mono.just(ip);
};
}
/**
* 基于用户 ID 的限流(从请求头获取)
*/
// @Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
return Mono.just(userId != null ? userId : "anonymous");
};
}
/**
* 基于 API 路径的限流
*/
// @Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}

View File

@@ -0,0 +1,177 @@
package org.xyzh.gateway.filter;
import com.alibaba.fastjson2.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.MediaType;
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 org.xyzh.common.auth.config.AuthProperties;
import org.xyzh.common.auth.contants.AuthContants;
import org.xyzh.common.auth.token.TokenParser;
import org.xyzh.common.core.domain.ResultDomain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @description Gateway 全局认证过滤器 - 用于验证 JWT Token
* @author yslg
* @since 2025-12-02
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AuthGlobalFilter.class);
@Autowired
private TokenParser tokenParser;
@Autowired
private AuthProperties authProperties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
log.debug("Gateway 请求路径: {}", path);
// 1. 检查认证功能是否启用
if (!authProperties.isEnabled()) {
log.debug("认证功能未启用,直接放行");
return chain.filter(exchange);
}
// 2. 检查是否在白名单中
if (isWhitelisted(path)) {
log.debug("路径在白名单中,跳过认证: {}", path);
return chain.filter(exchange);
}
// 3. 提取 Token
String token = extractToken(request);
if (!StringUtils.hasText(token)) {
log.warn("请求缺少 Token: {}", path);
return unauthorizedResponse(exchange, "未提供认证令牌,请先登录");
}
try {
// 4. 验证 Token
if (tokenParser.isTokenExpired(token)) {
log.warn("Token 已过期: {}", path);
return unauthorizedResponse(exchange, "认证令牌已过期,请重新登录");
}
// 5. 获取用户信息
String userId = tokenParser.getUserIdFromToken(token);
if (!StringUtils.hasText(userId)) {
log.warn("Token 中未找到用户ID: {}", path);
return unauthorizedResponse(exchange, "认证令牌无效");
}
// 6. 验证 Token 有效性
if (!tokenParser.validateToken(token, userId)) {
log.warn("Token 验证失败: userId={}, path={}", userId, path);
return unauthorizedResponse(exchange, "认证令牌验证失败");
}
// 7. 将用户信息添加到请求头中,传递给下游服务
ServerHttpRequest mutatedRequest = request.mutate()
.header(AuthContants.USER_ID_ATTRIBUTE, userId)
.header(AuthContants.TOKEN_ATTRIBUTE, token)
.build();
log.debug("Token 验证成功: userId={}, path={}", userId, path);
// 8. 继续执行过滤器链
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (Exception e) {
log.error("Token 解析或验证异常: path={}, error={}", path, e.getMessage(), e);
return unauthorizedResponse(exchange, "认证令牌解析失败: " + e.getMessage());
}
}
/**
* 检查路径是否在白名单中
*/
private boolean isWhitelisted(String path) {
// 1. 检查认证相关接口
if (authProperties.getAuthPaths() != null) {
for (String pattern : authProperties.getAuthPaths()) {
if (pattern != null && pathMatcher.match(pattern, path)) {
return true;
}
}
}
// 2. 检查通用白名单
if (authProperties.getWhitelist() != null) {
for (String pattern : authProperties.getWhitelist()) {
if (pattern != null && pathMatcher.match(pattern, path)) {
return true;
}
}
}
return false;
}
/**
* 从请求头中提取 Token
*/
private String extractToken(ServerHttpRequest request) {
List<String> headers = request.getHeaders().get(authProperties.getTokenHeader());
if (headers == null || headers.isEmpty()) {
return null;
}
String header = headers.get(0);
if (!StringUtils.hasText(header)) {
return null;
}
// 支持 Bearer 前缀
String prefix = authProperties.getTokenPrefix();
if (StringUtils.hasText(prefix) && header.startsWith(prefix)) {
return header.substring(prefix.length()).trim();
}
// 也支持直接传递 Token不带前缀
return header.trim();
}
/**
* 返回未授权响应
*/
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ResultDomain<Object> result = ResultDomain.failure(HttpStatus.UNAUTHORIZED.value(), message);
String json = JSON.toJSONString(result);
DataBuffer buffer = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
// 优先级设置为最高,确保在其他过滤器之前执行
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,54 @@
package org.xyzh.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.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @description Gateway 全局日志过滤器 - 记录请求信息
* @author yslg
* @since 2025-12-02
*/
@Component
public class LogGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(LogGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
long startTime = System.currentTimeMillis();
String path = request.getURI().getPath();
String method = request.getMethod().name();
String remoteAddress = request.getRemoteAddress() != null
? request.getRemoteAddress().getAddress().getHostAddress()
: "unknown";
log.info("==> Gateway 请求: {} {} | 来源: {}", method, path, remoteAddress);
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
int statusCode = exchange.getResponse().getStatusCode() != null
? exchange.getResponse().getStatusCode().value()
: 0;
log.info("<== Gateway 响应: {} {} | 状态: {} | 耗时: {}ms",
method, path, statusCode, duration);
})
);
}
@Override
public int getOrder() {
// 优先级设置较低,在认证过滤器之后执行
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,70 @@
package org.xyzh.gateway.handler;
import com.alibaba.fastjson2.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
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 org.xyzh.common.core.domain.ResultDomain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @description Gateway 全局异常处理器
* @author yslg
* @since 2025-12-02
*/
@Component
@Order(-1) // 优先级设置为最高
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ResultDomain<Object> result;
HttpStatus httpStatus;
// 根据不同异常类型返回不同的错误信息
if (ex instanceof NotFoundException) {
log.error("服务未找到: {}", ex.getMessage());
httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
result = ResultDomain.failure(httpStatus.value(), "服务暂时不可用: " + ex.getMessage());
} else if (ex instanceof ResponseStatusException rse) {
log.error("响应状态异常: {}", rse.getMessage());
httpStatus = (HttpStatus) rse.getStatusCode();
result = ResultDomain.failure(httpStatus.value(), rse.getReason());
} else if (ex instanceof IllegalStateException) {
log.error("非法状态异常: {}", ex.getMessage());
httpStatus = HttpStatus.BAD_REQUEST;
result = ResultDomain.failure(httpStatus.value(), "请求参数错误: " + ex.getMessage());
} else {
log.error("Gateway 内部错误", ex);
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
result = ResultDomain.failure(httpStatus.value(), "网关内部错误,请稍后重试");
}
response.setStatusCode(httpStatus);
String json = JSON.toJSONString(result);
DataBuffer buffer = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}

View File

@@ -0,0 +1,19 @@
spring:
cloud:
gateway:
routes:
# 开发环境可以添加更详细的路由配置或测试路由
# Nacos 管理界面路由(开发专用)
- id: nacos-console
uri: http://${NACOS_SERVER_ADDR:localhost:8848}
predicates:
- Path=/nacos/**
# 开发环境日志
logging:
level:
org.springframework.cloud.gateway: DEBUG
org.springframework.web.reactive: DEBUG
reactor.netty: DEBUG
org.xyzh: DEBUG

View File

@@ -0,0 +1,133 @@
server:
port: 8080
spring:
application:
name: gateway-service
# 配置中心
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: dev
group: DEFAULT_GROUP
config:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
file-extension: yml
namespace: dev
group: DEFAULT_GROUP
# Gateway 路由配置
gateway:
# 服务发现路由(自动路由)
discovery:
locator:
enabled: false # 关闭自动路由,使用手动配置
# 手动配置路由
routes:
# ==================== 认证服务路由 ====================
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
# ==================== 系统服务路由 ====================
- id: system-service
uri: lb://system-service
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# ==================== 日志服务路由 ====================
- id: log-service
uri: lb://log-service
predicates:
- Path=/log/**
filters:
- StripPrefix=1
# ==================== 文件服务路由 ====================
- id: file-service
uri: lb://file-service
predicates:
- Path=/file/**
filters:
- StripPrefix=1
# 全局跨域配置
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
# Redis 配置(用于限流、缓存)
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
database: 0
timeout: 5000ms
lettuce:
pool:
max-active: 20
max-wait: -1ms
max-idle: 10
min-idle: 5
# 认证配置
auth:
enabled: true
token-header: Authorization
token-prefix: "Bearer "
# 认证接口白名单login/logout/captcha/refresh
auth-paths:
- /auth/login
- /auth/logout
- /auth/captcha
- /auth/refresh
# 通用白名单Swagger、健康检查等
whitelist:
- /actuator/**
- /v3/api-docs/**
- /swagger-ui/**
- /swagger-resources/**
- /webjars/**
- /doc.html
- /favicon.ico
- /error
# Actuator 监控端点
management:
endpoints:
web:
exposure:
include: health,info,gateway
endpoint:
health:
show-details: always
# 日志配置
logging:
level:
org.springframework.cloud.gateway: DEBUG
org.xyzh.gateway: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"

View File

@@ -10,6 +10,7 @@
<modules> <modules>
<module>common</module> <module>common</module>
<module>apis</module> <module>apis</module>
<module>gateway</module>
<module>log</module> <module>log</module>
<module>system</module> <module>system</module>
<module>auth</module> <module>auth</module>