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
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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 设置认证 cookie(SameSite=Lax 防 CSRF;https 下应启用 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
|
||||
}
|
||||
Reference in New Issue
Block a user