1. 使用 bcrypt 進行密碼雜湊處理
go get golang.org/x/crypto/bcrypt
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// ⚠️ bcrypt rules:
// - DefaultCost = 10 (OK cho 2026)
// - Max password length = 72 bytes
// - Nếu password > 72 bytes, hash SHA-256 trước rồi bcrypt
// - KHÔNG dùng MD5, SHA-256 alone cho passwords
2.JWT認證
2.1.設置
go get github.com/golang-jwt/jwt/v5
2.2.服務令牌
package auth
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type TokenService struct {
accessSecret []byte
refreshSecret []byte
accessTTL time.Duration
refreshTTL time.Duration
}
func NewTokenService(accessSecret, refreshSecret string) *TokenService {
return &TokenService{
accessSecret: []byte(accessSecret),
refreshSecret: []byte(refreshSecret),
accessTTL: 15 * time.Minute,
refreshTTL: 7 * 24 * time.Hour, // 7 days
}
}
// Custom claims
type AccessClaims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type RefreshClaims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
// Generate access token
func (s *TokenService) GenerateAccessToken(userID uint, email, role string) (string, error) {
claims := AccessClaims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.accessTTL)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "my-api",
Subject: fmt.Sprintf("%d", userID),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.accessSecret)
}
// Generate refresh token
func (s *TokenService) GenerateRefreshToken(userID uint) (string, error) {
claims := RefreshClaims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.refreshTTL)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "my-api",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.refreshSecret)
}
// Generate token pair
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` // seconds
}
func (s *TokenService) GenerateTokenPair(userID uint, email, role string) (*TokenPair, error) {
accessToken, err := s.GenerateAccessToken(userID, email, role)
if err != nil {
return nil, fmt.Errorf("generate access token: %w", err)
}
refreshToken, err := s.GenerateRefreshToken(userID)
if err != nil {
return nil, fmt.Errorf("generate refresh token: %w", err)
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int64(s.accessTTL.Seconds()),
}, nil
}
// Validate access token
func (s *TokenService) ValidateAccessToken(tokenString string) (*AccessClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &AccessClaims{}, 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 s.accessSecret, nil
})
if err != nil {
return nil, fmt.Errorf("parse token: %w", err)
}
claims, ok := token.Claims.(*AccessClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token claims")
}
return claims, nil
}
// Validate refresh token
func (s *TokenService) 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 s.refreshSecret, nil
})
if err != nil {
return nil, fmt.Errorf("parse refresh token: %w", err)
}
claims, ok := token.Claims.(*RefreshClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid refresh token")
}
return claims, nil
}
3. 認證服務
package service
type AuthService struct {
userRepo repository.UserRepository
tokenService *auth.TokenService
}
func NewAuthService(userRepo repository.UserRepository, tokenService *auth.TokenService) *AuthService {
return &AuthService{
userRepo: userRepo,
tokenService: tokenService,
}
}
type LoginInput struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type RegisterInput struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}
func (s *AuthService) Login(ctx context.Context, input LoginInput) (*auth.TokenPair, error) {
user, err := s.userRepo.GetByEmail(ctx, input.Email)
if err != nil {
return nil, fmt.Errorf("invalid credentials")
}
if !auth.CheckPassword(input.Password, user.Password) {
return nil, fmt.Errorf("invalid credentials")
}
if !user.IsActive {
return nil, fmt.Errorf("account is deactivated")
}
tokens, err := s.tokenService.GenerateTokenPair(user.ID, user.Email, user.Role)
if err != nil {
return nil, fmt.Errorf("generate tokens: %w", err)
}
return tokens, nil
}
func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*auth.TokenPair, error) {
// Check duplicate email
existing, _ := s.userRepo.GetByEmail(ctx, input.Email)
if existing != nil {
return nil, fmt.Errorf("email already registered")
}
hashedPassword, err := auth.HashPassword(input.Password)
if err != nil {
return nil, fmt.Errorf("hash password: %w", err)
}
user := &model.User{
Name: input.Name,
Email: input.Email,
Password: hashedPassword,
Role: "user",
IsActive: true,
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return s.tokenService.GenerateTokenPair(user.ID, user.Email, user.Role)
}
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*auth.TokenPair, error) {
claims, err := s.tokenService.ValidateRefreshToken(refreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
user, err := s.userRepo.GetByID(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("user not found")
}
if !user.IsActive {
return nil, fmt.Errorf("account is deactivated")
}
// Token rotation: generate new token pair
return s.tokenService.GenerateTokenPair(user.ID, user.Email, user.Role)
}
4. 認證中介軟體
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
type contextKey string
const (
UserIDKey contextKey = "user_id"
UserRoleKey contextKey = "user_role"
)
func AuthMiddleware(tokenService *auth.TokenService) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header required",
})
return
}
// Extract Bearer token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid authorization format. Use: Bearer {token}",
})
return
}
// Validate token
claims, err := tokenService.ValidateAccessToken(parts[1])
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid or expired token",
})
return
}
// Set user info in context
c.Set(string(UserIDKey), claims.UserID)
c.Set(string(UserRoleKey), claims.Role)
c.Set("user_email", claims.Email)
c.Next()
}
}
// Helper: get current user ID from context
func GetUserID(c *gin.Context) uint {
id, _ := c.Get(string(UserIDKey))
return id.(uint)
}
func GetUserRole(c *gin.Context) string {
role, _ := c.Get(string(UserRoleKey))
return role.(string)
}
// Role-based middleware
func RequireRole(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole := GetUserRole(c)
for _, role := range roles {
if userRole == role {
c.Next()
return
}
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Insufficient permissions",
})
}
}
5. 身份驗證處理程序
package handler
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
func (h *AuthHandler) Login(c *gin.Context) {
var input service.LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
return
}
tokens, err := h.authService.Login(c.Request.Context(), input)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": tokens,
})
}
func (h *AuthHandler) Register(c *gin.Context) {
var input service.RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
return
}
tokens, err := h.authService.Register(c.Request.Context(), input)
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"data": tokens,
})
}
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var input struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
return
}
tokens, err := h.authService.RefreshToken(c.Request.Context(), input.RefreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": tokens,
})
}
func (h *AuthHandler) GetProfile(c *gin.Context) {
userID := middleware.GetUserID(c)
user, err := h.authService.GetProfile(c.Request.Context(), userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": user,
})
}
// Router setup
func SetupAuthRoutes(r *gin.Engine, authHandler *AuthHandler, tokenService *auth.TokenService) {
auth := r.Group("/auth")
{
auth.POST("/login", authHandler.Login)
auth.POST("/register", authHandler.Register)
auth.POST("/refresh", authHandler.RefreshToken)
}
// Protected routes
protected := r.Group("/api")
protected.Use(middleware.AuthMiddleware(tokenService))
{
protected.GET("/profile", authHandler.GetProfile)
// Admin only
admin := protected.Group("/admin")
admin.Use(middleware.RequireRole("admin"))
{
admin.GET("/users", adminListUsers)
admin.DELETE("/users/:id", adminDeleteUser)
}
}
}
6. 安全最佳實踐
- 密碼:bcrypt(成本≥10),不單獨使用md5/sha256
- 智威湯遜:短期訪問令牌(15 分鐘),長期刷新令牌(7 天)
- 代幣輪換:刷新時發出新的令牌對
- 秘密:使用環境變量,程式碼中沒有硬編碼
- HTTPS:在生產中始終使用 HTTPS
- 速率限制:限制登入嘗試(暴力保護)
- 錯誤訊息:通用“無效憑證”,不顯示電子郵件存在
下一篇: 授權、安全和中介軟體 — RBAC、Casbin 與進階安全性。