在Web开发中,用户认证是一个永恒的话题。今天,让我们一起深入探讨Cookie、Session和Token这三种最常见的认证机制,并通过Go语言的实际代码来理解它们的工作原理。
想象一下,你正在开发一个用户管理系统。用户登录后,如何让服务器"记住"这个用户?如何确保每次请求都能识别出是哪个用户?这就是我们今天要解决的问题。
HTTP协议是无状态的,这意味着服务器不会记住之前的请求。每一次HTTP请求都是独立的,服务器无法知道两次请求是否来自同一个用户。这就像每次去咖啡店,店员都不认识你,你需要重新自我介绍。
为了解决这个问题,我们需要一种机制来维持用户的登录状态,这就是Cookie、Session和Token的用武之地。
Cookie是存储在用户浏览器中的小型文本文件,由服务器发送给浏览器,浏览器会在后续的请求中自动携带这些Cookie。就像是服务器给你的一张"会员卡",每次访问时出示这张卡片,服务器就知道你是谁了。
1. 用户登录 → 服务器验证
2. 服务器生成Cookie → 发送给浏览器
3. 浏览器保存Cookie
4. 后续请求自动携带Cookie → 服务器识别用户
让我们通过代码来实现一个简单的Cookie认证系统:
package main
import (
"fmt"
"net/http"
"time"
)
// 模拟用户数据库
var users = map[string]string{
"alice": "password123",
"bob": "secret456",
}
// 登录处理函数
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析表单数据
username := r.FormValue("username")
password := r.FormValue("password")
// 验证用户名和密码
if storedPassword, exists := users[username]; exists && storedPassword == password {
// 创建Cookie
cookie := &http.Cookie{
Name: "user_id",
Value: username,
Path: "/",
MaxAge: 3600, // 1小时过期
HttpOnly: true, // 防止JavaScript访问,提高安全性
Secure: false, // 在生产环境中应设置为true(仅HTTPS)
SameSite: http.SameSiteStrictMode, // 防止CSRF攻击
}
// 设置Cookie
http.SetCookie(w, cookie)
fmt.Fprintf(w, "登录成功!欢迎,%s", username)
} else {
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
}
}
// 受保护的页面
func protectedHandler(w http.ResponseWriter, r *http.Request) {
// 读取Cookie
cookie, err := r.Cookie("user_id")
if err != nil {
http.Error(w, "请先登录", http.StatusUnauthorized)
return
}
// Cookie存在,用户已登录
fmt.Fprintf(w, "欢迎来到受保护页面,%s!", cookie.Value)
}
// 登出处理函数
func logoutHandler(w http.ResponseWriter, r *http.Request) {
// 创建一个立即过期的Cookie来删除原Cookie
cookie := &http.Cookie{
Name: "user_id",
Value: "",
Path: "/",
MaxAge: -1, // 立即过期
HttpOnly: true,
}
http.SetCookie(w, cookie)
fmt.Fprintln(w, "您已成功登出")
}
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/protected", protectedHandler)
http.HandleFunc("/logout", logoutHandler)
fmt.Println("服务器启动在 http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
优点:
缺点:
Session是在服务器端存储用户会话信息的机制。与Cookie不同,Session将用户数据保存在服务器上,只在Cookie中存储一个Session ID。这就像是银行的保险箱:你只拿着钥匙(Session ID),贵重物品(用户数据)都存在银行(服务器)里。
1. 用户登录 → 服务器创建Session
2. 生成唯一的Session ID
3. Session ID通过Cookie发送给浏览器
4. 服务器端存储Session数据
5. 后续请求携带Session ID → 服务器查找对应Session数据
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"sync"
"time"
)
// Session结构体
type Session struct {
Username string
Expiry time.Time
}
// 检查Session是否过期
func (s Session) isExpired() bool {
return time.Now().After(s.Expiry)
}
// Session存储(实际应用中应使用Redis等)
type SessionStore struct {
mu sync.RWMutex
sessions map[string]Session
}
// 创建新的SessionStore
func NewSessionStore() *SessionStore {
return &SessionStore{
sessions: make(map[string]Session),
}
}
// 生成随机的Session ID
func generateSessionID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// 创建Session
func (store *SessionStore) CreateSession(username string) string {
sessionID := generateSessionID()
store.mu.Lock()
store.sessions[sessionID] = Session{
Username: username,
Expiry: time.Now().Add(30 * time.Minute), // 30分钟过期
}
store.mu.Unlock()
return sessionID
}
// 获取Session
func (store *SessionStore) GetSession(sessionID string) (Session, bool) {
store.mu.RLock()
session, exists := store.sessions[sessionID]
store.mu.RUnlock()
if !exists || session.isExpired() {
return Session{}, false
}
return session, true
}
// 删除Session
func (store *SessionStore) DeleteSession(sessionID string) {
store.mu.Lock()
delete(store.sessions, sessionID)
store.mu.Unlock()
}
// 清理过期的Session(应定期运行)
func (store *SessionStore) CleanupExpiredSessions() {
store.mu.Lock()
defer store.mu.Unlock()
for sessionID, session := range store.sessions {
if session.isExpired() {
delete(store.sessions, sessionID)
}
}
}
var (
sessionStore = NewSessionStore()
users = map[string]string{
"alice": "password123",
"bob": "secret456",
}
)
// Session登录处理
func sessionLoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
// 验证用户
if storedPassword, exists := users[username]; exists && storedPassword == password {
// 创建Session
sessionID := sessionStore.CreateSession(username)
// 设置Cookie存储Session ID
cookie := &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: false, // 生产环境设为true
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
fmt.Fprintf(w, "登录成功!Session ID: %s", sessionID)
} else {
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
}
}
// Session中间件
func sessionMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "未登录", http.StatusUnauthorized)
return
}
session, valid := sessionStore.GetSession(cookie.Value)
if !valid {
http.Error(w, "Session无效或已过期", http.StatusUnauthorized)
return
}
// 将用户信息添加到请求上下文中(实际应用中使用context)
r.Header.Set("X-Username", session.Username)
next(w, r)
}
}
// 受保护的页面
func sessionProtectedHandler(w http.ResponseWriter, r *http.Request) {
username := r.Header.Get("X-Username")
fmt.Fprintf(w, "欢迎,%s!这是受保护的页面。", username)
}
// Session登出
func sessionLogoutHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err == nil {
sessionStore.DeleteSession(cookie.Value)
}
// 删除Cookie
deleteCookie := &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, deleteCookie)
fmt.Fprintln(w, "您已成功登出")
}
func main() {
// 启动定期清理过期Session的goroutine
go func() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
sessionStore.CleanupExpiredSessions()
}
}()
http.HandleFunc("/session/login", sessionLoginHandler)
http.HandleFunc("/session/protected", sessionMiddleware(sessionProtectedHandler))
http.HandleFunc("/session/logout", sessionLogoutHandler)
fmt.Println("Session服务器启动在 http://localhost:8081")
http.ListenAndServe(":8081", nil)
}
优点:
缺点:
Token是一种无状态的认证方式,所有信息都包含在Token中。最流行的是JWT(JSON Web Token)。Token就像是一张"通行证",上面写着你的身份信息,并且有防伪标记。
让我们先对比一下传统的Session Token和JWT:
Session Token:
JWT:
JWT由三部分组成,用.
分隔:
header.payload.signature
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// JWT密钥(实际应用中应从环境变量读取)
var jwtSecret = []byte("your-secret-key-here")
// JWT Header
type JWTHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
// JWT Payload
type JWTPayload struct {
Username string `json:"username"`
Exp int64 `json:"exp"` // 过期时间
Iat int64 `json:"iat"` // 签发时间
}
// base64 URL编码(移除填充字符)
func base64URLEncode(data []byte) string {
encoded := base64.URLEncoding.EncodeToString(data)
// 移除填充字符
encoded = strings.TrimRight(encoded, "=")
return encoded
}
// base64 URL解码
func base64URLDecode(data string) ([]byte, error) {
// 添加必要的填充
if m := len(data) % 4; m != 0 {
data += strings.Repeat("=", 4-m)
}
return base64.URLEncoding.DecodeString(data)
}
// 创建JWT
func createJWT(username string) (string, error) {
// Header
header := JWTHeader{
Alg: "HS256",
Typ: "JWT",
}
headerJSON, _ := json.Marshal(header)
headerEncoded := base64URLEncode(headerJSON)
// Payload
now := time.Now()
payload := JWTPayload{
Username: username,
Exp: now.Add(time.Hour).Unix(), // 1小时后过期
Iat: now.Unix(),
}
payloadJSON, _ := json.Marshal(payload)
payloadEncoded := base64URLEncode(payloadJSON)
// Signature
message := headerEncoded + "." + payloadEncoded
h := hmac.New(sha256.New, jwtSecret)
h.Write([]byte(message))
signature := base64URLEncode(h.Sum(nil))
// 组合成完整的JWT
token := message + "." + signature
return token, nil
}
// 验证JWT
func verifyJWT(tokenString string) (*JWTPayload, error) {
// 分割token
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// 验证签名
message := parts[0] + "." + parts[1]
h := hmac.New(sha256.New, jwtSecret)
h.Write([]byte(message))
expectedSignature := base64URLEncode(h.Sum(nil))
if parts[2] != expectedSignature {
return nil, fmt.Errorf("invalid signature")
}
// 解码payload
payloadData, err := base64URLDecode(parts[1])
if err != nil {
return nil, err
}
var payload JWTPayload
if err := json.Unmarshal(payloadData, &payload); err != nil {
return nil, err
}
// 检查是否过期
if time.Now().Unix() > payload.Exp {
return nil, fmt.Errorf("token expired")
}
return &payload, nil
}
// JWT登录处理
func jwtLoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
// 验证用户(使用之前定义的users map)
if storedPassword, exists := users[username]; exists && storedPassword == password {
// 创建JWT
token, err := createJWT(username)
if err != nil {
http.Error(w, "Failed to create token", http.StatusInternalServerError)
return
}
// 返回token(实际应用中可能通过JSON返回)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
"type": "Bearer",
})
} else {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
}
}
// JWT中间件
func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 从Authorization header获取token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
// 检查Bearer前缀
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
// 验证JWT
payload, err := verifyJWT(parts[1])
if err != nil {
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
return
}
// 将用户信息添加到请求上下文
r.Header.Set("X-Username", payload.Username)
next(w, r)
}
}
// 受保护的API端点
func jwtProtectedHandler(w http.ResponseWriter, r *http.Request) {
username := r.Header.Get("X-Username")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Hello from protected route",
"username": username,
"time": time.Now().Format(time.RFC3339),
})
}
// 刷新Token
func refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
// 验证现有token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
payload, err := verifyJWT(parts[1])
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// 生成新token
newToken, err := createJWT(payload.Username)
if err != nil {
http.Error(w, "Failed to create new token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": newToken,
"type": "Bearer",
})
}
func main() {
http.HandleFunc("/jwt/login", jwtLoginHandler)
http.HandleFunc("/jwt/protected", jwtMiddleware(jwtProtectedHandler))
http.HandleFunc("/jwt/refresh", refreshTokenHandler)
fmt.Println("JWT服务器启动在 http://localhost:8082")
http.ListenAndServe(":8082", nil)
}
在实际项目中,建议使用成熟的JWT库,如github.com/golang-jwt/jwt/v5
:
package main
import (
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 自定义Claims
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
var jwtKey = []byte("your-secret-key")
// 使用jwt库创建token
func createTokenWithLib(username string) (string, error) {
expirationTime := time.Now().Add(1 * time.Hour)
claims := &Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "your-app",
Subject: username,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
return tokenString, err
}
// 使用jwt库验证token
func validateTokenWithLib(tokenString string) (*Claims, error) {
claims := &Claims{}
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 jwtKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
优点:
缺点:
特性 | Cookie | Session | JWT |
---|---|---|---|
存储位置 | 客户端 | 服务器端 | 客户端 |
安全性 | 较低 | 高 | 中等 |
服务器压力 | 低 | 高 | 低 |
扩展性 | 一般 | 差 | 优秀 |
跨域支持 | 差 | 差 | 优秀 |
数据容量 | 4KB | 无限制 | 有限制 |
状态管理 | 有状态 | 有状态 | 无状态 |
使用Cookie的场景:
使用Session的场景:
使用JWT的场景:
cookie := &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
Domain: ".example.com",
Expires: time.Now().Add(24 * time.Hour),
Secure: true, // 仅HTTPS传输
HttpOnly: true, // 防止XSS攻击
SameSite: http.SameSiteStrictMode, // 防止CSRF攻击
}
// 密码加密存储
import "golang.org/x/crypto/bcrypt"
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// 防暴力破解
type LoginAttempt struct {
mu sync.Mutex
attempts map[string][]time.Time
}
func (la *LoginAttempt) isBlocked(ip string) bool {
la.mu.Lock()
defer la.mu.Unlock()
attempts := la.attempts[ip]
// 清理1小时前的尝试记录
cutoff := time.Now().Add(-1 * time.Hour)
validAttempts := []time.Time{}
for _, t := range attempts {
if t.After(cutoff) {
validAttempts = append(validAttempts, t)
}
}
la.attempts[ip] = validAttempts
// 1小时内超过5次则封锁
return len(validAttempts) >= 5
}
在这篇文章中,我们深入探讨了Web认证的三种主要方式:
选择哪种方案取决于你的具体需求:
记住,安全性永远是第一位的。无论选择哪种方案,都要:
希望这篇文章能帮助你更好地理解和实现用户认证系统。Happy coding!
扩展阅读:
- OWASP Authentication Cheat Sheet
- JWT官方网站
- Go Web编程