订单接口最小化与私有文件访问加固
- 订单列表使用独立最小 DTO 并分页,号主待办提供独立接口与统计 - 用户 token 增加版本控制,冻结/改密/退出即时撤销会话 - 移除 URL token 传参,SSE 与接口统一使用 HttpOnly Cookie - 私有文件按上传归属与业务关联授权,收款凭证转私有访问并校验归属 - 公开商品接口返回最小字段,隐藏号主身份与内部状态 - 每日清理超过 30 天未关联业务的上传归属,上传归属失败时补偿删除对象
This commit is contained in:
@@ -25,6 +25,7 @@ const (
|
||||
ContextAuthCurrentVersion = "auth_current_token_version"
|
||||
ContextAuthFailureDetail = "auth_failure_detail"
|
||||
AdminAccessCookieName = "hfb_admin_access"
|
||||
UserAccessCookieName = "hfb_user_access"
|
||||
)
|
||||
|
||||
type AdminTokenContext struct {
|
||||
@@ -43,16 +44,20 @@ func extractBearerToken(c *gin.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractToken(c *gin.Context) string {
|
||||
if tokenText := extractBearerToken(c); tokenText != "" {
|
||||
return tokenText
|
||||
}
|
||||
return c.Query("token")
|
||||
}
|
||||
type UserTokenValidatorFunc func(ctx context.Context, userID uint64, tokenVersion int64) error
|
||||
|
||||
func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
|
||||
func Auth(jwtManager *auth.JWTManager, validators ...UserTokenValidatorFunc) gin.HandlerFunc {
|
||||
var validate UserTokenValidatorFunc
|
||||
if len(validators) > 0 {
|
||||
validate = validators[0]
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
tokenText := extractToken(c)
|
||||
tokenText := extractBearerToken(c)
|
||||
if tokenText == "" {
|
||||
if cookieToken, err := c.Cookie(UserAccessCookieName); err == nil {
|
||||
tokenText = strings.TrimSpace(cookieToken)
|
||||
}
|
||||
}
|
||||
if tokenText == "" {
|
||||
response.Unauthorized(c, "缺少访问令牌")
|
||||
c.Abort()
|
||||
@@ -65,6 +70,13 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if validate != nil {
|
||||
if err := validate(c.Request.Context(), claims.UserID, claims.TokenVersion); err != nil {
|
||||
response.Unauthorized(c, "访问令牌无效或已过期")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(ContextUserID, claims.UserID)
|
||||
c.Set(ContextPhone, claims.Phone)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/modules/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestUserAuthRejectsQueryToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
manager := auth.NewJWTManager("test-jwt-secret-for-query-token-rejection")
|
||||
pair, err := manager.GenerateSubjectPairWithVersion(1, "13800000000", "user", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSubjectPairWithVersion() error = %v", err)
|
||||
}
|
||||
|
||||
engine := gin.New()
|
||||
engine.GET("/protected", Auth(manager), func(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
})
|
||||
|
||||
queryRequest := httptest.NewRequest(http.MethodGet, "/protected?token="+pair.AccessToken, nil)
|
||||
queryResponse := httptest.NewRecorder()
|
||||
engine.ServeHTTP(queryResponse, queryRequest)
|
||||
if queryResponse.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("query token status = %d, want %d", queryResponse.Code, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
bearerRequest := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
||||
bearerRequest.Header.Set("Authorization", "Bearer "+pair.AccessToken)
|
||||
bearerResponse := httptest.NewRecorder()
|
||||
engine.ServeHTTP(bearerResponse, bearerRequest)
|
||||
if bearerResponse.Code != http.StatusNoContent {
|
||||
t.Fatalf("bearer token status = %d, want %d", bearerResponse.Code, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user