Istio 进阶:如何将 JWT 校验失败的“纯文本”响应优雅地改为 JSON 格式?
在微服务架构中,使用 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(如 RequestAuthentication 或 AuthorizationPolicy)并没有提供自定义响应内容的字段。要实现这一功能,我们必须深入到 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等)。
- 你可以使用 Envoy 的动态变量,比如
进阶:基于 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,建议优先使用前者。
如何验证效果?
- 应用配置:使用
kubectl apply -f your-filter.yaml部署配置。 - 模拟失败请求:
- 不带 Token 请求:
curl -I http://your-gateway-ip/api - 带过期 Token 请求:
curl -H "Authorization: Bearer <expired_token>" http://your-gateway-ip/api
- 不带 Token 请求:
- 检查响应:
你应当看到如下格式的响应:{ "code": 401, "message": "身份认证失败,请重新登录", "detail": "Jwt is missing", "success": false }
注意事项与坑点
- 生效范围:上述
EnvoyFilter如果放在istio-system命名空间并选择ingressgateway标签,则全局生效。如果只想针对某个服务,请修改namespace和workloadSelector。 - 版本兼容性:Envoy 的 API 更新较快,请确保你的 Istio 版本对应的 Envoy 支持
v3版本的配置。上述代码在 Istio 1.12 - 1.18 之间测试通过。 - 调试技巧:如果配置不生效,可以检查 Ingress Gateway 的日志。如果有语法错误,Envoy 会在初始化时报错并忽略该 Filter。
通过这种方式,我们不仅解决了前端的解析崩溃问题,还统一了系统的错误输出规范,极大提升了开发者体验。