74fa759274
- 安全: 认证改随机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
110 lines
2.6 KiB
Go
110 lines
2.6 KiB
Go
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
|
||
}
|