使用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。

配置

  1. 创建第三个微服务项目: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
    2
    spring.application.name=gateway-service
    spring.cloud.nacos.discovery.server-addr=localhost:8848

测试

  1. 在IDEA中创建seckill-api的第二个启动配置,通过 -Dserver.port=8088 让其在8088端口启动。
  2. 可以看到,Nacos UI中seckill-api服务显示有两个健康实例:
  3. 利用 JMeter 进行测试,发现 seckill-api 两个应用服务有交替日志打出:
  4. order-service 应用服务正常运行

V4.3 安全网关:统一身份认证

  • V4.2的网关虽然智能,但是来者不拒,seckill-api的接口 …(@RequestParam Long userId) 盲目信任了URL中的userId,不合法用户可以伪造userId刷单。
  • 在V4.3,希望能够在网关层实现统一身份认证,只有持令牌(Token)的合法用户才能进入,并且UserId必须从令牌中安全的提取,而不是由用户随意提供。为此,引入Spring Security + JWT (JSON Web Tokens)。

配置

  1. 创建第四个微服务项目:auth-service
  • 职责:提供/auth/login接口,验证用户,并使用jjwt库和密钥签发一个包含userId的JWT Token。
  • pom.xml : 引入 web, nacos-discovery, bootstrap, jjwt。
  • application.properties :
    1
    2
    3
    4
    5
    6
    server.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
    @Component
    public class JwtUtil {

    @Value("${jwt.secret-key}")
    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
    @RestController
    @RequestMapping("/auth")
    public class AuthController {

    @Autowired
    private JwtUtil jwtUtil;

    // 模拟登录
    @PostMapping("/login")
    public LoginResponse login(@RequestBody 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");
    }
    }

    @Data
    class LoginRequest {
    private String username;
    private String password;
    }

    @Data
    class LoginResponse {
    private String token;
    public LoginResponse(String token) {
    this.token = token;
    }
    }
  1. 修改 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
    19
    server.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
    @Component
    public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    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);
    }

    @Override
    public int getOrder() {
    return -1; // 保证这个过滤器在所有路由过滤器之前执行
    }
    }
  1. 修改 seckill-api
  • 修改 SeckillController.java:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @RestController
    public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    // 【修改】秒杀主接口
    @PostMapping("/seckill/{productId}")
    public String submitSeckillOrder(@PathVariable Long productId,
    // 【关键】不再使用 @RequestParam
    // @RequestParam Long userId

    // 【关键】从请求头中获取 Gateway 注入的 userId
    @RequestHeader("X-User-Id") Long userId) {

    return seckillService.submitSeckillOrder(productId, userId);
    }

    }

主要流程

流程A 获得token
流程B 携带令牌发起请求

测试

  1. 启动四个项目。
  2. 通过 JMeter 构建 POST http://localhost/auth/login (带 Content-Type: application/json 和 body),成功获取Token。
  3. 通过 JMeter 构建 POST http://localhost/seckill/1(没带Token),失败,返回 401。
  4. 通过 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)对请求/响应进行修改。

统一身份认证

  1. 为什么需要 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,只需要用本地的密钥验证一下“签名”是否伪造,就能确认用户的身份。
  1. 为什么在“网关层”做统一认证?
  • 职责单一: 否则,每一个微服务(seckill-api, order-service 等)都必须自己写一套“验证 Token”的逻辑,这造成了巨大的代码冗余和安全风险。
  • 安全隔离: 通过在 gateway-service 这个唯一入口进行统一拦截和鉴权,可以确保任何到达后端业务微服务的请求,都必定是已经通过身份验证的、可信的请求。

本站由 Xylumina 使用 Stellar 1.30.0 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本"页面"访问 次 | 总访问 次 | 总访客