WEBKT

Istio 进阶:如何将 JWT 校验失败的“纯文本”响应优雅地改为 JSON 格式?

4 0 0 0

在微服务架构中,使用 Istio 的 RequestAuthentication 进行 JWT 校验是常规操作。然而,很多开发者在实战中都会遇到一个头疼的问题:当 JWT 过期、缺失或非法时,Istio(底层的 Envoy)默认会返回一个 401 Unauthorized 状态码,但响应体却是纯文本(例如 Jwt is missing)。

对于现代化的前后端分离项目,前端通常配置了统一的 Axios 拦截器,期望所有响应都是 application/json 格式。如果此时收到一段纯文本,前端在执行 JSON.parse 时就会直接抛出异常,导致逻辑中断。

本文将分享如何通过 EnvoyFilter 修改 Istio 的默认行为,将 JWT 校验失败的响应定制为标准的 JSON 格式。

为什么 Istio 默认返回纯文本?

Istio 的 JWT 校验是由 Envoy 的 jwt_authn 过滤器实现的。当校验不通过时,Envoy 会直接在数据面拦截请求并生成一个“局部回复”(Local Reply)。为了保持轻量和通用,Envoy 默认采用了简单的文本描述。

目前,Istio 的标准 CRD(如 RequestAuthenticationAuthorizationPolicy)并没有提供自定义响应内容的字段。要实现这一功能,我们必须深入到 Envoy 层面,通过 EnvoyFilter 进行微操。

核心方案:使用 EnvoyFilter 定制 Local Reply

从 Envoy 1.16+ 开始,支持通过 local_reply_config 对局部回复进行重写。我们可以捕获特定的错误码(如 401),并将其修改为我们需要的 JSON 格式。

1. 编写 EnvoyFilter 配置

下面是一个适用于 Istio 1.1x 及以上版本的 EnvoyFilter 示例。该配置作用于 istio-ingressgateway,当发生 401 错误时,将响应头改为 application/json,并自定义响应体。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: customize-jwt-error-response
  namespace: istio-system # 作用于网关所在的命名空间
spec:
  workloadSelector:
    labels:
      istio: ingressgateway # 匹配入口网关
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
    patch:
      operation: MERGE
      value:
        typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
          local_reply_config:
            mappers:
            - filter:
                status_code_filter:
                  comparison:
                    op: EQ
                    value:
                      default_value: 401
                      runtime_key: "none"
              headers_to_add:
              - header:
                  key: "content-type"
                  value: "application/json"
              body_format_override:
                json_format:
                  code: "%RESPONSE_CODE%"
                  message: "身份认证失败,请重新登录"
                  detail: "%LOCAL_REPLY_BODY%" # 保留 Envoy 原本的错误原因
                  success: false

2. 参数解析

  • mappers: 这是核心部分。它定义了匹配规则,这里我们指定匹配状态码为 401 的响应。
  • headers_to_add: 强制将 Content-Type 设置为 application/json,这是让前端正确解析的关键。
  • body_format_override: 重新定义响应体结构。
    • 你可以使用 Envoy 的动态变量,比如 %RESPONSE_CODE%(响应码)和 %LOCAL_REPLY_BODY%(Envoy 原始的报错信息,如 "Jwt expired")。
    • 通过 json_format 结构,你可以构建完全符合公司 API 规范的字段(如 code, message, data 等)。

进阶:基于 Lua 的更灵活处理

如果你的业务逻辑更复杂(例如需要根据不同的报错内容返回不同的 JSON 结构),可以使用 Lua 脚本。

patch:
  operation: INSERT_BEFORE
  value:
    name: envoy.filters.http.lua
    typed_config:
      "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
      inline_code: |
        function envoy_on_response(response_handle)
          local status = response_handle:headers():get(":status")
          if status == "401" then
            response_handle:headers():replace("content-type", "application/json")
            local body = '{"code": 401, "msg": "Token无效或已过期"}'
            response_handle:body():setBytes(body)
          end
        end

注意:Lua 脚本虽然灵活,但性能开销略高于配置式的 local_reply_config,建议优先使用前者。

如何验证效果?

  1. 应用配置:使用 kubectl apply -f your-filter.yaml 部署配置。
  2. 模拟失败请求
    • 不带 Token 请求:curl -I http://your-gateway-ip/api
    • 带过期 Token 请求:curl -H "Authorization: Bearer <expired_token>" http://your-gateway-ip/api
  3. 检查响应
    你应当看到如下格式的响应:
    {
      "code": 401,
      "message": "身份认证失败,请重新登录",
      "detail": "Jwt is missing",
      "success": false
    }
    

注意事项与坑点

  1. 生效范围:上述 EnvoyFilter 如果放在 istio-system 命名空间并选择 ingressgateway 标签,则全局生效。如果只想针对某个服务,请修改 namespaceworkloadSelector
  2. 版本兼容性:Envoy 的 API 更新较快,请确保你的 Istio 版本对应的 Envoy 支持 v3 版本的配置。上述代码在 Istio 1.12 - 1.18 之间测试通过。
  3. 调试技巧:如果配置不生效,可以检查 Ingress Gateway 的日志。如果有语法错误,Envoy 会在初始化时报错并忽略该 Filter。

通过这种方式,我们不仅解决了前端的解析崩溃问题,还统一了系统的错误输出规范,极大提升了开发者体验。

云原生老陈 IstioJWT

评论点评