登录状态是 Web 开发绕不开的话题。很多初学者一听到 Session,就会把它想成某种神秘机制。其实从 HTTP 角度看,Cookie 是浏览器保存并自动带回服务端的一小段数据;Session 是服务端用这段数据找到用户状态的一种做法。Cookie 在客户端,Session 数据通常在服务端。
Go 的 net/http 标准库已经提供了 Cookie 读写能力。理解它不难,难的是不要把敏感信息直接塞进 Cookie,也不要忽略过期、删除、安全属性这些细节。
写入一个 Cookie
最简单的 Cookie 可以这样写:
func setName(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "visitor",
Value: "nina",
Path: "/",
HttpOnly: true,
MaxAge: 3600,
})
fmt.Fprintln(w, "cookie set")
}
Name 是键,Value 是值,Path 决定哪些路径会带上这个 Cookie。HttpOnly 表示浏览器里的 JavaScript 不能直接读取它。它不能防止所有攻击,但能降低 XSS 后 Cookie 被脚本偷走的风险。MaxAge 是秒数,3600 表示一小时。
注意 Cookie 必须在响应头写出前设置。如果你先 fmt.Fprintln(w, ...),再 http.SetCookie,头部可能已经发送,Cookie 就不会生效。
读取 Cookie
读取时使用 r.Cookie:
func hello(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("visitor")
if err != nil {
if errors.Is(err, http.ErrNoCookie) {
fmt.Fprintln(w, "hello, guest")
return
}
http.Error(w, "bad cookie", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "hello, %s\n", c.Value)
}
没有 Cookie 是正常情况,不应该打 error 日志。很多用户第一次访问、清理浏览器、换设备,都会没有 Cookie。只有格式异常或业务校验失败时,才需要额外处理。
不要把用户资料直接放进 Cookie
如果你把 user_id=123 放进 Cookie,用户可以自己改成 456。如果把 role=admin 放进去,后果更明显。Cookie 来自客户端,服务端必须把它当成不可信输入。
更常见的做法是:Cookie 里只放一个随机 Session ID,服务端用它查真正的用户状态。
type Session struct {
UserID int64
ExpiresAt time.Time
}
var sessions = struct {
sync.Mutex
m map[string]Session
}{m: make(map[string]Session)}
这个内存 map 只适合教学或单进程小工具。生产环境通常会用 Redis、数据库或专门的 Session 存储,因为进程重启后内存会丢,多实例之间也不共享。
生成安全的 Session ID
Session ID 不能用时间戳、递增数字或用户名拼接。它应该足够随机,让别人猜不到。
func newSessionID() (string, error) {
var b [32]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
这里用的是 crypto/rand,不是 math/rand。前者适合安全随机,后者适合模拟、抽样、游戏里的非安全随机。RawURLEncoding 生成的字符串不会包含 / 和 +,放在 Cookie 里更省心。
登录时创建 Session
先省略密码校验,只演示 Session 创建流程:
func login(w http.ResponseWriter, r *http.Request) {
userID := int64(1001)
id, err := newSessionID()
if err != nil {
http.Error(w, "create session", http.StatusInternalServerError)
return
}
expires := time.Now().Add(2 * time.Hour)
sessions.Lock()
sessions.m[id] = Session{UserID: userID, ExpiresAt: expires}
sessions.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "sid",
Value: id,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Expires: expires,
Secure: true,
})
fmt.Fprintln(w, "logged in")
}
Secure: true 表示只在 HTTPS 下发送。线上应该开启;本地开发如果没有 HTTPS,可以临时关掉。SameSite 能降低跨站请求携带 Cookie 的机会,普通后台和内容站常用 Lax。如果你在做跨站嵌入或第三方登录回调,需要更细地理解 SameSite 策略。
中间件读取登录状态
登录后的接口需要从 Cookie 找 Session:
func currentUserID(r *http.Request) (int64, bool) {
c, err := r.Cookie("sid")
if err != nil {
return 0, false
}
sessions.Lock()
s, ok := sessions.m[c.Value]
sessions.Unlock()
if !ok {
return 0, false
}
if time.Now().After(s.ExpiresAt) {
return 0, false
}
return s.UserID, true
}
过期检查不能只依赖 Cookie 的 Expires。浏览器可能不按你预期保存,攻击者也可以构造请求。服务端存储里的过期时间才是最终依据。
用它保护页面:
func profile(w http.ResponseWriter, r *http.Request) {
userID, ok := currentUserID(r)
if !ok {
http.Error(w, "please login", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "user id: %d\n", userID)
}
退出登录要删除 Cookie 和服务端状态
退出不是只让前端跳转登录页。服务端要删除 Session,浏览器也要删除 Cookie。
func logout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("sid"); err == nil {
sessions.Lock()
delete(sessions.m, c.Value)
sessions.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: "sid",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
fmt.Fprintln(w, "logged out")
}
删除 Cookie 时 Name 和 Path 要和写入时一致,否则浏览器可能删的是另一个路径下的 Cookie。这个细节很容易漏。
清理过期 Session
内存 Session map 如果不清理,会慢慢变大。可以启动一个后台清理任务:
func cleanupSessions(ctx context.Context) {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
now := time.Now()
sessions.Lock()
for id, s := range sessions.m {
if now.After(s.ExpiresAt) {
delete(sessions.m, id)
}
}
sessions.Unlock()
}
}
}
如果服务已经有统一的后台任务管理,就把它接进去。不要在每个请求里顺手清理一大堆过期数据,那会让某些用户请求突然变慢。
小结
Cookie 是浏览器和服务端之间的状态载体,Session 是服务端管理登录状态的一种方式。入门项目里可以用内存 map 理解流程,但要知道它不适合多实例生产环境。
安全上记住几条:不要信任 Cookie 值,不要把角色和敏感资料明文放进 Cookie;Session ID 用 crypto/rand 生成;线上开启 HttpOnly、Secure、合理的 SameSite;退出时同时删除服务端状态和浏览器 Cookie。把这些基础做好,后面接 Redis、数据库或成熟认证框架都会顺很多。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。