网关问题
This commit is contained in:
396
docs/网关认证方案.md
Normal file
396
docs/网关认证方案.md
Normal 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **模式二:微服务独立认证**
|
||||||
|
|
||||||
|
每个微服务独立验证 JWT,Gateway 不做认证。
|
||||||
|
|
||||||
|
#### 架构流程
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器
|
||||||
|
↓ (带 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 统一认证模式!**
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
121
urbanLifelineServ/gateway/pom.xml
Normal file
121
urbanLifelineServ/gateway/pom.xml
Normal 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>
|
||||||
|
|
||||||
|
<!-- WebFlux(Gateway依赖) -->
|
||||||
|
<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>
|
||||||
@@ -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("========================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 配置中心实现动态路由更新
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
133
urbanLifelineServ/gateway/src/main/resources/application.yml
Normal file
133
urbanLifelineServ/gateway/src/main/resources/application.yml
Normal 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"
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user