26 changed files with 1587 additions and 133 deletions
@ -0,0 +1,240 @@
@@ -0,0 +1,240 @@
|
||||
# GoFaster 认证服务 |
||||
|
||||
## 概述 |
||||
|
||||
GoFaster 认证服务提供了完整的用户身份验证和会话管理功能,包括: |
||||
|
||||
- 用户登录/登出 |
||||
- 图形验证码生成和验证 |
||||
- JWT 令牌管理 |
||||
- 密码错误次数限制和账户锁定 |
||||
- 用户角色管理 |
||||
|
||||
## 主要功能 |
||||
|
||||
### 1. 用户认证 |
||||
|
||||
- **登录接口**: `POST /api/auth/login` |
||||
- **登出接口**: `POST /api/auth/logout` |
||||
- **刷新令牌**: `POST /api/auth/refresh` |
||||
- **获取用户信息**: `GET /api/auth/userinfo` |
||||
|
||||
### 2. 验证码系统 |
||||
|
||||
- **生成验证码**: `GET /api/auth/captcha` |
||||
- 验证码有效期:5分钟 |
||||
- 一次性使用,验证后自动删除 |
||||
|
||||
### 3. 安全特性 |
||||
|
||||
- **密码错误限制**: 连续5次密码错误后账户锁定30分钟 |
||||
- **JWT令牌**: 访问令牌24小时有效期,刷新令牌7天有效期 |
||||
- **IP记录**: 记录用户最后登录IP地址 |
||||
|
||||
## 数据库结构 |
||||
|
||||
### 用户表 (users) |
||||
```sql |
||||
- id: 主键 |
||||
- username: 用户名(唯一) |
||||
- password: 密码(加密存储) |
||||
- email: 邮箱 |
||||
- phone: 手机号 |
||||
- status: 状态(1-正常,2-禁用,3-锁定) |
||||
- password_error_count: 密码错误次数 |
||||
- locked_at: 锁定时间 |
||||
- last_login_at: 最后登录时间 |
||||
- last_login_ip: 最后登录IP |
||||
- created_at: 创建时间 |
||||
- updated_at: 更新时间 |
||||
``` |
||||
|
||||
### 角色表 (roles) |
||||
```sql |
||||
- id: 主键 |
||||
- name: 角色名称 |
||||
- code: 角色代码(唯一) |
||||
- description: 角色描述 |
||||
- created_at: 创建时间 |
||||
- updated_at: 更新时间 |
||||
``` |
||||
|
||||
### 用户角色关联表 (user_roles) |
||||
```sql |
||||
- id: 主键 |
||||
- user_id: 用户ID |
||||
- role_id: 角色ID |
||||
- created_at: 创建时间 |
||||
- updated_at: 更新时间 |
||||
``` |
||||
|
||||
### 验证码表 (captchas) |
||||
```sql |
||||
- id: 验证码ID |
||||
- text: 验证码文本 |
||||
- expires_at: 过期时间 |
||||
- created_at: 创建时间 |
||||
``` |
||||
|
||||
## 配置说明 |
||||
|
||||
### JWT配置 |
||||
```yaml |
||||
jwt: |
||||
secret: "your-secret-key" # JWT签名密钥 |
||||
issuer: "gofaster" # JWT发行者 |
||||
expire: 24 # 令牌过期时间(小时) |
||||
``` |
||||
|
||||
### 数据库配置 |
||||
```yaml |
||||
db: |
||||
host: localhost |
||||
port: "5432" |
||||
user: postgres |
||||
password: post1024 |
||||
name: gofaster |
||||
``` |
||||
|
||||
## 使用示例 |
||||
|
||||
### 1. 用户登录 |
||||
|
||||
```bash |
||||
curl -X POST http://localhost:8080/api/auth/login \ |
||||
-H "Content-Type: application/json" \ |
||||
-d '{ |
||||
"username": "admin", |
||||
"password": "password", |
||||
"captcha": "ABCD" |
||||
}' |
||||
``` |
||||
|
||||
**响应示例**: |
||||
```json |
||||
{ |
||||
"code": 200, |
||||
"message": "登录成功", |
||||
"data": { |
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
||||
"token_type": "Bearer", |
||||
"expires_in": 86400, |
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
||||
"user": { |
||||
"id": 1, |
||||
"username": "admin", |
||||
"email": "admin@gofaster.com", |
||||
"phone": "13800138000", |
||||
"status": 1, |
||||
"roles": [ |
||||
{ |
||||
"id": 1, |
||||
"name": "超级管理员", |
||||
"code": "SUPER_ADMIN" |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 2. 获取验证码 |
||||
|
||||
```bash |
||||
curl -X GET http://localhost:8080/api/auth/captcha |
||||
``` |
||||
|
||||
**响应示例**: |
||||
```json |
||||
{ |
||||
"code": 200, |
||||
"message": "验证码生成成功", |
||||
"data": { |
||||
"captcha_id": "abc123def456", |
||||
"captcha_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", |
||||
"expires_in": 300 |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 3. 使用JWT令牌访问受保护的接口 |
||||
|
||||
```bash |
||||
curl -X GET http://localhost:8080/api/auth/userinfo \ |
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |
||||
``` |
||||
|
||||
## 安全说明 |
||||
|
||||
1. **密码加密**: 使用 bcrypt 算法加密存储密码 |
||||
2. **令牌安全**: JWT 令牌使用 HMAC-SHA256 签名 |
||||
3. **验证码**: 一次性使用,验证后立即删除 |
||||
4. **账户锁定**: 防止暴力破解攻击 |
||||
5. **IP记录**: 记录登录IP,便于安全审计 |
||||
|
||||
## 错误处理 |
||||
|
||||
### 常见错误码 |
||||
|
||||
- `400`: 请求参数错误 |
||||
- `401`: 认证失败(用户名/密码错误、验证码错误) |
||||
- `423`: 账户被锁定 |
||||
- `500`: 系统内部错误 |
||||
|
||||
### 错误响应格式 |
||||
|
||||
```json |
||||
{ |
||||
"code": 401, |
||||
"message": "认证失败", |
||||
"error": "密码错误,还可尝试3次" |
||||
} |
||||
``` |
||||
|
||||
## 开发说明 |
||||
|
||||
### 项目结构 |
||||
|
||||
``` |
||||
backend/internal/auth/ |
||||
├── controller/ # 控制器层 |
||||
├── service/ # 服务层 |
||||
├── repository/ # 仓储层 |
||||
├── model/ # 数据模型 |
||||
├── routes/ # 路由配置 |
||||
├── migration/ # 数据库迁移 |
||||
└── module.go # 模块初始化 |
||||
``` |
||||
|
||||
### 依赖注入 |
||||
|
||||
认证服务使用依赖注入模式,各层之间通过接口解耦: |
||||
|
||||
1. **Repository**: 数据访问层 |
||||
2. **Service**: 业务逻辑层 |
||||
3. **Controller**: HTTP处理层 |
||||
4. **Middleware**: JWT认证中间件 |
||||
|
||||
### 扩展建议 |
||||
|
||||
1. **Redis缓存**: 可以将验证码存储在Redis中,提高性能 |
||||
2. **日志记录**: 添加详细的登录日志记录 |
||||
3. **多因素认证**: 支持短信验证码、邮箱验证等 |
||||
4. **OAuth集成**: 支持第三方登录(Google、GitHub等) |
||||
5. **权限管理**: 基于角色的访问控制(RBAC) |
||||
|
||||
## 部署说明 |
||||
|
||||
1. 确保PostgreSQL数据库已启动 |
||||
2. 配置数据库连接参数 |
||||
3. 设置JWT密钥和发行者 |
||||
4. 启动应用服务 |
||||
5. 访问 `/api/auth/captcha` 生成验证码 |
||||
6. 使用默认管理员账户登录(admin/password) |
||||
|
||||
## 注意事项 |
||||
|
||||
1. **生产环境**: 请修改默认的JWT密钥和管理员密码 |
||||
2. **数据库**: 建议定期备份用户数据 |
||||
3. **监控**: 建议监控登录失败次数和异常登录行为 |
||||
4. **更新**: 定期更新依赖包以修复安全漏洞 |
Binary file not shown.
@ -0,0 +1,225 @@
@@ -0,0 +1,225 @@
|
||||
package controller |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"gofaster/internal/auth/model" |
||||
"gofaster/internal/auth/service" |
||||
"gofaster/internal/shared/response" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
type AuthController struct { |
||||
authService service.AuthService |
||||
} |
||||
|
||||
func NewAuthController(authService service.AuthService) *AuthController { |
||||
return &AuthController{ |
||||
authService: authService, |
||||
} |
||||
} |
||||
|
||||
// Login 用户登录
|
||||
// @Summary 用户登录
|
||||
// @Description 用户登录接口,支持验证码验证和密码错误次数限制
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.LoginRequest true "登录请求参数"
|
||||
// @Success 200 {object} response.Response{data=model.LoginResponse} "登录成功"
|
||||
// @Failure 400 {object} response.Response "请求参数错误"
|
||||
// @Failure 401 {object} response.Response "认证失败"
|
||||
// @Failure 423 {object} response.Response "账户被锁定"
|
||||
// @Router /auth/login [post]
|
||||
func (c *AuthController) Login(ctx *gin.Context) { |
||||
var req model.LoginRequest |
||||
if err := ctx.ShouldBindJSON(&req); err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
// 获取客户端IP
|
||||
clientIP := getClientIP(ctx.Request) |
||||
|
||||
// 调用服务层处理登录
|
||||
resp, err := c.authService.Login(ctx, &req, clientIP) |
||||
if err != nil { |
||||
// 根据错误类型返回不同的状态码
|
||||
if isLockedError(err) { |
||||
response.Error(ctx, http.StatusLocked, "账户被锁定", err.Error()) |
||||
return |
||||
} |
||||
if isAuthError(err) { |
||||
response.Error(ctx, http.StatusUnauthorized, "认证失败", err.Error()) |
||||
return |
||||
} |
||||
response.Error(ctx, http.StatusInternalServerError, "系统错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "登录成功", resp) |
||||
} |
||||
|
||||
// Logout 用户登出
|
||||
// @Summary 用户登出
|
||||
// @Description 用户登出接口
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.LogoutRequest true "登出请求参数"
|
||||
// @Success 200 {object} response.Response "登出成功"
|
||||
// @Router /auth/logout [post]
|
||||
func (c *AuthController) Logout(ctx *gin.Context) { |
||||
var req model.LogoutRequest |
||||
if err := ctx.ShouldBindJSON(&req); err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
err := c.authService.Logout(ctx, req.Token) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "登出失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "登出成功", nil) |
||||
} |
||||
|
||||
// RefreshToken 刷新访问令牌
|
||||
// @Summary 刷新访问令牌
|
||||
// @Description 使用刷新令牌获取新的访问令牌
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.RefreshTokenRequest true "刷新令牌请求参数"
|
||||
// @Success 200 {object} response.Response{data=model.LoginResponse} "刷新成功"
|
||||
// @Failure 400 {object} response.Response "请求参数错误"
|
||||
// @Failure 401 {object} response.Response "刷新令牌无效"
|
||||
// @Router /auth/refresh [post]
|
||||
func (c *AuthController) RefreshToken(ctx *gin.Context) { |
||||
var req model.RefreshTokenRequest |
||||
if err := ctx.ShouldBindJSON(&req); err != nil { |
||||
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error()) |
||||
return |
||||
} |
||||
|
||||
resp, err := c.authService.RefreshToken(ctx, req.RefreshToken) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusUnauthorized, "刷新令牌无效", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "令牌刷新成功", resp) |
||||
} |
||||
|
||||
// GenerateCaptcha 生成验证码
|
||||
// @Summary 生成验证码
|
||||
// @Description 生成图形验证码,用于登录验证
|
||||
// @Tags 认证
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=model.CaptchaResponse} "验证码生成成功"
|
||||
// @Router /auth/captcha [get]
|
||||
func (c *AuthController) GenerateCaptcha(ctx *gin.Context) { |
||||
resp, err := c.authService.GenerateCaptcha(ctx) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "验证码生成失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "验证码生成成功", resp) |
||||
} |
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
// @Summary 获取用户信息
|
||||
// @Description 获取当前登录用户的详细信息
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.Response{data=model.UserInfo} "获取成功"
|
||||
// @Failure 401 {object} response.Response "未授权"
|
||||
// @Router /auth/userinfo [get]
|
||||
func (c *AuthController) GetUserInfo(ctx *gin.Context) { |
||||
// 从JWT令牌中获取用户ID
|
||||
userID, exists := ctx.Get("user_id") |
||||
if !exists { |
||||
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在") |
||||
return |
||||
} |
||||
|
||||
// 转换用户ID类型
|
||||
var uid uint |
||||
switch v := userID.(type) { |
||||
case float64: |
||||
uid = uint(v) |
||||
case int: |
||||
uid = uint(v) |
||||
case string: |
||||
if parsed, err := strconv.ParseUint(v, 10, 32); err == nil { |
||||
uid = uint(parsed) |
||||
} else { |
||||
response.Error(ctx, http.StatusBadRequest, "用户ID格式错误", err.Error()) |
||||
return |
||||
} |
||||
default: |
||||
response.Error(ctx, http.StatusBadRequest, "用户ID类型错误", "无法识别的用户ID类型") |
||||
return |
||||
} |
||||
|
||||
// 获取用户信息
|
||||
userInfo, err := c.authService.GetUserInfo(ctx, uid) |
||||
if err != nil { |
||||
response.Error(ctx, http.StatusInternalServerError, "获取用户信息失败", err.Error()) |
||||
return |
||||
} |
||||
|
||||
response.Success(ctx, "获取用户信息成功", userInfo) |
||||
} |
||||
|
||||
// getClientIP 获取客户端IP地址
|
||||
func getClientIP(r *http.Request) string { |
||||
// 尝试从各种头部获取真实IP
|
||||
ip := r.Header.Get("X-Real-IP") |
||||
if ip != "" { |
||||
return ip |
||||
} |
||||
|
||||
ip = r.Header.Get("X-Forwarded-For") |
||||
if ip != "" { |
||||
// X-Forwarded-For可能包含多个IP,取第一个
|
||||
if idx := strings.Index(ip, ","); idx != -1 { |
||||
ip = ip[:idx] |
||||
} |
||||
return strings.TrimSpace(ip) |
||||
} |
||||
|
||||
ip = r.Header.Get("X-Forwarded") |
||||
if ip != "" { |
||||
return ip |
||||
} |
||||
|
||||
// 从RemoteAddr获取
|
||||
if r.RemoteAddr != "" { |
||||
if idx := strings.Index(r.RemoteAddr, ":"); idx != -1 { |
||||
return r.RemoteAddr[:idx] |
||||
} |
||||
return r.RemoteAddr |
||||
} |
||||
|
||||
return "127.0.0.1" |
||||
} |
||||
|
||||
// isLockedError 检查是否为锁定错误
|
||||
func isLockedError(err error) bool { |
||||
return strings.Contains(err.Error(), "锁定") |
||||
} |
||||
|
||||
// isAuthError 检查是否为认证错误
|
||||
func isAuthError(err error) bool { |
||||
return strings.Contains(err.Error(), "密码错误") ||
|
||||
strings.Contains(err.Error(), "用户不存在") ||
|
||||
strings.Contains(err.Error(), "验证码错误") |
||||
} |
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
package migration |
||||
|
||||
import ( |
||||
"gofaster/internal/auth/model" |
||||
"gofaster/internal/auth/repository" |
||||
|
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
// RunMigrations 运行数据库迁移
|
||||
func RunMigrations(db *gorm.DB) error { |
||||
// 自动迁移用户表
|
||||
if err := db.AutoMigrate(&model.User{}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 自动迁移角色表
|
||||
if err := db.AutoMigrate(&model.Role{}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 自动迁移用户角色关联表
|
||||
if err := db.AutoMigrate(&model.UserRole{}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 自动迁移验证码表
|
||||
if err := db.AutoMigrate(&repository.Captcha{}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 创建默认角色
|
||||
if err := createDefaultRoles(db); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 创建默认管理员用户
|
||||
if err := createDefaultAdmin(db); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// createDefaultRoles 创建默认角色
|
||||
func createDefaultRoles(db *gorm.DB) error { |
||||
// 检查是否已存在角色
|
||||
var count int64 |
||||
db.Model(&model.Role{}).Count(&count) |
||||
if count > 0 { |
||||
return nil // 已存在角色,跳过
|
||||
} |
||||
|
||||
roles := []model.Role{ |
||||
{ |
||||
Name: "超级管理员", |
||||
Code: "SUPER_ADMIN", |
||||
Description: "系统超级管理员,拥有所有权限", |
||||
}, |
||||
{ |
||||
Name: "管理员", |
||||
Code: "ADMIN", |
||||
Description: "系统管理员,拥有大部分权限", |
||||
}, |
||||
{ |
||||
Name: "普通用户", |
||||
Code: "USER", |
||||
Description: "普通用户,拥有基本权限", |
||||
}, |
||||
} |
||||
|
||||
for _, role := range roles { |
||||
if err := db.Create(&role).Error; err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// createDefaultAdmin 创建默认管理员用户
|
||||
func createDefaultAdmin(db *gorm.DB) error { |
||||
// 检查是否已存在管理员用户
|
||||
var count int64 |
||||
db.Model(&model.User{}).Where("username = ?", "admin").Count(&count) |
||||
if count > 0 { |
||||
return nil // 已存在管理员用户,跳过
|
||||
} |
||||
|
||||
// 创建默认管理员用户
|
||||
adminUser := &model.User{ |
||||
Username: "admin", |
||||
Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // "password"
|
||||
Email: "admin@gofaster.com", |
||||
Phone: "13800138000", |
||||
Status: 1, // 正常状态
|
||||
} |
||||
|
||||
if err := db.Create(adminUser).Error; err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 获取超级管理员角色
|
||||
var superAdminRole model.Role |
||||
if err := db.Where("code = ?", "SUPER_ADMIN").First(&superAdminRole).Error; err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 关联超级管理员角色
|
||||
if err := db.Model(adminUser).Association("Roles").Append(&superAdminRole); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
package model |
||||
|
||||
import "time" |
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct { |
||||
Username string `json:"username" binding:"required" validate:"required,min=3,max=50"` |
||||
Password string `json:"password" binding:"required" validate:"required,min=6,max=100"` |
||||
Captcha string `json:"captcha" binding:"required" validate:"required,len=4"` |
||||
} |
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct { |
||||
Token string `json:"token"` |
||||
TokenType string `json:"token_type"` |
||||
ExpiresIn int64 `json:"expires_in"` |
||||
RefreshToken string `json:"refresh_token"` |
||||
User UserInfo `json:"user"` |
||||
} |
||||
|
||||
// UserInfo 用户信息(登录后返回)
|
||||
type UserInfo struct { |
||||
ID uint `json:"id"` |
||||
Username string `json:"username"` |
||||
Email string `json:"email"` |
||||
Phone string `json:"phone"` |
||||
Status int `json:"status"` |
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"` |
||||
LastLoginIP string `json:"last_login_ip,omitempty"` |
||||
Roles []RoleInfo `json:"roles"` |
||||
} |
||||
|
||||
// RoleInfo 角色信息
|
||||
type RoleInfo struct { |
||||
ID uint `json:"id"` |
||||
Name string `json:"name"` |
||||
Code string `json:"code"` |
||||
} |
||||
|
||||
// RefreshTokenRequest 刷新令牌请求
|
||||
type RefreshTokenRequest struct { |
||||
RefreshToken string `json:"refresh_token" binding:"required"` |
||||
} |
||||
|
||||
// LogoutRequest 登出请求
|
||||
type LogoutRequest struct { |
||||
Token string `json:"token" binding:"required"` |
||||
} |
||||
|
||||
// CaptchaResponse 验证码响应
|
||||
type CaptchaResponse struct { |
||||
CaptchaID string `json:"captcha_id"` |
||||
CaptchaImage string `json:"captcha_image"` // Base64编码的图片
|
||||
ExpiresIn int64 `json:"expires_in"` |
||||
} |
@ -1,6 +1,12 @@
@@ -1,6 +1,12 @@
|
||||
package model |
||||
|
||||
import ( |
||||
"gofaster/internal/shared/model" |
||||
) |
||||
|
||||
// UserRole 用户角色关联表
|
||||
type UserRole struct { |
||||
UserID uint `gorm:"primaryKey"` |
||||
RoleID uint `gorm:"primaryKey"` |
||||
model.BaseModel |
||||
UserID uint `gorm:"not null;index" json:"user_id"` |
||||
RoleID uint `gorm:"not null;index" json:"role_id"` |
||||
} |
||||
|
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
package repository |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
type CaptchaRepository interface { |
||||
Create(ctx context.Context, captchaID, captchaText string, expiresAt time.Time) error |
||||
Get(ctx context.Context, captchaID string) (string, error) |
||||
Delete(ctx context.Context, captchaID string) error |
||||
CleanExpired(ctx context.Context) error |
||||
} |
||||
|
||||
type captchaRepository struct { |
||||
db *gorm.DB |
||||
} |
||||
|
||||
type Captcha struct { |
||||
ID string `gorm:"primarykey;size:32" json:"id"` |
||||
Text string `gorm:"size:10;not null" json:"-"` |
||||
ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"` |
||||
CreatedAt time.Time `gorm:"not null" json:"created_at"` |
||||
} |
||||
|
||||
func NewCaptchaRepository(db *gorm.DB) CaptchaRepository { |
||||
return &captchaRepository{ |
||||
db: db, |
||||
} |
||||
} |
||||
|
||||
func (r *captchaRepository) Create(ctx context.Context, captchaID, captchaText string, expiresAt time.Time) error { |
||||
captcha := &Captcha{ |
||||
ID: captchaID, |
||||
Text: captchaText, |
||||
ExpiresAt: expiresAt, |
||||
CreatedAt: time.Now(), |
||||
} |
||||
|
||||
return r.db.WithContext(ctx).Create(captcha).Error |
||||
} |
||||
|
||||
func (r *captchaRepository) Get(ctx context.Context, captchaID string) (string, error) { |
||||
var captcha Captcha |
||||
err := r.db.WithContext(ctx).Where("id = ? AND expires_at > ?", captchaID, time.Now()).First(&captcha).Error |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
// 获取后立即删除,确保一次性使用
|
||||
r.db.WithContext(ctx).Delete(&captcha) |
||||
|
||||
return captcha.Text, nil |
||||
} |
||||
|
||||
func (r *captchaRepository) Delete(ctx context.Context, captchaID string) error { |
||||
return r.db.WithContext(ctx).Where("id = ?", captchaID).Delete(&Captcha{}).Error |
||||
} |
||||
|
||||
func (r *captchaRepository) CleanExpired(ctx context.Context) error { |
||||
return r.db.WithContext(ctx).Where("expires_at <= ?", time.Now()).Delete(&Captcha{}).Error |
||||
} |
@ -1,63 +1,132 @@
@@ -1,63 +1,132 @@
|
||||
package repository |
||||
|
||||
import ( |
||||
"context" |
||||
"gofaster/internal/auth/model" |
||||
baseModel "gofaster/internal/shared/model" |
||||
"gofaster/internal/shared/repository" |
||||
|
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
type UserRepo struct { |
||||
repository.BaseRepo |
||||
type UserRepository interface { |
||||
Create(ctx context.Context, user *model.User) error |
||||
GetByID(ctx context.Context, id uint) (*model.User, error) |
||||
GetByUsername(ctx context.Context, username string) (*model.User, error) |
||||
GetByEmail(ctx context.Context, email string) (*model.User, error) |
||||
Update(ctx context.Context, user *model.User) error |
||||
Delete(ctx context.Context, id uint) error |
||||
List(ctx context.Context, offset, limit int) ([]*model.User, int64, error) |
||||
|
||||
// 登录相关方法
|
||||
IncrementPasswordError(ctx context.Context, userID uint) error |
||||
ResetPasswordError(ctx context.Context, userID uint) error |
||||
UpdateLastLogin(ctx context.Context, userID uint, ip string) error |
||||
GetUserWithRoles(ctx context.Context, userID uint) (*model.User, error) |
||||
} |
||||
|
||||
type userRepository struct { |
||||
db *gorm.DB |
||||
} |
||||
|
||||
func NewUserRepo(db *gorm.DB) *UserRepo { |
||||
return &UserRepo{BaseRepo: *repository.NewBaseRepo(db)} |
||||
func NewUserRepository(db *gorm.DB) UserRepository { |
||||
return &userRepository{ |
||||
db: db, |
||||
} |
||||
} |
||||
|
||||
func (r *UserRepo) Create(user *model.User) error { |
||||
return r.DB().Create(user).Error |
||||
func (r *userRepository) Create(ctx context.Context, user *model.User) error { |
||||
return r.db.WithContext(ctx).Create(user).Error |
||||
} |
||||
|
||||
func (r *UserRepo) GetByID(id uint) (*model.User, error) { |
||||
func (r *userRepository) GetByID(ctx context.Context, id uint) (*model.User, error) { |
||||
var user model.User |
||||
err := r.DB().Preload("Roles.Permissions").First(&user, id).Error |
||||
return &user, err |
||||
err := r.db.WithContext(ctx).First(&user, id).Error |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &user, nil |
||||
} |
||||
|
||||
func (r *UserRepo) GetByUsername(username string) (*model.User, error) { |
||||
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*model.User, error) { |
||||
var user model.User |
||||
err := r.DB().Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error |
||||
return &user, err |
||||
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &user, nil |
||||
} |
||||
|
||||
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) { |
||||
var user model.User |
||||
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &user, nil |
||||
} |
||||
|
||||
func (r *UserRepo) Update(user *model.User) error { |
||||
return r.DB().Save(user).Error |
||||
func (r *userRepository) Update(ctx context.Context, user *model.User) error { |
||||
return r.db.WithContext(ctx).Save(user).Error |
||||
} |
||||
|
||||
func (r *UserRepo) Delete(id uint) error { |
||||
return r.DB().Delete(&model.User{}, id).Error |
||||
func (r *userRepository) Delete(ctx context.Context, id uint) error { |
||||
return r.db.WithContext(ctx).Delete(&model.User{}, id).Error |
||||
} |
||||
|
||||
func (r *UserRepo) List(page, pageSize int) ([]*model.User, int64, error) { |
||||
func (r *userRepository) List(ctx context.Context, offset, limit int) ([]*model.User, int64, error) { |
||||
var users []*model.User |
||||
var count int64 |
||||
var total int64 |
||||
|
||||
err := r.DB().Model(&model.User{}).Count(&count).Error |
||||
err := r.db.WithContext(ctx).Model(&model.User{}).Count(&total).Error |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
offset := (page - 1) * pageSize |
||||
err = r.DB().Preload("Roles").Offset(offset).Limit(pageSize).Find(&users).Error |
||||
return users, count, err |
||||
err = r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return users, total, nil |
||||
} |
||||
|
||||
// IncrementPasswordError 增加密码错误次数
|
||||
func (r *userRepository) IncrementPasswordError(ctx context.Context, userID uint) error { |
||||
return r.db.WithContext(ctx).Model(&model.User{}). |
||||
Where("id = ?", userID). |
||||
Updates(map[string]interface{}{ |
||||
"password_error_count": gorm.Expr("password_error_count + 1"), |
||||
}).Error |
||||
} |
||||
|
||||
func (r *UserRepo) AssignRole(userID uint, roleIDs []uint) error { |
||||
var roles []model.Role |
||||
if err := r.DB().Find(&roles, roleIDs).Error; err != nil { |
||||
return err |
||||
// ResetPasswordError 重置密码错误次数
|
||||
func (r *userRepository) ResetPasswordError(ctx context.Context, userID uint) error { |
||||
return r.db.WithContext(ctx).Model(&model.User{}). |
||||
Where("id = ?", userID). |
||||
Updates(map[string]interface{}{ |
||||
"password_error_count": 0, |
||||
"status": 1, |
||||
"locked_at": nil, |
||||
}).Error |
||||
} |
||||
|
||||
// UpdateLastLogin 更新最后登录信息
|
||||
func (r *userRepository) UpdateLastLogin(ctx context.Context, userID uint, ip string) error { |
||||
return r.db.WithContext(ctx).Model(&model.User{}). |
||||
Where("id = ?", userID). |
||||
Updates(map[string]interface{}{ |
||||
"last_login_at": gorm.Expr("NOW()"), |
||||
"last_login_ip": ip, |
||||
}).Error |
||||
} |
||||
|
||||
// GetUserWithRoles 获取用户及其角色信息
|
||||
func (r *userRepository) GetUserWithRoles(ctx context.Context, userID uint) (*model.User, error) { |
||||
var user model.User |
||||
err := r.db.WithContext(ctx). |
||||
Preload("Roles"). |
||||
First(&user, userID).Error |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return r.DB().Model(&model.User{BaseModel: baseModel.BaseModel{ID: userID}}).Association("Roles").Replace(roles) |
||||
return &user, nil |
||||
} |
||||
|
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
package routes |
||||
|
||||
import ( |
||||
"gofaster/internal/auth/controller" |
||||
"gofaster/internal/auth/service" |
||||
"gofaster/internal/auth/repository" |
||||
"gofaster/internal/shared/middleware" |
||||
"gofaster/internal/shared/jwt" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
// RegisterAuthRoutes 注册认证相关路由
|
||||
func RegisterAuthRoutes(r *gin.RouterGroup, db *gorm.DB, jwtConfig middleware.JWTConfig) { |
||||
// 创建仓储层实例
|
||||
userRepo := repository.NewUserRepository(db) |
||||
captchaRepo := repository.NewCaptchaRepository(db) |
||||
|
||||
// 创建JWT管理器
|
||||
jwtManager := jwt.NewJWTManager(jwtConfig.SecretKey, jwtConfig.Issuer) |
||||
|
||||
// 创建服务层实例
|
||||
authService := service.NewAuthService(userRepo, captchaRepo, jwtManager) |
||||
|
||||
// 创建控制器实例
|
||||
authController := controller.NewAuthController(authService) |
||||
|
||||
// 认证路由组
|
||||
auth := r.Group("/auth") |
||||
{ |
||||
// 公开接口(无需认证)
|
||||
auth.POST("/login", authController.Login) // 用户登录
|
||||
auth.GET("/captcha", authController.GenerateCaptcha) // 生成验证码
|
||||
|
||||
// 需要认证的接口
|
||||
auth.Use(middleware.JWTAuth(jwtConfig)) |
||||
{ |
||||
auth.POST("/logout", authController.Logout) // 用户登出
|
||||
auth.POST("/refresh", authController.RefreshToken) // 刷新令牌
|
||||
auth.GET("/userinfo", authController.GetUserInfo) // 获取用户信息
|
||||
} |
||||
} |
||||
} |
@ -0,0 +1,315 @@
@@ -0,0 +1,315 @@
|
||||
package service |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/rand" |
||||
"encoding/base64" |
||||
"fmt" |
||||
mathrand "math/rand" |
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
|
||||
"gofaster/internal/auth/model" |
||||
"gofaster/internal/auth/repository" |
||||
"gofaster/internal/shared/jwt" |
||||
|
||||
"golang.org/x/crypto/bcrypt" |
||||
) |
||||
|
||||
type AuthService interface { |
||||
Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) |
||||
Logout(ctx context.Context, token string) error |
||||
RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) |
||||
GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) |
||||
ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error |
||||
GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) |
||||
} |
||||
|
||||
type authService struct { |
||||
userRepo repository.UserRepository |
||||
captchaRepo repository.CaptchaRepository |
||||
jwtManager jwt.JWTManager |
||||
} |
||||
|
||||
func NewAuthService(userRepo repository.UserRepository, captchaRepo repository.CaptchaRepository, jwtManager jwt.JWTManager) AuthService { |
||||
return &authService{ |
||||
userRepo: userRepo, |
||||
captchaRepo: captchaRepo, |
||||
jwtManager: jwtManager, |
||||
} |
||||
} |
||||
|
||||
// Login 用户登录
|
||||
func (s *authService) Login(ctx context.Context, req *model.LoginRequest, clientIP string) (*model.LoginResponse, error) { |
||||
// 1. 验证验证码
|
||||
if err := s.ValidateCaptcha(ctx, req.Username, req.Captcha); err != nil { |
||||
return nil, fmt.Errorf("验证码错误: %w", err) |
||||
} |
||||
|
||||
// 2. 根据用户名查找用户
|
||||
user, err := s.userRepo.GetByUsername(ctx, req.Username) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("用户不存在") |
||||
} |
||||
|
||||
// 3. 检查用户状态
|
||||
if !user.CanLogin() { |
||||
if user.IsLocked() { |
||||
return nil, fmt.Errorf("账户已被锁定,请30分钟后再试") |
||||
} |
||||
return nil, fmt.Errorf("账户已被禁用") |
||||
} |
||||
|
||||
// 4. 验证密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { |
||||
// 密码错误,增加错误次数
|
||||
if err := s.userRepo.IncrementPasswordError(ctx, user.ID); err != nil { |
||||
return nil, fmt.Errorf("系统错误,请稍后重试") |
||||
} |
||||
|
||||
// 检查是否被锁定
|
||||
if user.PasswordErrorCount >= 4 { // 已经是第5次错误
|
||||
return nil, fmt.Errorf("密码错误次数过多,账户已被锁定30分钟") |
||||
} |
||||
|
||||
remaining := 5 - user.PasswordErrorCount - 1 |
||||
return nil, fmt.Errorf("密码错误,还可尝试%d次", remaining) |
||||
} |
||||
|
||||
// 5. 密码正确,重置错误次数并更新登录信息
|
||||
if err := s.userRepo.ResetPasswordError(ctx, user.ID); err != nil { |
||||
return nil, fmt.Errorf("系统错误,请稍后重试") |
||||
} |
||||
|
||||
if err := s.userRepo.UpdateLastLogin(ctx, user.ID, clientIP); err != nil { |
||||
// 登录信息更新失败不影响登录流程
|
||||
fmt.Printf("更新登录信息失败: %v\n", err) |
||||
} |
||||
|
||||
// 6. 生成JWT令牌
|
||||
claims := map[string]interface{}{ |
||||
"user_id": user.ID, |
||||
"username": user.Username, |
||||
"email": user.Email, |
||||
} |
||||
|
||||
token, err := s.jwtManager.GenerateToken(claims, 24*time.Hour) // 24小时有效期
|
||||
if err != nil { |
||||
return nil, fmt.Errorf("生成令牌失败: %w", err) |
||||
} |
||||
|
||||
refreshToken, err := s.jwtManager.GenerateToken(claims, 7*24*time.Hour) // 7天有效期
|
||||
if err != nil { |
||||
return nil, fmt.Errorf("生成刷新令牌失败: %w", err) |
||||
} |
||||
|
||||
// 7. 获取用户角色信息
|
||||
userWithRoles, err := s.userRepo.GetUserWithRoles(ctx, user.ID) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("获取用户信息失败: %w", err) |
||||
} |
||||
|
||||
// 8. 构建响应
|
||||
userInfo := s.buildUserInfo(userWithRoles) |
||||
|
||||
return &model.LoginResponse{ |
||||
Token: token, |
||||
TokenType: "Bearer", |
||||
ExpiresIn: 24 * 60 * 60, // 24小时,单位秒
|
||||
RefreshToken: refreshToken, |
||||
User: *userInfo, |
||||
}, nil |
||||
} |
||||
|
||||
// Logout 用户登出
|
||||
func (s *authService) Logout(ctx context.Context, token string) error { |
||||
// 这里可以实现令牌黑名单机制
|
||||
// 目前简单返回成功
|
||||
return nil |
||||
} |
||||
|
||||
// RefreshToken 刷新令牌
|
||||
func (s *authService) RefreshToken(ctx context.Context, refreshToken string) (*model.LoginResponse, error) { |
||||
// 验证刷新令牌
|
||||
claims, err := s.jwtManager.ValidateToken(refreshToken) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("刷新令牌无效: %w", err) |
||||
} |
||||
|
||||
// 获取用户信息
|
||||
userID, ok := claims["user_id"].(float64) |
||||
if !ok { |
||||
return nil, fmt.Errorf("令牌格式错误") |
||||
} |
||||
|
||||
user, err := s.userRepo.GetUserWithRoles(ctx, uint(userID)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("用户不存在: %w", err) |
||||
} |
||||
|
||||
// 检查用户状态
|
||||
if !user.CanLogin() { |
||||
return nil, fmt.Errorf("用户状态异常") |
||||
} |
||||
|
||||
// 生成新的访问令牌
|
||||
newClaims := map[string]interface{}{ |
||||
"user_id": user.ID, |
||||
"username": user.Username, |
||||
"email": user.Email, |
||||
} |
||||
|
||||
newToken, err := s.jwtManager.GenerateToken(newClaims, 24*time.Hour) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("生成新令牌失败: %w", err) |
||||
} |
||||
|
||||
// 构建响应
|
||||
userInfo := s.buildUserInfo(user) |
||||
|
||||
return &model.LoginResponse{ |
||||
Token: newToken, |
||||
TokenType: "Bearer", |
||||
ExpiresIn: 24 * 60 * 60, |
||||
RefreshToken: refreshToken, // 保持原刷新令牌
|
||||
User: *userInfo, |
||||
}, nil |
||||
} |
||||
|
||||
// GenerateCaptcha 生成验证码
|
||||
func (s *authService) GenerateCaptcha(ctx context.Context) (*model.CaptchaResponse, error) { |
||||
// 生成随机验证码ID
|
||||
captchaID, err := s.generateRandomID() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("生成验证码ID失败: %w", err) |
||||
} |
||||
|
||||
// 生成4位随机验证码
|
||||
captchaText := s.generateRandomCaptcha(4) |
||||
|
||||
// 设置5分钟过期时间
|
||||
expiresAt := time.Now().Add(5 * time.Minute) |
||||
|
||||
// 保存到数据库
|
||||
if err := s.captchaRepo.Create(ctx, captchaID, captchaText, expiresAt); err != nil { |
||||
return nil, fmt.Errorf("保存验证码失败: %w", err) |
||||
} |
||||
|
||||
// 生成验证码图片(这里简化处理,实际应该生成图片)
|
||||
captchaImage := s.generateCaptchaImage(captchaText) |
||||
|
||||
return &model.CaptchaResponse{ |
||||
CaptchaID: captchaID, |
||||
CaptchaImage: captchaImage, |
||||
ExpiresIn: 5 * 60, // 5分钟,单位秒
|
||||
}, nil |
||||
} |
||||
|
||||
// ValidateCaptcha 验证验证码
|
||||
func (s *authService) ValidateCaptcha(ctx context.Context, captchaID, captchaText string) error { |
||||
// 从数据库获取验证码
|
||||
storedText, err := s.captchaRepo.Get(ctx, captchaID) |
||||
if err != nil { |
||||
return fmt.Errorf("验证码已过期或不存在") |
||||
} |
||||
|
||||
// 比较验证码(不区分大小写)
|
||||
if !strings.EqualFold(storedText, captchaText) { |
||||
return fmt.Errorf("验证码错误") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func (s *authService) GetUserInfo(ctx context.Context, userID uint) (*model.UserInfo, error) { |
||||
user, err := s.userRepo.GetUserWithRoles(ctx, userID) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("用户不存在: %w", err) |
||||
} |
||||
|
||||
return s.buildUserInfo(user), nil |
||||
} |
||||
|
||||
// buildUserInfo 构建用户信息
|
||||
func (s *authService) buildUserInfo(user *model.User) *model.UserInfo { |
||||
roles := make([]model.RoleInfo, 0, len(user.Roles)) |
||||
for _, role := range user.Roles { |
||||
roles = append(roles, model.RoleInfo{ |
||||
ID: role.ID, |
||||
Name: role.Name, |
||||
Code: role.Code, |
||||
}) |
||||
} |
||||
|
||||
return &model.UserInfo{ |
||||
ID: user.ID, |
||||
Username: user.Username, |
||||
Email: user.Email, |
||||
Phone: user.Phone, |
||||
Status: user.Status, |
||||
LastLoginAt: user.LastLoginAt, |
||||
LastLoginIP: user.LastLoginIP, |
||||
Roles: roles, |
||||
} |
||||
} |
||||
|
||||
// generateRandomID 生成随机ID
|
||||
func (s *authService) generateRandomID() (string, error) { |
||||
b := make([]byte, 16) |
||||
if _, err := rand.Read(b); err != nil { |
||||
return "", err |
||||
} |
||||
return base64.URLEncoding.EncodeToString(b), nil |
||||
} |
||||
|
||||
// generateRandomCaptcha 生成随机验证码
|
||||
func (s *authService) generateRandomCaptcha(length int) string { |
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" |
||||
result := make([]byte, length) |
||||
for i := range result { |
||||
result[i] = chars[mathrand.Intn(len(chars))] |
||||
} |
||||
return string(result) |
||||
} |
||||
|
||||
// generateCaptchaImage 生成验证码图片(简化版本,返回Base64编码)
|
||||
func (s *authService) generateCaptchaImage(text string) string { |
||||
// 这里应该使用图片生成库生成实际的验证码图片
|
||||
// 目前返回一个占位符
|
||||
return fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(text))) |
||||
} |
||||
|
||||
// GetClientIP 获取客户端IP地址
|
||||
func GetClientIP(r *http.Request) string { |
||||
// 尝试从各种头部获取真实IP
|
||||
ip := r.Header.Get("X-Real-IP") |
||||
if ip != "" { |
||||
return ip |
||||
} |
||||
|
||||
ip = r.Header.Get("X-Forwarded-For") |
||||
if ip != "" { |
||||
// X-Forwarded-For可能包含多个IP,取第一个
|
||||
if idx := strings.Index(ip, ","); idx != -1 { |
||||
ip = ip[:idx] |
||||
} |
||||
return strings.TrimSpace(ip) |
||||
} |
||||
|
||||
ip = r.Header.Get("X-Forwarded") |
||||
if ip != "" { |
||||
return ip |
||||
} |
||||
|
||||
// 从RemoteAddr获取
|
||||
if r.RemoteAddr != "" { |
||||
if idx := strings.Index(r.RemoteAddr, ":"); idx != -1 { |
||||
return r.RemoteAddr[:idx] |
||||
} |
||||
return r.RemoteAddr |
||||
} |
||||
|
||||
return "127.0.0.1" |
||||
} |
@ -1,34 +1,35 @@
@@ -1,34 +1,35 @@
|
||||
package service |
||||
|
||||
import ( |
||||
"context" |
||||
"gofaster/internal/auth/model" |
||||
"gofaster/internal/auth/repository" |
||||
) |
||||
|
||||
type UserService struct { |
||||
repo *repository.UserRepo |
||||
repo repository.UserRepository |
||||
} |
||||
|
||||
func NewUserService(repo *repository.UserRepo) *UserService { |
||||
func NewUserService(repo repository.UserRepository) *UserService { |
||||
return &UserService{repo: repo} |
||||
} |
||||
|
||||
func (s *UserService) CreateUser(user *model.User) error { |
||||
return s.repo.Create(user) |
||||
func (s *UserService) CreateUser(ctx context.Context, user *model.User) error { |
||||
return s.repo.Create(ctx, user) |
||||
} |
||||
|
||||
func (s *UserService) GetUserByID(id uint) (*model.User, error) { |
||||
return s.repo.GetByID(id) |
||||
func (s *UserService) GetUserByID(ctx context.Context, id uint) (*model.User, error) { |
||||
return s.repo.GetByID(ctx, id) |
||||
} |
||||
|
||||
func (s *UserService) UpdateUser(user *model.User) error { |
||||
return s.repo.Update(user) |
||||
func (s *UserService) UpdateUser(ctx context.Context, user *model.User) error { |
||||
return s.repo.Update(ctx, user) |
||||
} |
||||
|
||||
func (s *UserService) DeleteUser(id uint) error { |
||||
return s.repo.Delete(id) |
||||
func (s *UserService) DeleteUser(ctx context.Context, id uint) error { |
||||
return s.repo.Delete(ctx, id) |
||||
} |
||||
|
||||
func (s *UserService) ListUsers(page, pageSize int) ([]*model.User, int64, error) { |
||||
return s.repo.List(page, pageSize) |
||||
func (s *UserService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) { |
||||
return s.repo.List(ctx, page, pageSize) |
||||
} |
||||
|
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
package jwt |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/golang-jwt/jwt/v5" |
||||
) |
||||
|
||||
// JWTManager JWT管理器接口
|
||||
type JWTManager interface { |
||||
GenerateToken(claims map[string]interface{}, expiration time.Duration) (string, error) |
||||
ValidateToken(tokenString string) (map[string]interface{}, error) |
||||
RefreshToken(tokenString string, expiration time.Duration) (string, error) |
||||
} |
||||
|
||||
// jwtManager JWT管理器实现
|
||||
type jwtManager struct { |
||||
secretKey []byte |
||||
issuer string |
||||
} |
||||
|
||||
// NewJWTManager 创建新的JWT管理器
|
||||
func NewJWTManager(secretKey string, issuer string) JWTManager { |
||||
return &jwtManager{ |
||||
secretKey: []byte(secretKey), |
||||
issuer: issuer, |
||||
} |
||||
} |
||||
|
||||
// GenerateToken 生成JWT令牌
|
||||
func (j *jwtManager) GenerateToken(claims map[string]interface{}, expiration time.Duration) (string, error) { |
||||
// 创建标准声明
|
||||
standardClaims := jwt.RegisteredClaims{ |
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)), |
||||
IssuedAt: jwt.NewNumericDate(time.Now()), |
||||
Issuer: j.issuer, |
||||
} |
||||
|
||||
// 合并自定义声明和标准声明
|
||||
allClaims := make(map[string]interface{}) |
||||
for k, v := range claims { |
||||
allClaims[k] = v |
||||
} |
||||
allClaims["exp"] = standardClaims.ExpiresAt |
||||
allClaims["iat"] = standardClaims.IssuedAt |
||||
allClaims["iss"] = standardClaims.Issuer |
||||
|
||||
// 创建令牌
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(allClaims)) |
||||
|
||||
// 签名令牌
|
||||
return token.SignedString(j.secretKey) |
||||
} |
||||
|
||||
// ValidateToken 验证JWT令牌
|
||||
func (j *jwtManager) ValidateToken(tokenString string) (map[string]interface{}, error) { |
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { |
||||
// 验证签名方法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { |
||||
return nil, jwt.ErrSignatureInvalid |
||||
} |
||||
return j.secretKey, nil |
||||
}) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { |
||||
return claims, nil |
||||
} |
||||
|
||||
return nil, jwt.ErrInvalidKey |
||||
} |
||||
|
||||
// RefreshToken 刷新JWT令牌
|
||||
func (j *jwtManager) RefreshToken(tokenString string, expiration time.Duration) (string, error) { |
||||
// 验证原令牌
|
||||
claims, err := j.ValidateToken(tokenString) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
// 移除时间相关声明
|
||||
delete(claims, "exp") |
||||
delete(claims, "iat") |
||||
delete(claims, "nbf") |
||||
|
||||
// 生成新令牌
|
||||
return j.GenerateToken(claims, expiration) |
||||
} |
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
package middleware |
||||
|
||||
import ( |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"gofaster/internal/shared/jwt" |
||||
"gofaster/internal/shared/response" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
// JWTConfig JWT中间件配置
|
||||
type JWTConfig struct { |
||||
SecretKey string |
||||
Issuer string |
||||
} |
||||
|
||||
// JWTAuth JWT认证中间件
|
||||
func JWTAuth(config JWTConfig) gin.HandlerFunc { |
||||
return func(c *gin.Context) { |
||||
// 获取Authorization头部
|
||||
authHeader := c.GetHeader("Authorization") |
||||
if authHeader == "" { |
||||
response.Unauthorized(c, "未提供认证令牌", "Authorization头部缺失") |
||||
c.Abort() |
||||
return |
||||
} |
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") { |
||||
response.Unauthorized(c, "认证令牌格式错误", "令牌必须以Bearer开头") |
||||
c.Abort() |
||||
return |
||||
} |
||||
|
||||
// 提取令牌
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ") |
||||
|
||||
// 创建JWT管理器
|
||||
jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) |
||||
|
||||
// 验证令牌
|
||||
claims, err := jwtManager.ValidateToken(tokenString) |
||||
if err != nil { |
||||
response.Unauthorized(c, "认证令牌无效", err.Error()) |
||||
c.Abort() |
||||
return |
||||
} |
||||
|
||||
// 将用户信息存储到上下文中
|
||||
if userID, exists := claims["user_id"]; exists { |
||||
c.Set("user_id", userID) |
||||
} |
||||
if username, exists := claims["username"]; exists { |
||||
c.Set("username", username) |
||||
} |
||||
if email, exists := claims["email"]; exists { |
||||
c.Set("email", email) |
||||
} |
||||
|
||||
// 继续处理请求
|
||||
c.Next() |
||||
} |
||||
} |
||||
|
||||
// OptionalJWTAuth 可选的JWT认证中间件(不强制要求认证)
|
||||
func OptionalJWTAuth(config JWTConfig) gin.HandlerFunc { |
||||
return func(c *gin.Context) { |
||||
// 获取Authorization头部
|
||||
authHeader := c.GetHeader("Authorization") |
||||
if authHeader == "" { |
||||
// 没有令牌,继续处理请求
|
||||
c.Next() |
||||
return |
||||
} |
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") { |
||||
// 令牌格式错误,继续处理请求(不强制要求)
|
||||
c.Next() |
||||
return |
||||
} |
||||
|
||||
// 提取令牌
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ") |
||||
|
||||
// 创建JWT管理器
|
||||
jwtManager := jwt.NewJWTManager(config.SecretKey, config.Issuer) |
||||
|
||||
// 验证令牌
|
||||
claims, err := jwtManager.ValidateToken(tokenString) |
||||
if err != nil { |
||||
// 令牌无效,继续处理请求(不强制要求)
|
||||
c.Next() |
||||
return |
||||
} |
||||
|
||||
// 将用户信息存储到上下文中
|
||||
if userID, exists := claims["user_id"]; exists { |
||||
c.Set("user_id", userID) |
||||
} |
||||
if username, exists := claims["username"]; exists { |
||||
c.Set("username", username) |
||||
} |
||||
if email, exists := claims["email"]; exists { |
||||
c.Set("email", email) |
||||
} |
||||
|
||||
// 继续处理请求
|
||||
c.Next() |
||||
} |
||||
} |
||||
|
||||
// GetUserID 从上下文中获取用户ID
|
||||
func GetUserID(c *gin.Context) (uint, bool) { |
||||
userID, exists := c.Get("user_id") |
||||
if !exists { |
||||
return 0, false |
||||
} |
||||
|
||||
switch v := userID.(type) { |
||||
case float64: |
||||
return uint(v), true |
||||
case int: |
||||
return uint(v), true |
||||
case string: |
||||
if parsed, err := strconv.ParseUint(v, 10, 32); err == nil { |
||||
return uint(parsed), true |
||||
} |
||||
} |
||||
return 0, false |
||||
} |
||||
|
||||
// GetUsername 从上下文中获取用户名
|
||||
func GetUsername(c *gin.Context) (string, bool) { |
||||
username, exists := c.Get("username") |
||||
if !exists { |
||||
return "", false |
||||
} |
||||
if str, ok := username.(string); ok { |
||||
return str, true |
||||
} |
||||
return "", false |
||||
} |
||||
|
||||
// GetEmail 从上下文中获取邮箱
|
||||
func GetEmail(c *gin.Context) (string, bool) { |
||||
email, exists := c.Get("email") |
||||
if !exists { |
||||
return "", false |
||||
} |
||||
if str, ok := email.(string); ok { |
||||
return str, true |
||||
} |
||||
return "", false |
||||
} |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
package response |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"github.com/gin-gonic/gin" |
||||
) |
||||
|
||||
// Response 统一响应结构
|
||||
type Response struct { |
||||
Code int `json:"code"` // 响应状态码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data interface{} `json:"data,omitempty"` // 响应数据
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
} |
||||
|
||||
// Success 成功响应
|
||||
func Success(c *gin.Context, message string, data interface{}) { |
||||
c.JSON(http.StatusOK, Response{ |
||||
Code: http.StatusOK, |
||||
Message: message, |
||||
Data: data, |
||||
}) |
||||
} |
||||
|
||||
// Error 错误响应
|
||||
func Error(c *gin.Context, code int, message string, error string) { |
||||
c.JSON(code, Response{ |
||||
Code: code, |
||||
Message: message, |
||||
Error: error, |
||||
}) |
||||
} |
||||
|
||||
// BadRequest 400 错误响应
|
||||
func BadRequest(c *gin.Context, message string, error string) { |
||||
Error(c, http.StatusBadRequest, message, error) |
||||
} |
||||
|
||||
// Unauthorized 401 错误响应
|
||||
func Unauthorized(c *gin.Context, message string, error string) { |
||||
Error(c, http.StatusUnauthorized, message, error) |
||||
} |
||||
|
||||
// Forbidden 403 错误响应
|
||||
func Forbidden(c *gin.Context, message string, error string) { |
||||
Error(c, http.StatusForbidden, message, error) |
||||
} |
||||
|
||||
// NotFound 404 错误响应
|
||||
func NotFound(c *gin.Context, message string, error string) { |
||||
Error(c, http.StatusNotFound, message, error) |
||||
} |
||||
|
||||
// InternalServerError 500 错误响应
|
||||
func InternalServerError(c *gin.Context, message string, error string) { |
||||
Error(c, http.StatusInternalServerError, message, error) |
||||
} |
||||
|
||||
// Locked 423 错误响应(账户被锁定)
|
||||
func Locked(c *gin.Context, message string, error string) { |
||||
Error(c, http.StatusLocked, message, error) |
||||
} |
Loading…
Reference in new issue