第49课:Session 与 Cookie
🎯 学习目标
通过本节课的学习,你将能够:
- 理解 HTTP 协议无状态性的本质及 Cookie/Session 的作用
- 掌握在 Go 中使用标准库操作 Cookie 的方法
- 学会使用第三方库实现 Session 管理
- 了解 JWT 与传统 Session 认证的差异
- 掌握用户认证中的安全最佳实践
📚 核心概念
HTTP 的无状态性
HTTP 协议本身是无状态的——服务器不会记住之前与客户端的任何交互。就像你每次去图书馆,管理员都不记得你借过什么书一样。
Cookie 和 Session 的出现就是为了解决这个问题,让服务器能够"记住"用户。
Cookie:客户端的记忆卡
工作原理:
- 服务器在响应中通过
Set-Cookie头发送一个小数据包 - 浏览器收到后自动保存
- 后续每次请求都会自动在
Cookie头中带上这个数据
// Cookie 本质就是一个键值对
name := "user_id"
value := "123456"
Session:服务器的记忆库
工作原理:
- 服务器为每个用户创建唯一的 Session ID
- 将这个 ID 通过 Cookie 发送给客户端
- 服务器存储与该 ID 关联的用户数据
- 后续请求通过 Cookie 中的 Session ID 获取用户数据
类比:Cookie 是你的会员卡,Session 是商场里的会员系统。
JWT vs Session
| 特性 | Session | JWT |
|---|---|---|
| 存储位置 | 服务器 | 客户端 |
| 扩展性 | 需要共享存储 | 天然支持分布式 |
| 性能 | 需要查询存储 | 自包含,验证快 |
| 安全性 | 依赖服务器存储安全 | 需要妥善保管密钥 |
💻 代码示例
1. 使用标准库操作 Cookie
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
// 处理设置 Cookie 的请求
http.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) {
// 创建 Cookie
cookie := &http.Cookie{
Name: "user_id",
Value: "1234567890",
Path: "/",
MaxAge: 3600, // 1小时有效期
HttpOnly: true, // 禁止 JavaScript 访问
Secure: true, // 仅通过 HTTPS 传输
SameSite: http.SameSiteStrictMode,
}
// 设置 Cookie
http.SetCookie(w, cookie)
// 也可以设置多个 Cookie
themeCookie := &http.Cookie{
Name: "theme",
Value: "dark",
Path: "/",
}
http.SetCookie(w, themeCookie)
fmt.Fprintln(w, "Cookie 设置成功!")
log.Println("Cookie 已设置")
})
// 处理读取 Cookie 的请求
http.HandleFunc("/get-cookie", func(w http.ResponseWriter, r *http.Request) {
// 读取指定 Cookie
userIDCookie, err := r.Cookie("user_id")
if err != nil {
if err == http.ErrNoCookie {
http.Error(w, "Cookie 不存在", http.StatusNotFound)
return
}
http.Error(w, "读取 Cookie 失败", http.StatusInternalServerError)
return
}
// 读取所有 Cookie
allCookies := r.Cookies()
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"user_id": "%s",
"theme": "%s",
"cookie_count": %d
}`, userIDCookie.Value, r.FormValue("theme"), len(allCookies))
})
// 处理删除 Cookie 的请求
http.HandleFunc("/delete-cookie", func(w http.ResponseWriter, r *http.Request) {
// 删除 Cookie 的原理:设置过期时间为过去
cookie := &http.Cookie{
Name: "user_id",
Value: "",
Path: "/",
MaxAge: -1, // 立即过期
Expires: time.Now().Add(-1 * time.Hour),
}
http.SetCookie(w, cookie)
fmt.Fprintln(w, "Cookie 已删除")
})
log.Println("服务器启动在 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
2. 使用第三方库实现 Session 管理
package main
import (
"encoding/gob"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/sessions"
)
// 定义 Session 存储
var store = sessions.NewCookieStore([]byte("your-secret-key"))
func init() {
// 注册自定义类型以便序列化
gob.Register(time.Time{})
// 配置 Session
store.Options = &sessions.Options{
Path: "/",
MaxAge: 3600 * 8, // 8小时
HttpOnly: true,
Secure: true,
}
}
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/dashboard", authMiddleware(dashboardHandler))
http.HandleFunc("/logout", logoutHandler)
log.Println("服务器启动在 :8081")
log.Fatal(http.ListenAndServe(":8081", nil))
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
// 获取或创建 Session
session, _ := store.Get(r, "session-id")
// 设置 Session 值
session.Values["authenticated"] = true
session.Values["user_id"] = 1001
session.Values["username"] = "gopher"
session.Values["login_time"] = time.Now()
// 保存 Session
err := session.Save(r, w)
if err != nil {
http.Error(w, "Session 保存失败", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "登录成功!用户 ID: %d", session.Values["user_id"].(int))
}
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
// 从 context 获取用户信息(中间件已验证)
userID := r.Context().Value("user_id").(int)
username := r.Context().Value("username").(string)
loginTime := r.Context().Value("login_time").(time.Time)
fmt.Fprintf(w, `欢迎回来,%s!
用户 ID: %d
登录时间: %s
当前时间: %s`,
username, userID,
loginTime.Format("2006-01-02 15:04:05"),
time.Now().Format("2006-01-02 15:04:05"))
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
// 获取 Session
session, _ := store.Get(r, "session-id")
// 删除 Session
session.Options.MaxAge = -1
session.Save(r, w)
fmt.Fprintln(w, "已成功登出")
}
// 认证中间件
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-id")
// 检查是否已认证
auth, ok := session.Values["authenticated"].(bool)
if !ok || !auth {
http.Error(w, "请先登录", http.StatusUnauthorized)
return
}
// 将用户信息添加到请求上下文
ctx := r.Context()
ctx = setContextValue(ctx, "user_id", session.Values["user_id"])
ctx = setContextValue(ctx, "username", session.Values["username"])
ctx = setContextValue(ctx, "login_time", session.Values["login_time"])
// 调用下一个处理器
next.ServeHTTP(w, r.WithContext(ctx))
}
}
// 辅助函数:向上下文添加值
type contextKey string
const (
userIDKey contextKey = "user_id"
usernameKey contextKey = "username"
loginTimeKey contextKey = "login_time"
)
func setContextValue(ctx context.Context, key interface{}, value interface{}) context.Context {
return context.WithValue(ctx, key, value)
}
3. 使用 JWT 实现无状态认证
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// JWT 密钥
var jwtSecret = []byte("your-256-bit-secret")
// 声明 Claims
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
func main() {
http.HandleFunc("/jwt/login", jwtLoginHandler)
http.HandleFunc("/jwt/profile", jwtAuthMiddleware(jwtProfileHandler))
log.Println("JWT 服务器启动在 :8082")
log.Fatal(http.ListenAndServe(":8082", nil))
}
func jwtLoginHandler(w http.ResponseWriter, r *http.Request) {
// 模拟用户验证
username := r.FormValue("username")
password := r.FormValue("password")
if username != "admin" || password != "password" {
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
return
}
// 创建 JWT Claims
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: 1001,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "golang-web-app",
},
}
// 创建 Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
http.Error(w, "Token 生成失败", http.StatusInternalServerError)
return
}
// 返回 Token
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"token": tokenString,
"expires_at": expirationTime,
"user_id": claims.UserID,
"username": claims.Username,
})
}
func jwtProfileHandler(w http.ResponseWriter, r *http.Request) {
// 从上下文获取 Claims
claims := r.Context().Value("claims").(*Claims)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": claims.UserID,
"username": claims.Username,
"issued_at": claims.IssuedAt.Time,
"expires_at": claims.ExpiresAt.Time,
})
}
// JWT 认证中间件
func jwtAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 从 Authorization 头获取 Token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "缺少 Authorization 头", http.StatusBadRequest)
return
}
// 提取 Token(Bearer 格式)
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
http.Error(w, "Authorization 格式错误", http.StatusBadRequest)
return
}
// 解析和验证 Token
claims := &Claims{}
token, err := jwt.ParseWithClaims(parts[1], claims, func(token *jwt.Token) (interface{}, error) {
// 验证签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("意外的签名方法: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
http.Error(w, "Token 无效: "+err.Error(), http.StatusUnauthorized)
return
}
if !token.Valid {
http.Error(w, "Token 已过期", http.StatusUnauthorized)
return
}
// 将 Claims 添加到上下文
ctx := setContextValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
🏋️ 实践练习
练习 1:基础 Cookie 操作(简单)
创建一个简单的 Web 应用,实现以下功能:
- 访问
/color时设置一个主题颜色 Cookie - 访问
/show时读取并显示存储的颜色 - 访问
/reset时删除颜色 Cookie
预期输出示例:
# 访问 /color
Cookie 设置成功!
# 访问 /show
当前主题颜色: blue
# 访问 /reset
Cookie 已删除
练习 2:Session 用户认证(中等)
实现一个简单的登录系统:
- 创建登录页面,接收用户名和密码
- 登录成功后存储用户信息到 Session
- 创建受保护的
/profile页面,只有登录用户可以访问 - 实现登出功能
预期输出示例:
# 访问 /login (POST)
登录成功!欢迎回来,gopher
# 访问 /profile
个人资料页:用户ID: 1001, 用户名: gopher
# 访问 /logout
已成功登出
练习 3:JWT 认证系统(困难)
扩展 JWT 认证系统,实现以下功能:
- 添加注册接口
/register - 实现 Token 刷新机制
- 添加基于角色的访问控制(admin/user)
- 实现 Token 黑名单(登出功能)
预期输出示例:
// POST /register
{
"message": "注册成功",
"user_id": 1002,
"token": "eyJhbGciOiJIUzI1NiIs..."
}
// GET /admin/dashboard (需要 admin 角色)
{
"message": "管理面板",
"admin_data": ["统计信息", "用户管理"]
}
⚠️ 常见错误
1. Cookie 安全性错误
// ❌ 错误做法
cookie := &http.Cookie{
Name: "session",
Value: "abc123",
}
// ✅ 正确做法
cookie := &http.Cookie{
Name: "session",
Value: "abc123",
HttpOnly: true, // 防止 XSS
Secure: true, // 仅 HTTPS
SameSite: http.SameSiteStrictMode, // 防止 CSRF
}
2. Session 存储不当
// ❌ 错误:使用不安全的存储
store := sessions.NewFilesystemStore("", []byte("secret"))
// ✅ 正确:使用加密存储
store := sessions.NewCookieStore(secureKey)
3. JWT 密钥管理错误
// ❌ 错误:硬编码密钥
jwtSecret := []byte("my-secret-key")
// ✅ 正确:从环境变量读取
jwtSecret := []byte(os.Getenv("JWT_SECRET"))
4. Session 固定攻击
// ❌ 错误:登录后不更新 Session ID
session, _ := store.Get(r, "session-id")
// ... 设置认证状态
// ✅ 正确:登录后创建新的 Session
oldSession.Options.MaxAge = -1 // 销毁旧 Session
session, _ := store.New(r, "new-session-id")
5. 未验证 JWT 签名算法
// ❌ 错误:接受任何签名算法
token, _ := jwt.Parse(tokenString,
练习编辑器
go
Loading...