本文主要记录曾处理登录场景中的经常用到的三种的认证机制。
基于 Cookie 和Session 认证
在单体架构中,基本的认证过程都是简单的账号密码登录模式。以下主要基于Cookie进行身份验证的场景。

基本流程:
- 用户在输入账号密码确认登录,
- 由服务端对账号密码和数据库的存储的数据进行核验,确认存在且密码一致则为登录成功,
- 登录成功后返回用户信息。同时会将生成sessionid,同时将返回的用户信息存储在cookie中。
- 后续用户的所有的请求都会携带Cookie验证身份
像这种简单的单体架构中,用户量都比较少(<200),QPS也是比较低(<50)。适用在小系统或者内部系统的使用中。
功能实现:获取到账号和密码,直接和数据库匹配,如果匹配成功则登录成功。
go
//登录验证
func LoginHandler(c *gin.Context) {
var cred struct{ Username, Password string }
if err := c.BindJSON(&cred); err != nil { /* 错误处理 */ }
var storedHash string
err := db.QueryRow("SELECT password_hash FROM users WHERE username=?", cred.Username).
Scan(&storedHash)
if err == sql.ErrNoRows || bcrypt.CompareHashAndPassword(
[]byte(storedHash), []byte(cred.Password)) != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "登录成功"})
}
登录成功后,需要保证用户登录状态。常见的解决方案是 Cookie 机制,Session 会话管理以及Token机制。
- Cookie 机制:登录成功后将登录凭证加密并写入浏览器的Cookie 。后续客户端请求时携带Cookie,服务端解析Cookie 内容再去验证内容有效性。
- Session 会话管理:登录成功后服务器生成唯一的sessionId,存储在服务端并返回给客户端。客户端将其存储在 Cookie 或其他存储机制中,后续请求携带该标识符,服务器验证其有效性来判断登录状态。
- Token 机制:用户登录成功后生成Token(常见JWT),并返回给客户端。客户端在用户的后续请求中都需要携带 Token ,以便服务端验证 Token 的有效性。
存在问题:
- Cookie 机制不安全,如果被拦截破解就基本能获取到相关系统隐私信息
- 服务端再次验证客户端请求时所携带的信息,可能还需要解析信息,然后根据信息查数据库进行校验有效性确定登录状态,如果在高并发的场景中可能导致占用大量内存
- 在分布式或集群的环境下,Session机制存在共享的问题
基于Redis 和 Session 的认证
功能实现:
- 用户登录成功后,生成SessionId 并存储在 Redis 中。其中redis中存储登录用户信息
- 用户的后续请求,服务端会先获取SessionId 标识然后从 redis 中获取到相关信息
go
// 登录成功后
func AddSession(c *gin.Context, userID int) {
sessionID := uuid.New().String()
// 存储Session到Redis (24小时过期)
if err := redisClient.SetEX(ctx, "session:"+sessionID, userID,
24*time.Hour).Err(); err != nil {
// 错误处理
}
// 设置HTTP-only Cookie
c.SetCookie("SESSION_ID", sessionID, 86400, "/", "yourdomain.com", false, true)
}
// 认证中间件
func SessionAuth() gin.HandlerFunc {
return func(c *gin.Context) {
sessionID, err := c.Cookie("SESSION_ID")
if err != nil { /* 重定向到登录 */ }
userID, err := redisClient.Get(ctx, "session:"+sessionID).Int()
if err == redis.Nil { /* Session过期 */ }
c.Set("userID", userID)
c.Next()
}
}
基于 Token 的认证
随着微服务架构的广泛应用,相比较替代使用session, 具有相对的优势:
- 扩展性强, jwt 中的payload 包含用户身份和生命,且无需存储会话数据
- 通过 http header 传输不依赖Cookie ,避免CSRF攻击
- session 需要Cookie支持,所以对于原生App 或跨域场景处理有点复杂
- 性能优化:无需存储token ,减少数据库查询,提升性能
功能实现:
- 用户登录后,需要同时生成JWT
- 用户的其他请求都需要校验 Token 的有效性
go
// 生成安全JWT(EdDSA算法)
func GenerateJWT(userID int) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(2 * time.Hour).Unix(),
"iss": "auth_service",
}
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
privateKey := loadEd25519PrivateKey() // 从安全存储加载
return token.SignedString(privateKey)
}
// JWT验证中间件
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
publicKey := loadEd25519PublicKey()
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodEd25519); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return publicKey, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
c.Set("userID", claims["user_id"])
c.Next()
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
}
}
}
使用 JWT 生成Token ,还需要其他的工作需要处理和考虑:
- Token 刷新机制,使用双Token 设计方案,accessToken 用于业务请求,refreshToken 用于刷新AccessToken。需要注意,刷新accessToken时refeshToken 也要重新生成,refreshToken 绑定用户信息(设备、IP、用户ID ),避免跨设备连用
- 用户退出场景时删除 Token ,
- 检测用户操作异常时,强制重新登录,比如频繁登录的情况
- 当用户角色变化时,需强制token刷新