微服务 - 网关统一鉴权
一、什么是网关统一鉴权?
网关统一鉴权,顾名思义,就是将原本分散在各个微服务中的身份验证和权限校验逻辑,抽取出来并集中到API网关这一层来统一处理。
- 传统方式(无网关): 每个微服务(如用户服务、订单服务、支付服务)都需要自己实现一套鉴权逻辑,检查Token是否有效、用户是否有权限访问等。这会导致以下一系列问题:
- 代码重复与维护困难:每个服务都需要编写和维护相似的鉴权代码,违反了“Don‘t Repeat Yourself”原则。
- 标准不一:不同的开发团队可能实现不同的鉴权逻辑或安全标准,导致系统整体安全性不一致。
- 性能瓶颈:每次请求都需要在多个服务中进行重复的鉴权操作(如JWT解析、数据库查询),增加延迟。
- 耦合度增加:业务服务需要关心非业务性的安全逻辑,与服务无状态、高内聚的设计理念相悖。
- 统一鉴权方式: 鉴权逻辑只在API网关实现一次。请求到达网关后,网关先进行鉴权,只有通过鉴权的请求才会被转发到后端的微服务。后端微服务可以“信任”网关,无需再次鉴权,只需处理业务逻辑。
二、为什么需要网关统一鉴权?
网关统一鉴权就是为了解决上述问题而生的。它的核心思想是:将鉴权这个横切关注点从各个业务服务中剥离出来,集中到API网关这一层进行处理。其主要好处如下:
- 安全性集中管理:
- 将敏感的安全逻辑集中在一处,避免了安全漏洞分散在各个服务中。一旦发现安全策略需要调整,只需在网关修改,所有服务立即生效。
- 更容易实施统一的安全标准和审计。
- 架构解耦与业务纯净:
- 后端微服务不再需要关心复杂的鉴权逻辑,可以专注于实现业务功能,使得服务更加“纯净”和“高内聚”。
- 服务间的耦合度降低,更容易开发和维护。
- 提升性能:
- 对于非法请求(如Token无效、权限不足),网关可以在最外层直接拦截并拒绝,避免了请求穿透到后端服务,节省了宝贵的后端资源。
- 统一管控与监控:
- 可以方便地在网关层统一添加日志、限流、熔断等管控措施。所有认证和授权失败都可以在网关层面被监控和报警。
三、核心思想与流程
-
核心思想:
- 前置鉴权。在请求到达内部微服务之前,由网关作为一个统一的“安检站”,对所有请求进行身份验证和权限校验。只有通过检查的请求才会被路由到后端的业务服务;失败的请求则直接被网关拦截并返回错误响应。
-
基本流程:
-
客户端请求:客户端(Web、App等)携带访问令牌(通常是JWT)发起请求。
-
请求到达网关:所有外部请求首先到达API网关。
-
令牌提取与验证:
- 网关从请求头(通常是 Authorization: Bearer )或Cookie中提取令牌。
- 进行基础验证,例如检查令牌结构、签名、是否过期等。
-
身份认证:
- 验证令牌真伪:使用预先配置的密钥或公钥验证JWT的签名。
- (可选)检查黑名单:查询令牌是否已被注销(如用户已登出)。
-
权限鉴定:
- 从验证通过的令牌中解析出用户信息(如用户ID、角色、权限列表)。
- 根据请求的路径(URL) 和方法(HTTP Method),判断当前用户是否拥有访问该资源的权限。这一步通常需要查询权限规则或与专门的鉴权服务交互。
-
网关决策:
- 成功:网关将请求(通常会附加解析出的用户信息)路由到目标微服务。微服务无需再次鉴权,可直接处理业务逻辑。
- 失败:网关直接返回 401 Unauthorized(未认证)或 403 Forbidden(无权限)响应,请求不会到达后端服务。
-
请求转发:通过鉴权的请求被转发到相应的业务微服务。
-
四、 关键技术组件与实现
-
API网关:
- Spring Cloud Gateway: 基于Spring 5、Project Reactor的响应式网关,性能高,是当前Spring Cloud生态的首选。
- Netflix Zuul: Spring Cloud旧版本的网关组件,目前已进入维护模式。
- Kong / Apache APISIX: 基于Nginx/OpenResty的高性能、云原生API网关,功能强大,插件生态丰富。
-
认证与授权协议/技术:
- JWT: 最流行的无状态令牌。鉴权服务器签发JWT后,网关只需使用公钥验证其签名即可,无需每次请求都去查询数据库或鉴权服务,性能极高。
- OAuth 2.0 / OIDC: 行业标准的授权框架。网关可以扮演OAuth 2.0资源服务器的角色,验证Access Token。
- 自定义Token: 也可以使用自定义的Token,但需要网关每次去查询鉴权服务来验证Token的有效性,是有状态的。
五、 实践示例(以Spring Cloud Gateway + JWT为例)
步骤1:生成与验证JWT
首先,你需要一个认证服务(通常是独立的微服务,如 auth-service),负责用户登录并颁发JWT。
// 伪代码:在 auth-service 中登录成功后生成JWT
@Service
public class AuthService {public String login(String username, String password) {// 1. 验证用户名密码User user = userService.authenticate(username, password);// 2. 生成JWTString token = Jwts.builder().setSubject(user.getId()) // 用户标识.claim("roles", user.getRoles()) // 用户角色.claim("authorities", user.getAuthorities()) // 用户权限.setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期.signWith(SignatureAlgorithm.HS512, secretKey) // 使用密钥签名.compact();return token;}
}
步骤2:网关统一鉴权过滤器
在网关服务中,创建一个全局过滤器 AuthGlobalFilter,实现 GlobalFilter 接口。
过滤器逻辑:
- 排除登录、注册等白名单路径。
- 从请求头获取 Authorization。
- 使用JWT库(如jjwt)验证Token的签名和过期时间。
- 从JWT的Payload中解析出用户角色和权限。
- 查询权限规则(可以从数据库或配置中心加载),判断用户权限是否能匹配请求路径+方法。
- 通过验证,则将用户信息放入请求头,转发请求;否则,直接返回错误响应。
伪代码示例:
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {@Autowiredprivate JwtUtil jwtUtil; // 一个自定义的JWT工具类@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();// 1. 判断是否为无需鉴权的白名单路径(如登录、注册、公开API)String path = request.getURI().getPath();if (isExcludePath(path)) {return chain.filter(exchange); // 直接放行}// 2. 提取JWT令牌String token = getTokenFromRequest(request);if (StringUtils.isEmpty(token)) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete(); // 返回401}// 3. 验证并解析JWTClaims claims;try {claims = jwtUtil.parseToken(token); // 验证签名和过期时间} catch (Exception e) {response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete(); // 令牌无效,返回401}// 4. (可选)权限鉴定 - 这里以基于路径的简单RBAC为例String userRoles = (String) claims.get("roles");if (!hasPermission(userRoles, path, request.getMethodValue())) {response.setStatusCode(HttpStatus.FORBIDDEN);return response.setComplete(); // 权限不足,返回403}// 5. 鉴权通过,将用户信息添加到请求头,传递给下游服务String userId = claims.getSubject();ServerHttpRequest newRequest = request.mutate().header("X-User-Id", userId).header("X-User-Roles", userRoles).build();ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();return chain.filter(newExchange);}private boolean isExcludePath(String path) {// 从配置文件中读取白名单return Arrays.asList("/auth/login", "/auth/register", "/public/**").contains(path);}private String getTokenFromRequest(ServerHttpRequest request) {String authHeader = request.getHeaders().getFirst("Authorization");if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {return authHeader.substring(7);}return null;}private boolean hasPermission(String userRoles, String path, String method) {// 实现你的权限逻辑:例如,查询数据库或缓存,判断该角色是否有权访问此API// 这里可以简化成:检查 userRoles 是否包含访问此路径所需的角色// 更复杂的可以使用像Spring Security的AccessDecisionManagerreturn permissionService.checkPermission(userRoles, path, method);}@Overridepublic int getOrder() {return -100; // 过滤器执行顺序,数字越小优先级越高}
}
步骤3:业务微服务(无需鉴权)
下游的业务微服务(如 user-service)接收到请求后,可以直接从请求头中获取用户信息,并信任该信息。
@RestController
@RequestMapping("/users")
public class UserController {@GetMapping("/{id}")public User getUser(@PathVariable String id, @RequestHeader("X-User-Id") String currentUserId) {// 直接从网关传递的请求头中获取当前用户ID,无需再次解析JWT// 处理业务逻辑...return userService.getUserById(id);}
}
