Go 入门:Cookie、Session 和一个简单登录状态

用 Go 标准库理解 Cookie 的读写、安全属性、Session ID 的生成与校验,以及入门项目里如何保存登录状态。

登录状态是 Web 开发绕不开的话题。很多初学者一听到 Session,就会把它想成某种神秘机制。其实从 HTTP 角度看,Cookie 是浏览器保存并自动带回服务端的一小段数据;Session 是服务端用这段数据找到用户状态的一种做法。Cookie 在客户端,Session 数据通常在服务端。

Go 的 net/http 标准库已经提供了 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 就不会生效。

读取时使用 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。只有格式异常或业务校验失败时,才需要额外处理。

如果你把 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)
}

退出不是只让前端跳转登录页。服务端要删除 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 时 NamePath 要和写入时一致,否则浏览器可能删的是另一个路径下的 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 生成;线上开启 HttpOnlySecure、合理的 SameSite;退出时同时删除服务端状态和浏览器 Cookie。把这些基础做好,后面接 Redis、数据库或成熟认证框架都会顺很多。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页