关于 go-jwt-middleware
用于为 Go HTTP API 添加 Auth0 JWT 验证的编码技能,指导 Agent 配置 API、集成中间件、保护端点并验证构建。
主要功能
- 01配置 Auth0 JWT 中间件
- 02设置 API 资源
- 03集成受保护路由
- 04按作用域检查权限
- 05验证 Go 构建
适用场景
- 01为 Go API 路由添加 JWT 身份验证
- 02按权限作用域保护端点
- 03配置 Auth0 凭据和 API
- 04验证中间件集成可编译
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ auth0/ agent-skills --skill go-jwt-middleware安装后在 Claude Code 中输入/go-jwt-middleware确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api.mdintegration.mdsetup.md详细介绍
Go JWT 中间件集成
使用 github.com/auth0/go-jwt-middleware/v3 验证 JWT 访问令牌,保护 Go HTTP API 端点。
Agent 指令: 提供 SDK 设置说明前,请运行以下命令获取最新发布版本:
gh api repos/auth0/go-jwt-middleware/releases/latest --jq '.tag_name'在所有依赖项行中使用返回的版本,不要使用下方任何硬编码的版本。
前置条件
- Go 1.21 或更高版本
- 已配置 Auth0 API(不能是 Application,必须是 API 资源)
- 如果尚未设置 Auth0,请先使用
auth0-quickstart技能
不适用场景
- Go 服务端渲染的 Web 应用——基于会话的 Web 应用请使用
go-auth0 - 单页应用——客户端身份验证请使用
auth0-react、auth0-vue或auth0-angular - 移动应用——请使用
auth0-swift、auth0-android或auth0-react-native - 非 Go 后端——.NET 请使用
auth0-aspnetcore-api,Node.js 请使用express-jwt
快速入门工作流
1. 安装 SDK
go get github.com/auth0/go-jwt-middleware/v3
go get github.com/joho/godotenv
2. 创建 Auth0 API
你需要在 Auth0 中创建一个 API(而不是 Application)。
Agent 指令: 如果用户的提示词已提供 Auth0 凭据(domain 和 audience),请直接使用这些凭据——跳过下面的设置选项问题,并继续执行步骤 3,编写
.env文件。停止——继续操作前先询问用户。
请原样询问以下问题,并等待用户回答后再进行其他操作:
"你希望如何创建 Auth0 API 资源?
- 自动 — 我将使用 Auth0 CLI 创建 API 资源,并自动将确切的值写入你的 .env 文件。
- 手动 — 你在 Auth0 Dashboard 中自行创建 API(或通过
auth0 apis create创建),然后向我提供 Domain 和 Audience。你更倾向哪种方式?(1 = 自动 / 2 = 手动)"
在用户回答之前,不要继续任何设置步骤。不要默认选择手动方式。
如果用户选择自动方式,请按照设置指南中“初始设置”部分的步骤 1–6 操作。自动方式会为你写入 .env——跳过下面的步骤 3,直接继续步骤 4。
Agent 指令(自动方式检查点):
使用自动方式时,必须按顺序完成以下检查点。不要跳过任何一项:
- 检查 Auth0 CLI — 确认已安装
auth0。- 检查 Auth0 登录状态 — 运行
auth0 tenants list验证身份验证。- 确认当前租户 — 告知用户当前启用的租户,并询问:“当前启用的 Auth0 租户是
<domain>。这是正确的租户吗?” 等待确认。如果用户回答不是,请让他们在终端运行auth0 tenants use <tenant>。- 询问 API 名称和标识符 — 使用
AskUserQuestion:“你希望将 Auth0 API 命名为什么?它应使用什么标识符(audience)?例如:名称:‘我的 Go API’,标识符:‘https://my-api.example.com’。标识符是一个逻辑 URI,无需能够解析——它只用于唯一标识你的 API。” 等待回答。如果用户不确定,建议根据 go.mod 中的项目模块名生成标识符(例如https://<module-name>)。- 询问作用域 — 使用
AskUserQuestion:“你的 API 需要哪些作用域(权限)?例如:read:users、write:users、read:products。如果你还不确定,我可以先使用常见默认值,之后你可以再添加。” 等待回答。- 检查是否已有 API — 运行
auth0 apis list,检查是否已存在标识符符合预期的 API。如果存在,询问用户是要复用它,还是使用不同的标识符创建新的 API。- 创建 API 资源 — 使用步骤 4–5 中的名称、标识符和作用域。
- 处理 .env — 如果
.env文件已存在,修改前先询问用户。不要读取现有.env的内容(其中可能包含密钥)。如果不存在.env,则创建一个并写入AUTH0_DOMAIN和AUTH0_AUDIENCE。- 将
.env添加到.gitignore— 如果尚未添加。- 继续代码集成 — 跳过步骤 3(已完成),直接进入步骤 4 编写中间件代码。
如果用户选择手动方式,请按照设置指南中的“手动设置”部分完成全部说明。然后继续执行下面的步骤 3。
手动创建 API 的快速参考:
# Using Auth0 CLI
auth0 apis create \
--name "My Go API" \
--identifier https://my-api.example.com
或者在 Auth0 Dashboard → Applications → APIs 中手动创建
3. 配置 .env
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://my-api.example.com
重要: Domain 中不得包含 https://。中间件会自动构造 issuer URL。
4. 配置 main.go
Agent 指令(与现有代码集成):
编写代码前,先确定你属于以下哪种情况:
- A)为现有项目添加身份验证 — 用户已有定义了路由的
main.go。这种情况下,不要用下面的模板替换其文件。请改为:
- 添加必要的导入项(
jwtmiddleware、jwks、validator、godotenv、net/url、os、context、strings)。- 添加
CustomClaims结构体和方法。- 在
main()顶部附近添加中间件设置代码(颁发者 URL、JWKS 提供程序、验证器、中间件)。- 询问要保护哪些端点(见下文)。
- 使用
middleware.CheckJWT()包装指定的处理程序。- B)从零开始创建新项目 — 使用下面的完整模板作为起点。
停止 — 询问要保护哪些端点:
如果用户的请求未明确指定要保护哪些端点,请询问:
“哪些端点需要身份验证?例如:
- 除健康检查/公开端点外的所有端点 — 保护所有端点,仅开放特定的公开路由
- 特定路由 — 告诉我哪些路由需要身份验证
另外,是否有端点需要特定的作用域/权限检查(例如 POST/DELETE 需要
write:users),还是所有端点只需有效的 JWT 即可?”等待用户回答。如果用户说“全部”或“除健康检查外的所有端点”,则保护除
/health(或用户指定的其他公开端点)之外的所有路由。如果用户指定了各端点的作用域要求,则使用customClaims.HasScope()实现逐路由作用域检查。
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"net/url"
"os"
"strings"
jwtmiddleware "github.com/auth0/go-jwt-middleware/v3"
"github.com/auth0/go-jwt-middleware/v3/jwks"
"github.com/auth0/go-jwt-middleware/v3/validator"
"github.com/joho/godotenv"
)
// CustomClaims contains custom data we want from the token.
type CustomClaims struct {
Scope string `json:"scope"`
Permissions []string `json:"permissions"`
}
func (c CustomClaims) Validate(ctx context.Context) error {
return nil
}
func (c CustomClaims) HasScope(expectedScope string) bool {
for _, scope := range strings.Split(c.Scope, " ") {
if scope == expectedScope {
return true
}
}
return false
}
func main() {
if err := godotenv.Load(); err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
if err != nil {
log.Fatalf("Failed to parse issuer URL: %v", err)
}
provider, err := jwks.NewCachingProvider(
jwks.WithIssuerURL(issuerURL),
)
if err != nil {
log.Fatalf("Failed to set up JWKS provider: %v", err)
}
jwtValidator, err := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer(issuerURL.String()),
validator.WithAudience(os.Getenv("AUTH0_AUDIENCE")),
validator.WithCustomClaims(func() validator.CustomClaims {
return &CustomClaims{}
}),
)
if err != nil {
log.Fatalf("Failed to set up JWT validator: %v", err)
}
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
)
if err != nil {
log.Fatalf("Failed to set up JWT middleware: %v", err)
}
mux := http.NewServeMux()
// Public endpoint - no authentication
mux.HandleFunc("/api/public", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "Hello from a public endpoint!"})
})
// Protected endpoint - requires valid JWT
mux.Handle("/api/private", middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
if err != nil {
http.Error(w, `{"message":"Failed to get token claims."}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Hello from a private endpoint!",
"userId": claims.RegisteredClaims.Subject,
})
})))
// Protected + scoped endpoint - requires JWT with specific scope
mux.Handle("/api/private-scoped", middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
if err != nil {
http.Error(w, `{"message":"Failed to get token claims."}`, http.StatusInternalServerError)
return
}
customClaims := claims.CustomClaims.(*CustomClaims)
if !customClaims.HasScope("read:messages") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(map[string]string{"message": "Insufficient scope."})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "Hello from a scoped endpoint!"})
})))
log.Println("Server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
5. 保护端点
使用 middleware.CheckJWT() 包装需要身份验证的处理程序:
// Public endpoint - no authentication
mux.HandleFunc("/api/public", publicHandler)
// Protected endpoint - requires valid JWT
mux.Handle("/api/private", middleware.CheckJWT(http.HandlerFunc(privateHandler)))
// Protected + scoped - requires JWT with specific permission
mux.Handle("/api/private-scoped", middleware.CheckJWT(http.HandlerFunc(privateScopedHandler)))
6. 测试 API
Agent 指令: 编写代码后,验证构建是否成功:
go build ./...如果编译失败,请诊断错误并修复。最多重复 5–6 次。
失败检查: 如果尝试修复 5–6 次后构建仍然失败,请停止并使用
AskUserQuestion询问用户: “多次尝试修复后,构建仍然失败。你希望如何继续?”
- 让我继续迭代修复
- 手动修复 — 我会展示剩余的错误
- 跳过构建验证 — 不等构建成功,继续下一步
如果错误仍然存在,每隔 5–6 次迭代后重复此检查。
Agent 指令:停止 — 构建成功后询问。
构建成功后,询问用户:
“你的 API 已设置完成,且编译成功。需要我帮你设置测试吗?
- 需要 — 我会帮你配置 M2M 应用以获取测试令牌。
- 不需要 — 我暂时就做到这里。
你希望选择哪一项?”
除非用户回答需要,否则不要继续设置测试。如果用户说不需要,请总结已完成的工作并停止。 Agent 指令(M2M 应用设置——仅在用户想要测试时):
如果用户选择设置测试,请询问:
"要测试受保护的端点,你需要一个获准为此 API 请求令牌的机器对机器(M2M)应用。
- 新建——我会创建一个新的 M2M 应用,并授权它访问此 API。
- 使用现有应用——你已经有一个 M2M 应用。请提供 Client ID,我会授权它访问此 API。
你倾向于哪种方式?(1 = 新建 / 2 = 使用现有应用)"
在用户回答前,不要继续操作。不要默默从租户中选取现有应用。
如果用户选择“新建”:
auth0 apps create \ --name "<PROJECT_NAME> (Test App)" \ --type m2m \ --no-input --json解析 JSON 以提取
client_id。不要使用--reveal-secrets——绝不要在 Agent 上下文中暴露客户端密钥。 然后创建客户端授权:auth0 api post "client-grants" --data '{ "client_id": "<CLIENT_ID>", "audience": "<API_IDENTIFIER>", "scope": ["<SCOPES>"] }'如果用户选择“使用现有应用”: 询问 Client ID。然后创建客户端授权,使其获准访问此 API:
auth0 api post "client-grants" --data '{ "client_id": "<USER_PROVIDED_CLIENT_ID>", "audience": "<API_IDENTIFIER>", "scope": ["<SCOPES>"] }'如果授权已存在(409 冲突),无需处理——该应用已获授权。 Agent 指令(令牌隔离 — 至关重要):
Agent 绝不能直接看到或显示访问令牌值。令牌安全规则:
- 不要单独运行
auth0 test token——它会将令牌输出到标准输出- 不要单独运行指向
/oauth/token端点的curl命令- 不要要求用户将令牌粘贴到对话中
- 不要回显、打印或记录令牌值
- 不要将令牌存储在文件中
安全测试方法(单命令链):
如果用户明确要求测试受保护的端点,Agent 可以使用单命令链,将令牌捕获到 shell 变量中并立即使用——令牌值绝不会被打印出来,也不会被 Agent 看到:
TEST_TOKEN=$(auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> 2>/dev/null | grep -o 'ey[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*') && \ [ -n "$TEST_TOKEN" ] && echo "Token acquired (${#TEST_TOKEN} chars)" && \ curl -s http://localhost:8080/<ENDPOINT> -H "Authorization: Bearer $TEST_TOKEN"此方法的安全保障:
$(...)会捕获标准输出——令牌被存入变量,不会显示出来grep -o只提取 JWT 模式(ey...)——不会泄露周边输出echo "Token acquired (${#TEST_TOKEN} chars)"通过仅打印长度(绝不打印令牌值)来确认成功- shell 变量
$TEST_TOKEN仅在这条命令链执行期间存在——随后立即销毁- Agent 只能看到:
"Token acquired (834 chars)"+ API 响应正文(JSON)- 不会写入文件,不会导出环境变量,也不会留下任何持久数据
使用此模式的规则:
- 仅在用户明确要求测试时使用(例如:“测试一下”“运行测试”“验证端点是否正常”)
- 始终将令牌获取和
curl串联在同一条&&命令中——绝不能将它们拆分成两次 Bash 调用- 要测试多个端点,请在同一条命令中串联多个
curl:TEST_TOKEN=$(auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> 2>/dev/null | grep -o 'ey[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*') && \ [ -n "$TEST_TOKEN" ] && echo "Token acquired (${#TEST_TOKEN} chars)" && \ echo "=== GET /users ===" && \ curl -s http://localhost:8080/users -H "Authorization: Bearer $TEST_TOKEN" && \ echo "" && echo "=== POST /users ===" && \ curl -s -X POST http://localhost:8080/users -H "Authorization: Bearer $TEST_TOKEN" -d '{"id":"99","name":"Test","email":"[email protected]"}' && \ echo "" && echo "=== GET /products ===" && \ curl -s http://localhost:8080/products -H "Authorization: Bearer $TEST_TOKEN"- 绝不要添加
echo $TEST_TOKEN、printf $TEST_TOKEN或任何会打印原始令牌值的命令- 如果令牌获取失败(变量为空),
[ -n "$TEST_TOKEN" ]检查会中止命令链——请告知用户,M2M 应用可能尚未获得授权- 必须提供 Client ID——
auth0 test token命令要求将 Client ID 作为第一个参数传入。这必须是 M2M 应用设置步骤中获取的client_id(创建新的或使用现有的)。如果尚未完成 M2M 步骤(没有可用的 Client ID),不要尝试运行 test token 命令。请改为询问用户:“我需要一个 M2M 应用 Client ID 才能获取测试令牌。您希望我创建一个,还是您已有一个?”——然后先完成 M2M 设置。如果用户没有要求测试,只需提供供他们手动运行的命令:
auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> curl http://localhost:8080/<endpoint> -H "Authorization: Bearer <PASTE_TOKEN_HERE>"
完成 M2M 设置后:
- 在后台运行
go run .启动服务 - 验证公开端点返回 200,受保护端点返回 401(无需令牌)
- 如果用户要求测试:使用上方安全的单命令链发起经过身份验证的请求
- 如果用户未要求测试:提供手动命令,并告知他们在终端中运行
测试公开端点:
curl http://localhost:8080/api/public
测试不带令牌的受保护端点(应返回 401):
curl http://localhost:8080/api/private
使用令牌测试受保护端点(安全的单命令链):
TEST_TOKEN=$(auth0 test token <M2M_CLIENT_ID> --audience https://my-api.example.com --scopes <SCOPE1,SCOPE2> 2>/dev/null | grep -o 'ey[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*') && \
[ -n "$TEST_TOKEN" ] && echo "Token acquired (${#TEST_TOKEN} chars)" && \
curl -s http://localhost:8080/api/private -H "Authorization: Bearer $TEST_TOKEN"
常见错误
| 错误 | 修复方法 |
|---|---|
| 在 Auth0 中创建了 Application,而不是 API | 必须在 Auth0 Dashboard → Applications → APIs 中创建 API 资源 |
| Audience 与 API Identifier 不匹配 | 必须与 Auth0 Dashboard 中设置的 API Identifier 完全一致 |
Domain 包含 https:// | 仅使用 your-tenant.auth0.com 格式,颁发者 URL 会自动构建 |
| 使用 v2 位置参数,而不是 v3 选项 | v3 使用 validator.WithKeyFunc()、validator.WithAlgorithm() 等 |
| 颁发者 URL 缺少末尾斜杠 | 颁发者必须为末尾带斜杠的 https://domain/ |
RBAC 检查的是 scope 声明,而不是 permissions | 使用包含 Permissions []string 字段的自定义声明结构体 |
缺少 godotenv.Load() 调用 | 添加 github.com/joho/godotenv,并在读取环境变量前调用 godotenv.Load() |
使用 ContextKey{} 访问声明(v2 模式) | 改用类型安全的泛型 jwtmiddleware.GetClaims[T]() |
基于 Scope 的授权
有关定义和强制执行 scope 与权限策略,请参阅集成指南。
CORS 配置
对于由基于浏览器的 SPA 调用的 API,请在任何身份验证中间件之前配置 CORS:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
将其作为最外层处理程序包裹 mux:
handler := corsMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))
有关详细的 CORS 模式,请参阅集成指南。
DPoP 支持
内置符合 RFC 9449 的持有证明令牌绑定。有关配置,请参阅集成指南。
相关技能
auth0-quickstart- Auth0 基本设置auth0-mfa- 添加多因素身份验证
快速参考
配置选项:
validator.WithKeyFunc(provider.KeyFunc)- 用于签名验证的 JWKS 密钥函数(必需)validator.WithAlgorithm(validator.RS256)- 预期的签名算法(必需)validator.WithIssuer(url)- 末尾带斜杠的令牌颁发者 URL(必需)validator.WithAudience(aud)- Auth0 API 设置中的 API Identifier(必需)validator.WithCustomClaims(fn)- 自定义声明结构体的工厂函数validator.WithAllowedClockSkew(d)- 时钟偏差容差
声明访问:
jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())- 类型安全的声明检索claims.RegisteredClaims.Subject- 用户 ID(sub)claims.CustomClaims.(*CustomClaims).Scope- 以空格分隔的 scopeclaims.CustomClaims.(*CustomClaims).Permissions- 权限字符串
常见用例:
- 保护路由 →
middleware.CheckJWT(handler)(参见步骤 5) - 权限强制执行 → 集成指南
- DPoP 令牌绑定 → 集成指南
- 框架适配器(Gin、Echo)→ 集成指南
- 高级 JWT 配置 → API 参考