Compare commits
No commits in common. "main" and "v1.0.2" have entirely different histories.
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,2 @@
|
||||
.log.meta.json
|
||||
env.yml
|
||||
|
||||
.ai/
|
||||
|
||||
.geminiignore
|
||||
|
||||
@ -1,14 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.0.3 (2026-05-09)
|
||||
|
||||
### Security
|
||||
- **Frictionless Memory Safety**: introduced `HttpRequest.SetHeader(key, values...)` with automatic secret extraction and buffer tracking.
|
||||
- **Automated Lifecycle**: `api.Call` now automatically "Opens" all `SafeBuf` configuration values before calling Signers, and explicitly closes them via defer.
|
||||
- **Paradigm Signers**: refactored built-in signers to use naive, declarative syntax (`safe.Base64(safe.Concat(...))`) which leverages `SafeBuf` for encrypted intermediate states, preventing GC reliance.
|
||||
- **Ultimate Protection**: implemented post-hoc string erasure using `unsafe.String` over controlled buffers managed by a private `wipeableBuffers` list.
|
||||
- **Infrastructure Alignment**: aligned dependencies with `go/safe` v1.0.6, `go/http` v1.0.11, and `go/config` v1.0.7.
|
||||
|
||||
## v1.0.2 (2026-05-09)
|
||||
|
||||
### Changed
|
||||
|
||||
@ -29,13 +29,10 @@ go get apigo.cc/go/api
|
||||
* `URLAction` / `MethodAction`:动态指定 Endpoint 和 HTTP 方法。
|
||||
* `ValidatableAction`:业务参数自校验。
|
||||
|
||||
## 🔒 安全性 (Ultimate Memory Safety)
|
||||
## 🔒 安全性
|
||||
|
||||
* **内置解密**:支持自动识别并解密配置中的 AES 加密内容。
|
||||
* **内存保护**:敏感配置解密后以 `safe.SafeBuf` 形式存储,防止内存 Dump 泄露。
|
||||
- **防止字符串泄露**:通过 `unsafe.String` 零拷贝技术,确保敏感 Header(如 Authorization)在调用结束后可被物理擦除,彻底解决 Go 字符串不可变性导致的堆泄露问题。
|
||||
- **全生命周期闭环**:`api.Call` 结束后自动触发 `httpReq.Close()`,对所有中间缓冲区进行 `ZeroMemory` 随机覆盖。
|
||||
- **安全辅助函数**:内置 `SetBasicAuth`, `SetBearerAuth` 等工具,强制执行无拼接的内存安全逻辑。
|
||||
* **并发安全**:配置树操作受 `sync.RWMutex` 保护。
|
||||
|
||||
## 🧪 示例
|
||||
|
||||
|
||||
9
TEST.md
9
TEST.md
@ -16,15 +16,6 @@
|
||||
* **占位符处理移交**:验证 `{{.service}}` 等 URL 模板变量能够被自定义的 Mock 签名器接管、解析并替换。
|
||||
* **序列化与协议透传**:验证自动由 `http` 包进行 Payload 解析后的 JSON 数据流与 HTTP Method 和 Authentication Header 一致性。
|
||||
|
||||
## 🛡️ 安全性验证 (Security Audit)
|
||||
|
||||
### 4. 内存安全闭环与后置擦除 (`TestSafeConfigDecryption`, `TestFillSafeGuard`)
|
||||
验证敏感数据在内存中的全生命周期保护:
|
||||
* **SafeBuf 自动转换**:所有解密后的配置字段强制转换为 `safe.SafeBuf`,有效防止内存 Dump。
|
||||
* **后置字符串擦除**:验证通过 `unsafe.String` 传递给 `http.Header` 的敏感字符串,在 `httpReq.Close()` 后被物理覆盖(内容不再是原始密钥)。
|
||||
* **注入保护 (Guard)**:敏感数据禁止自动注入 Action 的 `string` 字段,杜绝无意中的内存留存。
|
||||
* **自动生命周期回收**:`api.Call` 结束时通过 `defer` 机制强制调用 `Close()` 擦除本次请求的所有明文密钥副本及 Header 缓冲区。
|
||||
|
||||
## ⏱ 性能基准测试 (Benchmark)
|
||||
|
||||
使用 `go test -bench=. ./...` 评估框架调用阶段的开销。
|
||||
|
||||
112
action.go
112
action.go
@ -1,12 +1,5 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/safe"
|
||||
)
|
||||
|
||||
// Action 是所有接口的基础标识接口
|
||||
type Action interface {
|
||||
ActionName() string // 例如: "tencent.sms.send"
|
||||
@ -39,107 +32,10 @@ type ValidatableAction interface {
|
||||
|
||||
// HttpRequest 内部使用的请求描述结构,供 Signer 使用
|
||||
type HttpRequest struct {
|
||||
Url string
|
||||
Method string
|
||||
headers map[string]string
|
||||
Payload any
|
||||
wipeableBuffers [][]byte // 追踪需要安全擦除的敏感缓冲区
|
||||
}
|
||||
|
||||
// GetHeader 获取指定 Header 的值
|
||||
func (r *HttpRequest) GetHeader(key string) string {
|
||||
if r == nil || r.headers == nil {
|
||||
return ""
|
||||
}
|
||||
return r.headers[key]
|
||||
}
|
||||
|
||||
// Close 物理覆盖并清除所有关联的敏感缓冲区,确保内存中不再留存明文
|
||||
func (r *HttpRequest) Close() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
// 擦除缓冲区
|
||||
for _, buffer := range r.wipeableBuffers {
|
||||
safe.ZeroMemory(buffer)
|
||||
}
|
||||
r.wipeableBuffers = nil
|
||||
}
|
||||
|
||||
// SetHeader 提供无感知的安全 Header 设置功能。支持传入多个参数进行自动拼接。
|
||||
// 如果参数中包含 *safe.SafeBuf, *safe.SecretPlaintext 或 []byte (标记为敏感),
|
||||
// 整个生成的 Header 缓冲区都将被注册用于后置物理擦除。
|
||||
// 安全的拼接与转换(如 safe.Concat, safe.Base64)建议使用 safe 包提供的返回 *safe.SafeBuf 的方法。
|
||||
func (r *HttpRequest) SetHeader(key string, values ...any) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if r.headers == nil {
|
||||
r.headers = make(map[string]string)
|
||||
}
|
||||
|
||||
// 1. 计算总长度并识别是否有敏感数据
|
||||
totalLen := 0
|
||||
isSensitive := false
|
||||
for _, v := range values {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
totalLen += len(t)
|
||||
case []byte:
|
||||
totalLen += len(t)
|
||||
isSensitive = true
|
||||
case *safe.SafeBuf:
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
p := t.Open()
|
||||
totalLen += len(p.Data)
|
||||
p.Close()
|
||||
isSensitive = true
|
||||
case *safe.SecretPlaintext:
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
totalLen += len(t.Data)
|
||||
isSensitive = true
|
||||
default:
|
||||
s := cast.String(v)
|
||||
totalLen += len(s)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 分配单一缓冲区进行拼接
|
||||
buf := make([]byte, totalLen)
|
||||
pos := 0
|
||||
for _, v := range values {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
pos += copy(buf[pos:], t)
|
||||
case []byte:
|
||||
pos += copy(buf[pos:], t)
|
||||
case *safe.SafeBuf:
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
p := t.Open()
|
||||
pos += copy(buf[pos:], p.Data)
|
||||
p.Close()
|
||||
case *safe.SecretPlaintext:
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
pos += copy(buf[pos:], t.Data)
|
||||
default:
|
||||
pos += copy(buf[pos:], cast.String(v))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 映射到 headers 并注册清理
|
||||
r.headers[key] = unsafe.String(&buf[0], len(buf))
|
||||
if isSensitive {
|
||||
r.wipeableBuffers = append(r.wipeableBuffers, buf)
|
||||
}
|
||||
Url string
|
||||
Method string
|
||||
Headers map[string]string
|
||||
Payload any
|
||||
}
|
||||
|
||||
// Result 定义 API 调用的标准返回结果
|
||||
|
||||
44
api.go
44
api.go
@ -6,32 +6,16 @@ import (
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/http"
|
||||
"apigo.cc/go/safe"
|
||||
)
|
||||
|
||||
// Call 是调度引擎的入口
|
||||
func Call[T any](action Action) (*T, error) {
|
||||
// 1. 获取并合并配置
|
||||
actionConfig, safeBufs := GetActionConfig(action.ActionName())
|
||||
defer func() {
|
||||
for _, sb := range safeBufs {
|
||||
sb.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
actionConfig := map[string]any{}
|
||||
if ca, ok := action.(ConfigurableAction); ok {
|
||||
MergeMap(actionConfig, ca.Config())
|
||||
}
|
||||
|
||||
// 1.5 预处理配置:自动 Open 所有 SafeBuf 变为临时的 SecretPlaintext
|
||||
// 这样 Signer 可以直接使用这些值,且在 defer 中会被自动回收
|
||||
var openedSecrets []*safe.SecretPlaintext
|
||||
preprocessSecrets(actionConfig, &openedSecrets)
|
||||
defer func() {
|
||||
for _, secret := range openedSecrets {
|
||||
secret.Close()
|
||||
}
|
||||
}()
|
||||
MergeMap(actionConfig, GetActionConfig(action.ActionName()))
|
||||
|
||||
// 2. 业务自校验
|
||||
if va, ok := action.(ValidatableAction); ok {
|
||||
@ -67,14 +51,14 @@ func Call[T any](action Action) (*T, error) {
|
||||
httpReq := &HttpRequest{
|
||||
Url: url,
|
||||
Method: strings.ToUpper(method),
|
||||
Headers: make(map[string]string),
|
||||
Payload: action,
|
||||
}
|
||||
defer httpReq.Close()
|
||||
|
||||
// 合并默认 Header
|
||||
if headers, ok := actionConfig["headers"].(map[string]any); ok {
|
||||
for k, v := range headers {
|
||||
httpReq.SetHeader(k, v)
|
||||
httpReq.Headers[k] = cast.String(v)
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +73,7 @@ func Call[T any](action Action) (*T, error) {
|
||||
timeout := cast.Duration(actionConfig["timeout"])
|
||||
client := http.NewClient(timeout)
|
||||
|
||||
res := client.Do(httpReq.Method, httpReq.Url, httpReq.Payload, headerSlice(httpReq)...)
|
||||
res := client.Do(httpReq.Method, httpReq.Url, httpReq.Payload, headerSlice(httpReq.Headers)...)
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
@ -105,21 +89,9 @@ func Call[T any](action Action) (*T, error) {
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func preprocessSecrets(m map[string]any, opened *[]*safe.SecretPlaintext) {
|
||||
for k, v := range m {
|
||||
if sb, ok := v.(*safe.SafeBuf); ok {
|
||||
secret := sb.Open()
|
||||
m[k] = secret
|
||||
*opened = append(*opened, secret)
|
||||
} else if subMap, ok := v.(map[string]any); ok {
|
||||
preprocessSecrets(subMap, opened)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func headerSlice(req *HttpRequest) []string {
|
||||
res := make([]string, 0, len(req.headers)*2)
|
||||
for k, v := range req.headers {
|
||||
func headerSlice(headers map[string]string) []string {
|
||||
res := make([]string, 0, len(headers)*2)
|
||||
for k, v := range headers {
|
||||
res = append(res, k, v)
|
||||
}
|
||||
return res
|
||||
|
||||
20
api_test.go
20
api_test.go
@ -42,7 +42,7 @@ func TestConfigInheritance(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, _ := api.GetActionConfig("mockSvc.subSvc.doAction")
|
||||
cfg := api.GetActionConfig("mockSvc.subSvc.doAction")
|
||||
if cfg == nil {
|
||||
t.Fatal("config not found")
|
||||
}
|
||||
@ -71,7 +71,7 @@ func TestConfigInheritance(t *testing.T) {
|
||||
// =========================================================================
|
||||
func TestBuiltinSigners(t *testing.T) {
|
||||
// 测试 Basic 签名器
|
||||
req := &api.HttpRequest{}
|
||||
req := &api.HttpRequest{Headers: make(map[string]string)}
|
||||
config := map[string]any{
|
||||
"username": "admin",
|
||||
"password": "123",
|
||||
@ -84,12 +84,12 @@ func TestBuiltinSigners(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := "Basic " + encoding.Base64ToString([]byte("admin:123"))
|
||||
if req.GetHeader("Authorization") != expected {
|
||||
t.Errorf("expected %s, got %s", expected, req.GetHeader("Authorization"))
|
||||
if req.Headers["Authorization"] != expected {
|
||||
t.Errorf("expected %s, got %s", expected, req.Headers["Authorization"])
|
||||
}
|
||||
|
||||
// 测试 Bearer 签名器
|
||||
req = &api.HttpRequest{}
|
||||
req = &api.HttpRequest{Headers: make(map[string]string)}
|
||||
config = map[string]any{
|
||||
"token": "secret-token",
|
||||
}
|
||||
@ -98,8 +98,8 @@ func TestBuiltinSigners(t *testing.T) {
|
||||
t.Fatal("bearer signer not found")
|
||||
}
|
||||
signer.Sign(req, config)
|
||||
if req.GetHeader("Authorization") != "Bearer secret-token" {
|
||||
t.Errorf("expected Bearer secret-token, got %s", req.GetHeader("Authorization"))
|
||||
if req.Headers["Authorization"] != "Bearer secret-token" {
|
||||
t.Errorf("expected Bearer secret-token, got %s", req.Headers["Authorization"])
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ func (s *mockExtSigner) Sign(req *api.HttpRequest, rawConfig map[string]any) err
|
||||
}
|
||||
|
||||
// 添加模拟签名
|
||||
req.SetHeader("Authorization", "Mock-Signature-Pass")
|
||||
req.Headers["Authorization"] = "Mock-Signature-Pass"
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -241,7 +241,7 @@ func BenchmarkCallEngineLogic(b *testing.B) {
|
||||
},
|
||||
}
|
||||
|
||||
actionConfig, _ := api.GetActionConfig("benchPlatform")
|
||||
actionConfig := api.GetActionConfig("benchPlatform")
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
@ -251,6 +251,6 @@ func BenchmarkCallEngineLogic(b *testing.B) {
|
||||
// 这里由于 fill 仅限于 package 内部,我们不能直接调 fill(),
|
||||
// 但我们实际上想衡量的是整个准备流程。
|
||||
// 由于 Call 会触发网络,这里只压测 GetActionConfig (最核心合并解析逻辑)
|
||||
_, _ = api.GetActionConfig("mockPlatform.mockAction")
|
||||
_ = api.GetActionConfig("mockPlatform.mockAction")
|
||||
}
|
||||
}
|
||||
|
||||
40
config.go
40
config.go
@ -9,7 +9,6 @@ import (
|
||||
"apigo.cc/go/config"
|
||||
"apigo.cc/go/crypto"
|
||||
"apigo.cc/go/encoding"
|
||||
"apigo.cc/go/safe"
|
||||
)
|
||||
|
||||
var confAes, _ = crypto.NewAESGCMAndEraseKey([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ"))
|
||||
@ -48,8 +47,8 @@ func Load(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActionConfig 获取某个动作经过层级合并后的完整配置,返回配置图和需要手动关闭的 SafeBuf 列表
|
||||
func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
|
||||
// GetActionConfig 获取某个动作经过层级合并后的完整配置
|
||||
func GetActionConfig(actionName string) map[string]any {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
@ -59,7 +58,7 @@ func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
|
||||
// 1. 获取 api 根节点
|
||||
curr, ok := GlobalConfigs["api"].(map[string]any)
|
||||
if !ok {
|
||||
return res, nil
|
||||
return res
|
||||
}
|
||||
|
||||
// 2. 逐级导航并合并
|
||||
@ -80,8 +79,8 @@ func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
|
||||
}
|
||||
}
|
||||
|
||||
safeBufs := decryptMap(res)
|
||||
return res, safeBufs
|
||||
decryptMap(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// fill 注入配置到 Action,非破坏性,仅注入零值
|
||||
@ -105,12 +104,6 @@ func fill(action any, actionConfig map[string]any) {
|
||||
|
||||
fieldName := t.Field(i).Name
|
||||
if val, ok := findConfigValue(actionConfig, fieldName); ok {
|
||||
// 如果目标是 string 但值是 SafeBuf,默认不注入以防泄露,除非手动处理
|
||||
if f.Kind() == reflect.String {
|
||||
if _, isSafe := val.(*safe.SafeBuf); isSafe {
|
||||
continue
|
||||
}
|
||||
}
|
||||
cast.Convert(f.Addr().Interface(), val)
|
||||
}
|
||||
}
|
||||
@ -178,27 +171,22 @@ func MergeMap(dst, src map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
func decryptMap(m map[string]any) []*safe.SafeBuf {
|
||||
var safeBufs []*safe.SafeBuf
|
||||
func decryptMap(m map[string]any) {
|
||||
for k, v := range m {
|
||||
if s, ok := v.(string); ok {
|
||||
var b64 []byte
|
||||
var err error
|
||||
if b64, err = encoding.UnUrlBase64FromString(s); err != nil {
|
||||
b64, err = encoding.UnBase64FromString(s)
|
||||
}
|
||||
|
||||
if err == nil && len(b64) > 0 {
|
||||
if b64, err := encoding.UnUrlBase64FromString(s); err == nil {
|
||||
if dec, err := confAes.DecryptBytes(b64); err == nil {
|
||||
sb := safe.NewSafeBufAndErase(dec)
|
||||
m[k] = sb
|
||||
safeBufs = append(safeBufs, sb)
|
||||
m[k] = string(dec)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if b64, err := encoding.UnBase64FromString(s); err == nil {
|
||||
if dec, err := confAes.DecryptBytes(b64); err == nil {
|
||||
m[k] = string(dec)
|
||||
}
|
||||
}
|
||||
} else if subMap, ok := v.(map[string]any); ok {
|
||||
safeBufs = append(safeBufs, decryptMap(subMap)...)
|
||||
decryptMap(subMap)
|
||||
}
|
||||
}
|
||||
return safeBufs
|
||||
}
|
||||
|
||||
25
go.mod
25
go.mod
@ -3,23 +3,18 @@ module apigo.cc/go/api
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
apigo.cc/go/cast v1.3.0
|
||||
apigo.cc/go/config v1.3.0
|
||||
apigo.cc/go/crypto v1.3.0
|
||||
apigo.cc/go/encoding v1.3.0
|
||||
apigo.cc/go/http v1.3.0
|
||||
apigo.cc/go/safe v1.3.0
|
||||
apigo.cc/go/cast v1.2.8
|
||||
apigo.cc/go/config v1.0.6
|
||||
apigo.cc/go/crypto v1.1.0
|
||||
apigo.cc/go/encoding v1.1.0
|
||||
apigo.cc/go/http v1.0.8
|
||||
)
|
||||
|
||||
require (
|
||||
apigo.cc/go/file v1.3.0 // indirect
|
||||
apigo.cc/go/id v1.3.0 // indirect
|
||||
apigo.cc/go/log v1.3.0 // indirect
|
||||
apigo.cc/go/rand v1.3.0 // indirect
|
||||
apigo.cc/go/shell v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
apigo.cc/go/file v1.0.6 // indirect
|
||||
apigo.cc/go/rand v1.0.5 // indirect
|
||||
apigo.cc/go/safe v1.0.5 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
59
go.sum
59
go.sum
@ -1,41 +1,18 @@
|
||||
apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8=
|
||||
apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60=
|
||||
apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw=
|
||||
apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig=
|
||||
apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw=
|
||||
apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA=
|
||||
apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20=
|
||||
apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs=
|
||||
apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s=
|
||||
apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4=
|
||||
apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68=
|
||||
apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g=
|
||||
apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE=
|
||||
apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ=
|
||||
apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk=
|
||||
apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE=
|
||||
apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI=
|
||||
apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE=
|
||||
apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA=
|
||||
apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI=
|
||||
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/crypto v1.1.0 h1:dv9ZRbtJHnnLbDHUfjP//GHLniu0/5ja0w5QE5hwwOU=
|
||||
apigo.cc/go/crypto v1.1.0/go.mod h1:0NUsQMGiP95TWHJexb3F1MxNdW+LR8TD1VqwHPN8PR8=
|
||||
apigo.cc/go/encoding v1.1.0 h1:dy+o6aw6rqBjutSaCLQm/DVLdRd0T8QQzvSXBNYuCbo=
|
||||
apigo.cc/go/encoding v1.1.0/go.mod h1:GeAz5OnCkFybTR1+GWFqdMgfq5v6r4MsjWVPOk/mpf4=
|
||||
apigo.cc/go/id v1.0.5 h1:23YkR7oklSA69gthYlu8zl/kpIkeIoEYxi1f1Sz5l3A=
|
||||
apigo.cc/go/id v1.0.5/go.mod h1:ZaYLIyrJvkf3j7J8a0lnKywSAHljaczWxU0x2HmQDzg=
|
||||
apigo.cc/go/rand v1.0.5 h1:AkUoWr0SELgeDmRjLEDjOIp29nXdzqQQvmGRIHpTN7U=
|
||||
apigo.cc/go/rand v1.0.5/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/safe v1.0.5 h1:yZJLhpMntJrtqU/ev0UlyOoHu/cLrnnGUO4aHyIZcwE=
|
||||
apigo.cc/go/safe v1.0.5/go.mod h1:i9xnh7reJIFPauLnlzuIDgvrQvhjxpFlpVh3O6ulWd0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
|
||||
103
security_test.go
103
security_test.go
@ -1,103 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/encoding"
|
||||
"apigo.cc/go/safe"
|
||||
)
|
||||
|
||||
func TestSafeConfigDecryption(t *testing.T) {
|
||||
// 1. 准备测试环境
|
||||
key := []byte("12345678123456781234567812345678")
|
||||
iv := []byte("123456781234")
|
||||
SetEncryptKeys(key, iv)
|
||||
|
||||
plaintext := "my-secret-password"
|
||||
ciphertext, _ := confAes.EncryptBytes([]byte(plaintext))
|
||||
b64 := encoding.Base64ToString(ciphertext)
|
||||
|
||||
GlobalConfigs = map[string]any{
|
||||
"api": map[string]any{
|
||||
"testSvc": map[string]any{
|
||||
"password": b64,
|
||||
"username": "admin",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 2. 测试获取配置
|
||||
cfg, sbs := GetActionConfig("testSvc")
|
||||
if len(sbs) != 1 {
|
||||
t.Fatalf("expected 1 SafeBuf, got %d", len(sbs))
|
||||
}
|
||||
|
||||
sb, ok := cfg["password"].(*safe.SafeBuf)
|
||||
if !ok {
|
||||
t.Fatal("password should be *safe.SafeBuf")
|
||||
}
|
||||
|
||||
p := sb.Open()
|
||||
if p.String() != plaintext {
|
||||
t.Errorf("expected %s, got %s", plaintext, p.String())
|
||||
}
|
||||
p.Close()
|
||||
|
||||
// 3. 测试签名器使用 SafeBuf
|
||||
req := &HttpRequest{}
|
||||
signer := GetSigner("basic")
|
||||
err := signer.Sign(req, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedAuth := "Basic " + encoding.Base64ToString([]byte("admin:"+plaintext))
|
||||
if req.GetHeader("Authorization") != expectedAuth {
|
||||
t.Errorf("expected %s, got %s", expectedAuth, req.GetHeader("Authorization"))
|
||||
}
|
||||
|
||||
// 4. 测试生命周期管理 (清理)
|
||||
authStr := req.GetHeader("Authorization")
|
||||
req.Close()
|
||||
|
||||
for _, sb := range sbs {
|
||||
sb.Close()
|
||||
}
|
||||
|
||||
// 验证 Authorization Header 已被擦除 (内容不再是原始数据)
|
||||
if authStr == expectedAuth {
|
||||
t.Error("Authorization header should be modified/erased after Close")
|
||||
}
|
||||
|
||||
// 再次尝试 Open 应该失败或得到空 (取决于 SafeBuf 实现,通常 Close 后内容被擦除)
|
||||
p2 := sb.Open()
|
||||
if p2.String() == plaintext && len(plaintext) > 0 {
|
||||
t.Error("SafeBuf should be cleared after Close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillSafeGuard(t *testing.T) {
|
||||
type SecretAction struct {
|
||||
Password string
|
||||
AppId string
|
||||
}
|
||||
|
||||
sb := safe.NewSafeBuf([]byte("secret"))
|
||||
defer sb.Close()
|
||||
|
||||
config := map[string]any{
|
||||
"Password": sb,
|
||||
"AppId": "my-app",
|
||||
}
|
||||
|
||||
action := &SecretAction{}
|
||||
fill(action, config)
|
||||
|
||||
if action.AppId != "my-app" {
|
||||
t.Errorf("AppId should be filled, got %s", action.AppId)
|
||||
}
|
||||
|
||||
if action.Password != "" {
|
||||
t.Error("Sensitive SafeBuf should NOT be filled into string field automatically")
|
||||
}
|
||||
}
|
||||
17
signer.go
17
signer.go
@ -3,7 +3,8 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"apigo.cc/go/safe"
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/encoding"
|
||||
)
|
||||
|
||||
// Signer 负责为请求附加签名信息
|
||||
@ -43,18 +44,20 @@ func sign(name string, req *HttpRequest, config map[string]any) error {
|
||||
type basicSigner struct{}
|
||||
|
||||
func (s *basicSigner) Sign(req *HttpRequest, config map[string]any) error {
|
||||
req.SetHeader("Authorization", "Basic ", safe.Base64(config["username"], ":", config["password"]))
|
||||
username := cast.String(config["username"])
|
||||
password := cast.String(config["password"])
|
||||
auth := username + ":" + password
|
||||
req.Headers["Authorization"] = "Basic " + encoding.Base64ToString([]byte(auth))
|
||||
return nil
|
||||
}
|
||||
|
||||
type bearerSigner struct{}
|
||||
|
||||
func (s *bearerSigner) Sign(req *HttpRequest, config map[string]any) error {
|
||||
token := config["token"]
|
||||
if token == nil {
|
||||
token = config["key"]
|
||||
token := cast.String(config["token"])
|
||||
if token == "" {
|
||||
token = cast.String(config["key"])
|
||||
}
|
||||
|
||||
req.SetHeader("Authorization", "Bearer ", token)
|
||||
req.Headers["Authorization"] = "Bearer " + token
|
||||
return nil
|
||||
}
|
||||
|
||||
27
utils.go
27
utils.go
@ -4,35 +4,8 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/safe"
|
||||
)
|
||||
|
||||
// GetStringOrSafe 从配置中安全地获取字符串值。如果是 SafeBuf,则返回明文副本及对应的清理函数
|
||||
func GetStringOrSafe(v any) (string, func()) {
|
||||
if v == nil {
|
||||
return "", func() {}
|
||||
}
|
||||
if sb, ok := v.(*safe.SafeBuf); ok {
|
||||
p := sb.Open()
|
||||
return p.String(), p.Close
|
||||
}
|
||||
return cast.String(v), func() {}
|
||||
}
|
||||
|
||||
// GetBytesOrSafe 从配置中安全地获取字节切片。如果是 SafeBuf,则返回明文副本及对应的清理函数
|
||||
func GetBytesOrSafe(v any) ([]byte, func()) {
|
||||
if v == nil {
|
||||
return nil, func() {}
|
||||
}
|
||||
if sb, ok := v.(*safe.SafeBuf); ok {
|
||||
p := sb.Open()
|
||||
return p.Data, p.Close
|
||||
}
|
||||
return []byte(cast.String(v)), func() {}
|
||||
}
|
||||
|
||||
// ExportGoStructs 根据全局配置生成 Go 结构体代码
|
||||
func ExportGoStructs(packageName string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user