fix: 修复权限边界与安全问题

- JWT 密钥改为环境变量/配置文件/随机生成注入,移除硬编码
- AuthMiddleware 用数据库 role 覆盖 claims,降级/禁用即时生效
- 改 role/status 时主动失效目标用户会话
- /ws 支持 JWT 认证(?token=),兼容密码认证,CheckOrigin 改同源校验
- 默认管理员改用随机密码,stdout 与文件双交付
- 登录与 WS 密码认证加限流(5次/分钟)
- role 字段白名单校验(仅 admin/user)
- socket/目录权限可配置(sock_mode/sock_group/sock_dir_mode),默认兼容现状
- 配置文件权限收紧 0644→0600,目录 0755→0700
- 静态文件改用 http.Dir.Open + NoRoute,移除手动路径拼接
- 隧道加 SetReadLimit(1MB) 与单用户连接数上限(3)
- README 补充安全配置与生产部署说明
This commit is contained in:
2026-07-03 14:12:03 +08:00
parent c1d560fd4a
commit 61189a53ec
11 changed files with 382 additions and 48 deletions
+10 -7
View File
@@ -12,10 +12,13 @@ import (
"github.com/golang-jwt/jwt/v5"
)
const (
jwtSecret = "lmvpn-jwt-secret-key-2024"
tokenExpire = 24 * time.Hour
)
const tokenExpire = 24 * time.Hour
var jwtSecret []byte
func SetJWTSecret(secret string) {
jwtSecret = []byte(secret)
}
type Claims struct {
SessionID string `json:"session_id,omitempty"`
@@ -38,12 +41,12 @@ func GenerateToken(sessionID string, userID uint, username, role string) (string
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
return token.SignedString(jwtSecret)
}
func ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
return jwtSecret, nil
})
if err != nil {
return nil, err
@@ -131,7 +134,7 @@ func AuthMiddleware() gin.HandlerFunc {
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("role", user.Role)
c.Set("session_id", claims.SessionID)
c.Next()
}
+86
View File
@@ -0,0 +1,86 @@
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type RateLimiter struct {
mu sync.Mutex
attempts map[string][]time.Time
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
attempts: make(map[string][]time.Time),
limit: limit,
window: window,
}
go rl.cleanup()
return rl
}
func (rl *RateLimiter) Allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
var recent []time.Time
for _, t := range rl.attempts[key] {
if t.After(cutoff) {
recent = append(recent, t)
}
}
if len(recent) >= rl.limit {
rl.attempts[key] = recent
return false
}
recent = append(recent, now)
rl.attempts[key] = recent
return true
}
func (rl *RateLimiter) cleanup() {
ticker := time.NewTicker(rl.window)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
cutoff := time.Now().Add(-rl.window)
for key, times := range rl.attempts {
var recent []time.Time
for _, t := range times {
if t.After(cutoff) {
recent = append(recent, t)
}
}
if len(recent) == 0 {
delete(rl.attempts, key)
} else {
rl.attempts[key] = recent
}
}
rl.mu.Unlock()
}
}
func LoginRateLimit() gin.HandlerFunc {
limiter := NewRateLimiter(5, time.Minute)
return func(c *gin.Context) {
key := c.ClientIP()
if !limiter.Allow(key) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "请求过于频繁,请稍后再试"})
c.Abort()
return
}
c.Next()
}
}