diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index b1ef6d1..160d1a9 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" + "strconv" "strings" "fashionapi/internal/pkg/jwt" @@ -25,21 +26,43 @@ const bearerPrefix = "Bearer " // 令牌签发器由外部注入,避免像原实现那样在每个请求里重新加载一次配置。 func Auth(manager *jwt.Manager) gin.HandlerFunc { return func(c *gin.Context) { - header := c.GetHeader("Authorization") - if !strings.HasPrefix(header, bearerPrefix) { - response.AbortError(c, http.StatusUnauthorized, "missing token") - return - } + requireAuth(c, manager) + } +} - claims, err := manager.Parse(strings.TrimPrefix(header, bearerPrefix)) - if err != nil { - response.AbortError(c, http.StatusUnauthorized, "invalid token") - return - } +// requireAuth 校验 Bearer token 并把身份信息写入 Context;失败则 401 中断。 +func requireAuth(c *gin.Context, manager *jwt.Manager) { + header := c.GetHeader("Authorization") + if !strings.HasPrefix(header, bearerPrefix) { + response.AbortError(c, http.StatusUnauthorized, "missing token") + return + } - c.Set(ContextUserID, claims.UserID) - c.Set(ContextUsername, claims.Username) - c.Next() + claims, err := manager.Parse(strings.TrimPrefix(header, bearerPrefix)) + if err != nil { + response.AbortError(c, http.StatusUnauthorized, "invalid token") + return + } + + c.Set(ContextUserID, claims.UserID) + c.Set(ContextUsername, claims.Username) + c.Next() +} + +// PublicFirstPageAuth 列表接口「首页公开、翻页需登录」鉴权中间件。 +// +// 仅当分页参数 page 缺失或 <= 1 时放行(无需 token),其余页要求有效的 +// Authorization: Bearer ,否则返回 401。用于让列表首屏对游客/SEO 可见, +// 同时保留翻页 / 批量枚举的登录门槛(防爬虫直接 dump 全量)。 +func PublicFirstPageAuth(manager *jwt.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + if pageStr := c.Query("page"); pageStr != "" { + if n, err := strconv.Atoi(pageStr); err == nil && n > 1 { + requireAuth(c, manager) // 非首页:走完整 JWT 校验 + return + } + } + c.Next() // 首页(page 缺失或 <= 1)放行 } } diff --git a/internal/router/router.go b/internal/router/router.go index 393b8f6..bdfdcbb 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -47,6 +47,12 @@ func New(opt Options) *gin.Engine { // ClientSign 仅挂在「列表 / 品牌」两个批量查询接口上: // 这俩是爬虫批量 dump 的主要目标,要求前端 JS 签名可抬高抓取成本。 public.Use(middleware.ClientSign(opt.Config.ClientSign)) + // 列表接口「首页公开、翻页需登录」:page=1(或缺省)无需 token 直接放行, + // page>1 才要求 Authorization: Bearer ,否则 401。 + // 这样既让游客/SEO 看到首屏,又保留翻页 / 批量枚举的登录门槛(防爬虫 dump 全量)。 + // 注意:详情接口 /api/v1/public/*/:id 不在此组内,保持公开——详情页走 SSR + // 服务端渲染、无用户 token,且分享链接/SEO 需可访问;列表隐藏即可防批量枚举。 + public.Use(middleware.PublicFirstPageAuth(opt.JWT)) { // 走秀档案列表(按品牌过滤走 ?brand_id 查询参数) public.GET("/runway-looks", opt.Article.List)