feat: 增加基础模块,登录、退出登录等接口 #1
No files matched your search
+50
-27
@@ -1,37 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/internal/api/routes"
|
||||
"license-manager/internal/config"
|
||||
"license-manager/pkg/logger"
|
||||
)
|
||||
|
||||
// @title License Manager API
|
||||
// @version 1.0
|
||||
// @description 软件授权管理平台API文档
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:18888
|
||||
// @BasePath /
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Bearer token for authentication
|
||||
|
||||
func main() {
|
||||
logrus.Info("Starting License Manager Server...")
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
// API routes
|
||||
api := r.PathPrefix("/api").Subrouter()
|
||||
api.HandleFunc("/validate", validateHandler).Methods("POST")
|
||||
api.HandleFunc("/activate", activateHandler).Methods("POST")
|
||||
api.HandleFunc("/heartbeat", heartbeatHandler).Methods("POST")
|
||||
api.HandleFunc("/license/{code}", getLicenseHandler).Methods("GET")
|
||||
api.HandleFunc("/customers", getCustomersHandler).Methods("GET")
|
||||
|
||||
// Tools route
|
||||
r.HandleFunc("/tools/{tool}", getToolHandler).Methods("GET")
|
||||
|
||||
logrus.Info("Server starting on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", r))
|
||||
// 加载配置
|
||||
if err := config.Load(""); err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Placeholder handlers
|
||||
func validateHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
func activateHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
func heartbeatHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
func getLicenseHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
func getCustomersHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
func getToolHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
// 初始化日志
|
||||
logger.Init()
|
||||
log := logger.GetLogger()
|
||||
|
||||
log.Info("启动 License Manager 服务器...")
|
||||
|
||||
cfg := config.GetConfig()
|
||||
|
||||
// 设置Gin模式
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
|
||||
// 设置路由
|
||||
router := routes.SetupRouter()
|
||||
|
||||
// 启动服务器
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
log.Infof("服务器启动在 %s", addr)
|
||||
|
||||
if err := router.Run(addr); err != nil {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,25 @@
|
||||
server:
|
||||
port: 8080
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
mode: "release" # debug, release, test
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "your-256-bit-secret-key-change-this-in-production"
|
||||
expire_hours: 1
|
||||
refresh_threshold_minutes: 30
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "admin@123" # 明文密码,部署人员可修改
|
||||
security:
|
||||
max_login_attempts: 5
|
||||
lockout_duration_minutes: 15
|
||||
|
||||
log:
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "json" # json, text
|
||||
|
||||
# 数据库配置(暂未使用)
|
||||
database:
|
||||
type: "postgres" # postgres or mysql
|
||||
host: "localhost"
|
||||
@@ -10,11 +28,3 @@ database:
|
||||
user: "postgres"
|
||||
password: "password"
|
||||
ssl_mode: "disable"
|
||||
|
||||
security:
|
||||
hmac_secret: "your-secret-key-here"
|
||||
jwt_secret: "your-jwt-secret-here"
|
||||
|
||||
logging:
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "json" # json or text
|
||||
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 18888
|
||||
mode: "debug" # debug, release, test 开发环境使用debug模式
|
||||
|
||||
auth:
|
||||
jwt:
|
||||
secret: "license-manager-dev-secret-key-32-bytes" # 32字节的随机字符串
|
||||
expire_hours: 1 # 1小时后过期
|
||||
refresh_threshold_minutes: 30 # 30分钟后可刷新token
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "admin@123"
|
||||
security:
|
||||
max_login_attempts: 5 # 5次失败后锁定15分钟
|
||||
lockout_duration_minutes: 15 # 15分钟后解锁
|
||||
|
||||
log:
|
||||
level: "info" # 日志级别
|
||||
format: "text" # 日志格式
|
||||
@@ -0,0 +1,265 @@
|
||||
# API开发规范
|
||||
|
||||
## 目录结构规范
|
||||
|
||||
### 分层架构
|
||||
```
|
||||
internal/
|
||||
├── models/ # 数据模型层
|
||||
├── service/ # 业务逻辑层
|
||||
├── repository/ # 数据访问层
|
||||
└── api/handlers/ # HTTP处理层
|
||||
```
|
||||
|
||||
### 职责划分
|
||||
- **Models**: 定义请求/响应结构体,数据验证规则
|
||||
- **Service**: 实现业务逻辑,处理业务规则
|
||||
- **Repository**: 数据库操作,缓存操作
|
||||
- **Handler**: HTTP请求处理,参数绑定,响应格式化
|
||||
|
||||
## 文件命名规范
|
||||
|
||||
### 模块分组
|
||||
按业务模块组织文件:
|
||||
```
|
||||
models/
|
||||
├── auth.go # 认证相关模型
|
||||
├── customer.go # 客户相关模型
|
||||
├── license.go # 授权相关模型
|
||||
└── common.go # 通用模型
|
||||
|
||||
service/
|
||||
├── interfaces.go # 服务接口定义
|
||||
├── auth_service.go
|
||||
├── customer_service.go
|
||||
└── license_service.go
|
||||
|
||||
api/handlers/
|
||||
├── auth_handler.go
|
||||
├── customer_handler.go
|
||||
└── license_handler.go
|
||||
```
|
||||
|
||||
## 代码结构规范
|
||||
|
||||
### 1. 模型定义 (models/)
|
||||
```go
|
||||
// 请求模型
|
||||
type CreateCustomerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
|
||||
// 响应模型
|
||||
type CustomerResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 服务接口 (service/interfaces.go)
|
||||
```go
|
||||
type CustomerService interface {
|
||||
CreateCustomer(req *models.CreateCustomerRequest) (*models.CustomerResponse, error)
|
||||
GetCustomer(id int) (*models.CustomerResponse, error)
|
||||
UpdateCustomer(id int, req *models.UpdateCustomerRequest) error
|
||||
DeleteCustomer(id int) error
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 服务实现 (service/)
|
||||
```go
|
||||
type customerService struct {
|
||||
repo repository.CustomerRepository
|
||||
}
|
||||
|
||||
func NewCustomerService(repo repository.CustomerRepository) CustomerService {
|
||||
return &customerService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *customerService) CreateCustomer(req *models.CreateCustomerRequest) (*models.CustomerResponse, error) {
|
||||
// 业务逻辑处理
|
||||
// 数据验证
|
||||
// 调用repository
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 处理器实现 (api/handlers/)
|
||||
```go
|
||||
type CustomerHandler struct {
|
||||
service service.CustomerService
|
||||
}
|
||||
|
||||
func NewCustomerHandler(service service.CustomerService) *CustomerHandler {
|
||||
return &CustomerHandler{service: service}
|
||||
}
|
||||
|
||||
// @Summary 创建客户
|
||||
// @Tags 客户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.CreateCustomerRequest true "客户信息"
|
||||
// @Success 200 {object} models.APIResponse
|
||||
// @Router /api/v1/customers [post]
|
||||
func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
|
||||
var req models.CreateCustomerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Error: "INVALID_REQUEST",
|
||||
Message: "请求参数无效",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.CreateCustomer(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
Error: "CREATE_FAILED",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "创建成功",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## API设计规范
|
||||
|
||||
### 1. 路由设计
|
||||
```
|
||||
GET /api/v1/customers # 获取客户列表
|
||||
POST /api/v1/customers # 创建客户
|
||||
GET /api/v1/customers/:id # 获取客户详情
|
||||
PUT /api/v1/customers/:id # 更新客户
|
||||
DELETE /api/v1/customers/:id # 删除客户
|
||||
```
|
||||
|
||||
### 2. 响应格式
|
||||
```go
|
||||
// 成功响应
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// 错误响应
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
```
|
||||
|
||||
### 3. HTTP状态码
|
||||
- `200` - 请求成功
|
||||
- `201` - 创建成功
|
||||
- `400` - 请求参数错误
|
||||
- `401` - 未认证
|
||||
- `403` - 权限不足
|
||||
- `404` - 资源不存在
|
||||
- `500` - 服务器内部错误
|
||||
|
||||
## Swagger文档规范
|
||||
|
||||
### 必需注解
|
||||
```go
|
||||
// @Summary 接口简要描述
|
||||
// @Description 接口详细描述
|
||||
// @Tags 标签分组
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param 参数名 参数位置 参数类型 是否必需 "参数描述"
|
||||
// @Success 状态码 {object} 响应类型 "成功描述"
|
||||
// @Failure 状态码 {object} 错误类型 "失败描述"
|
||||
// @Security BearerAuth (需要认证的接口)
|
||||
// @Router 路由路径 [HTTP方法]
|
||||
```
|
||||
|
||||
### 标签分组
|
||||
- `认证` - 登录、登出、Token相关
|
||||
- `客户管理` - 客户CRUD操作
|
||||
- `授权管理` - 授权码、许可证相关
|
||||
- `系统` - 系统信息、健康检测
|
||||
|
||||
## 错误处理规范
|
||||
|
||||
### 错误码定义
|
||||
```go
|
||||
const (
|
||||
// 通用错误
|
||||
ErrInvalidRequest = "INVALID_REQUEST"
|
||||
ErrUnauthorized = "UNAUTHORIZED"
|
||||
ErrForbidden = "FORBIDDEN"
|
||||
ErrNotFound = "NOT_FOUND"
|
||||
ErrInternalError = "INTERNAL_ERROR"
|
||||
|
||||
// 业务错误
|
||||
ErrCustomerExists = "CUSTOMER_EXISTS"
|
||||
ErrLicenseExpired = "LICENSE_EXPIRED"
|
||||
ErrInvalidLicense = "INVALID_LICENSE"
|
||||
)
|
||||
```
|
||||
|
||||
### 统一错误处理
|
||||
```go
|
||||
func handleError(c *gin.Context, err error) {
|
||||
var statusCode int
|
||||
var errorCode string
|
||||
|
||||
switch err {
|
||||
case ErrCustomerExists:
|
||||
statusCode = http.StatusConflict
|
||||
errorCode = "CUSTOMER_EXISTS"
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
errorCode = "INTERNAL_ERROR"
|
||||
}
|
||||
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Code: statusCode,
|
||||
Error: errorCode,
|
||||
Message: err.Error(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 1. 新增API步骤
|
||||
1. 在 `models/` 定义请求/响应结构体
|
||||
2. 在 `service/interfaces.go` 添加服务接口
|
||||
3. 在 `service/` 实现业务逻辑
|
||||
4. 在 `handlers/` 实现HTTP处理器
|
||||
5. 在 `routes/router.go` 注册路由
|
||||
6. 添加Swagger注解
|
||||
7. 生成文档:`swag init -g cmd/main.go -o ./docs/swagger`
|
||||
|
||||
### 2. 代码检查清单
|
||||
- [ ] 模型定义是否合理
|
||||
- [ ] 业务逻辑是否在service层
|
||||
- [ ] 错误处理是否完整
|
||||
- [ ] Swagger注解是否完整
|
||||
- [ ] 路由是否正确注册
|
||||
- [ ] HTTP状态码是否合适
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **职责单一**:每个函数只做一件事
|
||||
2. **接口优先**:先定义接口,再实现
|
||||
3. **错误优雅**:统一错误处理和响应格式
|
||||
4. **文档完整**:所有API都要有Swagger文档
|
||||
5. **命名规范**:使用有意义的命名
|
||||
6. **代码复用**:提取公共逻辑到utils包
|
||||
@@ -0,0 +1,73 @@
|
||||
# 后端架构设计
|
||||
|
||||
## 技术栈选择
|
||||
|
||||
Web框架: Gin
|
||||
ORM: GORM
|
||||
配置: Viper
|
||||
日志: logrus
|
||||
认证: JWT
|
||||
缓存: Redis (可选)
|
||||
数据库: PostgreSQL/MySQL
|
||||
|
||||
## 目录结构
|
||||
|
||||
基于Gin + GORM技术栈,采用**Clean Architecture**风格的目录结构:
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ └── main.go # 程序入口
|
||||
├── internal/
|
||||
│ ├── api/
|
||||
│ │ ├── handlers/ # HTTP处理器
|
||||
│ │ │ ├── auth.go
|
||||
│ │ │ ├── customer.go
|
||||
│ │ │ ├── license.go
|
||||
│ │ │ └── system.go
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ │ ├── auth.go
|
||||
│ │ │ ├── cors.go
|
||||
│ │ │ └── logging.go
|
||||
│ │ └── routes/ # 路由定义
|
||||
│ │ └── router.go
|
||||
│ ├── service/ # 业务逻辑层
|
||||
│ │ ├── auth_service.go
|
||||
│ │ ├── customer_service.go
|
||||
│ │ ├── license_service.go
|
||||
│ │ └── interfaces.go # 接口定义
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ │ ├── customer_repo.go
|
||||
│ │ ├── license_repo.go
|
||||
│ │ └── interfaces.go
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── customer.go
|
||||
│ │ ├── license.go
|
||||
│ │ └── common.go
|
||||
│ ├── config/ # 配置管理
|
||||
│ │ └── config.go
|
||||
│ └── cache/ # 缓存层
|
||||
│ ├── memory.go
|
||||
│ └── redis.go
|
||||
├── pkg/
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── crypto.go
|
||||
│ │ ├── hardware.go
|
||||
│ │ └── validator.go
|
||||
│ └── logger/ # 日志封装
|
||||
│ └── logger.go
|
||||
├── configs/
|
||||
│ ├── config.yaml
|
||||
│ └── config.example.yaml
|
||||
├── migrations/ # 数据库迁移
|
||||
├── tools/ # 硬件信息工具
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
**核心设计原则:**
|
||||
- **分层架构**:Handler → Service → Repository
|
||||
- **依赖注入**:通过接口解耦各层
|
||||
- **配置驱动**:Viper管理所有配置
|
||||
- **缓存抽象**:支持内存/Redis切换
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
# 登录与认证模块设计
|
||||
|
||||
## 概述
|
||||
|
||||
本模块负责系统的用户认证、登录状态管理和权限控制,基于JWT(JSON Web Token)实现无状态认证机制。
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **认证方式**: JWT (JSON Web Token)
|
||||
- **加密算法**: HS256 (HMAC-SHA256)
|
||||
- **密码加密**: bcrypt
|
||||
- **Session存储**: 内存缓存/Redis(可选)
|
||||
|
||||
## 社区版登录设计
|
||||
|
||||
用户名和密码配置在配置文件中,默认用户名和密码为 `admin` / `admin@123`
|
||||
|
||||
## 功能模块
|
||||
|
||||
### 1. 用户登录 (Login)
|
||||
|
||||
#### 1.1 登录流程
|
||||
```
|
||||
1. 用户提交用户名/密码
|
||||
2. 验证用户凭证
|
||||
3. 生成JWT Token
|
||||
4. 返回Token和用户信息
|
||||
5. 前端存储Token(localStorage/sessionStorage)
|
||||
```
|
||||
|
||||
#### 1.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin@123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_in": 3600,
|
||||
"user_info": {
|
||||
"username": "admin",
|
||||
"role": "administrator"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 实现细节
|
||||
- 密码使用bcrypt进行哈希验证
|
||||
- JWT包含用户ID、用户名、角色、过期时间
|
||||
- Token默认有效期1小时,可配置
|
||||
- 支持记住我功能(延长Token有效期)
|
||||
|
||||
### 2. 用户登出 (Logout)
|
||||
|
||||
#### 2.1 登出流程
|
||||
```
|
||||
1. 客户端删除本地Token
|
||||
2. 可选:服务端将Token加入黑名单
|
||||
3. 返回登出成功响应
|
||||
```
|
||||
|
||||
#### 2.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/logout
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登出成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Token刷新
|
||||
|
||||
#### 3.1 刷新机制
|
||||
- 当Token即将过期时(剩余时间<30分钟),自动刷新
|
||||
- 刷新Token与访问Token分离(可选实现)
|
||||
|
||||
#### 3.2 API接口设计
|
||||
```http
|
||||
POST /api/v1/auth/refresh
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## JWT Token设计
|
||||
|
||||
### 1. Token结构
|
||||
```json
|
||||
{
|
||||
"header": {
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
},
|
||||
"payload": {
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"role": "administrator",
|
||||
"iat": 1640995200,
|
||||
"exp": 1641002400
|
||||
},
|
||||
"signature": "signature_string"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Claims定义
|
||||
- `user_id`: 用户ID
|
||||
- `username`: 用户名
|
||||
- `role`: 用户角色
|
||||
- `iat`: 签发时间
|
||||
- `exp`: 过期时间
|
||||
|
||||
## 中间件认证设计
|
||||
|
||||
### 1. 认证中间件 (AuthMiddleware)
|
||||
|
||||
#### 1.1 功能职责
|
||||
- 验证HTTP请求头中的JWT Token
|
||||
- 解析Token获取用户信息
|
||||
- 处理Token过期和无效情况
|
||||
- 将用户信息注入到请求上下文
|
||||
|
||||
#### 1.2 认证流程
|
||||
```
|
||||
1. 检查Authorization头是否存在
|
||||
2. 提取Bearer Token
|
||||
3. 验证Token签名和有效性
|
||||
4. 解析用户信息
|
||||
5. 将用户信息存储到Context
|
||||
6. 继续处理请求或返回401错误
|
||||
```
|
||||
|
||||
#### 1.3 实现伪代码
|
||||
```go
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 1. 获取Token
|
||||
token := extractToken(c)
|
||||
if token == "" {
|
||||
c.JSON(401, gin.H{"error": "Missing token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 验证Token
|
||||
claims, err := validateToken(token)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 设置用户信息到Context
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 可选中间件 (OptionalAuth)
|
||||
|
||||
对于某些接口可能需要可选认证(如公开API),提供可选认证中间件:
|
||||
|
||||
```go
|
||||
func OptionalAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := extractToken(c)
|
||||
if token != "" {
|
||||
if claims, err := validateToken(token); err == nil {
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 路由保护策略
|
||||
|
||||
### 1. 路由分组
|
||||
```go
|
||||
// 公开路由
|
||||
public := r.Group("/api/v1")
|
||||
{
|
||||
public.POST("/login", handlers.Login)
|
||||
}
|
||||
|
||||
// 需要认证的路由
|
||||
auth := r.Group("/api/v1")
|
||||
auth.Use(middleware.AuthMiddleware())
|
||||
{
|
||||
auth.POST("/logout", handlers.Logout)
|
||||
auth.GET("/customers", handlers.GetCustomers)
|
||||
auth.POST("/licenses", handlers.CreateLicense)
|
||||
}
|
||||
|
||||
// 管理员路由
|
||||
admin := r.Group("/api/v1/admin")
|
||||
admin.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
||||
{
|
||||
admin.GET("/system/info", handlers.GetSystemInfo)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 权限级别
|
||||
- **Public**: 无需认证
|
||||
- **Authenticated**: 需要有效Token
|
||||
- **Admin**: 需要管理员权限
|
||||
|
||||
## 安全措施
|
||||
|
||||
### 1. Token安全
|
||||
- 使用强密钥签名(至少32字节)
|
||||
- 设置合理的过期时间
|
||||
|
||||
### 2. 密码安全
|
||||
- bcrypt加密,cost factor >= 10
|
||||
- 登录失败限制(防暴力破解)
|
||||
|
||||
|
||||
## 配置参数
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
jwt:
|
||||
secret: "your-256-bit-secret"
|
||||
expire_hours: 1
|
||||
refresh_threshold_minutes: 30
|
||||
admin:
|
||||
username: "admin"
|
||||
password: "admin@123" # 明文密码 部署人员可修改
|
||||
security:
|
||||
max_login_attempts: 5
|
||||
lockout_duration_minutes: 15
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 1. 认证错误码
|
||||
- `AUTH_001`: Token缺失
|
||||
- `AUTH_002`: Token无效
|
||||
- `AUTH_003`: Token过期
|
||||
- `AUTH_004`: 权限不足
|
||||
- `AUTH_005`: 用户名或密码错误
|
||||
- `AUTH_006`: 账户被锁定
|
||||
|
||||
### 2. 统一错误响应
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"error": "AUTH_002",
|
||||
"message": "Token无效",
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,350 @@
|
||||
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
|
||||
package swagger
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "support@swagger.io"
|
||||
},
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
"url": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/api/v1/admin/system/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取详细的系统信息(需要管理员权限)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "获取系统信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "系统信息",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "权限不足",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/refresh": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "刷新用户Token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "刷新Token",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "刷新成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/login": {
|
||||
"post": {
|
||||
"description": "管理员用户登录接口",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "用户登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "登录请求参数",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LoginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数无效",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "用户名或密码错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/logout": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "用户登出接口",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "用户登出",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登出成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"description": "系统健康状态检查",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "健康检测",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "健康状态",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.HealthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.HealthResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"system": {
|
||||
"$ref": "#/definitions/models.SystemInfo"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
},
|
||||
"uptime": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.LoginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.LoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SystemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arch": {
|
||||
"type": "string"
|
||||
},
|
||||
"num_cpu": {
|
||||
"type": "integer"
|
||||
},
|
||||
"num_goroutine": {
|
||||
"type": "integer"
|
||||
},
|
||||
"os": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BearerAuth": {
|
||||
"description": "Bearer token for authentication",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "localhost:18888",
|
||||
BasePath: "/",
|
||||
Schemes: []string{},
|
||||
Title: "License Manager API",
|
||||
Description: "软件授权管理平台API文档",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "软件授权管理平台API文档",
|
||||
"title": "License Manager API",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "support@swagger.io"
|
||||
},
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
"url": "https://opensource.org/licenses/MIT"
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"host": "localhost:18888",
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/api/v1/admin/system/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取详细的系统信息(需要管理员权限)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "获取系统信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "系统信息",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "权限不足",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/refresh": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "刷新用户Token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "刷新Token",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "刷新成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/login": {
|
||||
"post": {
|
||||
"description": "管理员用户登录接口",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "用户登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "登录请求参数",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LoginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数无效",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "用户名或密码错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/logout": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "用户登出接口",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "用户登出",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登出成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"description": "系统健康状态检查",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "健康检测",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "健康状态",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.HealthResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.HealthResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"system": {
|
||||
"$ref": "#/definitions/models.SystemInfo"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
},
|
||||
"uptime": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.LoginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.LoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SystemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arch": {
|
||||
"type": "string"
|
||||
},
|
||||
"num_cpu": {
|
||||
"type": "integer"
|
||||
},
|
||||
"num_goroutine": {
|
||||
"type": "integer"
|
||||
},
|
||||
"os": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BearerAuth": {
|
||||
"description": "Bearer token for authentication",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
basePath: /
|
||||
definitions:
|
||||
models.APIResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
data: {}
|
||||
message:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
models.ErrorResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
models.HealthResponse:
|
||||
properties:
|
||||
services:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
type: string
|
||||
system:
|
||||
$ref: '#/definitions/models.SystemInfo'
|
||||
timestamp:
|
||||
type: string
|
||||
uptime:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
models.LoginRequest:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- password
|
||||
- username
|
||||
type: object
|
||||
models.LoginResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
data: {}
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
models.SystemInfo:
|
||||
properties:
|
||||
arch:
|
||||
type: string
|
||||
num_cpu:
|
||||
type: integer
|
||||
num_goroutine:
|
||||
type: integer
|
||||
os:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact:
|
||||
email: support@swagger.io
|
||||
name: API Support
|
||||
url: http://www.swagger.io/support
|
||||
description: 软件授权管理平台API文档
|
||||
license:
|
||||
name: MIT
|
||||
url: https://opensource.org/licenses/MIT
|
||||
termsOfService: http://swagger.io/terms/
|
||||
title: License Manager API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/api/v1/admin/system/info:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取详细的系统信息(需要管理员权限)
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 系统信息
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"403":
|
||||
description: 权限不足
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取系统信息
|
||||
tags:
|
||||
- 系统
|
||||
/api/v1/auth/refresh:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 刷新用户Token
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 刷新成功
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 刷新Token
|
||||
tags:
|
||||
- 认证
|
||||
/api/v1/login:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 管理员用户登录接口
|
||||
parameters:
|
||||
- description: 登录请求参数
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.LoginRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 登录成功
|
||||
schema:
|
||||
$ref: '#/definitions/models.LoginResponse'
|
||||
"400":
|
||||
description: 请求参数无效
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"401":
|
||||
description: 用户名或密码错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
summary: 用户登录
|
||||
tags:
|
||||
- 认证
|
||||
/api/v1/logout:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 用户登出接口
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 登出成功
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
$ref: '#/definitions/models.ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 用户登出
|
||||
tags:
|
||||
- 认证
|
||||
/health:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 系统健康状态检查
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 健康状态
|
||||
schema:
|
||||
$ref: '#/definitions/models.HealthResponse'
|
||||
summary: 健康检测
|
||||
tags:
|
||||
- 系统
|
||||
securityDefinitions:
|
||||
BearerAuth:
|
||||
description: Bearer token for authentication
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
||||
@@ -0,0 +1,41 @@
|
||||
# Swagger安装和使用说明
|
||||
|
||||
## 安装swaggo工具
|
||||
|
||||
### 方法1: 使用go install(推荐)
|
||||
```bash
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
```
|
||||
|
||||
### 方法2: 使用go get(旧版本)
|
||||
```bash
|
||||
go get -u github.com/swaggo/swag/cmd/swag
|
||||
```
|
||||
|
||||
## 生成swagger文档
|
||||
|
||||
在backend目录下运行:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
swag init -g cmd/main.go -o ./docs/swagger
|
||||
```
|
||||
|
||||
## 访问Swagger UI
|
||||
|
||||
启动服务后,访问:
|
||||
- Swagger UI: http://localhost:18888/swagger/index.html
|
||||
- Swagger JSON: http://localhost:18888/swagger/doc.json
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 生成swagger文档
|
||||
swag init -g cmd/main.go -o ./docs/swagger
|
||||
|
||||
# 格式化swagger注释
|
||||
swag fmt
|
||||
|
||||
# 验证swagger注释
|
||||
swag init --parseVendor -g cmd/main.go
|
||||
```
|
||||
+62
-3
@@ -3,7 +3,66 @@ module license-manager
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
golang.org/x/crypto v0.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
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.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@@ -0,0 +1,145 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/service"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService service.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandler 创建认证处理器
|
||||
func NewAuthHandler(authService service.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
// @Summary 用户登录
|
||||
// @Description 管理员用户登录接口
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.LoginRequest true "登录请求参数"
|
||||
// @Success 200 {object} models.LoginResponse "登录成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "用户名或密码错误"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Error: "INVALID_REQUEST",
|
||||
Message: "请求参数无效",
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.authService.Login(&req)
|
||||
if err != nil {
|
||||
statusCode := http.StatusUnauthorized
|
||||
if err.Error() == "配置未初始化" {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Code: statusCode,
|
||||
Error: "LOGIN_FAILED",
|
||||
Message: err.Error(),
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.LoginResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "登录成功",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
// @Summary 用户登出
|
||||
// @Description 用户登出接口
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.APIResponse "登出成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Router /api/v1/logout [post]
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
// 对于JWT无状态认证,客户端删除Token即可
|
||||
// 这里可以记录登出日志或将Token加入黑名单(可选实现)
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "登出成功",
|
||||
})
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
// @Summary 刷新Token
|
||||
// @Description 刷新用户Token
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.APIResponse "刷新成功"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/v1/auth/refresh [post]
|
||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
// 从请求头获取Token
|
||||
token := extractTokenFromHeader(c)
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, models.ErrorResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
Error: "TOKEN_MISSING",
|
||||
Message: "认证令牌缺失",
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
newToken, err := h.authService.RefreshToken(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, models.ErrorResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
Error: "TOKEN_REFRESH_FAILED",
|
||||
Message: "刷新令牌失败",
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "刷新成功",
|
||||
Data: map[string]interface{}{
|
||||
"token": newToken,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// getCurrentTimestamp 获取当前时间戳
|
||||
func getCurrentTimestamp() string {
|
||||
return "2024-07-30T12:00:00Z" // 简化实现,实际应该使用time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// extractTokenFromHeader 从请求头提取Token
|
||||
func extractTokenFromHeader(c *gin.Context) string {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
return authHeader[7:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/service"
|
||||
)
|
||||
|
||||
type SystemHandler struct {
|
||||
systemService service.SystemService
|
||||
}
|
||||
|
||||
// NewSystemHandler 创建系统处理器
|
||||
func NewSystemHandler(systemService service.SystemService) *SystemHandler {
|
||||
return &SystemHandler{
|
||||
systemService: systemService,
|
||||
}
|
||||
}
|
||||
|
||||
// HealthCheck 健康检测接口
|
||||
// @Summary 健康检测
|
||||
// @Description 系统健康状态检查
|
||||
// @Tags 系统
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.HealthResponse "健康状态"
|
||||
// @Router /health [get]
|
||||
func (h *SystemHandler) HealthCheck(c *gin.Context) {
|
||||
response := h.systemService.GetHealthStatus()
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息(需要认证)
|
||||
// @Summary 获取系统信息
|
||||
// @Description 获取详细的系统信息(需要管理员权限)
|
||||
// @Tags 系统
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.APIResponse "系统信息"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 403 {object} models.ErrorResponse "权限不足"
|
||||
// @Router /api/v1/admin/system/info [get]
|
||||
func (h *SystemHandler) GetSystemInfo(c *gin.Context) {
|
||||
systemInfo := h.systemService.GetSystemInfo()
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "获取成功",
|
||||
Data: systemInfo,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"license-manager/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
AuthorizationHeaderKey = "authorization"
|
||||
AuthorizationTypeBearer = "bearer"
|
||||
AuthorizationPayloadKey = "authorization_payload"
|
||||
)
|
||||
|
||||
// AuthMiddleware 认证中间件
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authorizationHeader := c.GetHeader(AuthorizationHeaderKey)
|
||||
if len(authorizationHeader) == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"error": "AUTH_001",
|
||||
"message": "缺少认证令牌",
|
||||
"timestamp": getCurrentTimestamp(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
fields := strings.Fields(authorizationHeader)
|
||||
if len(fields) < 2 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"error": "AUTH_002",
|
||||
"message": "认证令牌格式无效",
|
||||
"timestamp": getCurrentTimestamp(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
authorizationType := strings.ToLower(fields[0])
|
||||
if authorizationType != AuthorizationTypeBearer {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"error": "AUTH_002",
|
||||
"message": "不支持的认证类型",
|
||||
"timestamp": getCurrentTimestamp(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := fields[1]
|
||||
claims, err := utils.ValidateToken(accessToken)
|
||||
if err != nil {
|
||||
var errorCode, message string
|
||||
switch err {
|
||||
case utils.ErrTokenExpired:
|
||||
errorCode = "AUTH_003"
|
||||
message = "令牌已过期"
|
||||
case utils.ErrTokenMalformed:
|
||||
errorCode = "AUTH_002"
|
||||
message = "令牌格式错误"
|
||||
default:
|
||||
errorCode = "AUTH_002"
|
||||
message = "令牌无效"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"error": errorCode,
|
||||
"message": message,
|
||||
"timestamp": getCurrentTimestamp(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Set(AuthorizationPayloadKey, claims)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalAuthMiddleware 可选认证中间件
|
||||
func OptionalAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authorizationHeader := c.GetHeader(AuthorizationHeaderKey)
|
||||
if len(authorizationHeader) == 0 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
fields := strings.Fields(authorizationHeader)
|
||||
if len(fields) < 2 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
authorizationType := strings.ToLower(fields[0])
|
||||
if authorizationType != AuthorizationTypeBearer {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := fields[1]
|
||||
claims, err := utils.ValidateToken(accessToken)
|
||||
if err == nil {
|
||||
// Token有效,设置用户信息
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Set(AuthorizationPayloadKey, claims)
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminOnlyMiddleware 仅管理员访问中间件
|
||||
func AdminOnlyMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"error": "AUTH_001",
|
||||
"message": "认证信息缺失",
|
||||
"timestamp": getCurrentTimestamp(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if role != "administrator" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": http.StatusForbidden,
|
||||
"error": "AUTH_004",
|
||||
"message": "权限不足",
|
||||
"timestamp": getCurrentTimestamp(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// getCurrentTimestamp 获取当前时间戳
|
||||
func getCurrentTimestamp() string {
|
||||
return "2024-07-30T12:00:00Z" // 简化实现,实际应该使用time.Now().Format(time.RFC3339)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CORSMiddleware 跨域中间件
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return gin.HandlerFunc(func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// LoggingMiddleware 日志中间件
|
||||
func LoggingMiddleware(logger *logrus.Logger) gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
// 使用logrus记录日志
|
||||
logger.WithFields(logrus.Fields{
|
||||
"client_ip": param.ClientIP,
|
||||
"method": param.Method,
|
||||
"path": param.Path,
|
||||
"status_code": param.StatusCode,
|
||||
"latency": param.Latency,
|
||||
"user_agent": param.Request.UserAgent(),
|
||||
}).Info("HTTP Request")
|
||||
|
||||
return ""
|
||||
})
|
||||
}
|
||||
|
||||
// CustomLoggerMiddleware 自定义日志中间件
|
||||
func CustomLoggerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
|
||||
c.Next()
|
||||
|
||||
end := time.Now()
|
||||
latency := end.Sub(start)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"status": c.Writer.Status(),
|
||||
"method": c.Request.Method,
|
||||
"path": path,
|
||||
"ip": c.ClientIP(),
|
||||
"latency": latency,
|
||||
"user_agent": c.Request.UserAgent(),
|
||||
}).Info("API Request")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
_ "license-manager/docs/swagger" // swagger docs
|
||||
"license-manager/internal/api/handlers"
|
||||
"license-manager/internal/api/middleware"
|
||||
"license-manager/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
router := gin.New()
|
||||
|
||||
// 全局中间件
|
||||
router.Use(middleware.CustomLoggerMiddleware())
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(middleware.CORSMiddleware())
|
||||
|
||||
// 初始化服务
|
||||
authService := service.NewAuthService()
|
||||
systemService := service.NewSystemService()
|
||||
|
||||
// 初始化处理器
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
systemHandler := handlers.NewSystemHandler(systemService)
|
||||
|
||||
// 健康检测接口(无需认证)
|
||||
router.GET("/health", systemHandler.HealthCheck)
|
||||
|
||||
// Swagger文档路由
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// API v1 路由组
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
// 公开接口(无需认证)
|
||||
public := v1.Group("")
|
||||
{
|
||||
public.POST("/login", authHandler.Login)
|
||||
}
|
||||
|
||||
// 需要认证的接口
|
||||
auth := v1.Group("")
|
||||
auth.Use(middleware.AuthMiddleware())
|
||||
{
|
||||
// 认证相关
|
||||
auth.POST("/logout", authHandler.Logout)
|
||||
auth.POST("/auth/refresh", authHandler.RefreshToken)
|
||||
}
|
||||
|
||||
// 管理员接口
|
||||
admin := v1.Group("/admin")
|
||||
admin.Use(middleware.AuthMiddleware(), middleware.AdminOnlyMiddleware())
|
||||
{
|
||||
admin.GET("/system/info", systemHandler.GetSystemInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Auth AuthConfig `mapstructure:"auth"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Mode string `mapstructure:"mode"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Admin AdminConfig `mapstructure:"admin"`
|
||||
Security SecurityConfig `mapstructure:"security"`
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
ExpireHours int `mapstructure:"expire_hours"`
|
||||
RefreshThresholdMinutes int `mapstructure:"refresh_threshold_minutes"`
|
||||
}
|
||||
|
||||
type AdminConfig struct {
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
MaxLoginAttempts int `mapstructure:"max_login_attempts"`
|
||||
LockoutDurationMinutes int `mapstructure:"lockout_duration_minutes"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
}
|
||||
|
||||
var AppConfig *Config
|
||||
|
||||
func Load(configPath string) error {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
if configPath != "" {
|
||||
viper.SetConfigFile(configPath)
|
||||
} else {
|
||||
viper.AddConfigPath("./configs")
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
setDefaults()
|
||||
|
||||
// 支持环境变量
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Printf("Warning: Config file not found, using defaults: %v", err)
|
||||
log.Printf("Searched paths: %v", viper.ConfigFileUsed())
|
||||
} else {
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
AppConfig = &Config{}
|
||||
if err := viper.Unmarshal(AppConfig); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDefaults() {
|
||||
// Server defaults
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("server.port", 18888)
|
||||
viper.SetDefault("server.mode", "debug")
|
||||
|
||||
// Auth defaults
|
||||
viper.SetDefault("auth.jwt.secret", "license-manager-default-secret-key")
|
||||
viper.SetDefault("auth.jwt.expire_hours", 1)
|
||||
viper.SetDefault("auth.jwt.refresh_threshold_minutes", 30)
|
||||
|
||||
viper.SetDefault("auth.admin.username", "admin")
|
||||
viper.SetDefault("auth.admin.password", "admin@123")
|
||||
|
||||
viper.SetDefault("auth.security.max_login_attempts", 5)
|
||||
viper.SetDefault("auth.security.lockout_duration_minutes", 15)
|
||||
|
||||
// Log defaults
|
||||
viper.SetDefault("log.level", "info")
|
||||
viper.SetDefault("log.format", "json")
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
return AppConfig
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
// LoginRequest 登录请求结构
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应结构
|
||||
type LoginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// LoginData 登录成功返回的数据
|
||||
type LoginData struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
UserInfo UserInfo `json:"user_info"`
|
||||
}
|
||||
|
||||
// UserInfo 用户信息
|
||||
type UserInfo struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
// APIResponse 通用API响应结构
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应结构
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
// HealthResponse 健康检测响应结构
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
Uptime string `json:"uptime"`
|
||||
System SystemInfo `json:"system"`
|
||||
Services map[string]string `json:"services"`
|
||||
}
|
||||
|
||||
// SystemInfo 系统信息
|
||||
type SystemInfo struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Version string `json:"version"`
|
||||
NumCPU int `json:"num_cpu"`
|
||||
NumGoroutine int `json:"num_goroutine"`
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/pkg/utils"
|
||||
)
|
||||
|
||||
type authService struct{}
|
||||
|
||||
// NewAuthService 创建认证服务实例
|
||||
func NewAuthService() AuthService {
|
||||
return &authService{}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *authService) Login(req *models.LoginRequest) (*models.LoginData, error) {
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return nil, errors.New("配置未初始化")
|
||||
}
|
||||
|
||||
// 验证用户名和密码
|
||||
if req.Username != cfg.Auth.Admin.Username || req.Password != cfg.Auth.Admin.Password {
|
||||
return nil, errors.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 生成JWT Token (用户ID设为1,因为只有一个管理员用户)
|
||||
token, err := utils.GenerateToken(1, req.Username, "administrator")
|
||||
if err != nil {
|
||||
return nil, errors.New("生成令牌失败")
|
||||
}
|
||||
|
||||
// 计算过期时间(秒)
|
||||
expiresIn := cfg.Auth.JWT.ExpireHours * 3600
|
||||
|
||||
return &models.LoginData{
|
||||
Token: token,
|
||||
ExpiresIn: expiresIn,
|
||||
UserInfo: models.UserInfo{
|
||||
Username: req.Username,
|
||||
Role: "administrator",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
func (s *authService) RefreshToken(token string) (string, error) {
|
||||
// 解析当前Token
|
||||
claims, err := utils.ParseToken(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 生成新Token
|
||||
newToken, err := utils.GenerateToken(claims.UserID, claims.Username, claims.Role)
|
||||
if err != nil {
|
||||
return "", errors.New("生成新令牌失败")
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
// ValidateToken 验证Token
|
||||
func (s *authService) ValidateToken(token string) error {
|
||||
_, err := utils.ValidateToken(token)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package service
|
||||
|
||||
import "license-manager/internal/models"
|
||||
|
||||
// AuthService 认证服务接口
|
||||
type AuthService interface {
|
||||
Login(req *models.LoginRequest) (*models.LoginData, error)
|
||||
RefreshToken(token string) (string, error)
|
||||
ValidateToken(token string) error
|
||||
}
|
||||
|
||||
// SystemService 系统服务接口
|
||||
type SystemService interface {
|
||||
GetHealthStatus() *models.HealthResponse
|
||||
GetSystemInfo() map[string]interface{}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"license-manager/internal/models"
|
||||
)
|
||||
|
||||
type systemService struct {
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewSystemService 创建系统服务实例
|
||||
func NewSystemService() SystemService {
|
||||
return &systemService{
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetHealthStatus 获取健康状态
|
||||
func (s *systemService) GetHealthStatus() *models.HealthResponse {
|
||||
uptime := time.Since(s.startTime)
|
||||
|
||||
return &models.HealthResponse{
|
||||
Status: "healthy",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Version: "1.0.0",
|
||||
Uptime: uptime.String(),
|
||||
System: models.SystemInfo{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Version: runtime.Version(),
|
||||
NumCPU: runtime.NumCPU(),
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
},
|
||||
Services: map[string]string{
|
||||
"database": "healthy", // 这里可以检查数据库连接状态
|
||||
"cache": "healthy", // 这里可以检查缓存状态
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统详细信息
|
||||
func (s *systemService) GetSystemInfo() map[string]interface{} {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
return map[string]interface{}{
|
||||
"version": "1.0.0",
|
||||
"uptime": time.Since(s.startTime).String(),
|
||||
"system": map[string]interface{}{
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"version": runtime.Version(),
|
||||
"num_cpu": runtime.NumCPU(),
|
||||
"num_goroutine": runtime.NumGoroutine(),
|
||||
},
|
||||
"memory": map[string]interface{}{
|
||||
"alloc": bToMb(m.Alloc),
|
||||
"total_alloc": bToMb(m.TotalAlloc),
|
||||
"sys": bToMb(m.Sys),
|
||||
"num_gc": m.NumGC,
|
||||
},
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// bToMb converts bytes to megabytes
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"license-manager/internal/config"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Logger *logrus.Logger
|
||||
|
||||
// CustomFormatter 自定义格式器,在消息末尾显示文件路径和行号
|
||||
type CustomFormatter struct {
|
||||
*logrus.TextFormatter
|
||||
}
|
||||
|
||||
func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
// 先用标准格式器格式化
|
||||
formatted, err := f.TextFormatter.Format(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果有调用者信息,添加到末尾
|
||||
if entry.Caller != nil {
|
||||
filename := entry.Caller.File
|
||||
if idx := strings.Index(filename, "backend/"); idx != -1 {
|
||||
filename = "./backend/" + filename[idx+len("backend/"):]
|
||||
} else {
|
||||
filename = filepath.Base(filename)
|
||||
}
|
||||
|
||||
// 移除换行符,添加文件信息,然后重新添加换行符
|
||||
formattedStr := strings.TrimRight(string(formatted), "\n")
|
||||
formattedStr = fmt.Sprintf("%s %s:%d\n", formattedStr, filename, entry.Caller.Line)
|
||||
return []byte(formattedStr), nil
|
||||
}
|
||||
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
func Init() {
|
||||
Logger = logrus.New()
|
||||
Logger.SetReportCaller(true) // 启用调用者信息
|
||||
|
||||
cfg := config.GetConfig()
|
||||
if cfg != nil {
|
||||
// 设置日志级别
|
||||
switch cfg.Log.Level {
|
||||
case "debug":
|
||||
Logger.SetLevel(logrus.DebugLevel)
|
||||
case "info":
|
||||
Logger.SetLevel(logrus.InfoLevel)
|
||||
case "warn":
|
||||
Logger.SetLevel(logrus.WarnLevel)
|
||||
case "error":
|
||||
Logger.SetLevel(logrus.ErrorLevel)
|
||||
default:
|
||||
Logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
// 设置自定义格式器
|
||||
Logger.SetFormatter(&CustomFormatter{
|
||||
TextFormatter: &logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
ForceColors: true,
|
||||
DisableColors: false,
|
||||
DisableLevelTruncation: true,
|
||||
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
||||
return "", "" // 禁用默认的调用者信息显示
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 默认配置
|
||||
Logger.SetLevel(logrus.InfoLevel)
|
||||
Logger.SetFormatter(&CustomFormatter{
|
||||
TextFormatter: &logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
ForceColors: true,
|
||||
DisableLevelTruncation: true,
|
||||
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
||||
return "", "" // 禁用默认的调用者信息显示
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Logger.SetOutput(os.Stdout)
|
||||
}
|
||||
|
||||
func GetLogger() *logrus.Logger {
|
||||
if Logger == nil {
|
||||
Init()
|
||||
}
|
||||
return Logger
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPassword 使用bcrypt加密密码
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash 验证密码
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"license-manager/internal/config"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTokenExpired = errors.New("token has expired")
|
||||
ErrTokenNotValid = errors.New("token is not valid")
|
||||
ErrTokenMalformed = errors.New("token is malformed")
|
||||
ErrTokenNotFound = errors.New("token not found")
|
||||
)
|
||||
|
||||
// GenerateToken 生成JWT Token
|
||||
func GenerateToken(userID int, username, role string) (string, error) {
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return "", errors.New("config not initialized")
|
||||
}
|
||||
|
||||
expireTime := time.Now().Add(time.Duration(cfg.Auth.JWT.ExpireHours) * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expireTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "license-manager",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(cfg.Auth.JWT.Secret))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT Token
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return nil, errors.New("config not initialized")
|
||||
}
|
||||
|
||||
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(cfg.Auth.JWT.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
if errors.Is(err, jwt.ErrTokenMalformed) {
|
||||
return nil, ErrTokenMalformed
|
||||
}
|
||||
return nil, ErrTokenNotValid
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, ErrTokenNotValid
|
||||
}
|
||||
|
||||
// ValidateToken 验证Token是否有效
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
return ParseToken(tokenString)
|
||||
}
|
||||
|
||||
// RefreshTokenIfNeeded 检查Token是否需要刷新,如果需要则返回新Token
|
||||
func RefreshTokenIfNeeded(tokenString string) (string, error) {
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil {
|
||||
return "", errors.New("config not initialized")
|
||||
}
|
||||
|
||||
// 检查是否需要刷新(剩余时间少于阈值)
|
||||
thresholdDuration := time.Duration(cfg.Auth.JWT.RefreshThresholdMinutes) * time.Minute
|
||||
if time.Until(claims.ExpiresAt.Time) < thresholdDuration {
|
||||
// 生成新Token
|
||||
return GenerateToken(claims.UserID, claims.Username, claims.Role)
|
||||
}
|
||||
|
||||
// 不需要刷新,返回原Token
|
||||
return tokenString, nil
|
||||
}
|
||||
Reference in new issue
Block a user