用户认证与JWT
本文档介绍Go-ES项目中的用户认证与JWT(JSON Web Token)实现,包括用户注册、登录、令牌刷新以及接口权限控制。
1. 认证系统概述
1.1 认证流程
Go-ES项目采用基于JWT的认证机制,主要流程如下:
- 用户注册:用户提供用户名、密码和邮箱进行注册
- 用户登录:用户凭借用户名和密码登录,服务器返回访问令牌(Access Token)和刷新令牌(Refresh Token)
- 接口认证:客户端在请求头中添加访问令牌,服务器验证令牌有效性
- 令牌刷新:访问令牌过期后,客户端使用刷新令牌获取新的令牌对
1.2 JWT令牌设计
采用双令牌系统:
- 访问令牌(Access Token):短期有效(默认24小时),用于API访问认证
- 刷新令牌(Refresh Token):长期有效(默认7天),用于获取新的访问令牌
JWT负载包含以下信息:
- 用户ID(sub)
- 用户名(username)
- 用户角色(role)
- 令牌类型(type)
- 过期时间(exp)
- 签发时间(iat)
- 生效时间(nbf)
- 签发者(iss)
2. 核心组件
2.1 用户实体
// User 用户实体
type User struct {
ID uint `json:"id" gorm:"primarykey"`
Username string `json:"username" gorm:"type:varchar(50);uniqueIndex;not null"`
Password string `json:"-" gorm:"type:varchar(100);not null"`
Email string `json:"email" gorm:"type:varchar(100);uniqueIndex"`
Role string `json:"role" gorm:"type:varchar(20);default:'user'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SetPassword 设置密码(加密)
func (u *User) SetPassword(password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// CheckPassword 检查密码是否正确
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
2.2 JWT服务
// Claims JWT声明
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
Type TokenType `json:"type"`
jwt.RegisteredClaims
}
// Service JWT服务
type Service struct {
config *config.Config
}
// GenerateToken 生成JWT令牌
func (s *Service) GenerateToken(userID uint, username, role string) (string, string, error) {
// 生成访问令牌和刷新令牌
}
// RefreshToken 刷新访问令牌
func (s *Service) RefreshToken(refreshToken string) (string, string, error) {
// 验证刷新令牌并生成新的令牌对
}
// ParseToken 解析JWT令牌
func (s *Service) ParseToken(tokenString string) (*Claims, error) {
// 解析并验证令牌
}
2.3 认证服务接口
// AuthService 身份验证服务接口
type AuthService interface {
// Register 注册新用户
Register(ctx context.Context, username, password, email string) (*entity.User, error)
// Login 用户登录
Login(ctx context.Context, username, password string) (accessToken string, refreshToken string, user *entity.User, error)
// RefreshToken 刷新令牌
RefreshToken(ctx context.Context, refreshToken string) (accessToken string, newRefreshToken string, error)
// GetUserByID 通过ID获取用户
GetUserByID(ctx context.Context, id uint) (*entity.User, error)
// ValidateToken 验证token并返回用户信息
ValidateToken(ctx context.Context, token string) (*entity.User, error)
}
2.4 认证中间件
// AuthMiddleware 认证中间件
type AuthMiddleware struct {
authService service.AuthService
}
// Authenticate 验证用户身份
func (m *AuthMiddleware) Authenticate() gin.HandlerFunc {
// 从请求头中获取并验证令牌
}
// RequireRole 要求特定角色访问
func (m *AuthMiddleware) RequireRole(role string) gin.HandlerFunc {
// 验证用户角色权限
}
3. API端点
3.1 用户注册
POST /api/v1/auth/register
请求体:
{
"username": "johndoe",
"password": "securepassword",
"email": "john@example.com"
}
响应:
{
"id": 1,
"username": "johndoe",
"email": "john@example.com",
"role": "user"
}
3.2 用户登录
POST /api/v1/auth/login
请求体:
{
"username": "johndoe",
"password": "securepassword"
}
响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"username": "johndoe",
"email": "john@example.com",
"role": "user"
}
3.3 刷新令牌
POST /api/v1/auth/refresh
请求体:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
3.4 获取当前用户信息
GET /api/v1/user/me
请求头:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
响应:
{
"id": 1,
"username": "johndoe",
"email": "john@example.com",
"role": "user"
}
4. 配置说明
JWT相关配置在config.yaml
中:
# JWT 配置
jwt:
secret: "your_secret_key_here" # JWT密钥,生产环境请使用复杂的随机字符串
expires_in: 24 # 访问令牌过期时间(小时)
refresh_expires_in: 168 # 刷新令牌过期时间(小时,7天)
5. 安全最佳实践
5.1 密码存储
- 使用bcrypt算法对密码进行单向哈希存储
- 不在数据库中存储明文密码
- 使用适当的加密强度(默认使用bcrypt.DefaultCost)
5.2 令牌安全
- 使用HTTPS传输敏感信息
- 访问令牌设置较短的过期时间(默认24小时)
- 刷新令牌每次使用后都会被替换
5.3 API安全
- 所有敏感接口都使用认证中间件保护
- 基于角色的访问控制(RBAC)
- 对输入数据进行严格验证
6. 实现细节
6.1 认证流程详解
登录流程
用户登录 -> 验证凭证 -> 生成访问令牌和刷新令牌 -> 返回给客户端
API访问流程
请求API -> 提取Authorization头中的令牌 -> 验证令牌 -> 提取用户信息 -> 授权
令牌刷新流程
访问令牌过期 -> 客户端使用刷新令牌请求新令牌 -> 验证刷新令牌 -> 生成新的令牌对 -> 返回给客户端
6.2 令牌验证细节
- 签名验证:验证令牌签名是否有效
- 过期检查:验证令牌是否过期
- 类型检查:验证令牌类型是否匹配(访问令牌用于API访问,刷新令牌用于刷新)
- 用户验证:验证令牌中的用户是否存在
7. 客户端集成指南
7.1 前端存储
- 将访问令牌存储在内存或sessionStorage中
- 将刷新令牌安全存储在httpOnly cookie或localStorage中
7.2 请求认证
// 添加认证头
fetch('/api/v1/user/me', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
7.3 令牌刷新处理
async function refreshTokens(refreshToken) {
const response = await fetch('/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
return await response.json();
}
// 处理401错误
async function handleApiRequest(url, options) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
});
if (response.status === 401) {
// 令牌过期,尝试刷新
const tokens = await refreshTokens(refreshToken);
// 更新存储的令牌
accessToken = tokens.access_token;
refreshToken = tokens.refresh_token;
// 重试请求
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
});
}
return response;
}
8. 测试
8.1 单元测试
func TestAuthService_Login(t *testing.T) {
// 模拟用户仓库和JWT服务
// 测试登录逻辑
}
func TestJWTService_GenerateToken(t *testing.T) {
// 测试令牌生成
}
func TestJWTService_ParseToken(t *testing.T) {
// 测试令牌解析
}
8.2 集成测试
func TestAuthenticationFlow(t *testing.T) {
// 测试完整的认证流程:注册 -> 登录 -> 访问API -> 刷新令牌
}
9. 扩展与优化
9.1 潜在的扩展
- 支持OAuth2.0或OpenID Connect集成
- 添加多因素认证(MFA)
- 实现单点登录(SSO)
9.2 性能优化
- 使用缓存存储活跃用户信息
- 使用Redis存储无效/已撤销的令牌
- 令牌黑名单机制,提高安全性
9.3 安全增强
- 实现登录尝试次数限制
- 添加IP地址和设备指纹验证
- 实现令牌轮换策略