FileCodeBox Go 重写版 v2.5.6(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"filecodebox/internal/response"
|
||||
)
|
||||
|
||||
// jwtClaims 自定义声明:对齐参考实现(payload 含 is_admin 与 exp)。
|
||||
type jwtClaims struct {
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// 签发/校验相关错误。
|
||||
var (
|
||||
ErrTokenExpired = errors.New("token已过期")
|
||||
ErrTokenInvalid = errors.New("无效的签名")
|
||||
ErrNotAdmin = errors.New("未授权或授权校验失败")
|
||||
)
|
||||
|
||||
// SignAdminToken 用 HS256 签发管理员 JWT。
|
||||
// secret 为数据库 settings 中的 jwt_secret;expires 为会话有效期。
|
||||
func SignAdminToken(secret string, expires time.Duration) (string, time.Time, error) {
|
||||
if strings.TrimSpace(secret) == "" {
|
||||
return "", time.Time{}, errors.New("JWT签名密钥未初始化")
|
||||
}
|
||||
expiresAt := time.Now().Add(expires)
|
||||
claims := jwtClaims{
|
||||
IsAdmin: true,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "filecodebox",
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString([]byte(secret))
|
||||
return signed, expiresAt, err
|
||||
}
|
||||
|
||||
// VerifyAdminToken 校验管理员 JWT:签名、过期时间与 is_admin 声明。
|
||||
func VerifyAdminToken(secret, token string) (*jwtClaims, error) {
|
||||
if strings.TrimSpace(secret) == "" {
|
||||
return nil, errors.New("JWT签名密钥未初始化")
|
||||
}
|
||||
parsed, err := jwt.ParseWithClaims(token, &jwtClaims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
return []byte(secret), nil
|
||||
}, jwt.WithValidMethods([]string{"HS256"}))
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
claims, ok := parsed.Claims.(*jwtClaims)
|
||||
if !ok || !parsed.Valid {
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
if !claims.IsAdmin {
|
||||
return nil, ErrNotAdmin
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// SecretProvider 动态提供当前 jwt_secret(settings KV 运行时可变)。
|
||||
type SecretProvider func() string
|
||||
|
||||
// AdminAuth 管理员鉴权中间件:校验 Authorization: Bearer <token>。
|
||||
// 成功后把声明写入 gin 上下文(ctxClaims)。
|
||||
func AdminAuth(secret SecretProvider) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
header := c.GetHeader("Authorization")
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
response.Fail(c, 401, "未授权或授权校验失败")
|
||||
return
|
||||
}
|
||||
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
|
||||
if token == "" {
|
||||
response.Fail(c, 401, "未授权或授权校验失败")
|
||||
return
|
||||
}
|
||||
claims, err := VerifyAdminToken(secret(), token)
|
||||
if err != nil {
|
||||
response.Fail(c, 401, err.Error())
|
||||
return
|
||||
}
|
||||
c.Set("claims", claims)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user