Files
nestly/internal/auth/session.go
T
2026-10-05 07:40:34 +08:00

110 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package auth 提供以 HMAC 簽名 cookie 實作的輕量 session,
// 不需額外的 session 儲存,適合目前無狀態的 SSR 架構。
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
const (
// CookieName session cookie 的名稱。
CookieName = "nestly_session"
defaultTTL = 7 * 24 * time.Hour
)
// ErrInvalidSession 代表 cookie 不存在、簽名不符或已過期;
// 呼叫端可將其視為「未登入」而非錯誤。
var ErrInvalidSession = errors.New("session 無效或已過期")
// SessionManager 以伺服器端密鑰簽發並驗證 session cookie。
// cookie 值格式為 "uid.expUnix.signature"(HMAC-SHA256),
// 驗證僅保證簽名正確且未過期,帳號是否仍存在須由呼叫端查詢。
type SessionManager struct {
secret []byte
ttl time.Duration
secure bool
}
// NewSessionManager 以 secret 簽章建立 SessionManager;
// secure 控制 cookie 是否僅經 HTTPS 傳送(本機開發為 false)。
func NewSessionManager(secret string, secure bool) *SessionManager {
return &SessionManager{
secret: []byte(secret),
ttl: defaultTTL,
secure: secure,
}
}
func (m *SessionManager) sign(payload string) []byte {
mac := hmac.New(sha256.New, m.secret)
mac.Write([]byte(payload))
return mac.Sum(nil)
}
// Login 為 uid 簽發 session cookie。
func (m *SessionManager) Login(w http.ResponseWriter, uid uint) {
payload := fmt.Sprintf("%d.%d", uid, time.Now().Add(m.ttl).Unix())
http.SetCookie(w, &http.Cookie{
Name: CookieName,
Value: payload + "." + hex.EncodeToString(m.sign(payload)),
Path: "/",
MaxAge: int(m.ttl.Seconds()),
HttpOnly: true,
Secure: m.secure,
SameSite: http.SameSiteLaxMode,
})
}
// Logout 使 session cookie 立即失效。
func (m *SessionManager) Logout(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: CookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: m.secure,
SameSite: http.SameSiteLaxMode,
})
}
// UserID 驗證請求中的 session cookie 並取出使用者 ID。
func (m *SessionManager) UserID(r *http.Request) (uint, error) {
c, err := r.Cookie(CookieName)
if err != nil || c.Value == "" {
return 0, ErrInvalidSession
}
// 值格式為 uid.exp.sig,簽名為最後一段,從右側切分以免誤切數值中的點。
dot := strings.LastIndexByte(c.Value, '.')
if dot <= 0 {
return 0, ErrInvalidSession
}
payload, sigHex := c.Value[:dot], c.Value[dot+1:]
uidStr, expStr, ok := strings.Cut(payload, ".")
if !ok {
return 0, ErrInvalidSession
}
uid, err := strconv.ParseUint(uidStr, 10, 64)
if err != nil {
return 0, ErrInvalidSession
}
exp, err := strconv.ParseInt(expStr, 10, 64)
if err != nil || time.Now().Unix() >= exp {
return 0, ErrInvalidSession
}
sig, err := hex.DecodeString(sigHex)
if err != nil || !hmac.Equal(sig, m.sign(payload)) {
return 0, ErrInvalidSession
}
return uint(uid), nil
}