增加密码解锁与游客签名 cookie;发帖页对齐 1440 并默认双栏预览;修复 locked 解析、按钮对比度与回复聚焦。 Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
3.5 KiB
Go
158 lines
3.5 KiB
Go
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
|
||
}
|