Files
note-manager/middleware/session.go
T
Your Name d9793300f9 feat: 多租户账号体系 + 前台收藏按钮
- 新增 users 表(user_id 数据隔离,bcrypt 密码)
- 认证: 注册/登录(用户名+密码)/会话绑定用户, 首个用户成为管理员并接管旧数据
- 数据隔离: 笔记/分类/标签/回收站/版本/草稿/图谱/FTS 全部按用户隔离
- 前台: 登录/注册弹窗, 登录后★收藏自己的笔记, 游客只读公开笔记
- 后台: 用户名+密码登录, 每人管理自己的工作区, 越权访问返回404
- 冒烟测试重构+新增多租户隔离用例(78/78)
2026-08-11 12:58:12 +08:00

161 lines
3.6 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 middleware
import (
"crypto/rand"
"encoding/hex"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// ---- 服务端会话存储(内存)----
// 用随机 token 代替固定字符串 cookie,登出/过期即失效。
// 会话绑定到具体用户 ID(多租户)。
// session 会话数据
type session struct {
userID uint
expiry time.Time
}
var (
sessions = make(map[string]session) // token -> 会话
sessionsMu sync.RWMutex
)
const (
CookieName = "note_token" // 会话 cookie
SessionTTL = 7 * 24 * time.Hour
CookieMaxAge = 7 * 24 * 3600
sessionCleanT = 10
)
// NewSession 创建并绑定一个用户会话
func NewSession(userID uint) (string, time.Time) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
b = []byte(time.Now().Format("20060102150405.000000000"))
}
token := hex.EncodeToString(b)
exp := time.Now().Add(SessionTTL)
sessionsMu.Lock()
sessions[token] = session{userID: userID, expiry: exp}
sessionsMu.Unlock()
go cleanExpiredSessions()
return token, exp
}
// RevokeSession 登出时删除会话
func RevokeSession(token string) {
sessionsMu.Lock()
delete(sessions, token)
sessionsMu.Unlock()
}
// IsValidSession 校验 token 是否有效且未过期
func IsValidSession(token string) bool {
if token == "" {
return false
}
sessionsMu.RLock()
s, ok := sessions[token]
sessionsMu.RUnlock()
if !ok {
return false
}
if time.Now().After(s.expiry) {
RevokeSession(token)
return false
}
return true
}
// SessionUserID 获取 token 对应的用户 ID(无效返回 0)
func SessionUserID(token string) uint {
if token == "" {
return 0
}
sessionsMu.RLock()
s, ok := sessions[token]
sessionsMu.RUnlock()
if !ok {
return 0
}
if time.Now().After(s.expiry) {
RevokeSession(token)
return 0
}
return s.userID
}
// cleanExpiredSessions 定期清理过期会话,防止内存泄漏
func cleanExpiredSessions() {
sessionsMu.Lock()
defer sessionsMu.Unlock()
for token, s := range sessions {
if time.Now().After(s.expiry) {
delete(sessions, token)
}
}
}
// CurrentUser 解析当前登录用户 ID 并写入 context(可为 0 表示游客)。
// 供公开路由/混合路由使用。
func CurrentUser() gin.HandlerFunc {
return func(c *gin.Context) {
uid := SessionUserID(GetAuthToken(c))
c.Set("user_id", uid)
c.Next()
}
}
// AuthRequired 认证中间件:必须登录,否则返回 401。
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token, err := c.Cookie(CookieName)
if err != nil || !IsValidSession(token) {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "请先登录"})
c.Abort()
return
}
uid := SessionUserID(token)
c.Set("user_id", uid)
c.Next()
}
}
// GetUserID 从 context 取当前用户 ID(游客为 0)
func GetUserID(c *gin.Context) uint {
if v, ok := c.Get("user_id"); ok {
if uid, ok2 := v.(uint); ok2 {
return uid
}
}
return 0
}
// IsLoggedIn 判断当前请求是否已登录
func IsLoggedIn(c *gin.Context) bool {
return GetUserID(c) != 0
}
// SetAuthCookie 设置认证 cookieSameSite=Lax 防 CSRFhttps 下应启用 Secure
func SetAuthCookie(c *gin.Context, token string, secure bool) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(CookieName, token, CookieMaxAge, "/", "", secure, true)
}
// ClearAuthCookie 清除认证 cookie
func ClearAuthCookie(c *gin.Context) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(CookieName, "", -1, "/", "", false, true)
}
// GetAuthToken 从请求读取 token
func GetAuthToken(c *gin.Context) string {
token, _ := c.Cookie(CookieName)
return token
}