订单接口最小化与私有文件访问加固

- 订单列表使用独立最小 DTO 并分页,号主待办提供独立接口与统计
- 用户 token 增加版本控制,冻结/改密/退出即时撤销会话
- 移除 URL token 传参,SSE 与接口统一使用 HttpOnly Cookie
- 私有文件按上传归属与业务关联授权,收款凭证转私有访问并校验归属
- 公开商品接口返回最小字段,隐藏号主身份与内部状态
- 每日清理超过 30 天未关联业务的上传归属,上传归属失败时补偿删除对象
This commit is contained in:
yml2213
2026-08-16 21:47:46 +08:00
parent f48da14ed2
commit 85332df2bd
63 changed files with 1827 additions and 290 deletions
+56 -5
View File
@@ -1,22 +1,38 @@
package file
import (
"context"
"errors"
"net/http"
"strings"
"hfb_sys/backend/internal/logging"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type Handler struct {
service *Service
storage *Storage
service *Service
storage *Storage
objectAuthorizer ObjectAuthorizer
uploadOwnerRecorder UploadOwnerRecorder
}
func NewHandler(service *Service, storage *Storage) *Handler {
return &Handler{service: service, storage: storage}
// ObjectAuthorizer 校验用户是否可读取指定私有对象。
type ObjectAuthorizer func(ctx context.Context, userID uint64, key string) (bool, error)
// UploadOwnerRecorder 保存用户上传私有文件的归属。
type UploadOwnerRecorder func(ctx context.Context, userID uint64, objectKeys []string) error
func NewHandler(service *Service, storage *Storage, objectAuthorizer ObjectAuthorizer, uploadOwnerRecorder UploadOwnerRecorder) *Handler {
return &Handler{
service: service,
storage: storage,
objectAuthorizer: objectAuthorizer,
uploadOwnerRecorder: uploadOwnerRecorder,
}
}
func (h *Handler) Upload(c *gin.Context) {
@@ -44,6 +60,19 @@ func (h *Handler) Upload(c *gin.Context) {
writeFileError(c, err)
return
}
if userID, ok := c.Get("user_id"); ok && h.uploadOwnerRecorder != nil {
id, valid := userID.(uint64)
if !valid || h.uploadOwnerRecorder(c.Request.Context(), id, item.ObjectKeys()) != nil {
// 归属记录失败时补偿删除刚写入的对象,避免用户重试产生孤儿文件。
if h.storage != nil {
if removeErr := h.storage.RemoveObjects(c.Request.Context(), item.ObjectKeys()); removeErr != nil {
logging.FromContext(c.Request.Context()).Warn("补偿删除上传对象失败", zap.Error(removeErr))
}
}
response.ServiceUnavailable(c, "文件归属记录失败")
return
}
}
response.Created(c, item)
}
@@ -68,7 +97,6 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) {
if publicOnly &&
!strings.HasPrefix(key, "home-banner/") &&
!strings.HasPrefix(key, "avatar/") &&
!strings.HasPrefix(key, "payment-cert/") &&
!strings.HasPrefix(key, "announcement/") &&
!strings.HasPrefix(key, "mohong/") &&
!strings.HasPrefix(key, "crash/") &&
@@ -77,6 +105,29 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) {
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")
return
}
if !publicOnly {
if _, isAdmin := c.Get("admin_id"); !isAdmin {
userID, ok := c.Get("user_id")
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
id, ok := userID.(uint64)
if !ok || h.objectAuthorizer == nil {
response.Error(c, http.StatusForbidden, "forbidden", "无权访问该文件")
return
}
allowed, err := h.objectAuthorizer(c.Request.Context(), id, key)
if err != nil {
response.ServiceUnavailable(c, "文件权限校验失败")
return
}
if !allowed {
response.Error(c, http.StatusForbidden, "forbidden", "无权访问该文件")
return
}
}
}
object, err := h.storage.Get(c.Request.Context(), key)
if err != nil {
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")