可以把 LoadCurrentUser 分成两种状态来处理:

  1. 没有 cookie
    这是“匿名访问”,直接放行,不报错。
  2. 有 cookie,但 session 无效/过期/查不到用户
    在 context 里标记出来。

核心思路是:LoadCurrentUser 只负责“尝试加载”,不要默认把所有异常都 401 掉。
可以这样做:

func LoadCurrentUser(db *gorm.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionID, err := c.Cookie("session_id")
        if err != nil {
            c.Next()
            return
        }

        if sessionID == "" {
            c.Next()
            return
        }

        var session models.Session
        if err := db.Where("session_id = ?", sessionID).First(&session).Error; err != nil {
            c.Set("authError", "invalid_session")
            c.Next()
            return
        }

        if session.RevokedAt != nil || (session.ExpiresAt != nil && session.ExpiresAt.Before(time.Now())) {
            c.Set("authError", "expired_session")
            c.Next()
            return
        }

        var user models.User
        if err := db.Preload("Roles.Permissions").First(&user, session.UserId).Error; err != nil {
            c.Set("authError", "user_not_found")
            c.Next()
            return
        }

        c.Set("currentUser", user)
        c.Set("sessionId", sessionID)
        c.Next()
    }
}

然后 RequireAuth 再决定怎么报错:

func RequireAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        if user, ok := c.Get("currentUser"); ok && user != nil {
            c.Next()
            return
        }

        if authErr, ok := c.Get("authError"); ok {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": authErr})
            return
        }

        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "unauthorized"})
    }
}

这样效果就很清楚:

  • 公共接口:

    • 没 cookie,正常访问
    • 坏 cookie,也正常访问,但你可以选择是否在响应里提示前端重新登录
  • 私有接口:

    • 没 cookie,401 unauthorized
    • 坏 cookie,401 invalid_session / expired_session

本质其实就是把报错信息放到上下文,然后给需要经过auth中间件的路由去拦截,如果不会经过auth中间件就放行。

标签: gin, go

添加新评论