使用Spring Cloud Gateway代替Nginx,并实现JWT统一鉴权。
V4.2 智能网关:引入 Spring Cloud Gateway
- 在V4.1中,我虽然实现了服务间的Feign调用,但是系统的大门Nginx是定向的,在 nginx.conf 中,proxy_pass指令硬编码了 http://host.docker.internal:8080 ,即Nginx不知道Nacos的存在,如果为了扩展性启动多个服务实例,Nginx无法自动发现它们,它只能定向的把请求传递给在配置文件中指明的那一个。无法实现负载均衡。
- 在V4.2中,希望实现一个能与Nacos自动对话,动态获取服务列表,智能转发流量的网关。引入Spring Cloud Gateway。
配置
- 创建第三个微服务项目:gateway-service
- 依赖:gateway(核心),nacos-discovery(找路),loadbalancer(选路)
- application.properties:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# 网关在 80 端口启动,替换掉 Nginx
server.port=80
# 告诉 Nacos 你的真实地址
spring.cloud.nacos.discovery.ip=localhost
# --- 路由规则 ---
# 激活网关从 Nacos 自动发现服务的能力
spring.cloud.gateway.discovery.locator.enabled=true
# 定义第一条路由规则
spring.cloud.gateway.routes[0].id=seckill_api_route
# 【核心】 "lb://seckill-api"
# "lb" 的意思是:请使用 Nacos + LoadBalancer
# 去自动查找所有名为 "seckill-api" 的实例,并负载均衡
spring.cloud.gateway.routes[0].uri=lb://seckill-api
# 匹配条件:所有以 /seckill/ 开头的请求
spring.cloud.gateway.routes[0].predicates[0]=Path=/seckill/** - bootstrap.properties:
1
2spring.application.name=gateway-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
测试
- 在IDEA中创建seckill-api的第二个启动配置,通过 -Dserver.port=8088 让其在8088端口启动。
- 可以看到,Nacos UI中seckill-api服务显示有两个健康实例:

- 利用 JMeter 进行测试,发现 seckill-api 两个应用服务有交替日志打出:


- order-service 应用服务正常运行

V4.3 安全网关:统一身份认证
- V4.2的网关虽然智能,但是来者不拒,seckill-api的接口 …(@RequestParam Long userId) 盲目信任了URL中的userId,不合法用户可以伪造userId刷单。
- 在V4.3,希望能够在网关层实现统一身份认证,只有持令牌(Token)的合法用户才能进入,并且UserId必须从令牌中安全的提取,而不是由用户随意提供。为此,引入Spring Security + JWT (JSON Web Tokens)。
配置
- 创建第四个微服务项目:auth-service
- 职责:提供/auth/login接口,验证用户,并使用jjwt库和密钥签发一个包含userId的JWT Token。
- pom.xml : 引入 web, nacos-discovery, bootstrap, jjwt。
- application.properties :
1
2
3
4
5
6server.port=8082
spring.application.name=auth-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.ip=localhost
# 【关键】定义签名的密钥
jwt.secret-key=a-very-long-and-secure-secret-key-for-my-seckill-project - 新建JwtUtil.java文件 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class JwtUtil {
private String secretKey;
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secretKey.getBytes());
}
// 生成 Token (简化版)
public String generateToken(String userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setSubject(userId) // 将 userId 存入 subject
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时过期
.signWith(getSecretKey())
.compact();
}
// 解析所有 Claims
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 验证 Token 是否有效
public boolean isTokenValid(String token) {
try {
return !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
// 检查 Token 是否过期
private boolean isTokenExpired(String token) {
return getAllClaimsFromToken(token).getExpiration().before(new Date());
}
// 从 Token 中获取 UserID
public String getUserIdFromToken(String token) {
return getAllClaimsFromToken(token).getSubject();
}
} - AuthController.java :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AuthController {
private JwtUtil jwtUtil;
// 模拟登录
public LoginResponse login( LoginRequest loginRequest) {
// 【简化】在真实项目中,这里需要查询数据库并验证密码
// 我们暂时“假装”验证成功,只要用户名是 "user"
if ("user".equals(loginRequest.getUsername()) && "123456".equals(loginRequest.getPassword())) {
// 假设我们从数据库查到了该用户,ID为 "123"
String mockUserId = "123";
String token = jwtUtil.generateToken(mockUserId, loginRequest.getUsername());
return new LoginResponse(token);
}
throw new RuntimeException("Invalid credentials");
}
}
class LoginRequest {
private String username;
private String password;
}
class LoginResponse {
private String token;
public LoginResponse(String token) {
this.token = token;
}
}
- 修改 gateway-service
- 修改 pom.xml: 添加 jjwt 依赖三件套。
- 修改 application.properties:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server.port=80
spring.application.name=gateway-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.ip=localhost
# 【新增】JWT 密钥 (必须与 auth-service 完全一致)
jwt.secret-key=a-very-long-and-secure-secret-key-for-my-seckill-project
# --- 路由规则 ---
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.routes[0].id=seckill_api_route
spring.cloud.gateway.routes[0].uri=lb://seckill-api
spring.cloud.gateway.routes[0].predicates[0]=Path=/seckill/**
# 【新增】auth-service 的路由
spring.cloud.gateway.routes[1].id=auth_service_route
spring.cloud.gateway.routes[1].uri=lb://auth-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/auth/** - 增加 JwtUtil.java ,与 auth-service 项目中的 JwtUtil.java文件一致。
- 新建 AuthGlobalFilter.java ,创建全局认证过滤器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private JwtUtil jwtUtil;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 1. 白名单:登录接口,直接放行
if (path.contains("/auth/login")) {
return chain.filter(exchange);
}
// (可选) 放行 Prometheus 抓取
if (path.contains("/actuator")) {
return chain.filter(exchange);
}
// 2. 检查 Token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 拒绝
}
// 3. 验证 Token
String jwt = token.substring(7); // 截掉 "Bearer "
if (!jwtUtil.isTokenValid(jwt)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 拒绝
}
// 4. 【核心】将用户信息“注入”到下游请求中
String userId = jwtUtil.getUserIdFromToken(jwt);
// 修改原始请求,添加一个安全的 Header
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.build();
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
// 5. 放行(使用被修改过的请求)
return chain.filter(mutatedExchange);
}
public int getOrder() {
return -1; // 保证这个过滤器在所有路由过滤器之前执行
}
}
- 修改 seckill-api
- 修改 SeckillController.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SeckillController {
private SeckillService seckillService;
// 【修改】秒杀主接口
public String submitSeckillOrder( Long productId,
// 【关键】不再使用
// Long userId
// 【关键】从请求头中获取 Gateway 注入的 userId
Long userId) {
return seckillService.submitSeckillOrder(productId, userId);
}
}
主要流程
流程A 获得token

流程B 携带令牌发起请求

测试
- 启动四个项目。

- 通过 JMeter 构建 POST http://localhost/auth/login (带 Content-Type: application/json 和 body),成功获取Token。
- 通过 JMeter 构建 POST http://localhost/seckill/1(没带Token),失败,返回 401。
- 通过 JMeter 构建 POST http://localhost/seckill/1,并在 Header 中添加 Authorization: Bearer …。成功,受到 200 OK和“请求已接收…”。seckill-api后台日志显示已经获取到了这个userId,order-service 也正常处理消息队列。


学学八股
Spring Cloud Gateway
- Spring Cloud Gateway 是 Spring Cloud 生态中的第二代微服务网关(取代了 Netflix Zuul)。它基于 Spring 5、Spring Boot 2 和 Project Reactor,提供了异步非阻塞的、高性能的路由能力。
- Gateway 的三大核心概念
- Route (路由): 网关配置的基本单元。它包含一个 ID、一个目标 URI、一组 Predicate 和一组 Filter。
- Predicate (断言/匹配器): 这是路由的“匹配条件”。它告诉网关:“什么样的请求才归我管?”。
- Filter (过滤器): 这是路由的“加工逻辑”。它能在请求被转发到下游服务“之前”(pre-filter)或“之后”(post-filter)对请求/响应进行修改。
统一身份认证
- 为什么需要 JWT (JSON Web Token)?
- 单体时代 (Session): 用户登录后,服务器在内存里保存一份 Session,并给客户端一个 SessionID (存在 Cookie 里)。
- 微服务时代 (JWT): 微服务是无状态的。gateway-service 和 seckill-api 是两个独立的应用,它们不共享内存。如果使用 Session,seckill-api 根本不认识 gateway-service 创建的 Session。
- JWT 的优势: JWT 是一种“无状态”的认证机制。服务器在用户登录后,生成一个“令牌 (Token)”并交给客户端。这个 Token 本身就包含了用户信息(userId)和防伪签名(Signature)。
- 客户端每次请求都携带这个 Token。服务器收到后,不需要查数据库或 Session,只需要用本地的密钥验证一下“签名”是否伪造,就能确认用户的身份。
- 为什么在“网关层”做统一认证?
- 职责单一: 否则,每一个微服务(seckill-api, order-service 等)都必须自己写一套“验证 Token”的逻辑,这造成了巨大的代码冗余和安全风险。
- 安全隔离: 通过在 gateway-service 这个唯一入口进行统一拦截和鉴权,可以确保任何到达后端业务微服务的请求,都必定是已经通过身份验证的、可信的请求。






