feat: 多租户账号体系 + 前台收藏按钮
- 新增 users 表(user_id 数据隔离,bcrypt 密码) - 认证: 注册/登录(用户名+密码)/会话绑定用户, 首个用户成为管理员并接管旧数据 - 数据隔离: 笔记/分类/标签/回收站/版本/草稿/图谱/FTS 全部按用户隔离 - 前台: 登录/注册弹窗, 登录后★收藏自己的笔记, 游客只读公开笔记 - 后台: 用户名+密码登录, 每人管理自己的工作区, 越权访问返回404 - 冒烟测试重构+新增多租户隔离用例(78/78)
This commit is contained in:
+67
-16
@@ -10,32 +10,38 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ---- 服务端会话存储(内存) ----
|
||||
// 用随机 token 代替之前的固定字符串 cookie,登出/过期即失效。
|
||||
// ---- 服务端会话存储(内存)----
|
||||
// 用随机 token 代替固定字符串 cookie,登出/过期即失效。
|
||||
// 会话绑定到具体用户 ID(多租户)。
|
||||
|
||||
// session 会话数据
|
||||
type session struct {
|
||||
userID uint
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
sessions = make(map[string]time.Time) // token -> 过期时间
|
||||
sessions = make(map[string]session) // token -> 会话
|
||||
sessionsMu sync.RWMutex
|
||||
)
|
||||
|
||||
const (
|
||||
CookieName = "admin_token"
|
||||
SessionTTL = 7 * 24 * time.Hour // 会话有效期 7 天
|
||||
CookieMaxAge = 7 * 24 * 3600 // cookie 有效期(秒)
|
||||
sessionCleanT = 10 // 清理过期会话的间隔(分钟)
|
||||
CookieName = "note_token" // 会话 cookie
|
||||
SessionTTL = 7 * 24 * time.Hour
|
||||
CookieMaxAge = 7 * 24 * 3600
|
||||
sessionCleanT = 10
|
||||
)
|
||||
|
||||
// NewSessionToken 生成一个新的会话 token 并注册
|
||||
func NewSessionToken() (string, time.Time) {
|
||||
// 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] = exp
|
||||
sessions[token] = session{userID: userID, expiry: exp}
|
||||
sessionsMu.Unlock()
|
||||
go cleanExpiredSessions()
|
||||
return token, exp
|
||||
@@ -54,30 +60,58 @@ func IsValidSession(token string) bool {
|
||||
return false
|
||||
}
|
||||
sessionsMu.RLock()
|
||||
exp, ok := sessions[token]
|
||||
s, ok := sessions[token]
|
||||
sessionsMu.RUnlock()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if time.Now().After(exp) {
|
||||
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, exp := range sessions {
|
||||
if time.Now().After(exp) {
|
||||
for token, s := range sessions {
|
||||
if time.Now().After(s.expiry) {
|
||||
delete(sessions, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRequired 管理接口认证中间件
|
||||
// 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)
|
||||
@@ -86,10 +120,27 @@ func AuthRequired() gin.HandlerFunc {
|
||||
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 设置认证 cookie(SameSite=Lax 防 CSRF;https 下应启用 Secure)
|
||||
func SetAuthCookie(c *gin.Context, token string, secure bool) {
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
|
||||
Reference in New Issue
Block a user