导读:本期聚焦于陈远山创作的《怎么在网关层利用过滤器Filter拦截并统一封装鉴权异常》,敬请观看详情。微服务架构下,客户端的请求往往先经过网关再转发到后端服务,鉴权逻辑如果分散在各个业务模块里,既难维护也容易出安全漏洞。把Token校验统一放到网关层的全局过滤器中处理,遇到无效Token、过期凭证或权限不足时,直接在网关侧拦截并返回格式统一的错误响应,是目前比较主流的做法。本文以Spring Cloud Gateway为例,讲解如何自定义GatewayFilter和GlobalFilter,在过滤器中完成Token解析与校验,如何拦截认证过程中的各类异常,以及怎样设计一个统一的响应结构,让网关返回的错误信息与业务服务保持一致,最后还会给出过滤器执行顺序和异常传播方面的注意事项。

在微服务项目中,网关承担着请求入口的角色,所有外部请求都要先过网关这一关。如果鉴权逻辑散落在各个业务服务里,不仅代码重复,一旦某个服务漏写了校验就会形成安全缺口。把鉴权统一收敛到网关层,通过全局过滤器拦截请求、解析Token,遇到异常时直接返回统一的错误结构,是最常见的实践方案。这篇文章以Spring Cloud Gateway为基础,完整走一遍过滤器拦截和异常封装的实现过程。

怎么在网关层利用过滤器Filter拦截并统一封装鉴权异常

一、网关过滤器的基本原理与选择

Spring Cloud Gateway提供了两种过滤器接口:GlobalFilterGatewayFilter。前者对经过网关的所有路由生效,不需要额外配置;后者则需要绑定到具体路由上,适合针对个别服务做定制处理。做统一鉴权显然应该选择GlobalFilter,这样无论请求要转发到哪个后端服务,都会先经过我们的校验逻辑。

过滤器的执行顺序由getOrder方法的返回值决定,数值越小优先级越高。Spring Cloud Gateway内置的过滤器中,NettyWriteResponseFilter的order值是-1,负责把响应写回客户端。自定义鉴权过滤器通常设置在-100左右,确保在转发请求之前完成校验。另外要注意,如果项目里同时存在多个全局过滤器,比如日志过滤器、限流过滤器,order值的设计要保证鉴权在限流之后、路由转发之前执行,避免被限流拦截的请求白白消耗Token解析的性能。

过滤器接口的核心方法签名是Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)。校验通过后调用chain.filter(exchange)把请求交给下一个过滤器;校验失败则不调用链,而是直接向exchange中写入响应,请求到此为止。这个“是否继续调用链”的分支就是网关层拦截的本质。

二、实现一个鉴权全局过滤器

下面写一个完整的鉴权过滤器。它会从请求头中取出Token,校验其有效性,失败时根据不同的错误原因返回不同的HTTP状态码。Token的校验逻辑这里用JWT做示例,实际项目中也可以换成调用认证中心、查询Redis缓存等方式,拦截的结构完全一样。

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst("Authorization");

        // 无Token直接拒绝
        if (token == null || token.isEmpty()) {
            return writeErrorResponse(exchange, 401, "TOKEN_MISSING", "请求未携带令牌");
        }
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }

        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();
            // 校验通过,把用户信息透传给下游服务
            ServerHttpRequest newRequest = request.mutate()
                    .header("X-User-Id", claims.getSubject())
                    .build();
            return chain.filter(exchange.mutate().request(newRequest).build());
        } catch (ExpiredJwtException e) {
            return writeErrorResponse(exchange, 401, "TOKEN_EXPIRED", "令牌已过期");
        } catch (JwtException e) {
            return writeErrorResponse(exchange, 401, "TOKEN_INVALID", "令牌无效");
        }
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

代码里有几个细节值得注意。第一,校验通过后用request.mutate()构造了一个新的请求对象,把解析出的用户ID塞进自定义请求头X-User-Id,下游服务拿到这个头就能直接知道当前用户是谁,不需要重复解析JWT。第二,Token为空、过期、无效这三种情况要区分开,客户端拿到明确的错误码才知道该跳转登录页还是提示刷新令牌。

白名单也是绕不开的需求。登录接口、验证码接口、健康检查等路径不应该做鉴权,可以在过滤器开头加一段路径匹配逻辑,用AntPathMatcher判断当前路径是否命中白名单,命中则直接放行。白名单建议放在配置文件里而不是硬编码,方便运维随时调整。

三、统一封装异常响应结构

网关层返回的错误信息如果不加约束,可能一会儿是纯文本,一会儿是Spring默认的JSON结构,前端处理起来非常痛苦。规范的做法是定义一套统一的响应体,网关和所有业务服务共用。假设约定的结构包含code、message、data三个字段,那么writeErrorResponse方法的实现如下:

private Mono<Void> writeErrorResponse(ServerWebExchange exchange,
                                     int httpStatus,
                                     String code,
                                     String message) {
    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(HttpStatus.valueOf(httpStatus));
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

    Map<String, Object> body = new LinkedHashMap<>();
    body.put("code", code);
    body.put("message", message);
    body.put("data", null);

    DataBuffer buffer = null;
    try {
        byte[] bytes = objectMapper.writeValueAsBytes(body);
        buffer = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(buffer));
    } catch (Exception e) {
        return response.setComplete();
    }
}

这里必须强调一点:网关基于WebFlux,是响应式非阻塞模型,过滤器方法体内严禁写阻塞代码,比如直接的JDBC查询或者同步HTTP调用。如果Token校验必须查数据库或调用远程服务,要么换成响应式客户端WebClient,要么用Schedulers.boundedElastic()把阻塞操作调度到独立线程池,否则高并发下会拖垮整个网关的吞吐量。

还有一个容易踩坑的地方:过滤器里主动抛出的异常,比如直接throw new RuntimeException,默认会被网关的全局异常处理器接管,返回的是Spring自带格式的响应,前面定义的统一结构就白费了。所以在过滤器中处理异常时,优先用上面的writeErrorResponse主动写响应,而不是依赖异常向上抛。如果确实需要在多个过滤器之间传播错误,可以返回Mono.error,然后配合自定义的ErrorWebExceptionHandler兜底:

@Component
@Order(-2)
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        }
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        Map<String, Object> body = new HashMap<>();
        body.put("code", "GATEWAY_ERROR");
        body.put("message", ex.getMessage());
        body.put("data", null);

        byte[] bytes;
        try {
            bytes = objectMapper.writeValueAsBytes(body);
        } catch (Exception e) {
            bytes = "{}".getBytes(StandardCharsets.UTF_8);
        }
        return response.writeWith(Mono.just(response.bufferFactory().wrap(bytes)));
    }
}

四、测试验证与常见问题排查

过滤器写完后需要验证几种典型场景:不带Token访问受保护接口,应返回401和TOKEN_MISSING;携带过期Token,应返回TOKEN_EXPIRED;携带合法Token,请求应正常转发,且下游服务能读到X-User-Id请求头。用curl或者Postman逐个跑一遍即可覆盖。

实际部署中常见的问题有几个。一是过滤器没生效,多半是自定义过滤器类没有加@Component注解,Spring没有把它注册为Bean。二是响应是乱码,通常是没设置Content-Type为application/json导致的。三是下游服务读不到透传的请求头,检查是否在跨域预检OPTIONS请求上就被拦截了,OPTIONS请求本身不带Token,过滤器开头应该直接放行:

if (HttpMethod.OPTIONS.matches(request.getMethod().name())) {
    return chain.filter(exchange);
}

另外建议把错误码设计得足够细,除了令牌相关的三类错误,还可以加上TOKEN_SIGNATURE_INVALID、ACCOUNT_DISABLED等细分码,前端拿到错误码后可以做差异化处理,比如静默刷新令牌重发请求,用户体验会好很多。网关层把鉴权和异常封装做扎实了,后端业务服务就可以专注在权限判断上,整体架构会清爽不少。

网关过滤器统一异常封装Token鉴权修改时间:2026-09-15 07:06:34

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。