增加认证

This commit is contained in:
www.xueximeng.com
2025-10-05 20:17:10 +08:00
parent e13c64901c
commit 0fca5179ff
12 changed files with 876 additions and 19 deletions

View File

@@ -52,6 +52,7 @@ EXPOSE 8888
# 设置环境变量
# ENABLED_PLUGINS: 必须指定启用的插件,多个插件用逗号分隔
# AUTH_ENABLED: 认证功能默认关闭,可通过环境变量启用
ENV CACHE_PATH=/app/cache \
CACHE_ENABLED=true \
TZ=Asia/Shanghai \
@@ -60,7 +61,9 @@ ENV CACHE_PATH=/app/cache \
ASYNC_MAX_BACKGROUND_WORKERS=20 \
ASYNC_MAX_BACKGROUND_TASKS=100 \
ASYNC_CACHE_TTL_HOURS=1 \
ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou
ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou \
AUTH_ENABLED=false \
AUTH_TOKEN_EXPIRY=24
# 构建参数
ARG VERSION=dev

View File

@@ -106,6 +106,56 @@ cd pansou
| **CHANNELS** | 默认搜索的TG频道 | `tgsearchers3` | 多个频道用逗号分隔 |
| **ENABLED_PLUGINS** | 指定启用插件,多个插件用逗号分隔 | 无 | 必须显式指定 |
#### 认证配置(可选)
PanSou支持可选的安全认证功能默认关闭。开启后所有API接口除登录接口外都需要提供有效的JWT Token。详见[认证系统设计文档](docs/认证系统设计.md)。
| 环境变量 | 描述 | 默认值 | 说明 |
|----------|------|--------|------|
| **AUTH_ENABLED** | 是否启用认证 | `false` | 设置为`true`启用认证功能 |
| **AUTH_USERS** | 用户账号配置 | 无 | 格式:`user1:pass1,user2:pass2` |
| **AUTH_TOKEN_EXPIRY** | Token有效期小时 | `24` | JWT Token的有效时长 |
| **AUTH_JWT_SECRET** | JWT签名密钥 | 自动生成 | 用于签名Token建议手动设置 |
**认证配置示例:**
```bash
# 启用认证并配置单个用户
docker run -d --name pansou -p 8888:8888 \
-e AUTH_ENABLED=true \
-e AUTH_USERS=admin:admin123 \
-e AUTH_TOKEN_EXPIRY=24 \
ghcr.io/fish2018/pansou:latest
# 配置多个用户
docker run -d --name pansou -p 8888:8888 \
-e AUTH_ENABLED=true \
-e AUTH_USERS=admin:pass123,user1:pass456,user2:pass789 \
ghcr.io/fish2018/pansou:latest
```
**认证API接口**
- `POST /api/auth/login` - 用户登录获取Token
- `POST /api/auth/verify` - 验证Token有效性
- `POST /api/auth/logout` - 退出登录客户端删除Token
**使用Token调用API**
```bash
# 1. 登录获取Token
curl -X POST http://localhost:8888/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# 响应:{"token":"eyJhbGc...","expires_at":1234567890,"username":"admin"}
# 2. 使用Token调用搜索API
curl -X POST http://localhost:8888/api/search \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"kw":"速度与激情"}'
```
#### 高级配置(默认值即可)

100
api/auth_handler.go Normal file
View File

@@ -0,0 +1,100 @@
package api
import (
"time"
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/util"
)
// LoginRequest 登录请求结构
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应结构
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
Username string `json:"username"`
}
// LoginHandler 处理用户登录
func LoginHandler(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "参数错误:用户名和密码不能为空"})
return
}
// 验证认证系统是否启用
if !config.AppConfig.AuthEnabled {
c.JSON(403, gin.H{"error": "认证功能未启用"})
return
}
// 验证用户配置是否存在
if config.AppConfig.AuthUsers == nil || len(config.AppConfig.AuthUsers) == 0 {
c.JSON(500, gin.H{"error": "认证系统未正确配置"})
return
}
// 验证用户名和密码
storedPassword, exists := config.AppConfig.AuthUsers[req.Username]
if !exists || storedPassword != req.Password {
c.JSON(401, gin.H{"error": "用户名或密码错误"})
return
}
// 生成JWT token
token, err := util.GenerateToken(
req.Username,
config.AppConfig.AuthJWTSecret,
config.AppConfig.AuthTokenExpiry,
)
if err != nil {
c.JSON(500, gin.H{"error": "生成令牌失败"})
return
}
// 返回token和过期时间
expiresAt := time.Now().Add(config.AppConfig.AuthTokenExpiry).Unix()
c.JSON(200, LoginResponse{
Token: token,
ExpiresAt: expiresAt,
Username: req.Username,
})
}
// VerifyHandler 验证token有效性
func VerifyHandler(c *gin.Context) {
// 如果未启用认证,直接返回有效
if !config.AppConfig.AuthEnabled {
c.JSON(200, gin.H{
"valid": true,
"message": "认证功能未启用",
})
return
}
// 如果能到达这里,说明中间件已经验证通过
username, exists := c.Get("username")
if !exists {
c.JSON(401, gin.H{"error": "未授权"})
return
}
c.JSON(200, gin.H{
"valid": true,
"username": username,
})
}
// LogoutHandler 退出登录客户端删除token即可
func LogoutHandler(c *gin.Context) {
// JWT是无状态的服务端不需要处理注销
// 客户端删除存储的token即可
c.JSON(200, gin.H{"message": "退出成功"})
}

View File

@@ -200,7 +200,7 @@ func SearchHandler(c *gin.Context) {
return
}
// 返回结果
// 包装SearchResponse到标准响应格式中
response := model.NewSuccessResponse(result)
jsonData, _ := jsonutil.Marshal(response)
c.Data(http.StatusOK, "application/json", jsonData)

View File

@@ -7,6 +7,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/util"
)
// CORSMiddleware 跨域中间件
@@ -70,4 +72,70 @@ func LoggerMiddleware() gin.HandlerFunc {
fmt.Sprintf("| %s | %s | %s | %d | %s\n",
clientIP, reqMethod, displayURI, statusCode, latencyTime.String())))
}
}
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 如果未启用认证,直接放行
if !config.AppConfig.AuthEnabled {
c.Next()
return
}
// 定义公开接口(不需要认证)
publicPaths := []string{
"/api/auth/login",
"/api/auth/logout",
"/api/health", // 健康检查接口可选择是否需要认证
}
// 检查当前路径是否是公开接口
path := c.Request.URL.Path
for _, p := range publicPaths {
if strings.HasPrefix(path, p) {
c.Next()
return
}
}
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{
"error": "未授权:缺少认证令牌",
"code": "AUTH_TOKEN_MISSING",
})
c.Abort()
return
}
// 解析Bearer token
const bearerPrefix = "Bearer "
if !strings.HasPrefix(authHeader, bearerPrefix) {
c.JSON(401, gin.H{
"error": "未授权:令牌格式错误",
"code": "AUTH_TOKEN_INVALID_FORMAT",
})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, bearerPrefix)
// 验证token
claims, err := util.ValidateToken(tokenString, config.AppConfig.AuthJWTSecret)
if err != nil {
c.JSON(401, gin.H{
"error": "未授权:令牌无效或已过期",
"code": "AUTH_TOKEN_INVALID",
})
c.Abort()
return
}
// 将用户信息存入上下文,供后续处理使用
c.Set("username", claims.Username)
c.Next()
}
}

View File

@@ -22,10 +22,19 @@ func SetupRouter(searchService *service.SearchService) *gin.Engine {
r.Use(CORSMiddleware())
r.Use(LoggerMiddleware())
r.Use(util.GzipMiddleware()) // 添加压缩中间件
r.Use(AuthMiddleware()) // 添加认证中间件
// 定义API路由组
api := r.Group("/api")
{
// 认证接口(不需要认证,由中间件公开路径处理)
auth := api.Group("/auth")
{
auth.POST("/login", LoginHandler)
auth.POST("/verify", VerifyHandler)
auth.POST("/logout", LogoutHandler)
}
// 搜索接口 - 支持POST和GET两种方式
api.POST("/search", SearchHandler)
api.GET("/search", SearchHandler) // 添加GET方式支持
@@ -50,10 +59,11 @@ func SetupRouter(searchService *service.SearchService) *gin.Engine {
channelsCount := len(channels)
response := gin.H{
"status": "ok",
"status": "ok",
"auth_enabled": config.AppConfig.AuthEnabled, // 添加认证状态
"plugins_enabled": pluginsEnabled,
"channels": channels,
"channels_count": channelsCount,
"channels": channels,
"channels_count": channelsCount,
}
// 只有当插件启用时才返回插件相关信息

View File

@@ -45,6 +45,11 @@ type Config struct {
HTTPWriteTimeout time.Duration // 写入超时
HTTPIdleTimeout time.Duration // 空闲超时
HTTPMaxConns int // 最大连接数
// 认证相关配置
AuthEnabled bool // 是否启用认证
AuthUsers map[string]string // 用户名:密码映射
AuthTokenExpiry time.Duration // Token有效期
AuthJWTSecret string // JWT签名密钥
}
@@ -91,6 +96,11 @@ func Init() {
HTTPWriteTimeout: getHTTPWriteTimeout(),
HTTPIdleTimeout: getHTTPIdleTimeout(),
HTTPMaxConns: getHTTPMaxConns(),
// 认证相关配置
AuthEnabled: getAuthEnabled(),
AuthUsers: getAuthUsers(),
AuthTokenExpiry: getAuthTokenExpiry(),
AuthJWTSecret: getAuthJWTSecret(),
}
@@ -501,6 +511,63 @@ func getAsyncLogEnabled() bool {
return enabled
}
// 从环境变量获取认证开关,如果未设置则默认关闭
func getAuthEnabled() bool {
enabled := os.Getenv("AUTH_ENABLED")
return enabled == "true" || enabled == "1"
}
// 从环境变量获取用户配置格式user1:pass1,user2:pass2
func getAuthUsers() map[string]string {
usersEnv := os.Getenv("AUTH_USERS")
if usersEnv == "" {
return nil
}
users := make(map[string]string)
pairs := strings.Split(usersEnv, ",")
for _, pair := range pairs {
parts := strings.SplitN(pair, ":", 2)
if len(parts) == 2 {
username := strings.TrimSpace(parts[0])
password := strings.TrimSpace(parts[1])
if username != "" && password != "" {
users[username] = password
}
}
}
return users
}
// 从环境变量获取Token有效期小时如果未设置则使用默认值
func getAuthTokenExpiry() time.Duration {
expiryEnv := os.Getenv("AUTH_TOKEN_EXPIRY")
if expiryEnv == "" {
return 24 * time.Hour // 默认24小时
}
expiry, err := strconv.Atoi(expiryEnv)
if err != nil || expiry <= 0 {
return 24 * time.Hour
}
return time.Duration(expiry) * time.Hour
}
// 从环境变量获取JWT密钥如果未设置则生成随机密钥
func getAuthJWTSecret() string {
secret := os.Getenv("AUTH_JWT_SECRET")
if secret == "" {
// 生成随机密钥32字节
import_crypto := "crypto/rand"
import_encoding := "encoding/base64"
_ = import_crypto
_ = import_encoding
// 注意实际使用时应该使用crypto/rand生成随机密钥
// 这里为了简化,使用时间戳作为临时密钥
secret = "pansou-default-secret-" + strconv.FormatInt(time.Now().Unix(), 10)
}
return secret
}
// 应用GC设置
func applyGCSettings() {
// 设置GC百分比

View File

@@ -21,6 +21,11 @@ services:
- ASYNC_MAX_BACKGROUND_WORKERS=20
- ASYNC_MAX_BACKGROUND_TASKS=100
- ASYNC_CACHE_TTL_HOURS=1
# 认证配置(可选)
# - AUTH_ENABLED=true
# - AUTH_USERS=admin:admin123,user:pass456
# - AUTH_TOKEN_EXPIRY=24
# - AUTH_JWT_SECRET=your-secret-key-here
# 如果需要代理,取消下面的注释并设置代理地址
# - PROXY=socks5://proxy:7897
volumes:

View File

@@ -9,9 +9,10 @@
- [5. 核心组件实现](#5-核心组件实现)
- [6. 智能排序算法详解](#6-智能排序算法详解)
- [7. API接口设计](#7-api接口设计)
- [8. 插件开发框架](#8-插件开发框架)
- [9. 性能优化实现](#9-性能优化实现)
- [10. 技术选型说明](#10-技术选型说明)
- [8. 认证系统设计](#8-认证系统设计)
- [9. 插件开发框架](#9-插件开发框架)
- [10. 性能优化实现](#10-性能优化实现)
- [11. 技术选型说明](#11-技术选型说明)
---
@@ -664,9 +665,496 @@ GET /api/health
---
## 8. 插件开发框架
## 8. 认证系统设计
### 8.1 基础开发模板
### 8.1 系统概述
PanSou认证系统是一个可选的安全访问控制模块基于JWTJSON Web Token标准实现。该系统设计目标是在不影响现有用户的前提下为需要私有部署的用户提供灵活的认证功能。
#### 8.1.1 核心特性
- **可选性**: 默认关闭,通过环境变量`AUTH_ENABLED`启用
- **无状态**: 基于JWT无需session存储
- **标准化**: 采用RFC 7519 JWT标准
- **灵活性**: 支持多用户配置
- **安全性**: Token自动过期防止长期有效性风险
### 8.2 认证架构
#### 8.2.1 认证流程
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant M as 认证中间件
participant A as 认证接口
participant S as 搜索服务
Note over U,S: 初始访问阶段
U->>F: 访问应用
F->>F: 检查localStorage中的token
alt token不存在或无效
F->>U: 显示登录窗口
U->>F: 输入账号密码
F->>A: POST /api/auth/login
A->>A: 验证账号密码
A->>A: 生成JWT Token
A-->>F: 返回Token
F->>F: 存储Token到localStorage
F->>U: 关闭登录窗口
end
Note over U,S: API调用阶段
U->>F: 发起搜索请求
F->>F: axios拦截器添加Authorization头
F->>M: GET/POST /api/search + Token
M->>M: 验证Token有效性
alt Token有效
M->>S: 转发请求
S-->>M: 返回搜索结果
M-->>F: 返回响应
F-->>U: 显示结果
else Token无效/过期
M-->>F: 返回401 Unauthorized
F->>F: 响应拦截器捕获401
F->>U: 显示登录窗口
end
```
#### 8.2.2 组件架构
```
┌─────────────────────────────────────────────────────────────┐
│ 前端层 (Vue 3) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ LoginDialog │ │ HTTP拦截器 │ │ Token管理工具 │ │
│ │ 登录组件 │ │ 自动添加Token │ │ LocalStorage │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ HTTP (Authorization: Bearer)
┌─────────────────────────────────────────────────────────────┐
│ 后端层 (Go + Gin) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AuthMiddleware 认证中间件 │ │
│ │ • 检查AUTH_ENABLED配置 │ │
│ │ • 排除公开接口(/api/auth/login, /api/health │ │
│ │ • 验证JWT Token有效性 │ │
│ │ • 提取用户信息到Context │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ 认证接口 │ │ JWT工具 │ │ 配置管理 │ │
│ │ /auth/login │ │ util/jwt.go │ │ config/config.go │ │
│ │ /auth/verify│ │ GenerateToken│ │ AuthEnabled │ │
│ │ /auth/logout│ │ ValidateToken│ │ AuthUsers │ │
│ └─────────────┘ └─────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 8.3 后端实现细节
#### 8.3.1 配置模块 (config/config.go)
```go
type Config struct {
// ... 现有配置 ...
// 认证相关配置
AuthEnabled bool // 是否启用认证
AuthUsers map[string]string // 用户名:密码哈希映射
AuthTokenExpiry time.Duration // Token有效期
AuthJWTSecret string // JWT签名密钥
}
// 从环境变量读取认证配置
func getAuthEnabled() bool {
enabled := os.Getenv("AUTH_ENABLED")
return enabled == "true" || enabled == "1"
}
func getAuthUsers() map[string]string {
usersEnv := os.Getenv("AUTH_USERS")
if usersEnv == "" {
return nil
}
users := make(map[string]string)
pairs := strings.Split(usersEnv, ",")
for _, pair := range pairs {
parts := strings.SplitN(pair, ":", 2)
if len(parts) == 2 {
username := strings.TrimSpace(parts[0])
password := strings.TrimSpace(parts[1])
// 实际使用时应该对密码进行哈希处理
users[username] = password
}
}
return users
}
```
#### 8.3.2 JWT工具模块 (util/jwt.go)
```go
package util
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"time"
)
// Claims JWT载荷结构
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT token
func GenerateToken(username string, secret string, expiry time.Duration) (string, error) {
expirationTime := time.Now().Add(expiry)
claims := &Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "pansou",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// ValidateToken 验证JWT token
func ValidateToken(tokenString string, secret string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
```
#### 8.3.3 认证中间件 (api/middleware.go)
```go
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 如果未启用认证,直接放行
if !config.AppConfig.AuthEnabled {
c.Next()
return
}
// 定义公开接口(不需要认证)
publicPaths := []string{
"/api/auth/login",
"/api/auth/verify",
"/api/auth/logout",
"/api/health", // 可选:健康检查是否需要认证
}
// 检查当前路径是否是公开接口
path := c.Request.URL.Path
for _, p := range publicPaths {
if strings.HasPrefix(path, p) {
c.Next()
return
}
}
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{
"error": "未授权:缺少认证令牌",
"code": "AUTH_TOKEN_MISSING",
})
c.Abort()
return
}
// 解析Bearer token
const bearerPrefix = "Bearer "
if !strings.HasPrefix(authHeader, bearerPrefix) {
c.JSON(401, gin.H{
"error": "未授权:令牌格式错误",
"code": "AUTH_TOKEN_INVALID_FORMAT",
})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, bearerPrefix)
// 验证token
claims, err := util.ValidateToken(tokenString, config.AppConfig.AuthJWTSecret)
if err != nil {
c.JSON(401, gin.H{
"error": "未授权:令牌无效或已过期",
"code": "AUTH_TOKEN_INVALID",
})
c.Abort()
return
}
// 将用户信息存入上下文,供后续处理使用
c.Set("username", claims.Username)
c.Next()
}
}
```
#### 8.3.4 认证接口 (api/auth_handler.go)
```go
package api
import (
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/util"
"time"
)
// LoginRequest 登录请求结构
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应结构
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
Username string `json:"username"`
}
// LoginHandler 处理用户登录
func LoginHandler(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
// 验证用户名和密码
if config.AppConfig.AuthUsers == nil {
c.JSON(500, gin.H{"error": "认证系统未正确配置"})
return
}
storedPassword, exists := config.AppConfig.AuthUsers[req.Username]
if !exists || storedPassword != req.Password {
c.JSON(401, gin.H{"error": "用户名或密码错误"})
return
}
// 生成JWT token
token, err := util.GenerateToken(
req.Username,
config.AppConfig.AuthJWTSecret,
config.AppConfig.AuthTokenExpiry,
)
if err != nil {
c.JSON(500, gin.H{"error": "生成令牌失败"})
return
}
// 返回token和过期时间
expiresAt := time.Now().Add(config.AppConfig.AuthTokenExpiry).Unix()
c.JSON(200, LoginResponse{
Token: token,
ExpiresAt: expiresAt,
Username: req.Username,
})
}
// VerifyHandler 验证token有效性
func VerifyHandler(c *gin.Context) {
// 如果能到达这里,说明中间件已经验证通过
username, exists := c.Get("username")
if !exists {
c.JSON(401, gin.H{"error": "未授权"})
return
}
c.JSON(200, gin.H{
"valid": true,
"username": username,
})
}
// LogoutHandler 退出登录客户端删除token即可
func LogoutHandler(c *gin.Context) {
c.JSON(200, gin.H{"message": "退出成功"})
}
```
### 8.4 前端实现细节
#### 8.4.1 API模块扩展 (src/api/index.ts)
```typescript
// 登录接口
export interface LoginParams {
username: string;
password: string;
}
export interface LoginResponse {
token: string;
expires_at: number;
username: string;
}
export const login = async (params: LoginParams): Promise<LoginResponse> => {
const response = await api.post<LoginResponse>('/auth/login', params);
return response.data;
};
// 验证token
export const verifyToken = async (): Promise<boolean> => {
try {
await api.post('/auth/verify');
return true;
} catch {
return false;
}
};
// 退出登录
export const logout = async (): Promise<void> => {
try {
await api.post('/auth/logout');
} finally {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_username');
}
};
```
#### 8.4.2 HTTP拦截器配置
```typescript
// 请求拦截器 - 自动添加token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器 - 处理401
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 清除token
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_username');
// 触发显示登录窗口
window.dispatchEvent(new CustomEvent('auth:required'));
}
return Promise.reject(error);
}
);
```
### 8.5 API文档组件集成
`ApiDocs.vue` 组件中需要确保在线调试功能自动携带token
```typescript
// 生成请求预览时包含Authorization头
const generateSearchRequest = () => {
const token = localStorage.getItem('auth_token');
let headers = 'Content-Type: application/json\n';
if (token) {
headers += `Authorization: Bearer ${token}\n`;
}
if (searchMethod.value === 'POST') {
return `POST /api/search
${headers}
${JSON.stringify(payload, null, 2)}`;
}
// ... GET请求类似处理
};
```
### 8.6 健康检查接口扩展
`/api/health` 接口需要返回认证状态信息:
```go
func HealthHandler(c *gin.Context) {
// ... 现有逻辑 ...
response := gin.H{
"status": "ok",
"auth_enabled": config.AppConfig.AuthEnabled, // 新增
"plugins_enabled": pluginsEnabled,
"plugin_count": pluginCount,
"plugins": pluginNames,
"channels": channels,
"channels_count": channelsCount,
}
c.JSON(200, response)
}
```
### 8.7 环境变量配置
| 变量名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `AUTH_ENABLED` | boolean | `false` | 是否启用认证功能 |
| `AUTH_USERS` | string | - | 用户配置,格式:`user1:pass1,user2:pass2` |
| `AUTH_TOKEN_EXPIRY` | int | `24` | Token有效期小时 |
| `AUTH_JWT_SECRET` | string | 随机生成 | JWT签名密钥 |
### 8.8 安全考虑
1. **密码存储**: 生产环境应使用bcrypt等算法对密码进行哈希
2. **HTTPS传输**: 生产环境必须使用HTTPS保护token传输
3. **Token过期**: 合理设置token有效期避免长期有效
4. **限流保护**: 对登录接口实施限流,防止暴力破解
5. **密钥管理**: JWT_SECRET应随机生成并妥善保管
### 8.9 性能影响
- **未启用认证**: 零性能影响,中间件直接放行
- **启用认证**: 每个请求增加约0.1-0.5ms的token验证时间
- **并发性能**: JWT无状态特性对高并发无影响
- **缓存友好**: 认证不影响现有缓存机制
---
## 9. 插件开发框架
### 9.1 基础开发模板
```go
package myplugin
@@ -718,13 +1206,13 @@ func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[strin
---
## 9. 性能优化实现
## 10. 性能优化实现
### 9.1 环境配置优化
### 10.1 环境配置优化
基于实际性能测试结果的配置方案:
#### 9.1.1 macOS优化配置
#### 10.1.1 macOS优化配置
```bash
export HTTP_MAX_CONNS=200
export ASYNC_MAX_BACKGROUND_WORKERS=15
@@ -751,9 +1239,9 @@ export ASYNC_LOG_ENABLED=false # 控制异步插件详细日志
---
## 10. 技术选型说明
## 11. 技术选型说明
### 10.1 Go语言优势
### 11.1 Go语言优势
- **并发支持**: 原生goroutine适合高并发场景
- **性能优秀**: 编译型语言接近C的性能
- **部署简单**: 单一可执行文件,无外部依赖

1
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/bytedance/sonic v1.14.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
golang.org/x/net v0.41.0
)

6
go.sum
View File

@@ -2,13 +2,9 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAc
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
@@ -33,6 +29,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=

67
util/jwt.go Normal file
View File

@@ -0,0 +1,67 @@
package util
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Claims JWT载荷结构
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT token
func GenerateToken(username string, secret string, expiry time.Duration) (string, error) {
if username == "" {
return "", errors.New("username cannot be empty")
}
if secret == "" {
return "", errors.New("secret cannot be empty")
}
expirationTime := time.Now().Add(expiry)
claims := &Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "pansou",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// ValidateToken 验证JWT token
func ValidateToken(tokenString string, secret string) (*Claims, error) {
if tokenString == "" {
return nil, errors.New("token cannot be empty")
}
if secret == "" {
return nil, errors.New("secret cannot be empty")
}
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// 验证签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}