登录流程,相信大家都很熟悉的。传统网站采用session后端验证登录状态,大致流程如下:
综上所述,一个简单的登录流程就完成了,但是你会发现在现如今session机制已无法满足一些服务架构,特别是在分布式服务和单点登录等功能使用session机制完成登录认证过程就需要解决共享session的问题,再者如果用户量很多,服务器内存压力会很大等等。
Token 认证机制
是一种常用的身份验证方法,特别适用于现代化的 前后端分离 和 微服务架构 的应用。相比传统的 Session 认证机制,Token 认证具有一些明显的优势,特别是在 扩展性 和 跨平台 支持上,大致流程如下:
4. 客户端通过用户名和密码请求登录,后端对接收到的账号和密码进行验证
5. 如验证通过,则会签发一个Token返回给客户端进行存储
6. 后续客户端通过Authorization
头部携带Token
向服务器请求资源。
最常用的Token
类型是JWT(JSON Web Token)
,它是一种自包含的、基于JSON
格式的Token
。
JWT分为三个部分:
JWT
)和所使用的签名算法(例如HS256
)。参考路径:BackEnd/pkg/auth/jwt.go
package auth
import (
"errors"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
)
// Secret 从环境变量中加载 JWT 密钥
var jwtSecret = []byte(getJWTSecret())
// getJWTSecret 从环境变量中获取 JWT 密钥
func getJWTSecret() string {
// 加载.env文件
if err := godotenv.Load(); err != nil {
panic("无法加载 .env 文件,请确保文件存在并正确配置")
}
secret := os.Getenv("JWT_SECRET")
if secret == "" {
panic("环境变量 JWT_SECRET 未设置,请配置后再运行程序")
}
return secret
}
// GenerateToken 生成 JWT Token
func GenerateToken(username string) (string, error) {
claims := jwt.MapClaims{
"username": username,
"exp": time.Now().Add(time.Hour).Unix(), // 一小时后过期
"iat": time.Now().Unix(), // 签发时间
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// 验证签名时,通过解析 token ,验证 token 是否有效,再验证是否过期。验证Token 是用在用户登录后所有请求都需要携带 token ,然后服务端获取到 token 再进行验证过。
func ValidateToken(tokenString string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("签名方法无效,请检查 Token 的生成方式")
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("无效的 Token,请重新登录获取有效的 Token")
}
该文件用于区分不同环境的参数和记录密钥等信息
JWT_SECRET=kklive
参考路径:BackEnd/internal/user/handle.go
package user
import (
"database/sql"
"my-ecommerce-app/pkg/auth"
"net/http"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
// Response 定义统一的响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
}
// UserLoginRequest 定义登录请求结构
type UserLoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// UserQueryRequest 查询用户请求结构
type UserQueryRequest struct {
Username string `json:"username" binding:"required"`
}
// UserQueryResponse 查询用户响应结构
type UserQueryResponse struct {
ID int `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Face string `json:"face"`
Email string `json:"email"`
Created_at string `json:"created_at"`
}
// UserLoginResponse 定义登录成功的响应
type UserLoginResponse struct {
Message string `json:"message"`
Token string `json:"token,omitempty"`
Code int `json:"code"`
}
// LoginHandler 用户登录接口
// @Summary 用户登录
// @Description 用户通过用户名和密码登录
// @Tags 用户模块
// @Accept json
// @Produce json
// @Param user body UserLoginRequest true "登录请求参数"
// @Success 200 {object} UserLoginResponse
// @Failure 400 {object} Response
// @Failure 401 {object} Response
// @Router /api/login [post]
func LoginHandler(db *sql.DB) gin.HandlerFunc {
return func(ctx *gin.Context) {
var req UserLoginRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"code": 401,
"message": "参数错误",
})
return
}
var passwordHash string
err := db.QueryRow("SELECT password FROM li_admin_user WHERE username = ?", req.Username).Scan(&passwordHash)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusUnauthorized, gin.H{
"code": 405,
"message": "用户名或密码错误",
})
} else {
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "服务器错误",
})
}
return
}
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
ctx.JSON(http.StatusUnauthorized, gin.H{
"code": 405,
"message": "用户名或密码错误",
})
return
}
token, err := auth.GenerateToken(req.Username)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": 500, // Token生成错误
"message": "请重新登录",
})
return
}
ctx.JSON(http.StatusOK, UserLoginResponse{
Code: 200,
Message: "登录成功!",
Token: token,
})
}
}
使用Token进行无状态登录认证后,就可以轻松的实现多端登录和单点登录SSO,在签名算法中加入Token
机器信息,就可以有效的控制账号的多端登录登出场景。
当然,除了以上使用场景,还有其他的优点,欢迎在评论区一起交流~