Goa 长期以来把 Bearer token 认证绑在 JWT 的 DSL 上——明明只是想校验一个 Authorization: Bearer <token>,却得用 JWTSecurity 来描述设计。v3.27.0 终了这事:新增 BearerSecurity 和 BearerToken,让通用 bearer token API 的设计语言回归本意。同时,生成的代码也在多处做了收紧,方便实际项目开启更严格的类型和 lint 检查。
旧 DSL 的尴尬
在 v3.27.0 之前,如果你的 API 只需要一个标准 Bearer token(不关心 JWT 结构、不解析 claims),设计里只能这样写:
// 旧写法——语义上暗示了 JWT,但实际只做 token 提取
Security(JWTSecurity("token", func() {
Header("Authorization:Bearer")
}))
问题很明显:JWTSecurity 这个名字让读设计的人以为系统要做 JWT 验证和 claims 解析,但生成的代码其实只是从 header 里提取 token 字符串。语义和实现不一致,新人接手时容易误解安全边界。
新 DSL:BearerSecurity 与 BearerToken
v3.27.0 引入了两个新 DSL 元素:
BearerSecurity:在设计层声明一个标准 Bearer token 安全方案,对应 RFC 6750 的Authorization: Bearer <token>格式。BearerToken:在 endpoint 上引用该方案时使用,生成的代码只负责提取和传递 token 字串,不做 JWT 解析。
一个完整的设计示例:
package design
import (
. "goa.design/goa/v3/dsl"
)
var _ = API("myapp", func() {
Title("My Application")
Version("1.0")
})
// 声明 Bearer 安全方案——不再伪装成 JWT
var BearerAuth = BearerSecurity("bearer")
var _ = Service("orders", func() {
Description("订单管理服务")
// 在整个服务上应用 Bearer 认证
Security(BearerAuth)
Method("list", func() {
Description("列出当前用户的订单")
Result(ArrayOf(Order))
// 也可以在单个方法上单独声明
// Security(BearerAuth)
})
Method("create", func() {
Description("创建新订单")
Payload(CreateOrderPayload)
Result(Order)
Security(BearerAuth)
})
})
var Order = ResultType("application/vnd.myapp.order", func() {
Attribute("id", String, "订单 ID")
Attribute("total", Float64, "订单金额")
Required("id", "total")
})
var CreateOrderPayload = Type("CreateOrderPayload", func() {
Attribute("item", String, "商品名称")
Attribute("quantity", Int, "数量")
Required("item", "quantity")
})
运行 goa gen myapp/design 生成代码后,你会看到生成的 endpoint handler 里,认证中间件只提取 Authorization header 中 Bearer 后面的 token 字串,把它传给你的业务逻辑——不会尝试解析 JWT claims,也不会引入 JWT 依赖。
生成的服务端如何消费 Token
生成的代码会提供一个 BearerAuth 函数签名,你需要在实现里填充校验逻辑:
package orders
import (
"context"
"errors"
"strings"
"myapp"
)
// orders_service.go —— Goa 生成的接口会要求你实现这个函数
func BearerAuth(ctx context.Context, token string) (context.Context, error) {
// token 就是从 Authorization: Bearer <token> 提取出来的原始字符串
// 这里做你自己的校验:查数据库、调外部验证服务、或简单判断非空
if strings.TrimSpace(token) == "" {
return ctx, errors.New("empty bearer token")
}
// 示例:把 token 解析后的用户 ID 放进 context
userID, err := validateToken(token)
if err != nil {
return ctx, err
}
ctx = context.WithValue(ctx, myapp.UserIDKey, userID)
return ctx, nil
}
func validateToken(token string) (string, error) {
// 实际项目中替换为你的验证逻辑
// 比如:调用 Auth 服务、查 Redis 缓存、或用本地公钥验签
if token == "valid-test-token" {
return "user-42", nil
}
return "", errors.New("invalid token")
}
关键点:Goa 不再替你决定"token 必须是 JWT"。校验策略完全由你掌控——可以是 opaque token 查库、可以是引用 token 调远程 introspection 接口,也可以是 JWT 但解析逻辑你自己写。
生成代码的严格化改进
除了 DSL 层面的变化,v3.27.0 还对生成代码做了几处收紧,让项目在开启 go vet、staticcheck、或更严格的 golangci-lint 规则时不会因为生成代码而报错:
- 类型断言更精确:生成的类型转换代码不再使用宽泛的
interface{}断言,而是用具体类型,减少go vet的 complaint。 - 未使用变量清理:生成代码中过去可能残留的未使用导入或变量已被消除。
- 错误处理路径补全:部分生成的 error path 之前缺少显式返回,在严格检查下会被标记,现已修正。
如果你之前因为生成代码的 lint 问题不得不在 CI 里加 //nolint 注释或排除生成目录,升级后可以逐步去掉这些豁免,让整个仓库的检查标准统一。
升级与迁移清单
从 v3.26.x 升级到 v3.27.0 的实操步骤:
# 1. 更新 Goa 依赖
go get goa.design/goa/v3@v3.27.0
go mod tidy
# 2. 重新生成代码
goa gen yourpkg/design
# 3. 如果之前用了 JWTSecurity 但实际只做 Bearer 提取,替换 DSL
# 把设计中的 JWTSecurity("token", ...) 改为 BearerSecurity("bearer")
# 把 Security(JWT) 改为 Security(BearerAuth)
# 4. 更新实现层的认证函数签名
# 旧: func JWTAuth(ctx, token string) (context.Context, error)
# 新: func BearerAuth(ctx, token string) (context.Context, error)
# 函数体逻辑通常不变,只需改名
# 5. 运行 lint 检查,确认生成代码不再触发警告
golangci-lint run ./...
几个需要注意的边界:
- 真正的 JWT API 不受影响:如果你的项目确实在用
JWTSecurity解析 claims、提取 scopes,继续用旧 DSL 就行,JWTSecurity没有被移除。 - 生成的 transport 代码有变化:因为 DSL 名变了,生成的 OpenAPI spec 和 HTTP handler 函数名也会变,前端或网关如果硬编码了这些名字,需要同步更新。
- 不要混用:同一个服务里,不要既用
BearerSecurity又用JWTSecurity描述同一个 token 机制——选一个语义准确的即可。
Goa 这次改动不大,但解决了一个真实痛点:DSL 语义和运行行为对齐。如果你的 API 用的是 opaque token 或远程验证,不再需要假装自己在做 JWT。升级成本低,建议在下一个迭代窗口顺手改掉。