159 lines
5.0 KiB
Go
159 lines
5.0 KiB
Go
package clientapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
|
"go-admin/app/goauto/clientkey"
|
|
"go-admin/common/clientprincipal"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Both HTTP and HTTPS are supported by explicit operator decision (#237).
|
|
// Transport does not grant identity or permissions; secrets remain non-cacheable.
|
|
func NoStore(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-store")
|
|
c.Next()
|
|
}
|
|
func Gate(db *gorm.DB, e Endpoint) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-store")
|
|
deny := func(status int, message string) {
|
|
c.AbortWithStatusJSON(status, gin.H{"code": status, "message": message})
|
|
}
|
|
// Never accept a credential supplied through a URL or cookie fallback.
|
|
for k := range c.Request.URL.Query() {
|
|
if sensitive(k) || strings.EqualFold(k, "token") {
|
|
deny(400, "密钥只能通过 Authorization 请求头发送")
|
|
return
|
|
}
|
|
}
|
|
header := strings.Fields(c.GetHeader("Authorization"))
|
|
if len(header) != 2 || !strings.EqualFold(header[0], "Bearer") {
|
|
deny(401, "需要客户端密钥")
|
|
return
|
|
}
|
|
connection := db
|
|
var err error
|
|
if connection == nil {
|
|
connection, err = pkg.GetOrm(c)
|
|
}
|
|
if err != nil || connection == nil {
|
|
deny(503, "客户端认证暂不可用")
|
|
return
|
|
}
|
|
key, err := (clientkey.Service{DB: connection}).Authenticate(c.Request.Context(), header[1])
|
|
if err != nil {
|
|
if errors.Is(err, clientkey.ErrCredential) {
|
|
deny(401, "客户端密钥无效或已停用")
|
|
} else {
|
|
deny(503, "客户端认证暂不可用")
|
|
}
|
|
return
|
|
}
|
|
random := make([]byte, 16)
|
|
if _, err = rand.Read(random); err != nil {
|
|
deny(503, "审计暂不可用")
|
|
return
|
|
}
|
|
requestID := hex.EncodeToString(random)
|
|
c.Header("X-Client-Request-Id", requestID)
|
|
record := clientkey.Audit{KeyID: key.ID, Event: "request", RequestID: requestID, Method: e.Method, Route: "/api/client/v1" + e.Path}
|
|
if !key.Allows(e.Module, e.Capability) {
|
|
record.Status = 403
|
|
_ = connection.Create(&record).Error
|
|
deny(403, "密钥未授权此模块或执行动作")
|
|
return
|
|
}
|
|
// The intent must be durable before any business handler can run.
|
|
if err = connection.WithContext(c.Request.Context()).Create(&record).Error; err != nil {
|
|
deny(503, "审计暂不可用,未执行操作")
|
|
return
|
|
}
|
|
clientprincipal.Set(c, clientprincipal.Identity{KeyID: key.ID, RequestID: requestID, AuthorizedBy: key.UpdatedBy})
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 16<<20)
|
|
original := c.Writer
|
|
buffer := &responseBuffer{ResponseWriter: original, status: 200}
|
|
c.Writer = buffer
|
|
defer func() { c.Writer = original }()
|
|
c.Next()
|
|
c.Writer = original
|
|
status := buffer.status
|
|
var value any
|
|
if buffer.overflow || json.Unmarshal(buffer.body.Bytes(), &value) != nil {
|
|
status = 502
|
|
value = gin.H{"code": 502, "message": "无法生成客户端响应,请先核对操作结果,不要自动重试"}
|
|
} else {
|
|
value = redact(value)
|
|
}
|
|
auditCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
_ = connection.WithContext(auditCtx).Model(&clientkey.Audit{}).Where("id = ?", record.ID).Update("status", status).Error
|
|
now := time.Now().UTC()
|
|
_ = connection.WithContext(auditCtx).Model(&clientkey.Key{}).Where("id = ?", key.ID).UpdateColumn("last_used_at", now).Error
|
|
c.JSON(status, value)
|
|
}
|
|
}
|
|
|
|
type responseBuffer struct {
|
|
gin.ResponseWriter
|
|
body bytes.Buffer
|
|
status int
|
|
overflow bool
|
|
}
|
|
|
|
func (w *responseBuffer) WriteHeader(status int) { w.status = status }
|
|
func (w *responseBuffer) WriteHeaderNow() {}
|
|
func (w *responseBuffer) Status() int { return w.status }
|
|
func (w *responseBuffer) Size() int { return w.body.Len() }
|
|
func (w *responseBuffer) Written() bool { return w.body.Len() > 0 }
|
|
func (w *responseBuffer) Write(b []byte) (int, error) {
|
|
if w.body.Len()+len(b) > 32<<20 {
|
|
w.overflow = true
|
|
return len(b), nil
|
|
}
|
|
return w.body.Write(b)
|
|
}
|
|
func (w *responseBuffer) WriteString(s string) (int, error) { return w.Write([]byte(s)) }
|
|
func (w *responseBuffer) Flush() {}
|
|
|
|
func sensitive(k string) bool {
|
|
key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(k, "_", ""), "-", ""))
|
|
for _, part := range []string{"password", "passwd", "secret", "credential", "cookie", "authorization", "apikey", "accesstoken", "devicetoken", "refreshtoken", "recoverycode"} {
|
|
if strings.Contains(key, part) {
|
|
return true
|
|
}
|
|
}
|
|
switch key {
|
|
case "token", "tokenhash", "digest", "rawjson", "rawpayload", "rawresponse", "requestheaders", "responseheaders":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
func redact(v any) any {
|
|
switch value := v.(type) {
|
|
case map[string]any:
|
|
for k, item := range value {
|
|
if sensitive(k) {
|
|
delete(value, k)
|
|
} else {
|
|
value[k] = redact(item)
|
|
}
|
|
}
|
|
case []any:
|
|
for i, item := range value {
|
|
value[i] = redact(item)
|
|
}
|
|
}
|
|
return v
|
|
}
|