功能详解 02 · 登录、退出与 Session:Cookie ttms_sid 如何认人

📅 2026/7/28 1:27:12
功能详解 02 · 登录、退出与 Session:Cookie ttms_sid 如何认人
个人主页会编程的土豆欢迎来访作者简介后端学习者❄️个人专栏数据结构与算法数据库leetcode✨那些你一个人走过的夜路终将化作照亮未来的光系列《影院票务 GO》功能详解 · 第 02 篇对应代码handlers/auth.go·store/session.go·handlers/app.go·static/js/app.js前置第 01 篇注册知识点篇blogs/12-Cookie与Session登录态.md一、背景HTTP 无状态网站怎么「记住你」HTTP 协议本身是无状态的服务器处理完一次请求就「忘了」你是谁但购票系统需要连续认人——从登录到选座、下单不能每点一次按钮都重新输入密码。常见方案Session会话 Cookie浏览器存 Session ID。本项目约定项目值Cookie 名ttms_sidCookie 值32 字节随机数的 hex 字符串Session ID服务端存储内存map[string]*sessionData有效期24 小时NewSessionStore(24 * time.Hour)HttpOnlytrueJS 读不到防 XSS 偷 Cookie重要区分认证Authentication登录时验证用户名密码 ——APILogin。识别Identification之后每次请求用 Cookie 找 Session ——Sessions.Get。鉴权Authorization识别后检查能不能做某操作 —— 第 03 篇的requireAdmin。本篇聚焦登录 / 退出 / Session 读写。二、用户视角登录打开/login输入user/user123或admin/admin123。点登录 → 页面跳转管理员 →/admin顾客 →/顶栏显示昵称、「订单」「退出」管理员还多一个「后台」。退出点「退出」→ 调/api/logout→ 回首页顶栏变回「登录 / 注册」。刷新页面为什么仍登录因为浏览器自动带上 Cookiettms_sid服务端用 ID 查内存 map取出*models.User。三、完整流程图3.1 登录成功[浏览器 POST /api/login {username, password}] ▼ [APILogin] GetUserByUsername ▼ [CheckPassword] 比对 MD5(输入库中salt) 与 库中password ▼ [Sessions.Create(w, user)] ├─ newID() → sessionId ├─ map[sessionId] {User, ExpiresAt} └─ Set-Cookie: ttms_sidsessionId; HttpOnly; MaxAge86400 ▼ [utils.OK] 返回 {id, username, nickname, role} ▼ [app.js doLogin] role1 ? /admin : /3.2 后续任意请求[浏览器 GET /orders Cookie: ttms_sidabc...] ▼ [handler] a.currentUser(r) ▼ [Sessions.Get(r)] 读 Cookie → 查 map → 未过期则 *User ▼ [Render / 业务逻辑] 模板里 {{.User.Nickname}}3.3 退出[POST /api/logout] ▼ [Sessions.Destroy] ├─ delete(map, sessionId) └─ Set-Cookie: ttms_sid; MaxAge-1 让浏览器删 Cookie ▼ [utils.OK]四、路由与 Handlerhandlers/routes.gomux.HandleFunc(/login, a.PageLogin) mux.HandleFunc(/api/login, a.APILogin) mux.HandleFunc(/api/logout, a.APILogout) mux.HandleFunc(/api/me, a.APIMe)4.1 APILoginhandlers/auth.gofunc (a *App) APILogin(w http.ResponseWriter, r *http.Request) { if r.Method ! http.MethodPost { utils.Fail(w, 方法不允许) return } var req struct { Username string json:username Password string json:password } if err : utils.DecodeJSON(r, req); err ! nil { utils.Fail(w, 参数错误) return } u, err : dao.GetUserByUsername(strings.TrimSpace(req.Username)) if err ! nil { utils.Fail(w, err.Error()) return } if u nil || !utils.CheckPassword(req.Password, u.Salt, u.Password) { utils.Fail(w, 用户名或密码错误) return } a.Sessions.Create(w, u) utils.OK(w, map[string]interface{}{ id: u.ID, username: u.Username, nickname: u.Nickname, role: u.Role, }) }安全细节用户不存在和密码错误同一句话「用户名或密码错误」—— 避免攻击者枚举哪些用户名已注册。校验通过后才CreateSession失败不写 Cookie。4.2 APILogoutfunc (a *App) APILogout(w http.ResponseWriter, r *http.Request) { a.Sessions.Destroy(w, r) utils.OK(w, nil) }4.3 APIMe —— 当前登录用户是谁func (a *App) APIMe(w http.ResponseWriter, r *http.Request) { u : a.currentUser(r) if u nil { utils.Fail(w, 未登录) return } utils.OK(w, u) }可用于前端 SPA 拉用户信息本项目主要在服务端模板里注入User。五、SessionStore 源码精读文件store/session.go。5.1 数据结构与常量const cookieName ttms_sid type sessionData struct { User *models.User ExpiresAt time.Time } type SessionStore struct { mu sync.RWMutex data map[string]*sessionData ttl time.Duration secure bool }dataSession ID → 用户 过期时间。mu读写锁多个 HTTP 请求并发访问 map 必须加锁否则 race condition。ttlSession 寿命App 初始化时传24 * time.Hour。handlers/app.gofunc NewApp(cfg config.Config) *App { return App{ Cfg: cfg, Sessions: store.NewSessionStore(24 * time.Hour), } }5.2 Create —— 登录写 Sessionfunc (s *SessionStore) Create(w http.ResponseWriter, user *models.User) { id : newID() s.mu.Lock() s.data[id] sessionData{User: user, ExpiresAt: time.Now().Add(s.ttl)} s.mu.Unlock() http.SetCookie(w, http.Cookie{ Name: cookieName, Value: id, Path: /, HttpOnly: true, MaxAge: int(s.ttl.Seconds()), }) }newID()func newID() string { b : make([]byte, 16) _, _ rand.Read(b) return hex.EncodeToString(b) // 32 个 hex 字符 }Cookie 字段解释字段作用Namettms_sid项目自定义避免与别的站冲突Path/全站有效HttpOnlydocument.cookie读不到MaxAge秒数浏览器到期自动删响应头类似Set-Cookie: ttms_sida1b2c3...; Path/; MaxAge86400; HttpOnly注意Session 里存的是*models.User指针含密码哈希字段——只在服务端内存不发给浏览器。5.3 Get —— 每次请求认人func (s *SessionStore) Get(r *http.Request) *models.User { c, err : r.Cookie(cookieName) if err ! nil || c.Value { return nil } s.mu.RLock() defer s.mu.RUnlock() item, ok : s.data[c.Value] if !ok || time.Now().After(item.ExpiresAt) { return nil } return item.User }三种「未登录」情况统一返回nil没有 CookieSession ID 不在 map服务端重启、被 Destroy、GC 清掉已过期。handlers/app.go封装func (a *App) currentUser(r *http.Request) *models.User { return a.Sessions.Get(r) }模板templates/header.html{{if .User}} span classuser{{.User.Nickname}}/span button onclicklogout()退出/button {{else}} a href/login登录/a {{end}}5.4 Destroy —— 退出func (s *SessionStore) Destroy(w http.ResponseWriter, r *http.Request) { c, err : r.Cookie(cookieName) if err nil { s.mu.Lock() delete(s.data, c.Value) s.mu.Unlock() } http.SetCookie(w, http.Cookie{Name: cookieName, Value: , Path: /, MaxAge: -1}) }服务端删 map 浏览器删 Cookie双保险。5.5 过期清理 gcLoopfunc (s *SessionStore) gcLoop() { t : time.NewTicker(10 * time.Minute) for range t.C { now : time.Now() s.mu.Lock() for k, v : range s.data { if now.After(v.ExpiresAt) { delete(s.data, k) } } s.mu.Unlock() } }NewSessionStore里go s.gcLoop()启动后台 goroutine防止过期 Session 占内存。即使用户不退出24 小时后Get也会判过期。5.6 RefreshUser扩展func (s *SessionStore) RefreshUser(r *http.Request, user *models.User) { // 更新 Session 内 User 指针并续期 }改昵称等场景可刷新 Session当前项目注册后未单独调用了解即可。六、前端credentials 与跳转逻辑static/js/app.jsasync function api(url, options {}) { const res await fetch(url, { headers: { Content-Type: application/json, ...(options.headers || {}) }, credentials: same-origin, // 关键同源请求携带 Cookie ...options, }); ... } async function doLogin(e) { e.preventDefault(); const fd new FormData(e.target); try { const user await api(/api/login, { method: POST, body: JSON.stringify({ username: fd.get(username), password: fd.get(password) }), }); location.href user.role 1 ? /admin : /; } catch (err) { alert(err.message); } return false; } async function logout() { await api(/api/logout, { method: POST, body: {} }); location.href /; }credentials: same-origin含义默许fetch不带 Cookie设为same-origin后对/api/login的响应Set-Cookie会被保存后续/api/*和页面请求都会带上ttms_sid。登录后按role分流role 1→ 管理后台role 0→ 顾客首页。与models.RoleAdmin、RoleCustomer常量一致。七、Session 存什么为什么不存密码Session 存完整*models.User包括Password、Salt字段——只在服务器内存。每次Get直接返回该指针handler 可立刻用u.ID、u.IsAdmin()不必再查库。权衡优点缺点快无 DB 压力改用户 role/昵称后 Session 内是旧数据直到重新登录实现简单服务重启 Session 全丢用户要重新登录课设够用多机部署不能共享内存 map生产环境常用Redis Session或JWT本项目注释写明「内存 Session适合课程项目」。八、与页面/API 的配合场景是否必须登录怎么判断首页/否currentUser可为 nil仍显示片单选座/seats/是requireLogin→ 302/login下单 API是currentUser nil→ JSON Fail顶栏昵称可选有 User 就显示登录态贯穿所有带Cookie的请求未登录访问公开页不会报错只是User为空。九、调试技巧Chrome DevTools → Application → Cookies看是否有ttms_sid。Network → 任意请求 → Request HeadersCookie: ttms_sid...。重启 Go 程序内存 Session 清空Cookie 仍在但Get返回 nil —— 表现为「Cookie 在但显示未登录」。curl 模拟curl -c jar.txt -X POST http://localhost:8080/api/login -H Content-Type: application/json -d {\username\:\user\,\password\:\user123\} curl -b jar.txt http://localhost:8080/api/me十、小结登录验证密码后SessionStore.Create生成随机 ID内存 map Set-Cookiettms_sid。之后Get(r)读 Cookie 查 map得到*User过期或无效返回nil。退出Destroy删 map 并MaxAge-1清 Cookie。前端fetch必须credentials: same-origin才能收发 Cookie。内存 Session 24h TTL 10 分钟 GC重启丢失——课设清晰上线需换方案。一句话「登录成功后服务器发一张『(sessionId) 号手环』给浏览器 Cookie以后每次请求亮手环内存里查对应用户。」下一篇03-顾客与管理员权限.md——role字段如何驱动页面与 API 的requireLogin/requireAdmin/apiUser。