@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -12,11 +13,17 @@ import (
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
// usernameKey 是鉴权通过后写入 gin.Context 的用户名键。
|
||||
const usernameKey = "username"
|
||||
// usernameKey 是鉴权通过后写入 gin.Context 的用户名键;
|
||||
// tokenVerKey/tokenJtiKey 是鉴权时观察到的令牌快照,敏感事务提交前复核。
|
||||
const (
|
||||
usernameKey = "username"
|
||||
tokenVerKey = "tokenVer"
|
||||
tokenJtiKey = "tokenJti"
|
||||
)
|
||||
|
||||
// RequireAuth 校验 Authorization: Bearer 令牌(签名、有效期与令牌版本),
|
||||
// 通过后把用户名放进上下文。
|
||||
// 通过后把用户名与令牌快照(版本/jti)放进上下文——快照供敏感事务在
|
||||
// 行锁下复核,防「请求挂起期间令牌被撤销,恢复后仍完成变更」的在途绕过。
|
||||
func RequireAuth(auth *service.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||
@@ -24,16 +31,25 @@ func RequireAuth(auth *service.AuthService) gin.HandlerFunc {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"})
|
||||
return
|
||||
}
|
||||
username, err := auth.ParseToken(c.Request.Context(), token)
|
||||
username, proof, err := auth.ParseTokenProof(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
||||
return
|
||||
}
|
||||
c.Set(usernameKey, username)
|
||||
c.Set(tokenVerKey, proof.Ver)
|
||||
c.Set(tokenJtiKey, proof.Jti)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// tokenProofOf 取出鉴权时的令牌快照,交给敏感 service 事务复核。
|
||||
func tokenProofOf(c *gin.Context) service.TokenProof {
|
||||
v, _ := c.Get(tokenVerKey)
|
||||
ver, _ := v.(uint)
|
||||
return service.TokenProof{Ver: ver, Jti: c.GetString(tokenJtiKey)}
|
||||
}
|
||||
|
||||
// bodyLimit 给请求体套上限(S-03):超限读取由 MaxBytesReader 截断报错,
|
||||
// 绑定失败返回 400;webhook 入口另有独立 256KiB 自限,不经此中间件。
|
||||
func bodyLimit(n int64) gin.HandlerFunc {
|
||||
@@ -45,7 +61,7 @@ func bodyLimit(n int64) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// systemLogMiddleware 把写请求(POST/PUT/DELETE)异步记入系统日志,只读请求跳过。
|
||||
// systemLogMiddleware 把写请求(POST/PUT/PATCH/DELETE)异步记入系统日志,只读请求跳过。
|
||||
// 只记方法、路径等元数据,绝不读请求体——请求体可能含私钥 / 口令等敏感数据。
|
||||
func systemLogMiddleware(logs *service.SystemLogService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -101,17 +117,23 @@ func errMsgOf(status int, head []byte) string {
|
||||
return truncateLogField(body.Error, 256)
|
||||
}
|
||||
|
||||
// truncateLogField 按字节截断留痕字段,防超长 UA / 错误串撑爆列宽。
|
||||
// truncateLogField 按字节截断留痕字段,防超长 UA / 错误串撑爆列宽;
|
||||
// 截断点回退到 rune 边界,避免产生非法 UTF-8(严格 MySQL/PG 会拒写整行)。
|
||||
func truncateLogField(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
for max > 0 && !utf8.RuneStart(s[max]) {
|
||||
max--
|
||||
}
|
||||
return s[:max]
|
||||
}
|
||||
|
||||
// isWriteMethod 判断是否需要留痕的写方法。
|
||||
// isWriteMethod 判断是否需要留痕的写方法;PATCH 承载 OAuth / 安全设置等
|
||||
// 敏感配置变更,必须与 POST/PUT/DELETE 一样进入审计。
|
||||
func isWriteMethod(m string) bool {
|
||||
return m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete
|
||||
return m == http.MethodPost || m == http.MethodPut ||
|
||||
m == http.MethodPatch || m == http.MethodDelete
|
||||
}
|
||||
|
||||
// requestPath 优先取注册的路由模板(含 :id 占位符,避免把敏感 query 记入日志),
|
||||
|
||||
Reference in New Issue
Block a user