Skip to content

Commit 7bb1406

Browse files
refactor(aegis): 声明式鉴权 Guard + 布尔表达式 Relation + 代码审查修复 (#29)
1 parent 58ac644 commit 7bb1406

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+3492
-2650
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ linters:
2525
- errname
2626
# 安全
2727
- gosec
28+
# 函数排序
29+
- funcorder
2830
# 其他
2931
- unparam
3032
- nakedret

QUESTIONS.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# pkg/aegis/web Review 问题清单
2+
3+
## Critical
4+
5+
### C1. NewTokenContext 静默丢数据
6+
7+
`NewTokenContext``switch` 没有 `default` 分支。如果传入 SSOToken 等不匹配的类型,返回空 `TokenContext`,认证"通过"但身份全为 nil。
8+
9+
**位置**: `pkg/aegis/web/context.go:31-42`
10+
11+
**修复方案**: 签名改为 `(*TokenContext, error)``default` 分支返回 error。
12+
13+
---
14+
15+
## Major
16+
17+
### M1. Interpret 中 audience 从未验签的 token 提取
18+
19+
`Interpret()``UnsafeParseToken` 提取 `audience`,后 `Verify` 验签。虽然 `pasetoToken` 被重新赋值,但 `audience` 变量仍是旧值。应从验签后的 `t.GetAudience()` 获取。
20+
21+
**位置**: `pkg/aegis/web/interpreter.go:46,66`
22+
23+
**修复方案**: 删除 L46 的 audience 提取,L66 改为 `i.decryptUserSub(ctx, encryptedSub, t.GetAudience())`
24+
25+
### M2. WithAudience 每次创建新 Interpreter — 缓存不共享
26+
27+
每次 `WithAudience``NewInterpreter`,多 audience 场景下 verifier/decryptor 缓存完全独立,浪费内存。
28+
29+
**位置**: `pkg/aegis/web/middleware.go:60-67`
30+
31+
**修复方案**: 在 `Factory` 层持有一个共享的 `Interpreter` 实例,`WithAudience` 复用它。
32+
33+
### M3. X-Challenge-Token 应用 Verify 而非 Interpret
34+
35+
ChallengeToken 不含加密的 sub,调用 `Interpret` 多余。且类型断言失败(非 ChallengeToken 放进 header)时被静默忽略。
36+
37+
**位置**: `pkg/aegis/web/middleware.go:179-186`
38+
39+
**修复方案**: 改为 `m.interpreter.Verify()`;类型断言失败时 warn。
40+
41+
### M4. RequireToken 与 web 包的 Bearer 提取逻辑不一致
42+
43+
`aegis/middleware/auth.go``RequireToken` 接受裸 token(不带 `Bearer ` 前缀),`web/middleware.go``extractBearerToken` 则不接受。
44+
45+
**位置**: `aegis/middleware/auth.go:31-34` vs `pkg/aegis/web/middleware.go:259-268`
46+
47+
**修复方案**: `RequireToken` 对齐 web 包行为,不带 `Bearer ` 前缀时返回 401。
48+
49+
---
50+
51+
## Minor
52+
53+
### m1. ContextKeyUser 已成死代码
54+
55+
`aegis/consts.go``ContextKeyUser = "aegis:user"` 不再被任何代码引用,所有中间件已改用 `web.ClaimsKey`
56+
57+
**位置**: `aegis/consts.go:15`
58+
59+
**修复方案**: 删除该常量。
60+
61+
### m2. iris/handler.go 中 `_ = openid` 死代码
62+
63+
`UpdateEmail``UpdatePhone``BindIdentity``UnbindIdentity` 四个 TODO 方法中取了 `openid` 然后 `_ = openid` 丢弃。
64+
65+
**位置**: `iris/handler.go` 多处
66+
67+
**修复方案**: 改为 `if web.OpenIDFromGin(c) == "" { ... return }` 模式,不赋值。
68+
69+
### m3. WithChallenge 命名暗示不可变模式
70+
71+
Go 惯例中 `WithXxx` 常用于返回新对象,但这里直接修改 receiver。
72+
73+
**位置**: `pkg/aegis/web/context.go:44-47`
74+
75+
**修复方案**: 改名为 `SetChallengeToken`
76+
77+
### m4. errForbidden 使用自定义类型
78+
79+
`errForbidden` 用了 `&forbiddenError{}` 指针,`errors.New("forbidden")` 更简洁且语义更清晰。
80+
81+
**位置**: `pkg/aegis/web/middleware.go:155-159`
82+
83+
**修复方案**: 改为 `var errForbidden = errors.New("forbidden")`
84+
85+
### m5. extractBearerToken 大小写敏感
86+
87+
RFC 7235 规定 auth-scheme 是 case-insensitive,`"bearer "``"BEARER "` 应被接受。
88+
89+
**位置**: `pkg/aegis/web/middleware.go:265`
90+
91+
**修复方案**: 用 `strings.EqualFold``strings.ToLower` 比较前缀。
92+
93+
---
94+
95+
## Suggestion
96+
97+
### S1. 新增 RequireFactor() 中间件
98+
99+
需要二次验证的路由应通过 `RequireFactor()` 中间件强制要求 `tc.ChallengeToken() != nil`
100+
101+
### S2. net/http 层缺少 TokenContextFromRequest 辅助函数
102+
103+
Gin 层有 `TokenContextFromGin`,标准 http 层没有对应的取值函数。
104+
105+
### S3. Gin 层和 net/http 层大量重复代码
106+
107+
4 个方法各写两遍(Gin + net/http),可提取共享逻辑减少重复。
108+
109+
### S4. iris/handler.go 的 getOpenID wrapper 多余
110+
111+
只是 `web.OpenIDFromGin` 的代理,建议直接调用以保持与 zwei handler 的一致性。

aegis/config/config.go

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -261,41 +261,6 @@ func GetPublicKeyCacheMaxAge() time.Duration {
261261
return DefaultAegisPublicKeyCacheMaxAge
262262
}
263263

264-
// ==================== Mail 配置 ====================
265-
266-
// MailConfig 邮件配置
267-
type MailConfig struct {
268-
Provider string
269-
Host string
270-
Port int
271-
UseSSL bool
272-
Username string
273-
Password string
274-
}
275-
276-
// mailProviderDefaults 邮件服务商默认配置
277-
type mailProviderDefaults struct {
278-
host string
279-
port int
280-
useSSL bool
281-
}
282-
283-
// getMailProviderDefaults 根据服务商获取默认配置
284-
func getMailProviderDefaults(provider string) mailProviderDefaults {
285-
defaults := map[string]mailProviderDefaults{
286-
"qq-exmail": {host: "smtp.exmail.qq.com", port: 465, useSSL: true},
287-
"qq": {host: "smtp.qq.com", port: 465, useSSL: true},
288-
"163": {host: "smtp.163.com", port: 465, useSSL: true},
289-
"gmail": {host: "smtp.gmail.com", port: 587, useSSL: false},
290-
"outlook": {host: "smtp.office365.com", port: 587, useSSL: false},
291-
"aliyun": {host: "smtp.mxhichina.com", port: 465, useSSL: true},
292-
}
293-
if d, ok := defaults[provider]; ok {
294-
return d
295-
}
296-
return mailProviderDefaults{port: 587}
297-
}
298-
299264
// GetMailConfig 获取邮件配置
300265
func GetMailConfig() *MailConfig {
301266
c := Cfg()
@@ -453,22 +418,6 @@ func GetLoginACFailWindow(connection string) time.Duration {
453418
return DefaultLoginACFailWindow
454419
}
455420

456-
// parseIntMap 解析 map[string]interface{} 为 map[string]int
457-
func parseIntMap(raw map[string]interface{}) map[string]int {
458-
result := make(map[string]int, len(raw))
459-
for k, v := range raw {
460-
switch n := v.(type) {
461-
case int64:
462-
result[k] = int(n)
463-
case float64:
464-
result[k] = int(n)
465-
case int:
466-
result[k] = n
467-
}
468-
}
469-
return result
470-
}
471-
472421
// ==================== SSO 配置 ====================
473422

474423
// SSO 默认值
@@ -534,3 +483,54 @@ func GetSecretBytes(audience string) ([]byte, error) {
534483
}
535484
return secretBytes, nil
536485
}
486+
487+
// ==================== Mail 配置 ====================
488+
489+
// MailConfig 邮件配置
490+
type MailConfig struct {
491+
Provider string
492+
Host string
493+
Port int
494+
UseSSL bool
495+
Username string
496+
Password string
497+
}
498+
499+
// mailProviderDefaults 邮件服务商默认配置
500+
type mailProviderDefaults struct {
501+
host string
502+
port int
503+
useSSL bool
504+
}
505+
506+
// getMailProviderDefaults 根据服务商获取默认配置
507+
func getMailProviderDefaults(provider string) mailProviderDefaults {
508+
defaults := map[string]mailProviderDefaults{
509+
"qq-exmail": {host: "smtp.exmail.qq.com", port: 465, useSSL: true},
510+
"qq": {host: "smtp.qq.com", port: 465, useSSL: true},
511+
"163": {host: "smtp.163.com", port: 465, useSSL: true},
512+
"gmail": {host: "smtp.gmail.com", port: 587, useSSL: false},
513+
"outlook": {host: "smtp.office365.com", port: 587, useSSL: false},
514+
"aliyun": {host: "smtp.mxhichina.com", port: 465, useSSL: true},
515+
}
516+
if d, ok := defaults[provider]; ok {
517+
return d
518+
}
519+
return mailProviderDefaults{port: 587}
520+
}
521+
522+
// parseIntMap 解析 map[string]interface{} 为 map[string]int
523+
func parseIntMap(raw map[string]interface{}) map[string]int {
524+
result := make(map[string]int, len(raw))
525+
for k, v := range raw {
526+
switch n := v.(type) {
527+
case int64:
528+
result[k] = int(n)
529+
case float64:
530+
result[k] = int(n)
531+
case int:
532+
result[k] = n
533+
}
534+
}
535+
return result
536+
}

aegis/consts.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ const (
1111
// Query 参数
1212
QueryClientID = "client_id" // Client ID 查询参数
1313

14-
// Gin Context Key
15-
ContextKeyUser = "user" // 用户 Token 在 Gin Context 中的 key
16-
1714
// Cookie
1815
AuthSessionCookie = "aegis-session" // Auth 会话 Cookie 名称
1916
)

0 commit comments

Comments
 (0)