Skip to content

Commit de8b2f4

Browse files
refactor(aegis): 重构 Extractor/Verifier/Decryptor 架构,清晰化职责边界 (#31)
1 parent d7985bb commit de8b2f4

File tree

21 files changed

+412
-359
lines changed

21 files changed

+412
-359
lines changed

CLAUDE.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ main.go + wire.go
7575
```
7676

7777
**关键约束**
78+
7879
- Aegis 不直接访问数据库,所有数据通过 Hermes 服务获取
7980
- Hermes 的 `models/` 是公开包(aegis 和 iris 依赖其数据类型)
8081
- Zwei 的 `internal/models/` 是内部包(仅 zwei 使用)
@@ -199,17 +200,17 @@ if req.ExpiresAt.IsPresent() {
199200

200201
### 必须使用 antd 组件替代原生 HTML 元素
201202

202-
| 原生 HTML | antd 替代 | 说明 |
203-
| --- | --- | --- |
204-
| `<button>` | `Button` | 所有按钮必须使用 antd Button,根据场景选择 type(primary/default/text/link) |
205-
| `<input>` | `Input` / `Input.TextArea` / `InputNumber` | 表单输入统一用 antd Input 系列 |
206-
| `<select>` / `<option>` | `Select` | 下拉选择器 |
207-
| `<table>` | `Table` | 数据表格 |
208-
| `<form>` | `Form` + `Form.Item` | 表单容器 |
209-
| `<img>` | `Image` | 图片展示,支持预览、加载状态、错误处理 |
210-
| 加载占位 `<div>加载中...</div>` | `Spin` | 加载状态统一使用 Spin |
211-
| `<input type="checkbox">` | `Checkbox` | 复选框 |
212-
| `<input type="radio">` | `Radio` | 单选框 |
203+
| 原生 HTML | antd 替代 | 说明 |
204+
| ------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------- |
205+
| `<button>` | `Button` | 所有按钮必须使用 antd Button,根据场景选择 type(primary/default/text/link) |
206+
| `<input>` | `Input` / `Input.TextArea` / `InputNumber` | 表单输入统一用 antd Input 系列 |
207+
| `<select>` / `<option>` | `Select` | 下拉选择器 |
208+
| `<table>` | `Table` | 数据表格 |
209+
| `<form>` | `Form` + `Form.Item` | 表单容器 |
210+
| `<img>` | `Image` | 图片展示,支持预览、加载状态、错误处理 |
211+
| 加载占位 `<div>加载中...</div>` | `Spin` | 加载状态统一使用 Spin |
212+
| `<input type="checkbox">` | `Checkbox` | 复选框 |
213+
| `<input type="radio">` | `Radio` | 单选框 |
213214

214215
### 允许保留原生 HTML 的场景
215216

aegis/init.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func Initialize(hermesSvc *hermes.Service, userSvc *hermes.UserService, credenti
6363
}
6464

6565
// 域密钥:clientID → domain.Main(id="aegis" 时返回 SSO master key)
66-
domainKeyProvider := key.LoadKeysFunc(func(ctx context.Context, clientID string) ([][]byte, error) {
66+
domainKeyProvider := key.MultiOf(func(ctx context.Context, clientID string) ([][]byte, error) {
6767
if clientID == token.SSOIssuer {
6868
return ssoMasterKeyFetcher()
6969
}
@@ -79,7 +79,7 @@ func Initialize(hermesSvc *hermes.Service, userSvc *hermes.UserService, credenti
7979
})
8080

8181
// 服务密钥:audience → service.Key(id="aegis" 时返回 SSO master key)
82-
serviceKeyProvider := key.LoadKeysFunc(func(ctx context.Context, audience string) ([][]byte, error) {
82+
serviceKeyProvider := key.MultiOf(func(ctx context.Context, audience string) ([][]byte, error) {
8383
if audience == token.SSOAudience {
8484
return ssoMasterKeyFetcher()
8585
}
@@ -91,7 +91,7 @@ func Initialize(hermesSvc *hermes.Service, userSvc *hermes.UserService, credenti
9191
})
9292

9393
// 应用密钥:clientID → app.Key
94-
appKeyProvider := key.LoadKeysFunc(func(ctx context.Context, clientID string) ([][]byte, error) {
94+
appKeyProvider := key.MultiOf(func(ctx context.Context, clientID string) ([][]byte, error) {
9595
app, err := cacheManager.GetApplication(ctx, clientID)
9696
if err != nil {
9797
return nil, fmt.Errorf("get application: %w", err)

aegis/internal/token/service.go

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/heliannuuthus/helios/pkg/aegis/key"
1414
pkgtoken "github.com/heliannuuthus/helios/pkg/aegis/token"
1515
tokendef "github.com/heliannuuthus/helios/pkg/aegis/utils/token"
16-
"github.com/heliannuuthus/helios/pkg/logger"
1716
)
1817

1918
// Service is the token service that handles issuing and verifying all token types.
@@ -26,10 +25,9 @@ type Service struct {
2625
appKeyProvider key.Provider // clientID → app.Key
2726

2827
domainSigners map[string]*Signer
29-
domainVerifiers map[string]*pkgtoken.Verifier
3028
serviceEncryptors map[string]*Encryptor
31-
serviceDecryptors map[string]*pkgtoken.Decryptor
32-
appVerifiers map[string]*pkgtoken.Verifier
29+
domainDecryptors map[string]*pkgtoken.Decryptor // audience → Decryptor (signKey=domain, encryptKey=service)
30+
appDecryptor *pkgtoken.Decryptor // CAT 专用(signKey=app, 只验签, encryptKey=nil)
3331
mu sync.RWMutex
3432
}
3533

@@ -46,10 +44,9 @@ func NewService(
4644
serviceKeyProvider: serviceKeyProvider,
4745
appKeyProvider: appKeyProvider,
4846
domainSigners: make(map[string]*Signer),
49-
domainVerifiers: make(map[string]*pkgtoken.Verifier),
5047
serviceEncryptors: make(map[string]*Encryptor),
51-
serviceDecryptors: make(map[string]*pkgtoken.Decryptor),
52-
appVerifiers: make(map[string]*pkgtoken.Verifier),
48+
domainDecryptors: make(map[string]*pkgtoken.Decryptor),
49+
appDecryptor: pkgtoken.NewDecryptor("", nil, appKeyProvider),
5350
}
5451
}
5552

@@ -101,14 +98,21 @@ func (s *Service) Verify(ctx context.Context, tokenString string) (Token, error)
10198
return nil, fmt.Errorf("get client_id: %w", err)
10299
}
103100

104-
var verifier *pkgtoken.Verifier
105101
if tokenType == tokendef.TokenTypeCAT {
106-
verifier = s.appVerifier(clientID)
107-
} else {
108-
verifier = s.domainVerifier(clientID)
102+
pasetoToken, err = s.appDecryptor.Verifier(clientID).Verify(ctx, tokenString)
103+
if err != nil {
104+
return nil, fmt.Errorf("verify signature: %w", err)
105+
}
106+
return tokendef.ParseToken(pasetoToken, tokenType)
107+
}
108+
109+
audience, err := tokendef.GetAudience(pasetoToken)
110+
if err != nil {
111+
return nil, fmt.Errorf("get audience: %w", err)
109112
}
110113

111-
pasetoToken, err = verifier.Verify(ctx, tokenString)
114+
decryptor := s.domainDecryptor(audience)
115+
pasetoToken, err = decryptor.Verifier(clientID).Verify(ctx, tokenString)
112116
if err != nil {
113117
return nil, fmt.Errorf("verify signature: %w", err)
114118
}
@@ -129,11 +133,7 @@ func (s *Service) Verify(ctx context.Context, tokenString string) (Token, error)
129133
return nil, errors.New("missing encrypted sub")
130134
}
131135

132-
audience, err := tokendef.GetAudience(pasetoToken)
133-
if err != nil {
134-
logger.Warnf("failed to get audience from token: %v", err)
135-
}
136-
innerToken, err := s.serviceDecryptor(audience).Decrypt(ctx, encryptedSub)
136+
innerToken, err := decryptor.Decrypt(ctx, encryptedSub)
137137
if err != nil {
138138
return nil, fmt.Errorf("decrypt sub: %w", err)
139139
}
@@ -166,26 +166,6 @@ func (s *Service) domainSigner(clientID string) *Signer {
166166
return signer
167167
}
168168

169-
func (s *Service) domainVerifier(clientID string) *pkgtoken.Verifier {
170-
s.mu.RLock()
171-
verifier, ok := s.domainVerifiers[clientID]
172-
s.mu.RUnlock()
173-
if ok {
174-
return verifier
175-
}
176-
177-
s.mu.Lock()
178-
defer s.mu.Unlock()
179-
180-
if verifier, ok := s.domainVerifiers[clientID]; ok {
181-
return verifier
182-
}
183-
184-
verifier = pkgtoken.NewVerifier(s.domainKeyProvider, clientID)
185-
s.domainVerifiers[clientID] = verifier
186-
return verifier
187-
}
188-
189169
func (s *Service) serviceEncryptor(audience string) *Encryptor {
190170
s.mu.RLock()
191171
encryptor, ok := s.serviceEncryptors[audience]
@@ -206,9 +186,9 @@ func (s *Service) serviceEncryptor(audience string) *Encryptor {
206186
return encryptor
207187
}
208188

209-
func (s *Service) serviceDecryptor(audience string) *pkgtoken.Decryptor {
189+
func (s *Service) domainDecryptor(audience string) *pkgtoken.Decryptor {
210190
s.mu.RLock()
211-
decryptor, ok := s.serviceDecryptors[audience]
191+
decryptor, ok := s.domainDecryptors[audience]
212192
s.mu.RUnlock()
213193
if ok {
214194
return decryptor
@@ -217,35 +197,15 @@ func (s *Service) serviceDecryptor(audience string) *pkgtoken.Decryptor {
217197
s.mu.Lock()
218198
defer s.mu.Unlock()
219199

220-
if decryptor, ok := s.serviceDecryptors[audience]; ok {
200+
if decryptor, ok := s.domainDecryptors[audience]; ok {
221201
return decryptor
222202
}
223203

224-
decryptor = pkgtoken.NewDecryptor(s.serviceKeyProvider, audience)
225-
s.serviceDecryptors[audience] = decryptor
204+
decryptor = pkgtoken.NewDecryptor(audience, s.serviceKeyProvider, s.domainKeyProvider)
205+
s.domainDecryptors[audience] = decryptor
226206
return decryptor
227207
}
228208

229-
func (s *Service) appVerifier(clientID string) *pkgtoken.Verifier {
230-
s.mu.RLock()
231-
verifier, ok := s.appVerifiers[clientID]
232-
s.mu.RUnlock()
233-
if ok {
234-
return verifier
235-
}
236-
237-
s.mu.Lock()
238-
defer s.mu.Unlock()
239-
240-
if verifier, ok := s.appVerifiers[clientID]; ok {
241-
return verifier
242-
}
243-
244-
verifier = pkgtoken.NewVerifier(s.appKeyProvider, clientID)
245-
s.appVerifiers[clientID] = verifier
246-
return verifier
247-
}
248-
249209
// ============= Payload Encryption Helpers =============
250210

251211
// marshalPayload extracts the payload that needs encryption for UAT and SSO tokens.

aegis/middleware/auth.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7-
"strings"
87

98
"github.com/gin-gonic/gin"
109

@@ -16,7 +15,7 @@ import (
1615

1716
func RequireToken(v *web.Interpreter) gin.HandlerFunc {
1817
return func(c *gin.Context) {
19-
tokenStr := extractBearer(c.GetHeader("Authorization"))
18+
tokenStr := web.TrimBearer(c.GetHeader(web.AuthorizationHeader))
2019
if tokenStr == "" {
2120
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"detail": "unauthorized"})
2221
return
@@ -27,7 +26,7 @@ func RequireToken(v *web.Interpreter) gin.HandlerFunc {
2726
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"detail": "unauthorized"})
2827
return
2928
}
30-
tc, err := web.NewTokenContext(identity)
29+
tc, err := web.NewTokenContext(identity, nil)
3130
if err != nil {
3231
logger.Warnf("[Auth] TokenContext failed - Path: %s, Error: %v", c.Request.URL.Path, err)
3332
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"detail": "unauthorized"})
@@ -40,7 +39,7 @@ func RequireToken(v *web.Interpreter) gin.HandlerFunc {
4039

4140
func OptionalToken(v *web.Interpreter) gin.HandlerFunc {
4241
return func(c *gin.Context) {
43-
tokenStr := extractBearer(c.GetHeader("Authorization"))
42+
tokenStr := web.TrimBearer(c.GetHeader(web.AuthorizationHeader))
4443
if tokenStr == "" {
4544
c.Next()
4645
return
@@ -50,7 +49,7 @@ func OptionalToken(v *web.Interpreter) gin.HandlerFunc {
5049
c.Next()
5150
return
5251
}
53-
tc, err := web.NewTokenContext(identity)
52+
tc, err := web.NewTokenContext(identity, nil)
5453
if err != nil {
5554
c.Next()
5655
return
@@ -60,13 +59,13 @@ func OptionalToken(v *web.Interpreter) gin.HandlerFunc {
6059
}
6160
}
6261

63-
// NewHermesKeyProvider 创建 Hermes 使用的密钥 Provider
64-
func NewHermesKeyProvider() (key.Provider, error) {
62+
// NewHermesKeyProvider 创建 Hermes 使用的 seed Provider
63+
func NewHermesKeyProvider() (*key.SeedProvider, error) {
6564
masterKey, err := hermesconfig.GetAegisSecretKeyBytes()
6665
if err != nil {
6766
return nil, fmt.Errorf("get hermes aegis secret key: %w", err)
6867
}
69-
return key.LoadKeyFunc(func(_ context.Context, _ string) ([]byte, error) {
68+
return key.SingleOf(func(_ context.Context, _ string) ([]byte, error) {
7069
return masterKey, nil
7170
}), nil
7271
}
@@ -77,10 +76,3 @@ func tokenPreview(tokenStr string, length int) string {
7776
}
7877
return tokenStr[:length]
7978
}
80-
81-
func extractBearer(authorization string) string {
82-
if len(authorization) > 7 && strings.EqualFold(authorization[:7], "Bearer ") {
83-
return authorization[7:]
84-
}
85-
return ""
86-
}

config/example.toml

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ password = "" # 如果未指定,使用 database.password
4040
name = "auth" # 数据库名称
4141

4242
# Redis 配置(用于存储 session、authorization_code、refresh_token)
43-
# URL 格式: redis://:password@host:port/db
4443
[redis]
45-
url = "redis://:helios@127.0.0.1:6379/0"
44+
host = "127.0.0.1"
45+
port = 6379
46+
password = "helios"
47+
db = 0
48+
# 前缀配置(可选)
49+
# session-prefix = "auth:session:"
50+
# code-prefix = "auth:code:"
51+
# refresh-token-prefix = "auth:rt:"
52+
# user-token-prefix = "auth:user:rt:"
4653

4754
[cors]
4855
origins = ["*"]
@@ -80,14 +87,12 @@ appid = "" # 支付宝小程序 AppID
8087
secret = "" # 应用私钥(PKCS8 DER Base64,用于签名请求)
8188
verify-key = "" # 支付宝公钥(PKCS8 DER Base64,上传应用公钥后支付宝返回,用于验证响应签名)
8289

83-
# VChan 人机验证配置(Cloudflare Turnstile)
90+
# Captcha 人机验证配置(Cloudflare Turnstile)
8491
# 获取密钥: https://dash.cloudflare.com/turnstile
85-
[vchan.captcha]
86-
enabled = true # 是否启用人机验证
87-
88-
[vchan.captcha.turnstile]
89-
app_id = "" # 站点密钥 / site key(前端使用)
90-
secret = "" # 密钥 / secret key(后端验证使用)
92+
[captcha]
93+
provider = "turnstile" # 验证提供商:turnstile
94+
site-key = "" # 站点密钥(前端使用)
95+
secret-key = "" # 密钥(后端验证使用)
9196

9297
# Aegis 认证配置
9398
[aegis]
@@ -97,13 +102,6 @@ refresh-expires-in = 365 # refresh_token 过期时间(天),1年
97102
max-refresh-token = 10 # 每用户最大 refresh_token 数
98103
service-url = "http://localhost:18000" # Aegis 服务地址(内部调用)
99104

100-
# SSO 配置(跨应用单点登录)
101-
# 密钥使用 python3 scripts/initialize-hermes.py 生成
102-
[sso]
103-
# master-key = "" # SSO master key(32 字节,Base64URL 编码,通过 KDF 派生签名和加密密钥)
104-
# ttl = "168h" # SSO Token 有效期(默认 7 天)
105-
# cookie-name = "aegis-sso" # SSO Cookie 名称
106-
107105
# Cookie 配置(Aegis 模块登录会话)
108106
[aegis.cookie]
109107
domain = "" # Cookie 域名(留空则从 endpoint 自动提取)
@@ -128,8 +126,8 @@ callback = "/callback" # IDP 回调路径
128126

129127
# 域配置(注意:实际配置在 hermes.toml 中,这里仅作参考)
130128
# 密钥使用 python3 scripts/initialize-hermes.py 生成
131-
# [aegis.domains.consumer]
132-
# name = "Consumer Identity" # 域名称
129+
# [aegis.domains.ciam]
130+
# name = "Customer Identity" # 域名称
133131
# description = "C端用户身份域" # 域描述
134132
# 签名密钥(32 字节 Ed25519 seed,Base64URL 编码)
135133
# 支持密钥轮换:逗号分隔多个密钥,第一把是主密钥用于签发,其余是旧密钥用于验证

0 commit comments

Comments
 (0)