Files
note-manager/middleware/session.go
T
Your Name 74fa759274 feat: 云笔记增强 - 安全加固 + 回收站/版本历史/分享/批量导出 + 前端优化
- 安全: 认证改随机token会话(弃固定cookie), 笔记密码SHA256升级为bcrypt(自动迁移),
  堵住GET /api/notes/:id泄露带密码笔记, CORS收紧+SameSite防CSRF, 上传图片内容嗅探
- 回收站: 软删除(deleted_at), 列表/恢复/彻底删除/清空, 目录子树连删连恢复
- 版本历史: note_versions表存快照, 每次保存自动留档, 支持查看/回滚
- 分享: 生成随机token分享链接, 支持过期时间, 公开阅读页share.html
- 批量导出: 全部笔记打包zip(按目录结构+front matter)
- 前端: 深色模式, Mermaid图表, 待办清单checkbox, 字数统计;
  后台新增回收站/历史/分享面板和批量导出按钮
- 新增deploy/note-manager.service systemd单元与smoke_test.py
2026-08-11 11:21:29 +08:00

110 lines
2.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,登出/过期即失效。
var (
sessions = make(map[string]time.Time) // token -> 过期时间
sessionsMu sync.RWMutex
)
const (
CookieName = "admin_token"
SessionTTL = 7 * 24 * time.Hour // 会话有效期 7 天
CookieMaxAge = 7 * 24 * 3600 // cookie 有效期(秒)
sessionCleanT = 10 // 清理过期会话的间隔(分钟)
)
// NewSessionToken 生成一个新的会话 token 并注册
func NewSessionToken() (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
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()
exp, ok := sessions[token]
sessionsMu.RUnlock()
if !ok {
return false
}
if time.Now().After(exp) {
RevokeSession(token)
return false
}
return true
}
// cleanExpiredSessions 定期清理过期会话,防止内存泄漏
func cleanExpiredSessions() {
sessionsMu.Lock()
defer sessionsMu.Unlock()
for token, exp := range sessions {
if time.Now().After(exp) {
delete(sessions, token)
}
}
}
// AuthRequired 管理接口认证中间件
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
}
c.Next()
}
}
// 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
}