功能详解 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。

相关新闻

​​​​​​功能详解 01 · 用户注册:从表单到数据库一行

​​​​​​功能详解 01 · 用户注册:从表单到数据库一行

🥰个人主页:会编程的土豆(欢迎来访) 💎作者简介:后端学习者 ❄️个人专栏:数据结构与算法,数据库,leetcode ✨那些你一个人走过的夜路,终将化作照亮未来的光 …

2026/10/27 6:01:49 阅读更多 →
目前 Claude / GPT 的订阅建议与反代避坑

目前 Claude / GPT 的订阅建议与反代避坑

半年来在 Claude 和 GPT 的 CLI 上累计消耗了约 30,000$ 用量的 tokens(Claude : GPT 2: 1,Fable 5 出来之后 GPT 用量为 0)。这期间关于模型对比体验的文章断断续续其实写了不少,但 Agent 相关技术和概念迭代太快,新…

2026/10/25 21:17:34 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-@Extend 的使用

HarmonyOS应用开发实战:猫猫大作战-@Extend 的使用

前言 Extend 装饰器用于为特定组件类型扩展样式方法,与 Styles 的通用属性集合不同,Extend 可以访问组件特有的属性。 本文以「猫猫大作战」的 Text 标题样式为锚点,讲解 Extend 的使用。 提示:本系列不讲 ArkTS 基础语法与环境…

2026/10/23 19:35:21 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/27 17:33:32 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/27 4:06:49 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/27 7:21:53 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/27 18:20:50 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/27 6:20:47 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/27 4:06:50 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 13:55:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 7:21:50 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 19:19:37 阅读更多 →