微服务 - 网关统一鉴权

一、什么是网关统一鉴权?

网关统一鉴权,顾名思义,就是将原本分散在各个微服务中的身份验证和权限校验逻辑,抽取出来并集中到API网关这一层来统一处理。

  • 传统方式(无网关): 每个微服务(如用户服务、订单服务、支付服务)都需要自己实现一套鉴权逻辑,检查Token是否有效、用户是否有权限访问等。这会导致以下一系列问题:
    • 代码重复与维护困难:每个服务都需要编写和维护相似的鉴权代码,违反了"Don't Repeat Yourself"原则。
    • 标准不一:不同的开发团队可能实现不同的鉴权逻辑或安全标准,导致系统整体安全性不一致。
    • 性能瓶颈:每次请求都需要在多个服务中进行重复的鉴权操作(如JWT解析、数据库查询),增加延迟。
    • 耦合度增加:业务服务需要关心非业务性的安全逻辑,与服务无状态、高内聚的设计理念相悖。
  • 统一鉴权方式: 鉴权逻辑只在API网关实现一次。请求到达网关后,网关先进行鉴权,只有通过鉴权的请求才会被转发到后端的微服务。后端微服务可以"信任"网关,无需再次鉴权,只需处理业务逻辑。

二、为什么需要网关统一鉴权?

网关统一鉴权就是为了解决上述问题而生的。它的核心思想是:将鉴权这个横切关注点从各个业务服务中剥离出来,集中到API网关这一层进行处理。其主要好处如下:

  1. 安全性集中管理:
    • 将敏感的安全逻辑集中在一处,避免了安全漏洞分散在各个服务中。一旦发现安全策略需要调整,只需在网关修改,所有服务立即生效。
    • 更容易实施统一的安全标准和审计。
  2. 架构解耦与业务纯净:
    • 后端微服务不再需要关心复杂的鉴权逻辑,可以专注于实现业务功能,使得服务更加"纯净"和"高内聚"。
    • 服务间的耦合度降低,更容易开发和维护。
  3. 提升性能:
    • 对于非法请求(如Token无效、权限不足),网关可以在最外层直接拦截并拒绝,避免了请求穿透到后端服务,节省了宝贵的后端资源。
  4. 统一管控与监控:
    • 可以方便地在网关层统一添加日志、限流、熔断等管控措施。所有认证和授权失败都可以在网关层面被监控和报警。

三、核心思想与流程

  • 核心思想:

    • 前置鉴权。在请求到达内部微服务之前,由网关作为一个统一的"安检站",对所有请求进行身份验证和权限校验。只有通过检查的请求才会被路由到后端的业务服务;失败的请求则直接被网关拦截并返回错误响应。
  • 基本流程:

    1. 客户端请求:客户端(Web、App等)携带访问令牌(通常是JWT)发起请求。

    2. 请求到达网关:所有外部请求首先到达API网关。

    3. 令牌提取与验证:

      • 网关从请求头(通常是 Authorization: Bearer )或Cookie中提取令牌。
      • 进行基础验证,例如检查令牌结构、签名、是否过期等。
    4. 身份认证:

      • 验证令牌真伪:使用预先配置的密钥或公钥验证JWT的签名。
      • (可选)检查黑名单:查询令牌是否已被注销(如用户已登出)。
    5. 权限鉴定:

      • 从验证通过的令牌中解析出用户信息(如用户ID、角色、权限列表)。
      • 根据请求的路径(URL) 和方法(HTTP Method),判断当前用户是否拥有访问该资源的权限。这一步通常需要查询权限规则或与专门的鉴权服务交互。
    6. 网关决策:

      • 成功:网关将请求(通常会附加解析出的用户信息)路由到目标微服务。微服务无需再次鉴权,可直接处理业务逻辑。
      • 失败:网关直接返回 401 Unauthorized(未认证)或 403 Forbidden(无权限)响应,请求不会到达后端服务。
    7. 请求转发:通过鉴权的请求被转发到相应的业务微服务。

四、 关键技术组件与实现

  1. API网关:

    • Spring Cloud Gateway: 基于Spring 5、Project Reactor的响应式网关,性能高,是当前Spring Cloud生态的首选。
    • Netflix Zuul: Spring Cloud旧版本的网关组件,目前已进入维护模式。
    • Kong / Apache APISIX: 基于Nginx/OpenResty的高性能、云原生API网关,功能强大,插件生态丰富。
  2. 认证与授权协议/技术:

    • JWT: 最流行的无状态令牌。鉴权服务器签发JWT后,网关只需使用公钥验证其签名即可,无需每次请求都去查询数据库或鉴权服务,性能极高。
    • OAuth 2.0 / OIDC: 行业标准的授权框架。网关可以扮演OAuth 2.0资源服务器的角色,验证Access Token。
    • 自定义Token: 也可以使用自定义的Token,但需要网关每次去查询鉴权服务来验证Token的有效性,是有状态的。

五、 实践示例(以Spring Cloud Gateway + JWT为例)

步骤1:生成与验证JWT

首先,你需要一个认证服务(通常是独立的微服务,如 auth-service),负责用户登录并颁发JWT。

java 复制代码
// 伪代码:在 auth-service 中登录成功后生成JWT
@Service
public class AuthService {
    public String login(String username, String password) {
        // 1. 验证用户名密码
        User user = userService.authenticate(username, password);
        // 2. 生成JWT
        String 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中解析出用户角色和权限。
  • 查询权限规则(可以从数据库或配置中心加载),判断用户权限是否能匹配请求路径+方法。
  • 通过验证,则将用户信息放入请求头,转发请求;否则,直接返回错误响应。

伪代码示例:

java 复制代码
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    private JwtUtil jwtUtil; // 一个自定义的JWT工具类

    @Override
    public 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. 验证并解析JWT
        Claims 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的AccessDecisionManager
        return permissionService.checkPermission(userRoles, path, method);
    }

    @Override
    public int getOrder() {
        return -100; // 过滤器执行顺序,数字越小优先级越高
    }
}
步骤3:业务微服务(无需鉴权)

下游的业务微服务(如 user-service)接收到请求后,可以直接从请求头中获取用户信息,并信任该信息。

java 复制代码
@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);
    }
}
相关推荐
Wang's Blog3 小时前
Nestjs框架: gRPC微服务通信及安全实践全解析
安全·微服务·架构·nestjs
拾忆,想起3 小时前
TCP粘包拆包全解析:数据流中的“藕断丝连”与“一刀两断”
java·网络·数据库·网络协议·tcp/ip·哈希算法
梁正雄3 小时前
18、docker-macvlan-2-示例
运维·docker·容器·macvlan
梁正雄3 小时前
17、docker-macvlan-1-理论
运维·docker·macvlan·docker macvlan
唐兴通个人3 小时前
新品上市咨询顾问新药上市顾问培训讲师唐兴通讲PMF从0到1天使用户种子用户冷启动问题
运维·服务器
我想吃余4 小时前
Linux信号(下):信号保存和信号处理
linux·运维·信号处理
网安小白的进阶之路4 小时前
A模块 系统与网络安全 第四门课 弹性交换网络-4
网络·web安全·php
七夜zippoe4 小时前
高性能网络编程实战:用Tokio构建自定义协议服务器
linux·服务器·网络·rust·tokio
桃子不吃李子4 小时前
简单搭建express服务器
运维·服务器·express