Kratos的JWT中间件仅做认证(验证身份),不负责鉴权(判断权限);jwt.Server()只校验签名、过期等并注入Claims,必须手动实现角色比对或集成OpenFGA等授权服务。

JWT认证拦截在Kratos里不是“开箱即用的鉴权”,它只负责认证(你是谁),不解决鉴权(你能干啥)——这点必须先搞清,否则后续权限绕过风险极高。
jwt.Server() 只做签名验证和 Claims 解析,不校验权限
Kratos 的 middleware/auth/jwt/jwt.go 中 jwt.Server() 是一个纯认证中间件:它检查 token 是否有效、未过期、签名正确,并把解析出的 Claims 注入 context.Context。但它不会读取 role 字段、不比对路由白名单、不调用权限服务。
常见误用现象:
- 开发者在
CustomClaims里塞了Roles []string,却没在业务 handler 里手动检查,以为“用了 JWT 就自动鉴权了” - 错误地认为
jwt.WithClaims(&CustomClaims{})会触发权限逻辑 —— 它只是告诉中间件“用这个结构体去反序列化 payload”
实操建议:
- 认证后必须显式提取并校验权限字段,例如:
claims, ok := jwt.FromContext(ctx).(*CustomClaims),再判断claims.Roles是否包含当前接口所需角色 - 不要把权限判断逻辑散落在每个 handler 里,应封装成独立中间件(如
authz.RoleRequired("admin")) - 若用 RBAC,建议把角色-权限映射关系抽离为配置或数据库,避免硬编码在中间件参数中
鉴权逻辑必须单独实现,推荐放在 transport 层中间件链中
Kratos 的中间件执行顺序是倒序组合(Chain(m1, m2, m3) 实际先执行 m3),所以典型链路应为:jwt.Server() → authz.RoleRequired() → 业务 handler。
一个轻量级角色鉴权中间件示例:
func RoleRequired(allowed ...string) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req any) (any, error) {
claims, ok := jwt.FromContext(ctx).(*CustomClaims)
if !ok {
return nil, errors.Unauthorized("no valid claims")
}
for _, r := range allowed {
if slices.Contains(claims.Roles, r) {
return handler(ctx, req)
}
}
return nil, errors.PermissionDenied("insufficient role")
}
}
}
使用时注意:
- 必须确保
jwt.Server()已在该中间件之前执行,否则jwt.FromContext(ctx)拿不到 Claims - 若用 gRPC,需确认 transport/grpc/server.go 中已将 JWT Claims 正确注入 context(默认支持);HTTP 则依赖
transport/http/server.go的FromContext实现 - 性能影响:每次请求都做字符串遍历,万级并发下建议预构建
map[string]struct{}缓存角色集
复杂场景别硬写中间件,直接对接 Zanzibar 类系统
当权限模型超出简单角色判断(比如 “用户A能编辑文档B,仅当B属于团队C且A是C的管理员”),手写中间件极易出错,且难以审计。
Kratos 生态已有成熟集成路径:
- OpenFGA:通过
openfga/go-sdk调用其 Check API,把ctx中的 subject/object/relation 透传过去,返回布尔结果 - Ory Keto:适配
keto-client-go,利用其check端点做实时授权决策 - 关键点:这类系统需要你提前建模关系元组(tuple),例如
{"user:alice", "editor", "doc:report"},Kratos 中间件只负责构造并发起请求
容易踩的坑:
- 网络延迟导致鉴权超时 —— 必须设置合理 timeout(建议 ≤200ms),失败策略设为 deny-by-default
- OpenFGA 的 store ID 需从 config 动态加载,不能写死在中间件构造函数里
- 本地开发时缺少 FGA 服务,要用
if os.Getenv("ENV") == "dev"短路为 mock 返回,但上线前务必删掉
最常被忽略的一点:JWT 里的 exp 是时间戳(秒级),而 Go 的 time.Now().Unix() 是整数,但某些自定义 Claims 结构体误用 int64 做 ExpiresAt 字段,导致与标准库解析不一致 —— 最终 token 提前失效或永不失效。务必用 jwt.StandardClaims 或严格对齐类型。


















