package server import ( "crypto/sha256" "crypto/subtle" "encoding/hex" "net/http" "strings" "github.com/gin-gonic/gin" ) // authCookieName 保存已通过授权校验的凭证。值为授权码的 SHA-256 十六进制摘要, // 不落明文;HttpOnly 使前端 JS 无法读取,降低泄露面。 const authCookieName = "blackbean_auth" // authCookieValue 计算授权码的稳定凭证值(SHA-256 摘要)。 func authCookieValue(code string) string { sum := sha256.Sum256([]byte(code)) return hex.EncodeToString(sum[:]) } // authRequired 是访问授权中间件:cfg.AuthCode 为空(未启用授权)时直接放行; // 否则要求请求携带与授权码匹配的 Cookie,不匹配则 401 并中止后续处理。 func (s *Server) authRequired(c *gin.Context) { if s.cfg.AuthCode == "" { c.Next() return } if cookie, err := c.Cookie(authCookieName); err == nil && cookie == authCookieValue(s.cfg.AuthCode) { c.Next() return } fail(c, http.StatusUnauthorized, "需要授权码才能访问,请先在首页输入授权码") c.Abort() } // authStatus 返回授权状态:required=是否启用了授权,authorized=当前请求是否已通过授权。 // 该接口始终放行(不挂授权中间件),前端据此决定是否展示授权界面。 func (s *Server) authStatus(c *gin.Context) { required := s.cfg.AuthCode != "" authorized := false if required { if cookie, err := c.Cookie(authCookieName); err == nil { authorized = cookie == authCookieValue(s.cfg.AuthCode) } } ok(c, gin.H{"required": required, "authorized": authorized}) } // authLogin 校验授权码并签发 Cookie。 // 该接口始终放行(不挂授权中间件);未启用授权时返回 400。 func (s *Server) authLogin(c *gin.Context) { if s.cfg.AuthCode == "" { fail(c, http.StatusBadRequest, "本实例未启用访问授权") return } var req struct { Code string `json:"code"` } if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求体格式错误") return } // 常量时间比较,避免通过响应时差探测授权码 want := []byte(s.cfg.AuthCode) got := []byte(strings.TrimSpace(req.Code)) if len(want) != len(got) || subtle.ConstantTimeCompare(want, got) != 1 { fail(c, http.StatusUnauthorized, "授权码错误") return } http.SetCookie(c.Writer, &http.Cookie{ Name: authCookieName, Value: authCookieValue(s.cfg.AuthCode), Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) ok(c, gin.H{"authorized": true}) }