package service import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "net/http" "sort" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) const ( hidePwdCookiePrefix = "j13_hp_" hidePwdCookieMaxAge = 30 * 24 * 3600 // 30 天 hidePwdCookieVersion = "1" ) // HidePasswordCookie 帖子密码隐藏块解锁 cookie(游客可用) type HidePasswordCookie struct { secret []byte secure bool } func NewHidePasswordCookie(jwtSecret string, secure bool) *HidePasswordCookie { sum := sha256.Sum256([]byte("j13-hide-pwd-v1:" + jwtSecret)) return &HidePasswordCookie{secret: sum[:], secure: secure} } func (h *HidePasswordCookie) cookieName(postID uint) string { return hidePwdCookiePrefix + strconv.FormatUint(uint64(postID), 10) } // ReadUnlocked 读取某帖已解锁的隐藏块下标 func (h *HidePasswordCookie) ReadUnlocked(c *gin.Context, postID uint) map[int]bool { out := map[int]bool{} if h == nil || c == nil { return out } raw, err := c.Cookie(h.cookieName(postID)) if err != nil || raw == "" { return out } idxs, ok := h.verify(postID, raw) if !ok { return out } for _, i := range idxs { out[i] = true } return out } // WriteUnlocked 写入/合并已解锁下标 func (h *HidePasswordCookie) WriteUnlocked(c *gin.Context, postID uint, idxs []int) { if h == nil || c == nil || len(idxs) == 0 { return } merged := h.ReadUnlocked(c, postID) for _, i := range idxs { merged[i] = true } list := make([]int, 0, len(merged)) for i := range merged { list = append(list, i) } sort.Ints(list) val := h.sign(postID, list) http.SetCookie(c.Writer, &http.Cookie{ Name: h.cookieName(postID), Value: val, Path: "/", MaxAge: hidePwdCookieMaxAge, HttpOnly: true, Secure: h.secure, SameSite: http.SameSiteLaxMode, }) } func (h *HidePasswordCookie) sign(postID uint, idxs []int) string { payload := fmt.Sprintf("%s|%d|%s|%d", hidePwdCookieVersion, postID, joinInts(idxs), time.Now().Add(hidePwdCookieMaxAge*time.Second).Unix(), ) mac := hmac.New(sha256.New, h.secret) _, _ = mac.Write([]byte(payload)) sig := hex.EncodeToString(mac.Sum(nil)) return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + sig } func (h *HidePasswordCookie) verify(postID uint, raw string) ([]int, bool) { parts := strings.Split(raw, ".") if len(parts) != 2 { return nil, false } payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return nil, false } payload := string(payloadBytes) mac := hmac.New(sha256.New, h.secret) _, _ = mac.Write([]byte(payload)) expect := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expect), []byte(parts[1])) { return nil, false } fields := strings.Split(payload, "|") if len(fields) != 4 || fields[0] != hidePwdCookieVersion { return nil, false } pid, err := strconv.ParseUint(fields[1], 10, 64) if err != nil || uint(pid) != postID { return nil, false } exp, err := strconv.ParseInt(fields[3], 10, 64) if err != nil || time.Now().Unix() > exp { return nil, false } return parseInts(fields[2]), true } func joinInts(idxs []int) string { if len(idxs) == 0 { return "" } var b strings.Builder for i, n := range idxs { if i > 0 { b.WriteByte(',') } b.WriteString(strconv.Itoa(n)) } return b.String() } func parseInts(s string) []int { if s == "" { return nil } parts := strings.Split(s, ",") out := make([]int, 0, len(parts)) for _, p := range parts { n, err := strconv.Atoi(p) if err != nil || n < 0 { continue } out = append(out, n) } return out }