2、网关统一认证 + 服务内部鉴权
这是一个非常好的问题,它触及了微服务架构设计的核心挑战之一。对于Spring Cloud电商项目,用户认证与鉴权的处理方式直接影响到系统的安全性、复杂度和可维护性。
我会从理论推荐、实际项目做法以及具体实现三个层面来为你详细解答。
一、理论推荐:网关统一认证 + 服务内部鉴权
这是目前最主流、最被推荐的架构模式。其核心思想是:将身份认证(Authentication)这个动作与业务逻辑解耦,在API网关层统一处理;而将权限鉴定(Authorization)下放到各个微服务,由它们根据自身业务需求灵活处理。
1. 认证 (Authentication) - 放在API网关
为什么放在网关?
- 单一职责与关注点分离:认证是一个横切关注点,几乎所有请求都需要。如果每个微服务都自己实现一遍认证,会造成大量的代码冗余和维护负担。网关作为所有外部流量的入口,是处理这类跨领域问题的理想场所。
- 安全边界:网关是系统的“门卫”,它将不可信的外部请求转换为内部可信的请求。一旦在网关认证通过,该请求就可以被信任地在内部服务间传递,内部服务无需再次验证用户身份的真伪(但需要验证权限)。
- 性能:避免在每个服务中重复进行令牌解析、校验等操作,减少网络开销(如每次校验都调用认证服务)。网关校验一次后,可以将用户信息(如UserID、角色等)直接传递给下游服务。
- 易于管理:证书、密钥等敏感信息可以集中管理在网关层,降低了泄露风险。
网关的工作流程:
- 拦截所有进入的HTTP请求。
- 检查请求中是否携带Token(通常是JWT,放在
Authorization
header中)。 - 校验Token的有效性(签名是否正确、是否过期等)。
- 如果Token无效或缺失,直接返回
401 Unauthorized
错误,请求不会进入下游微服务。 - 如果Token有效,解析出其中的用户信息(如用户ID、用户名、角色列表等)。
- 将解析出的用户信息以HTTP Header的形式(例如
X-User-Id
,X-User-Roles
)添加到请求中,然后将请求转发给下游的微服务。
2. 鉴权 (Authorization) - 放在各个微服务
为什么下放到微服务?
- 业务相关性:权限鉴定通常与具体业务紧密相关。例如:
- 订单服务:需要鉴定用户是否有权限查询某个订单(“这个订单是不是你的?”)。
- 商品管理服务:需要鉴定用户角色是否是“管理员”才有权上下架商品。
- 优惠券服务:需要鉴定用户是否满足领取某种优惠券的条件。
- 灵活性:每个微服务对自己的资源拥有最完整的上下文信息,可以做出最精确的权限判断。如果将所有鉴权逻辑都集中到网关,网关会变得异常臃肿且难以维护。
微服务的工作流程:
- 接收从网关转发过来的请求,请求头中已包含用户身份信息。
- 从请求头中提取用户信息(如UserID、角色)。
- 根据当前请求的资源和要执行的操作,结合用户信息,执行鉴权逻辑。
- 简单场景:使用Spring Security的
@PreAuthorize("hasRole('ADMIN')")
或@PreAuthorize("#userId == principal.username")
等注解。 - 复杂场景:在Service方法中编写自定义逻辑,例如查询数据库,判断当前用户是否是某个资源的所有者。
- 简单场景:使用Spring Security的
- 如果鉴权通过,正常处理业务;如果不通过,抛出异常(如
AccessDeniedException
),返回403 Forbidden
错误。
二、真实的实际项目开发中的处理方式
在实际项目中,理论会结合一些具体的技术栈和优化策略。
1. 技术栈选型
- API网关:Spring Cloud Gateway(主流推荐)或 Netflix Zuul。
- 认证方式:JWT (JSON Web Token) 是绝对的主流。因为它自包含、无状态,非常适合分布式场景,无需在服务端存储会话。
- 鉴权框架:Spring Security +
OAuth2.0
资源服务器模式。Spring Security提供了强大且灵活的安全能力,OAuth2.0是现代API安全的工业标准。
2. 具体实现细节(以Spring Cloud Gateway + JWT + Spring Security为例)
步骤一:网关统一认证
在Spring Cloud Gateway中,通过实现一个GlobalFilter
来拦截和校验Token。
@Component
public class AuthenticationFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = extractToken(exchange.getRequest());if (token == null) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}try {// 使用JWT工具类校验并解析TokenClaims claims = JwtUtils.parseToken(token); String userId = claims.getSubject();String roles = claims.get("roles", String.class);// 将用户信息添加到请求Header,转发给下游服务ServerHttpRequest mutatedRequest = exchange.getRequest().mutate().header("X-User-Id", userId).header("X-User-Roles", roles).build();return chain.filter(exchange.mutate().request(mutatedRequest).build());} catch (Exception e) {// Token无效或过期exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}}private String extractToken(ServerHttpRequest request) {// 从Authorization头提取TokenList<String> headers = request.getHeaders().get("Authorization");if (headers == null || headers.isEmpty()) {return null;}String header = headers.get(0);if (header.startsWith("Bearer ")) {return header.substring(7);}return null;}
}
步骤二:微服务内部鉴权
在订单服务、用户服务等业务微服务中配置Spring Security。
# application.yml
spring:security:oauth2:resourceserver:jwt:issuer-uri: http://auth-server # 配置JWT签发者,用于验签
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法级安全注解
public class SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authz -> authz.requestMatchers("/api/orders/**").authenticated() // 需要认证.anyRequest().permitAll()).oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); // 配置为JWT资源服务器return http.build();}
}
在Service或Controller层使用注解进行鉴权:
@RestController
@RequestMapping("/api/orders")
public class OrderController {@GetMapping("/{orderId}")// 方法一:使用SpEL表达式,要求当前认证主体的用户名等于订单资源中的ownerId字段@PreAuthorize("@orderService.canAccess(#orderId, principal.claims['userId'])")// 方法二:简单角色校验// @PreAuthorize("hasAuthority('ROLE_USER')")public Order getOrder(@PathVariable String orderId) {// ... 业务逻辑}
}@Service
public class OrderService {// 供@PreAuthorize调用的鉴权方法public boolean canAccess(String orderId, String userId) {Order order = findOrderById(orderId);return order != null && order.getUserId().equals(userId);}
}
三、总结与关键要点
处理环节 | 推荐位置 | 核心技术 | 职责 |
---|---|---|---|
认证 (Authentication) | API 网关 | JWT, Spring Cloud Gateway Filter | 校验Token真伪,确认用户身份 |
鉴权 (Authorization) | 各个微服务 | Spring Security @PreAuthorize , 自定义逻辑 | 校验用户是否有权限执行操作 |
真实项目中的额外考量:
- Token如何下发? 会有一个独立的认证服务(Auth Service) 专门负责登录(
/oauth/token
)和注册。它负责生成JWT。这个服务不处理业务,只负责“发身份证”。 - 内部服务调用(Feign)如何传递身份? 需要使用Feign拦截器,自动将当前请求的用户信息(来自网关添加的Header)再传递给下一个服务。
@Bean public FeignRequestInterceptor feignRequestInterceptor() {return template -> {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest();String userId = request.getHeader("X-User-Id");template.header("X-User-Id", userId);}}; }
- 黑名单/令牌注销:JWT的无状态性是优点也是缺点。如果需要实现“立即下线”功能,需要在网关或认证服务维护一个令牌黑名单,或者采用短期JWT配合刷新令牌(Refresh Token)的策略。
- 监控与日志:在网关层记录所有认证失败和成功的行为,便于安全审计。
总而言之,“网关认证,服务鉴权” 是经过大量实践检验的、平衡了安全、性能和复杂度的最佳方案,也是你在开发Spring Cloud电商项目时应该首先考虑的架构设计。