14.5Gateway安全集成

分类: Spring Cloud Security

Gateway 安全集成

在 API 网关层面实现安全控制是微服务安全的重要环节。本节将学习 Gateway 安全集成。

本节将学习:Gateway 认证过滤器、令牌验证、路由安全配置,以及权限控制。

在商城项目中实现 Gateway 安全集成

步骤1:添加 JWT 依赖

文件路径: mall-microservices/gateway-service/pom.xml

<!-- JWT 支持 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </dependency>

步骤2:创建 JWT 工具类

文件路径: mall-microservices/gateway-service/src/main/java/com/mall/gateway/util/JwtUtil.java

package com.mall.gateway.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; @Component public class JwtUtil { @Value("${jwt.secret:mall-secret-key-for-jwt-token-generation-2024}") private String secret; private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); } public boolean validateToken(String token) { try { Claims claims = parseToken(token); return !claims.getExpiration().before(new Date()); } catch (Exception e) { return false; } } public Long getUserId(String token) { Claims claims = parseToken(token); return claims.get("userId", Long.class); } public String getUsername(String token) { Claims claims = parseToken(token); return claims.getSubject(); } }

步骤3:创建认证过滤器

文件路径: mall-microservices/gateway-service/src/main/java/com/mall/gateway/filter/AuthenticationFilter.java

package com.mall.gateway.filter; import com.mall.gateway.util.JwtUtil; 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.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @Component public class AuthenticationFilter implements GlobalFilter, Ordered { @Autowired private JwtUtil jwtUtil; // 公开路径,不需要认证 private static final List<String> PUBLIC_PATHS = Arrays.asList( "/api/users/login", "/api/users/register", "/api/products", "/api/products/" ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); // 跳过公开接口 if (isPublicPath(path)) { return chain.filter(exchange); } // 获取 Token String token = getToken(request); if (token == null || !jwtUtil.validateToken(token)) { return handleUnauthorized(exchange); } // 将用户信息添加到请求头,传递给下游服务 Long userId = jwtUtil.getUserId(token); String username = jwtUtil.getUsername(token); ServerHttpRequest modifiedRequest = request.mutate() .header("X-User-Id", String.valueOf(userId)) .header("X-Username", username) .build(); return chain.filter(exchange.mutate().request(modifiedRequest).build()); } private String getToken(ServerHttpRequest request) { String authHeader = request.getHeaders().getFirst("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } return null; } private boolean isPublicPath(String path) { return PUBLIC_PATHS.stream().anyMatch(path::startsWith); } private Mono<Void> handleUnauthorized(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); String body = "{\"code\":401,\"message\":\"Unauthorized\",\"data\":null}"; DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return -100; // 优先级最高 } }

自定义过滤器

@Component public class JwtAuthenticationFilter implements GatewayFilter, Ordered { @Autowired private TokenService tokenService; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String token = extractToken(request); if (token != null && tokenService.validateToken(token)) { // 添加用户信息到请求头 ServerHttpRequest modifiedRequest = request.mutate() .header("X-User-Id", tokenService.extractUserId(token)) .build(); return chain.filter(exchange.mutate().request(modifiedRequest).build()); } ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } private String extractToken(ServerHttpRequest request) { String authHeader = request.getHeaders().getFirst("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } return null; } @Override public int getOrder() { return -1; } }

令牌验证

令牌验证服务

@Service public class TokenValidationService { @Autowired private TokenService tokenService; public boolean validateToken(String token) { try { return tokenService.validateToken(token); } catch (Exception e) { return false; } } public UserInfo extractUserInfo(String token) { String username = tokenService.extractUsername(token); List<String> roles = tokenService.extractRoles(token); return new UserInfo(username, roles); } }

验证过滤器

@Component public class TokenValidationFilter implements GlobalFilter { @Autowired private TokenValidationService tokenValidationService; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String token = getToken(request); if (token != null) { if (!tokenValidationService.validateToken(token)) { return handleUnauthorized(exchange); } UserInfo userInfo = tokenValidationService.extractUserInfo(token); ServerHttpRequest modifiedRequest = request.mutate() .header("X-User-Info", userInfo.toJson()) .build(); return chain.filter(exchange.mutate().request(modifiedRequest).build()); } return chain.filter(exchange); } }

步骤4:配置路由安全

更新 Gateway 路由配置

文件路径: mall-microservices/gateway-service/src/main/resources/application.yml

spring: cloud: gateway: routes: # 用户服务路由 - id: user-service-route uri: lb://user-service predicates: - Path=/api/users/** filters: - StripPrefix=1 order: 1 # 商品服务路由(公开接口) - id: product-service-route uri: lb://product-service predicates: - Path=/api/products/** filters: - StripPrefix=1 order: 2 # 订单服务路由(需要认证) - id: order-service-route uri: lb://order-service predicates: - Path=/api/orders/** filters: - StripPrefix=1 order: 3 # 支付服务路由(需要认证) - id: payment-service-route uri: lb://payment-service predicates: - Path=/api/payments/** filters: - StripPrefix=1 order: 4

安全配置说明

商城项目中的安全策略:

  1. 公开接口(不需要认证):

    • /api/users/login - 用户登录
    • /api/users/register - 用户注册
    • /api/products/** - 商品查询(GET请求)
  2. 受保护接口(需要认证):

    • /api/users/** - 用户管理(除登录注册外)
    • /api/orders/** - 订单管理
    • /api/payments/** - 支付管理
    • /api/products/** - 商品管理(POST/PUT/DELETE请求)

安全路由配置

@Configuration public class GatewaySecurityConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("secure-route", r -> r .path("/api/**") .filters(f -> f .filter(new JwtAuthenticationFilter()) .stripPrefix(1) ) .uri("lb://user-service") ) .route("public-route", r -> r .path("/public/**") .uri("lb://public-service") ) .build(); } }

步骤5:实现权限控制

创建权限验证过滤器

文件路径: mall-microservices/gateway-service/src/main/java/com/mall/gateway/filter/PermissionFilter.java

package com.mall.gateway.filter; 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.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @Component public class PermissionFilter implements GlobalFilter, Ordered { // 管理员角色才能访问的路径 private static final List<String> ADMIN_PATHS = Arrays.asList( "/api/users/delete", "/api/products/delete", "/api/orders/cancel" ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); String method = request.getMethod().name(); // 检查是否需要管理员权限 if (isAdminPath(path, method)) { String userRole = request.getHeaders().getFirst("X-User-Role"); if (userRole == null || !"ADMIN".equals(userRole)) { return handleForbidden(exchange); } } return chain.filter(exchange); } private boolean isAdminPath(String path, String method) { // DELETE 操作需要管理员权限 if ("DELETE".equals(method)) { return true; } // 特定路径需要管理员权限 return ADMIN_PATHS.stream().anyMatch(path::startsWith); } private Mono<Void> handleForbidden(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.FORBIDDEN); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); String body = "{\"code\":403,\"message\":\"Forbidden: Insufficient permissions\",\"data\":null}"; DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return -99; // 在认证过滤器之后执行 } }

测试安全集成

测试步骤:

  1. 测试公开接口(不需要Token)

    # 用户登录 curl -X POST http://localhost:8080/api/users/login \ -H "Content-Type: application/json" \ -d '{"username": "testuser", "password": "123456"}' # 查询商品(公开接口) curl http://localhost:8080/api/products/1
  2. 测试受保护接口(需要Token)

    # 创建订单(需要Token) curl -X POST http://localhost:8080/api/orders \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{"userId": 1, "totalAmount": 100.00}' # 无Token访问(应该返回401) curl -X POST http://localhost:8080/api/orders \ -H "Content-Type: application/json" \ -d '{"userId": 1, "totalAmount": 100.00}'
  3. 测试权限控制

    # 普通用户尝试删除商品(应该返回403) curl -X DELETE http://localhost:8080/api/products/1 \ -H "Authorization: Bearer USER_TOKEN" # 管理员删除商品(应该成功) curl -X DELETE http://localhost:8080/api/products/1 \ -H "Authorization: Bearer ADMIN_TOKEN"

角色验证

@Component public class RoleFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String userInfo = request.getHeaders().getFirst("X-User-Info"); if (userInfo != null) { UserInfo user = UserInfo.fromJson(userInfo); if (!hasRequiredRole(user, request)) { return handleForbidden(exchange); } } return chain.filter(exchange); } private boolean hasRequiredRole(UserInfo user, ServerHttpRequest request) { // 角色验证逻辑 return true; } }

官方资源

本节小结

在本节中,我们学习了:

第一个是 Gateway 认证过滤器。 如何实现认证过滤器。

第二个是令牌验证。 如何在网关层验证令牌。

第三个是路由安全配置。 如何配置路由安全。

第四个是权限控制。 如何实现权限控制。

这就是 Gateway 安全集成。在网关层实现安全控制可以统一管理微服务安全。

在下一节,我们将学习服务间安全通信。