在Golang的Web开发中,Cookie与Session是维持用户状态的基础机制。Cookie由服务端通过响应头下发给浏览器,后续请求自动携带;Session则通常将用户数据保存在服务端,客户端仅持有一个标识ID。理解二者的分工,是构建安全登录系统的第一步。

一、Golang中Cookie的基础操作
标准库net/http中的http.Cookie结构体描述了Cookie的各个属性。我们可以在处理器中通过http.SetCookie写入,通过r.Cookie读取。合理设置HttpOnly和Secure能有效降低XSS和中间人窃听风险。
下面的示例展示如何设置一个七天过期、仅HTTP访问的Cookie:
package main
import (
"net/http"
"time"
)
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: "token",
Value: "abc123",
Path: "/",
HttpOnly: true,
Secure: true,
Expires: time.Now().Add(7 * 24 * time.Hour),
}
http.SetCookie(w, cookie)
w.Write([]byte("cookie set"))
}
func readCookieHandler(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("token")
if err != nil {
w.Write([]byte("no cookie"))
return
}
w.Write([]byte("value: " + c.Value))
}
func main() {
http.HandleFunc("/set", setCookieHandler)
http.HandleFunc("/read", readCookieHandler)
http.ListenAndServe(":8080", nil)
}
上述代码里,HttpOnly: true禁止了前端脚本读取该Cookie,Secure: true要求仅通过HTTPS传输。若遗漏这两项,攻击者在XSS场景下可能窃取令牌。生产环境中还应结合SameSite属性防御CSRF。
读取Cookie时需要注意错误处理,因为用户可能首次访问或手动清除了Cookie。对于敏感信息,绝不应直接明文存入Cookie,而应使用签名或加密,或者仅保存Session ID。
二、基于服务端内存的Session管理
Session的核心思路是:服务端用唯一ID映射用户数据,ID通过Cookie传给浏览器。最简易的方案是使用全局map配合互斥锁,在进程内保存Session。这种方式适合单机调试,但重启会丢失数据,且无法多实例共享。
下面实现一个基础的内存Session管理器:
package main
import (
"crypto/rand"
"encoding/hex"
"net/http"
"sync"
"time"
)
type Session struct {
Data map[string]interface{}
Expires time.Time
}
var (
sessionMap = make(map[string]Session)
mu sync.Mutex
)
func newSessionID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func createSession(w http.ResponseWriter) string {
id := newSessionID()
mu.Lock()
sessionMap[id] = Session{Data: make(map[string]interface{}), Expires: time.Now().Add(time.Hour)}
mu.Unlock()
cookie := &http.Cookie{Name: "sid", Value: id, HttpOnly: true, Path: "/"}
http.SetCookie(w, cookie)
return id
}
func getSession(r *http.Request) (Session, bool) {
c, err := r.Cookie("sid")
if err != nil {
return Session{}, false
}
mu.Lock()
s, ok := sessionMap[c.Value]
mu.Unlock()
if ok && s.Expires.After(time.Now()) {
return s, true
}
return Session{}, false
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
id := createSession(w)
mu.Lock()
s := sessionMap[id]
s.Data["user"] = "alice"
sessionMap[id] = s
mu.Unlock()
w.Write([]byte("logged in"))
}
func profileHandler(w http.ResponseWriter, r *http.Request) {
s, ok := getSession(r)
if !ok {
w.Write([]byte("please login"))
return
}
w.Write([]byte("hello " + s.Data["user"].(string)))
}
该实现中,createSession生成随机ID并写入Cookie,getSession校验存在性与过期时间。由于使用了sync.Mutex,并发读写不会出现竞态。但这种方案的缺点是Session存储在单一进程内存,水平扩容时请求可能被分配到无该Session的节点。
另外,内存Session不会自动清理过期数据,长期运行会造成泄漏。实际项目可启动一个后台 goroutine 定期遍历sessionMap删除过期项,或使用现成的gorilla/sessions配合存储引擎。
三、使用Redis实现分布式Session
当Web服务部署多个实例时,必须让Session可被所有节点访问。Redis凭借高性能和过期机制成为常用选择。我们将Session数据以JSON格式存入Redis,Key为Session ID,并设置相同的过期时间。
借助github.com/go-redis/redis/v8的示例代码如下:
package main
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
var rdb = redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
func redisSetSession(id string, data map[string]interface{}) error {
b, _ := json.Marshal(data)
return rdb.Set(ctx, "sess:"+id, b, time.Hour).Err()
}
func redisGetSession(id string) (map[string]interface{}, bool) {
val, err := rdb.Get(ctx, "sess:"+id).Result()
if err != nil {
return nil, false
}
var data map[string]interface{}
json.Unmarshal([]byte(val), &data)
return data, true
}
func redisLogin(w http.ResponseWriter, r *http.Request) {
id := newSessionID()
cookie := &http.Cookie{Name: "sid", Value: id, HttpOnly: true, Path: "/", Secure: true}
http.SetCookie(w, cookie)
redisSetSession(id, map[string]interface{}{"user": "bob"})
w.Write([]byte("ok"))
}
func redisProfile(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("sid")
if err != nil {
w.Write([]byte("no session"))
return
}
data, ok := redisGetSession(c.Value)
if !ok {
w.Write([]byte("expired"))
return
}
w.Write([]byte("hi " + data["user"].(string)))
}
Redis方案的优势在于多个Golang实例可共享同一份Session,并且Redis的EXPIRE能自动清理陈旧数据。网络调用虽比内存慢,但在多数业务中是可接受的开销。需要注意的是,Redis本身应开启持久化或主从复制,防止单点故障导致全员掉线。
如果担心Session被暴力遍历,ID生成必须足够随机;同时建议对Redis设置访问密码,并在内网隔离。对于极高并发场景,还可引入Session粘性负载均衡作为补充。
四、登录态校验中间件
在路由层面统一拦截未登录请求,比在每个处理器里重复判断更清晰。Golang的http.HandlerFunc可以包装成中间件,在调用下一层前检查Session。
一个简单的中间件写法如下:
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("sid")
if err != nil {
http.Redirect(w, r, "/login", 302)
return
}
if _, ok := redisGetSession(c.Value); !ok {
http.Redirect(w, r, "/login", 302)
return
}
next(w, r)
}
}
func dashboard(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("secret dashboard"))
}
func main() {
http.HandleFunc("/login", redisLogin)
http.HandleFunc("/dashboard", authMiddleware(dashboard))
http.ListenAndServe(":8080", nil)
}
中间件将鉴权逻辑从业务代码中剥离,使dashboard只需关注数据展示。若未来更换Session存储方式,只需修改redisGetSession等底层函数,上层路由不受影响。
在复杂系统中,还可将用户角色、权限也写入Session,由中间件做细粒度访问控制。配合HTTPS和合理的Cookie属性,整套机制足以支撑中小规模Web应用的身份管理需求。