81 lines
2.5 KiB
Go
81 lines
2.5 KiB
Go
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})
|
|
}
|