auth — 认证与授权

pkg/core/auth 把 Bearer token 验签落到 httpx 的中间件槽上: handler 不解析 Authorization 头,也不手写 401——中间件验完把 Principal 放进 ctx,handler 直接读。这个示例用三条路由演示 「公开 → 登录态 → 更高权限」的阶梯是如何用书写顺序表达的。

运行

go run ./examples/auth

TOKEN=$(curl -s -X POST localhost:8081/api/v1/login \
  -d '{"username":"bob","password":"bob-pw"}' | sed 's/.*"token":"\([^"]*\)".*/\1/')

curl -i localhost:8081/api/v1/profile                                        # 401,无 token
curl -i localhost:8081/api/v1/profile     -H "Authorization: Bearer $TOKEN"   # 200
curl -i localhost:8081/api/v1/admin/audit -H "Authorization: Bearer $TOKEN"   # 200,bob 是 admin
# 换 alice 的 token 调 /admin/audit → 403,她没有 admin 角色

没有外部依赖,不需要 make dev。监听 :8081, -conf 默认指向 examples/auth/config.yaml; 那份配置必须包含 auth 段,否则 Setup 会以 authdemo: the `auth` section must be configured 失败退出。

它演示了什么

三条路由,两个 tier

这段来自 examples/auth/main.go 的 BuildRouter。 整个授权模型就藏在这几行的顺序里:/login 在第一个 Use 之前注册,所以公开;/profile 在验签中间件之后; /admin/audit 又在 RequireRole("admin") 之后。

func (s *authDemo) BuildRouter(srv *httpx.Server) error {
	// Public: the one route that hands out tokens.
	srv.Handle("POST", "/api/v1/login", s.login)

	// Everything below this line needs a valid token.
	srv.Use(s.verifier.Middleware())
	srv.Handle("GET", "/api/v1/profile", profile)

	// Middleware accumulates, so this route needs the token *and* the
	// admin role. That is the whole authorization model: register public
	// routes, then Use, then the next tier.
	srv.Use(auth.RequireRole("admin"))
	srv.Handle("GET", "/api/v1/admin/audit", audit)

	return nil
}

登录与读取主体

同一份文件里的两个 handler。登录那侧演示了签发,注意它给了 Scopes 与 Roles 两项 claim; profile 那侧演示了读取——它完全不知道 token 是怎么来的。

	u, ok := demoUsers[req.Username]
	if !ok || u.password != req.Password {
		// Same message for "no such user" and "wrong password": telling
		// them apart hands an attacker a user-enumeration oracle.
		return nil, errs.Unauthorized(40101, "invalid credentials")
	}
	token, err := s.verifier.Issue(auth.IssueRequest{
		Subject: req.Username,
		Scopes:  u.scopes,
		Roles:   u.roles,
	}, time.Hour)
	if err != nil {
		return nil, errs.Internal(50001, "issue token: %v", err)
	}
	return map[string]any{"token": token, "token_type": "Bearer", "expires_in": 3600}, nil
// profile reads the principal the middleware put on the context.
func profile(ctx context.Context, _ *http.Request) (any, error) {
	p, _ := auth.FromContext(ctx)
	return map[string]any{"subject": p.Subject, "scopes": p.Scopes, "roles": p.Roles}, nil
}

阶梯与失败语义

flowchart TD
    L["POST /api/v1/login
公开 · 换 token"] --> T["verifier.Issue
→ Bearer token"] T --> M["srv.Use(verifier.Middleware())"] M --> P["GET /api/v1/profile
需要有效 token"] P --> R["srv.Use(auth.RequireRole('admin'))"] R --> A["GET /api/v1/admin/audit
需要 token + admin 角色"] M -.->|"缺 / 格式错 / 签名错 / 过期"| E401["401 code 40101"] R -.->|"token 有效但角色不够"| E403["403 code 40301"] J["JWKS 端点不可达"] -.->|"从未成功拉取过"| E503["503 code 50301"] classDef bad fill:#fdf0f0,stroke:#a33,color:#2b2722 classDef guard fill:#fdf6f0,stroke:#c8641f,color:#2b2722 class E401,E403 bad class E503 guard
每一行 Use 都往下累积一层要求,所以 tier 就是书写顺序。 三条失败出口刻意分开:凭证问题给 401/403,身份提供方故障给 503—— 让客户端知道该换 token 还是该重试。

表格里的第三种情况值得单独记:JWKS 端点不可达返回 503, 因为身份提供方短暂不可用不该让客户端以为自己的 token 坏了。 拉取失败时会继续用上一次的公钥(可用性优先),只有从未成功拉取过才对外报 503。

验证

# 1. 拿 token
curl -s -X POST localhost:8081/api/v1/login -d '{"username":"bob","password":"bob-pw"}'
# {"code":0,"msg":"ok","data":{"expires_in":3600,"token":"eyJ…","token_type":"Bearer"}}

# 2. 无 token
curl -i localhost:8081/api/v1/profile
# 401 {"code":40101,"msg":"…","data":null}

# 3. 带 token
TOKEN=$(curl -s -X POST localhost:8081/api/v1/login -d '{"username":"bob","password":"bob-pw"}' | sed 's/.*"token":"\([^"]*\)".*/\1/')
curl -s localhost:8081/api/v1/profile -H "Authorization: Bearer $TOKEN"
# {"code":0,"msg":"ok","data":{"roles":["admin"],"scopes":["profile:read","audit:read"],"subject":"bob"}}

# 4. admin 路由:bob 可以
curl -s localhost:8081/api/v1/admin/audit -H "Authorization: Bearer $TOKEN"
# {"code":0,"msg":"ok","data":{"entries":["alice logged in","bob rotated the signing key"],"requested_by":"bob"}}

# 5. admin 路由:alice 是普通用户,403
ALICE=$(curl -s -X POST localhost:8081/api/v1/login -d '{"username":"alice","password":"alice-pw"}' | sed 's/.*"token":"\([^"]*\)".*/\1/')
curl -i localhost:8081/api/v1/admin/audit -H "Authorization: Bearer $ALICE"
# 403 {"code":40301,"msg":"…","data":null}

# 6. 凭证错误一律同一条消息
curl -i -X POST localhost:8081/api/v1/login -d '{"username":"nobody","password":"x"}'
# 401 {"code":40101,"msg":"invalid credentials","data":null}

测试

main_test.go 用真实 verifier(auth.New(auth.Config{Issuer:"matex-authdemo", Secret:testSecret})) 搭起完整链路,断言的是:

go test ./examples/auth

相关

认证与安全概念页 两种验签模式、分级保护、安全响应头与 CORS。 matex-setup-auth 让 AI 按规范把认证授权接进你的项目。 示例索引 全部 20 个可运行示例。

相关示例