Go~JWT登陆授权
前言
在现代 Web 应用中,JWT(JSON Web Token)已经成为最流行的身份认证方案之一。相比传统的 Session 认证,JWT 具有无状态、可扩展、跨域支持等优势,特别适合微服务架构和前后端分离的应用。
本文将使用 github.com/golang-jwt/jwt/v5 库,从零开始构建一个完整的 JWT 认证系统,包括用户注册、登录、Token 刷新、权限验证等核心功能。
JWT 基础知识
什么是 JWT
JWT 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。
JWT 的结构
JWT 由三部分组成,使用点(.)分隔:
Header.Payload.Signature
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header(头部):包含令牌类型和签名算法
{ "alg": "HS256", "typ": "JWT" }Payload(负载):包含声明(Claims),即用户信息和元数据
{ "user_id": 123, "username": "john_doe", "role": "admin", "exp": 1516239022 }Signature(签名):用于验证消息在传输过程中没有被篡改
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
JWT 的优势
- ✅ 无状态:服务器不需要存储 Session,易于水平扩展
- ✅ 跨域支持:可以在不同域之间使用
- ✅ 移动友好:适合移动端应用
- ✅ 性能好:减少数据库查询
- ✅ 灵活性:可以携带自定义信息
项目初始化
1. 创建项目结构
jwt-auth-system/
├── main.go
├── config/
│ └── config.go
├── models/
│ └── user.go
├── middleware/
│ └── auth.go
├── handlers/
│ ├── auth.go
│ └── user.go
├── services/
│ └── jwt.go
├── database/
│ └── db.go
└── go.mod
2. 安装依赖
go mod init jwt-auth-system
# 安装最新版 JWT 库
go get -u github.com/golang-jwt/jwt/v5
# 安装 Echo 框架
go get -u github.com/labstack/echo/v4
# 安装数据库驱动
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
# 安装密码加密库
go get -u golang.org/x/crypto/bcrypt
# 安装环境变量库
go get -u github.com/joho/godotenv
核心模块实现
1. 配置管理 (config/config.go)
package config
import (
"os"
"time"
)
type Config struct {
// 服务器配置
ServerPort string
// JWT 配置
JWTSecret string
JWTAccessExpiry time.Duration
JWTRefreshExpiry time.Duration
// 数据库配置
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
}
func LoadConfig() *Config {
return &Config{
ServerPort: getEnv("SERVER_PORT", "8080"),
// JWT 配置
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-this-in-production"),
JWTAccessExpiry: time.Hour * 1, // Access Token 1小时
JWTRefreshExpiry: time.Hour * 24 * 7, // Refresh Token 7天
// 数据库配置
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "3306"),
DBUser: getEnv("DB_USER", "root"),
DBPassword: getEnv("DB_PASSWORD", "password"),
DBName: getEnv("DB_NAME", "jwt_auth"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
2. 用户模型 (models/user.go)
package models
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primarykey" json:"id"`
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
Password string `gorm:"size:255;not null" json:"-"` // 不在 JSON 中显示
Role string `gorm:"size:20;default:user" json:"role"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// RegisterRequest 注册请求
type RegisterRequest struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
// TokenResponse Token 响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
// RefreshTokenRequest 刷新 Token 请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
3. JWT 服务 (services/jwt.go)
这是核心的 JWT 处理逻辑,使用最新版 JWT 库:
package services
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
// JWTService JWT 服务
type JWTService struct {
secretKey string
accessExpiry time.Duration
refreshExpiry time.Duration
}
// Claims JWT 自定义声明
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// RefreshClaims Refresh Token 声明
type RefreshClaims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
// NewJWTService 创建 JWT 服务实例
func NewJWTService(secretKey string, accessExpiry, refreshExpiry time.Duration) *JWTService {
return &JWTService{
secretKey: secretKey,
accessExpiry: accessExpiry,
refreshExpiry: refreshExpiry,
}
}
// GenerateAccessToken 生成 Access Token
func (s *JWTService) GenerateAccessToken(userID uint, username, email, role string) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(s.accessExpiry)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: "jwt-auth-system",
Subject: fmt.Sprintf("%d", userID),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.secretKey))
}
// GenerateRefreshToken 生成 Refresh Token
func (s *JWTService) GenerateRefreshToken(userID uint) (string, error) {
now := time.Now()
claims := RefreshClaims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(s.refreshExpiry)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: "jwt-auth-system",
Subject: fmt.Sprintf("%d", userID),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.secretKey))
}
// ValidateAccessToken 验证 Access Token
func (s *JWTService) ValidateAccessToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// 验证签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.secretKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
// ValidateRefreshToken 验证 Refresh Token
func (s *JWTService) ValidateRefreshToken(tokenString string) (*RefreshClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &RefreshClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.secretKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*RefreshClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid refresh token")
}
// GetAccessTokenExpiry 获取 Access Token 过期时间(秒)
func (s *JWTService) GetAccessTokenExpiry() int64 {
return int64(s.accessExpiry.Seconds())
}
4. 数据库连接 (database/db.go)
package database
import (
"fmt"
"log"
"jwt-auth-system/config"
"jwt-auth-system/models"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
// InitDB 初始化数据库连接
func InitDB(cfg *config.Config) error {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.DBUser,
cfg.DBPassword,
cfg.DBHost,
cfg.DBPort,
cfg.DBName,
)
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return fmt.Errorf("failed to connect database: %w", err)
}
// 自动迁移数据库表
if err := DB.AutoMigrate(&models.User{}); err != nil {
return fmt.Errorf("failed to migrate database: %w", err)
}
log.Println("Database connected and migrated successfully")
return nil
}
// GetDB 获取数据库实例
func GetDB() *gorm.DB {
return DB
}
5. 认证中间件 (middleware/auth.go)
package middleware
import (
"net/http"
"strings"
"jwt-auth-system/services"
"github.com/labstack/echo/v4"
)
// JWTMiddleware JWT 认证中间件
func JWTMiddleware(jwtService *services.JWTService) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 从请求头获取 Token
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header")
}
// 检查 Bearer 前缀
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid authorization header format")
}
tokenString := parts[1]
// 验证 Token
claims, err := jwtService.ValidateAccessToken(tokenString)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired token")
}
// 将用户信息存入上下文
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
return next(c)
}
}
}
// RoleMiddleware 角色权限中间件
func RoleMiddleware(allowedRoles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 获取用户角色
userRole, ok := c.Get("role").(string)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "user role not found")
}
// 检查角色权限
for _, role := range allowedRoles {
if userRole == role {
return next(c)
}
}
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}
}
}
6. 认证处理器 (handlers/auth.go)
package handlers
import (
"net/http"
"jwt-auth-system/database"
"jwt-auth-system/models"
"jwt-auth-system/services"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
type AuthHandler struct {
jwtService *services.JWTService
}
func NewAuthHandler(jwtService *services.JWTService) *AuthHandler {
return &AuthHandler{
jwtService: jwtService,
}
}
// Register 用户注册
func (h *AuthHandler) Register(c echo.Context) error {
var req models.RegisterRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
}
// 验证请求参数
if err := c.Validate(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// 检查用户名是否已存在
var existingUser models.User
if err := database.GetDB().Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
return echo.NewHTTPError(http.StatusConflict, "username already exists")
}
// 检查邮箱是否已存在
if err := database.GetDB().Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
return echo.NewHTTPError(http.StatusConflict, "email already exists")
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to hash password")
}
// 创建用户
user := models.User{
Username: req.Username,
Email: req.Email,
Password: string(hashedPassword),
Role: "user", // 默认角色
IsActive: true,
}
if err := database.GetDB().Create(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create user")
}
return c.JSON(http.StatusCreated, map[string]interface{}{
"message": "user registered successfully",
"user": map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
},
})
}
// Login 用户登录
func (h *AuthHandler) Login(c echo.Context) error {
var req models.LoginRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
}
if err := c.Validate(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// 查找用户
var user models.User
if err := database.GetDB().Where("username = ?", req.Username).First(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid username or password")
}
// 检查用户是否激活
if !user.IsActive {
return echo.NewHTTPError(http.StatusForbidden, "user account is disabled")
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid username or password")
}
// 生成 Access Token
accessToken, err := h.jwtService.GenerateAccessToken(user.ID, user.Username, user.Email, user.Role)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token")
}
// 生成 Refresh Token
refreshToken, err := h.jwtService.GenerateRefreshToken(user.ID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate refresh token")
}
// 返回 Token
response := models.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: h.jwtService.GetAccessTokenExpiry(),
}
return c.JSON(http.StatusOK, response)
}
// RefreshToken 刷新 Token
func (h *AuthHandler) RefreshToken(c echo.Context) error {
var req models.RefreshTokenRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
}
if err := c.Validate(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// 验证 Refresh Token
claims, err := h.jwtService.ValidateRefreshToken(req.RefreshToken)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired refresh token")
}
// 查找用户
var user models.User
if err := database.GetDB().First(&user, claims.UserID).Error; err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "user not found")
}
// 检查用户是否激活
if !user.IsActive {
return echo.NewHTTPError(http.StatusForbidden, "user account is disabled")
}
// 生成新的 Access Token
accessToken, err := h.jwtService.GenerateAccessToken(user.ID, user.Username, user.Email, user.Role)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token")
}
// 生成新的 Refresh Token
newRefreshToken, err := h.jwtService.GenerateRefreshToken(user.ID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate refresh token")
}
response := models.TokenResponse{
AccessToken: accessToken,
RefreshToken: newRefreshToken,
TokenType: "Bearer",
ExpiresIn: h.jwtService.GetAccessTokenExpiry(),
}
return c.JSON(http.StatusOK, response)
}
// Logout 用户登出
func (h *AuthHandler) Logout(c echo.Context) error {
// 在实际生产环境中,可以将 Token 加入黑名单(使用 Redis 等)
// 这里简单返回成功消息
return c.JSON(http.StatusOK, map[string]string{
"message": "logged out successfully",
})
}
7. 用户处理器 (handlers/user.go)
package handlers
import (
"net/http"
"jwt-auth-system/database"
"jwt-auth-system/models"
"github.com/labstack/echo/v4"
)
type UserHandler struct{}
func NewUserHandler() *UserHandler {
return &UserHandler{}
}
// GetProfile 获取当前用户信息
func (h *UserHandler) GetProfile(c echo.Context) error {
userID := c.Get("user_id").(uint)
var user models.User
if err := database.GetDB().First(&user, userID).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
"is_active": user.IsActive,
"created_at": user.CreatedAt,
})
}
// GetAllUsers 获取所有用户(仅管理员)
func (h *UserHandler) GetAllUsers(c echo.Context) error {
var users []models.User
if err := database.GetDB().Find(&users).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to fetch users")
}
var userList []map[string]interface{}
for _, user := range users {
userList = append(userList, map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
"is_active": user.IsActive,
"created_at": user.CreatedAt,
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"users": userList,
"total": len(userList),
})
}
// UpdateProfile 更新用户信息
func (h *UserHandler) UpdateProfile(c echo.Context) error {
userID := c.Get("user_id").(uint)
var req struct {
Email string `json:"email" validate:"omitempty,email"`
}
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
}
var user models.User
if err := database.GetDB().First(&user, userID).Error; err != nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
// 更新邮箱
if req.Email != "" {
// 检查邮箱是否已被使用
var existingUser models.User
if err := database.GetDB().Where("email = ? AND id != ?", req.Email, userID).First(&existingUser).Error; err == nil {
return echo.NewHTTPError(http.StatusConflict, "email already in use")
}
user.Email = req.Email
}
if err := database.GetDB().Save(&user).Error; err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to update user")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "profile updated successfully",
"user": map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
},
})
}
8. 主程序 (main.go)
package main
import (
"log"
"jwt-auth-system/config"
"jwt-auth-system/database"
"jwt-auth-system/handlers"
"jwt-auth-system/middleware"
"jwt-auth-system/services"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
echomiddleware "github.com/labstack/echo/v4/middleware"
)
// CustomValidator 自定义验证器
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
func main() {
// 加载配置
cfg := config.LoadConfig()
// 初始化数据库
if err := database.InitDB(cfg); err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
// 创建 JWT 服务
jwtService := services.NewJWTService(
cfg.JWTSecret,
cfg.JWTAccessExpiry,
cfg.JWTRefreshExpiry,
)
// 创建 Echo 实例
e := echo.New()
// 注册自定义验证器
e.Validator = &CustomValidator{validator: validator.New()}
// 全局中间件
e.Use(echomiddleware.Logger())
e.Use(echomiddleware.Recover())
e.Use(echomiddleware.CORS())
// 创建处理器
authHandler := handlers.NewAuthHandler(jwtService)
userHandler := handlers.NewUserHandler()
// 公开路由(无需认证)
public := e.Group("/api/v1")
{
public.POST("/register", authHandler.Register)
public.POST("/login", authHandler.Login)
public.POST("/refresh", authHandler.RefreshToken)
}
// 受保护的路由(需要认证)
protected := e.Group("/api/v1")
protected.Use(middleware.JWTMiddleware(jwtService))
{
// 用户相关
protected.GET("/profile", userHandler.GetProfile)
protected.PUT("/profile", userHandler.UpdateProfile)
protected.POST("/logout", authHandler.Logout)
// 管理员路由
admin := protected.Group("/admin")
admin.Use(middleware.RoleMiddleware("admin"))
{
admin.GET("/users", userHandler.GetAllUsers)
}
}
// 健康检查
e.GET("/health", func(c echo.Context) error {
return c.JSON(200, map[string]string{
"status": "ok",
})
})
// 启动服务器
log.Printf("Server starting on port %s", cfg.ServerPort)
if err := e.Start(":" + cfg.ServerPort); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
9. 环境变量配置 (.env)
# 服务器配置
SERVER_PORT=8080
# JWT 配置
JWT_SECRET=your-super-secret-key-change-this-in-production-min-32-chars
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=jwt_auth
完整的 API 测试
1. 用户注册
curl -X POST http://localhost:8080/api/v1/register \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"email": "john@example.com",
"password": "SecurePass123!"
}'
响应:
{
"message": "user registered successfully",
"user": {
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"role": "user"
}
}
2. 用户登录
curl -X POST http://localhost:8080/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"password": "SecurePass123!"
}'
响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
3. 获取用户信息
curl -X GET http://localhost:8080/api/v1/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
响应:
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"role": "user",
"is_active": true,
"created_at": "2024-01-15T10:30:00Z"
}
4. 刷新 Token
curl -X POST http://localhost:8080/api/v1/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "YOUR_REFRESH_TOKEN"
}'
响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
5. 更新用户信息
curl -X PUT http://localhost:8080/api/v1/profile \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "newemail@example.com"
}'
6. 管理员获取所有用户
curl -X GET http://localhost:8080/api/v1/admin/users \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN"
响应:
{
"users": [
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"role": "user",
"is_active": true,
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"is_active": true,
"created_at": "2024-01-15T09:00:00Z"
}
],
"total": 2
}
7. 用户登出
curl -X POST http://localhost:8080/api/v1/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
安全最佳实践
1. 密钥管理
// ❌ 不要硬编码密钥
const SECRET_KEY = "my-secret-key"
// ✅ 从环境变量读取
secretKey := os.Getenv("JWT_SECRET")
if len(secretKey) < 32 {
log.Fatal("JWT_SECRET must be at least 32 characters")
}
2. Token 过期时间设置
// 建议的过期时间设置
const (
AccessTokenExpiry = 15 * time.Minute // 短期访问令牌
RefreshTokenExpiry = 7 * 24 * time.Hour // 长期刷新令牌
)
3. HTTPS 传输
// 生产环境必须使用 HTTPS
func main() {
e := echo.New()
// 配置路由...
// 使用 TLS
e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem"))
}
4. Token 黑名单实现
使用 Redis 实现 Token 黑名单:
package services
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
type TokenBlacklist struct {
client *redis.Client
}
func NewTokenBlacklist(client *redis.Client) *TokenBlacklist {
return &TokenBlacklist{client: client}
}
// AddToBlacklist 将 Token 加入黑名单
func (tb *TokenBlacklist) AddToBlacklist(token string, expiry time.Duration) error {
ctx := context.Background()
return tb.client.Set(ctx, "blacklist:"+token, "1", expiry).Err()
}
// IsBlacklisted 检查 Token 是否在黑名单中
func (tb *TokenBlacklist) IsBlacklisted(token string) (bool, error) {
ctx := context.Background()
result, err := tb.client.Get(ctx, "blacklist:"+token).Result()
if err == redis.Nil {
return false, nil
}
if err != nil {
return false, err
}
return result == "1", nil
}
在中间件中使用:
func JWTMiddleware(jwtService *services.JWTService, blacklist *services.TokenBlacklist) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString := extractToken(c)
// 检查是否在黑名单中
if blacklisted, err := blacklist.IsBlacklisted(tokenString); err == nil && blacklisted {
return echo.NewHTTPError(http.StatusUnauthorized, "token has been revoked")
}
// 验证 Token
claims, err := jwtService.ValidateAccessToken(tokenString)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
}
c.Set("user_id", claims.UserID)
return next(c)
}
}
}
5. 密码强度验证
package validators
import (
"errors"
"regexp"
)
// ValidatePasswordStrength 验证密码强度
func ValidatePasswordStrength(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters")
}
// 至少包含一个大写字母
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
return errors.New("password must contain at least one uppercase letter")
}
// 至少包含一个小写字母
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
return errors.New("password must contain at least one lowercase letter")
}
// 至少包含一个数字
if matched, _ := regexp.MatchString(`[0-9]`, password); !matched {
return errors.New("password must contain at least one digit")
}
// 至少包含一个特殊字符
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
return errors.New("password must contain at least one special character")
}
return nil
}
6. 速率限制
使用中间件限制登录尝试次数:
package middleware
import (
"net/http"
"sync"
"time"
"github.com/labstack/echo/v4"
)
type RateLimiter struct {
attempts map[string]*AttemptInfo
mu sync.RWMutex
}
type AttemptInfo struct {
Count int
ResetTime time.Time
}
func NewRateLimiter() *RateLimiter {
rl := &RateLimiter{
attempts: make(map[string]*AttemptInfo),
}
// 定期清理过期记录
go rl.cleanup()
return rl
}
func (rl *RateLimiter) cleanup() {
ticker := time.NewTicker(time.Hour)
for range ticker.C {
rl.mu.Lock()
now := time.Now()
for key, info := range rl.attempts {
if now.After(info.ResetTime) {
delete(rl.attempts, key)
}
}
rl.mu.Unlock()
}
}
func (rl *RateLimiter) LoginRateLimitMiddleware(maxAttempts int, window time.Duration) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 获取客户端 IP
ip := c.RealIP()
rl.mu.Lock()
info, exists := rl.attempts[ip]
if !exists || time.Now().After(info.ResetTime) {
rl.attempts[ip] = &AttemptInfo{
Count: 1,
ResetTime: time.Now().Add(window),
}
rl.mu.Unlock()
return next(c)
}
if info.Count >= maxAttempts {
rl.mu.Unlock()
return echo.NewHTTPError(
http.StatusTooManyRequests,
"too many login attempts, please try again later",
)
}
info.Count++
rl.mu.Unlock()
return next(c)
}
}
}
在路由中使用:
rateLimiter := middleware.NewRateLimiter()
public := e.Group("/api/v1")
{
// 登录接口限制:5次/15分钟
public.POST("/login", authHandler.Login,
rateLimiter.LoginRateLimitMiddleware(5, 15*time.Minute))
public.POST("/register", authHandler.Register)
public.POST("/refresh", authHandler.RefreshToken)
}
高级功能扩展
1. 多设备登录管理
package models
type UserSession struct {
ID uint `gorm:"primarykey"`
UserID uint `gorm:"index"`
RefreshToken string `gorm:"size:500;uniqueIndex"`
DeviceInfo string `gorm:"size:255"`
IPAddress string `gorm:"size:45"`
UserAgent string `gorm:"size:500"`
ExpiresAt time.Time
CreatedAt time.Time
}
// 在登录时保存 Session
func (h *AuthHandler) Login(c echo.Context) error {
// ... 验证用户 ...
// 生成 Token
accessToken, _ := h.jwtService.GenerateAccessToken(...)
refreshToken, _ := h.jwtService.GenerateRefreshToken(...)
// 保存 Session
session := models.UserSession{
UserID: user.ID,
RefreshToken: refreshToken,
DeviceInfo: c.Request().Header.Get("Device-Info"),
IPAddress: c.RealIP(),
UserAgent: c.Request().UserAgent(),
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
}
database.GetDB().Create(&session)
// 返回 Token
return c.JSON(http.StatusOK, response)
}
2. 邮箱验证功能
package models
type EmailVerification struct {
ID uint `gorm:"primarykey"`
UserID uint `gorm:"index"`
Token string `gorm:"size:64;uniqueIndex"`
ExpiresAt time.Time
CreatedAt time.Time
}
// 生成验证 Token
func GenerateVerificationToken(userID uint) (string, error) {
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
return "", err
}
tokenStr := hex.EncodeToString(token)
verification := EmailVerification{
UserID: userID,
Token: tokenStr,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
if err := database.GetDB().Create(&verification).Error; err != nil {
return "", err
}
return tokenStr, nil
}
// 验证邮箱
func (h *AuthHandler) VerifyEmail(c echo.Context) error {
token := c.QueryParam("token")
var verification EmailVerification
if err := database.GetDB().Where("token = ? AND expires_at > ?",
token, time.Now()).First(&verification).Error; err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid or expired token")
}
// 更新用户状态
database.GetDB().Model(&models.User{}).
Where("id = ?", verification.UserID).
Update("email_verified", true)
// 删除验证记录
database.GetDB().Delete(&verification)
return c.JSON(http.StatusOK, map[string]string{
"message": "email verified successfully",
})
}
3. 双因素认证(2FA)
package services
import (
"github.com/pquerna/otp/totp"
)
// Generate2FASecret 生成 2FA 密钥
func Generate2FASecret(username string) (string, string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "YourApp",
AccountName: username,
})
if err != nil {
return "", "", err
}
return key.Secret(), key.URL(), nil
}
// Verify2FACode 验证 2FA 代码
func Verify2FACode(secret, code string) bool {
return totp.Validate(code, secret)
}
// 在用户模型中添加
type User struct {
// ... 其他字段 ...
TwoFactorSecret string `gorm:"size:64" json:"-"`
TwoFactorEnabled bool `gorm:"default:false" json:"two_factor_enabled"`
}
4. 社交登录集成
package handlers
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var googleOAuthConfig = &oauth2.Config{
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
RedirectURL: "http://localhost:8080/api/v1/auth/google/callback",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
// GoogleLogin 发起 Google 登录
func (h *AuthHandler) GoogleLogin(c echo.Context) error {
url := googleOAuthConfig.AuthCodeURL("state", oauth2.AccessOnline)
return c.Redirect(http.StatusTemporaryRedirect, url)
}
// GoogleCallback Google 登录回调
func (h *AuthHandler) GoogleCallback(c echo.Context) error {
code := c.QueryParam("code")
token, err := googleOAuthConfig.Exchange(context.Background(), code)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "failed to exchange token")
}
// 获取用户信息
client := googleOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get user info")
}
defer resp.Body.Close()
var userInfo struct {
Email string `json:"email"`
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to decode user info")
}
// 查找或创建用户
var user models.User
result := database.GetDB().Where("email = ?", userInfo.Email).First(&user)
if result.Error != nil {
// 创建新用户
user = models.User{
Username: userInfo.Name,
Email: userInfo.Email,
Role: "user",
IsActive: true,
}
database.GetDB().Create(&user)
}
// 生成 JWT Token
accessToken, _ := h.jwtService.GenerateAccessToken(
user.ID, user.Username, user.Email, user.Role)
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID)
// 返回 Token
return c.JSON(http.StatusOK, models.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: h.jwtService.GetAccessTokenExpiry(),
})
}
单元测试
测试 JWT 服务
package services
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestJWTService_GenerateAndValidateAccessToken(t *testing.T) {
service := NewJWTService("test-secret-key", time.Hour, time.Hour*24)
// 生成 Token
token, err := service.GenerateAccessToken(1, "testuser", "test@example.com", "user")
assert.NoError(t, err)
assert.NotEmpty(t, token)
// 验证 Token
claims, err := service.ValidateAccessToken(token)
assert.NoError(t, err)
assert.Equal(t, uint(1), claims.UserID)
assert.Equal(t, "testuser", claims.Username)
assert.Equal(t, "test@example.com", claims.Email)
assert.Equal(t, "user", claims.Role)
}
func TestJWTService_ValidateExpiredToken(t *testing.T) {
service := NewJWTService("test-secret-key", time.Millisecond, time.Hour)
token, _ := service.GenerateAccessToken(1, "testuser", "test@example.com", "user")
// 等待 Token 过期
time.Sleep(time.Millisecond * 10)
_, err := service.ValidateAccessToken(token)
assert.Error(t, err)
}
测试认证处理器
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
func TestAuthHandler_Register(t *testing.T) {
// 设置测试数据库
setupTestDB()
defer teardownTestDB()
e := echo.New()
handler := NewAuthHandler(testJWTService)
reqBody := `{
"username": "testuser",
"email": "test@example.com",
"password": "TestPass123!"
}`
req := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(reqBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handler.Register(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code)
var response map[string]interface{}
json.Unmarshal(rec.Body.Bytes(), &response)
assert.Equal(t, "user registered successfully", response["message"])
}
性能优化建议
1. 连接池配置
func InitDB(cfg *config.Config) error {
// ... 连接数据库 ...
sqlDB, err := DB.DB()
if err != nil {
return err
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
return nil
}
2. 缓存用户信息
package cache
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
"jwt-auth-system/models"
)
type UserCache struct {
client *redis.Client
}
func NewUserCache(client *redis.Client) *UserCache {
return &UserCache{client: client}
}
func (uc *UserCache) GetUser(userID uint) (*models.User, error) {
ctx := context.Background()
key := fmt.Sprintf("user:%d", userID)
data, err := uc.client.Get(ctx, key).Result()
if err == nil {
var user models.User
json.Unmarshal([]byte(data), &user)
return &user, nil
}
// 缓存未命中,从数据库查询
var user models.User
if err := database.GetDB().First(&user, userID).Error; err != nil {
return nil, err
}
// 存入缓存
userData, _ := json.Marshal(user)
uc.client.Set(ctx, key, userData, time.Hour)
return &user, nil
}
3. 并发处理优化
// 使用 sync.Pool 复用对象
var claimsPool = sync.Pool{
New: func() interface{} {
return new(Claims)
},
}
func (s *JWTService) ValidateAccessToken(tokenString string) (*Claims, error) {
claims := claimsPool.Get().(*Claims)
defer claimsPool.Put(claims)
// ... 验证逻辑 ...
}
部署建议
1. Docker 部署
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/.env .
EXPOSE 8080
CMD ["./main"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=password
- DB_NAME=jwt_auth
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=jwt_auth
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
2. Kubernetes 部署
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: jwt-auth-api
spec:
replicas: 3
selector:
matchLabels:
app: jwt-auth-api
template:
metadata:
labels:
app: jwt-auth-api
spec:
containers:
- name: api
image: your-registry/jwt-auth-api:latest
ports:
- containerPort: 8080
env:
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: jwt-secret
key: secret
- name: DB_HOST
value: mysql-service
resources:
limits:
memory: "512Mi"
cpu: "500m"
总结
通过本文,我们实现了一个功能完整、安全可靠的 JWT 认证系统,包括:
- ✅ 完整的认证流程:注册、登录、Token 刷新、登出
- ✅ 安全机制:密码加密、Token 签名验证、HTTPS 传输
- ✅ 权限管理:基于角色的访问控制(RBAC)
- ✅ 最新技术栈:golang-jwt/jwt/v5、Echo v4、GORM
- ✅ 生产就绪:错误处理、日志记录、速率限制
- ✅ 可扩展性:支持多设备、2FA、社交登录等扩展功能
- ✅ 性能优化:连接池、缓存、并发处理
- ✅ 测试覆盖:单元测试、集成测试