[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-1018":3,"consumer-news-interaction-1018":38,"consumer-news-related-1018":41},{"detail":4,"item":34},{"card":5,"schemaVersion":21,"fields":22,"content":28},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":14,"href":15,"sourceName":12,"meta":16,"metrics":18,"tags":19,"resolved":20},"NEWS_ARTICLE:1018","news","NEWS_ARTICLE",1018,"资讯","Go - Gin中使用sessions","博客园","前言 HTTP 是无状态的，sessions 让服务能在多个请求之间保存同一用户的数据；服务端通过 cookie 等机制识别用户，取回其先前存储的数据。 gin-contrib\u002Fsessions 中间件提供支持多种存储后端的 session 管理能力。 go get github.com\u002Fgin-c","","\u002Fnews\u002F1018",[17],"2026",{},[],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":23,"summary":13,"description":13,"publishTime":24,"updateTime":25,"sourceUrl":26,"language":27},"花酒锄作田","2026-09-06T21:50","2026-09-11T15:22:03","https:\u002F\u002Fwww.cnblogs.com\u002FXY-Heruo\u002Fp\u002F22864951","中文",{"format":29,"policy":30,"normalized":20,"html":31,"text":32,"wordCount":33,"hasBody":20},"HTML","NEWS_CONTENT_V1","\u003Ch2>前言\u003C\u002Fh2>\n\u003Cp>HTTP 是无状态的，sessions 让服务能在多个请求之间保存同一用户的数据；服务端通过 cookie 等机制识别用户，取回其先前存储的数据。\u003C\u002Fp>\n\u003Cp>\u003Ccode>gin-contrib\u002Fsessions\u003C\u002Fcode> 中间件提供支持多种存储后端的 session 管理能力。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>go get github.com\u002Fgin-contrib\u002Fsessions\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>参考：\u003Ca href=\"https:\u002F\u002Fgin-gonic.com\u002Fen\u002Fdocs\u002Fmiddleware\u002Fsession-management\u002F\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">https:\u002F\u002Fgin-gonic.com\u002Fen\u002Fdocs\u002Fmiddleware\u002Fsession-management\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>概念\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>cookie 型 session：session 数据序列化并签名加密后，整个塞到浏览器 cookie。服务端无状态、天然支持水平扩容；缺点是数据可见体积受限（~4KB）、\u003Cstrong>服务端无法主动吊销\u003C\u002Fstrong>（只能靠删 cookie 或引入服务端黑名单）、每次请求都传输整份数据。\u003C\u002Fli>\n \u003Cli>服务端存储型：cookie 里只放一个随机 session ID，数据存在服务端。这类后端能真正“服务端登出”（删掉\u002F过期掉 session 记录）。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>cookie 型示例\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>package main\n\nimport (\n\t\"net\u002Fhttp\"\n\t\"time\"\n\n\t\"github.com\u002Fgin-contrib\u002Fsessions\"\n\t\"github.com\u002Fgin-contrib\u002Fsessions\u002Fcookie\"\n\t\"github.com\u002Fgin-gonic\u002Fgin\"\n)\n\nvar (\n\tmockUsers = map[string]string{\n\t\t\"zhangsan\": \"123456\",\n\t\t\"lisi\":     \"654321\",\n\t}\n\t\u002F\u002F secretKey = \"your-secret-key\"\n\thmacSecretKey  = \"5KiinhGcvoXFmgOttvgMWd9ugadE0KVP\"\n\tblockSecretKey = \"OLZmfTWHGyjCfJoH1Y8SjbSY\"\n)\n\nconst sessionTTL = 60 * 5\n\nfunc sessionOptions(maxAge int) sessions.Options {\n\treturn sessions.Options{\n\t\tPath:     \"\u002F\",\n\t\tDomain:   \"\",                      \u002F\u002F 设置为 \"\" 表示当前域名\n\t\tMaxAge:   maxAge,                  \u002F\u002F 单位为秒。0 表示不写Max-Age(浏览器会话cookie); -1 表示删除 cookie\n\t\tSecure:   false,                   \u002F\u002F 生产走 HTTPS 时改 true\n\t\tHttpOnly: true,                    \u002F\u002F true 表示禁止 JS 读取 cookie\n\t\tSameSite: http.SameSiteStrictMode, \u002F\u002F 严格模式，仅在同源请求中发送 cookie\n\t}\n}\n\nfunc main() {\n\tr := gin.Default()\n\n\t\u002F\u002F Create cookie-based session store with a secret key\n\t\u002F\u002F 传单个密钥时, 只做签名认证，不加密cookie内容. 认证密钥建议 32 或 64 字节\n\t\u002F\u002F 传一对密钥时，第一个用于认证（HMAC），第二个用于加密（AES-128, AES-192, AES-256）。加密密钥必须是16、24或32字节长度，对应 AES-128\u002F192\u002F256。\n\t\u002F\u002F 多对密钥用于密钥轮换，第一对用于当前加密和认证，后续的用于解密和认证旧的cookie。Example: cookie.NewStore([]byte(\"newAuth\"), []byte(\"newEnc\"), []byte(\"oldAuth\"), []byte(\"oldEnc\"))\n\tstore := cookie.NewStore([]byte(hmacSecretKey), []byte(blockSecretKey))\n\tstore.Options(sessionOptions(sessionTTL)) \u002F\u002F 全局设置 cookie 过期时间为 5 分钟\n\tr.Use(sessions.Sessions(\"mysession\", store))\n\n\tr.POST(\"\u002Flogin\", login)\n\n\tauth := r.Group(\"\u002F\", authRequired())\n\t{\n\t\tauth.GET(\"\u002Fprofile\", profile)\n\t\tauth.POST(\"\u002Flogout\", logout)\n\t}\n\n\tr.Run(\":8080\")\n}\n\nfunc login(c *gin.Context) {\n\tsession := sessions.Default(c)\n\tusername := c.PostForm(\"username\")\n\tpassword := c.PostForm(\"password\")\n\n\tif username == \"\" || password == \"\" {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"username or password is empty\"})\n\t\treturn\n\t}\n\n\tif mockUsers[username] != password {\n\t\tc.JSON(http.StatusUnauthorized, gin.H{\"error\": \"invalid username or password\"})\n\t\treturn\n\t}\n\n\tsession.Set(\"user\", username)\n\tsession.Set(\"expire_at\", time.Now().Add(time.Second*sessionTTL).Unix()) \u002F\u002F 设置过期时间\n\tif err := session.Save(); err != nil {\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": \"save session failed\"})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\"message\": \"logged in\"})\n}\n\nfunc logout(c *gin.Context) {\n\tsession := sessions.Default(c)\n\tsession.Clear()\n\tsession.Options(sessionOptions(-1)) \u002F\u002F 让浏览器删除 cookie\n\tif err := session.Save(); err != nil {\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": \"save session failed\"})\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, gin.H{\"message\": \"logged out\"})\n}\n\nfunc profile(c *gin.Context) {\n\tsession := sessions.Default(c)\n\tuser := session.Get(\"user\").(string)\n\tc.JSON(http.StatusOK, gin.H{\"user\": user})\n}\n\nfunc authRequired() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tsession := sessions.Default(c)\n\t\tif session.Get(\"user\") == nil {\n\t\t\tc.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"})\n\t\t\treturn\n\t\t}\n\n\t\tif expireAt, ok := session.Get(\"expire_at\").(int64); !ok || time.Now().Unix() &gt; expireAt {\n\t\t\tsession.Clear()\n\t\t\tsession.Options(sessionOptions(-1))\n\t\t\tsession.Save()\n\t\t\tc.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\"error\": \"expired session\"})\n\t\t\treturn\n\t\t}\n\t\tc.Next()\n\t}\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n \u003Cli>\u003Ccode>session.Delete(key)\u003C\u002Fcode> 只删除一个 key 对应的条目\u003C\u002Fli>\n \u003Cli>\u003Ccode>session.Clear()\u003C\u002Fcode> 清空所有条目。适用于登出时整体作废。\u003C\u002Fli>\n \u003Cli>\u003Ccode>sessions.Options\u003C\u002Fcode> 中的 \u003Ccode>SameSite\u003C\u002Fcode> 用来控制跨域 cookie 行为，有 \u003Ccode>Lax\u003C\u002Fcode>, \u003Ccode>Strict\u003C\u002Fcode> 和 \u003Ccode>None\u003C\u002Fcode> 选项。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>服务端内存型\u003C\u002Fh2>\n\u003Cp>服务端内存型就是把 session 存在服务的内存中，这种一般只适合本地\u003Cstrong>单实例\u003C\u002Fstrong>开发测试。服务一旦重启，所有 session 即失效，需要客户端重新登录。而且多个实例之间内存数据不共享，如果没有做粘性会话，可能会让客户端反复登录。\u003C\u002Fp>\n\u003Cp>如果使用 \u003Ccode>gin-contrib\u002Fsessions\u003C\u002Fcode>，只需要在上面 cookie 型的基础上，将 \u003Ccode>store\u003C\u002Fcode> 替换掉即可。以下为需要的改动，其它的都不用改。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import (\n    \u002F\u002F 引入 memstore\n    \"github.com\u002Fgin-contrib\u002Fsessions\u002Fmemstore\"\n)\n\nfunc main() {\n    \u002F\u002F 替换store类型\n    store := memstore.NewStore([]byte(hmacSecretKey), []byte(blockSecretKey))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>替换成服务端内存型后，登出的代码虽然没改，但是行为有所变化。在 cookie 型中，登出只是让浏览器把 cookie 删了；换成服务端内存型后，\u003Ccode>session.Save()\u003C\u002Fcode> 会额外把内存里的会话记录删了，让登出变得真正吊销了。\u003C\u002Fp>\n\u003Ch2>Redis 后端型\u003C\u002Fh2>\n\u003Cp>类似上面的服务端内存型，只是把会话数据放到了 Redis 中，实现多实例之间共享会话数据，生产环境常用。\u003C\u002Fp>\n\u003Cp>\u003Ccode>redis.NewStore()\u003C\u002Fcode> 的函数签名如下，其中 \u003Ccode>size\u003C\u002Fcode> 代表最大空闲连接数；\u003Ccode>network\u003C\u002Fcode> 应该为 \u003Ccode>tcp\u003C\u002Fcode> 或 \u003Ccode>udp\u003C\u002Fcode>；\u003Ccode>address\u003C\u002Fcode> 是 \u003Ccode>host:port\u003C\u002Fcode> 的格式。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>func redis.NewStore(size int, network string, address string, username string, password string, keyPairs ...[]byte) (redis.Store, error)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>代码改动点:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import (\n    \"github.com\u002Fgin-contrib\u002Fsessions\u002Fredis\"\n)\n\nfunc main() {\n    \u002F\u002F 替换store类型\n    \u002F\u002F 第一个参数\n    store, err := redis.NewStore(10, \"tcp\", \"localhost:6379\", \"\", \"\", []byte(hmacSecretKey), []byte(blockSecretKey))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>客户端请求示例\u003C\u002Fh3>\n\u003Cp>以 \u003Ccode>curl\u003C\u002Fcode> 为例:\u003C\u002Fp>\n\u003Col>\n \u003Cli>登录\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cpre>\u003Ccode>curl -X POST http:\u002F\u002F127.0.0.1:8080\u002Flogin -d 'username=zhangsan&amp;password=123456' -v\nNote: Unnecessary use of -X or --request, POST is already inferred.\n*   Trying 127.0.0.1:8080...\n* Connected to 127.0.0.1 (127.0.0.1) port 8080\n* using HTTP\u002F1.x\n&gt; POST \u002Flogin HTTP\u002F1.1\n&gt; Host: 127.0.0.1:8080\n&gt; User-Agent: curl\u002F8.14.1\n&gt; Accept: *\u002F*\n&gt; Content-Length: 33\n&gt; Content-Type: application\u002Fx-www-form-urlencoded\n&gt;\n* upload completely sent off: 33 bytes\n&lt; HTTP\u002F1.1 200 OK\n&lt; Content-Type: application\u002Fjson; charset=utf-8\n&lt; Set-Cookie: mysession=MTc4ODYxOTIyNHwyN2FJTXR2QW5fQkVEMTJXa3dCUm96b0FxMWhmT0trTk15TG9LZmdSbjFxZk1pYjVHUjJabUZDTTBYTTJ6emFReG9FS0ZEQmVvY3c5aUgxQTdwSEJKd3lqOFQ5YnRVYUd8gZKrbW95srVahjgHg3FVThw6B1AXbJFseRs-DMIcp_w=; Path=\u002F; Expires=Sat, 05 Sep 2026 14:45:24 GMT; Max-Age=300; HttpOnly; SameSite=Strict\n&lt; Date: Sat, 05 Sep 2026 14:40:24 GMT\n&lt; Content-Length: 23\n&lt;\n* Connection #0 to host 127.0.0.1 left intact\n{\"message\":\"logged in\"}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Col>\n \u003Cli>请求 \u003Ccode>\u002Fprofile\u003C\u002Fcode> API\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cpre>\u003Ccode># 不带cookie测试, 提示未认证\ncurl http:\u002F\u002F127.0.0.1:8080\u002Fprofile\n{\"error\":\"unauthorized\"}\n\n# 带 cookie 测试\ncurl http:\u002F\u002F127.0.0.1:8080\u002Fprofile -b 'mysession=MTc4ODYxOTIyNHwyN2FJTXR2QW5fQkVEMTJXa3dCUm96b0FxMWhmT0trTk15TG9LZmdSbjFxZk1pYjVHUjJabUZDTTBYTTJ6emFReG9FS0ZEQmVvY3c5aUgxQTdwSEJKd3lqOFQ5YnRVYUd8gZKrbW95srVahjgHg3FVThw6B1AXbJFseRs-DMIcp_w=; Path=\u002F;Expires=Sat, 05 Sep 2026 14:45:24 GMT; Max-Age=300; HttpOnly; SameSite=Strict'\n{\"user\":\"zhangsan\"}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Col>\n \u003Cli>登出\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cpre>\u003Ccode>curl -X POST http:\u002F\u002F127.0.0.1:8080\u002Flogout -b 'mysession=MTc4ODYxOTIyNHwyN2FJTXR2QW5fQkVEMTJXa3dCUm96b0FxMWhmT0trTk15TG9LZmdSbjFxZk1pYjVHUjJabUZDTTBYTTJ6emFReG9FS0ZEQmVvY3c5aUgxQTdwSEJKd3lqOFQ5YnRVYUd8gZKrbW95srVahjgHg3FVThw6B1AXbJFseRs-DMIcp_w=; Path=\u002F; Expires=Sat, 05 Sep 2026 14:45:24 GMT; Max-Age=300; HttpOnly; SameSite=Strict'\n{\"message\":\"logged out\"}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Col>\n \u003Cli>再次请求 \u003Ccode>\u002Fprofile\u003C\u002Fcode> 会提示未认证。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>补充\u003C\u002Fh2>\n\u003Ch3>其它存储类型\u003C\u002Fh3>\n\u003Cp>除了 \u003Ccode>memstore\u003C\u002Fcode> 和 \u003Ccode>redis\u003C\u002Fcode>，\u003Ccode>gin-contrib\u002Fsessions\u003C\u002Fcode> 还支持以下存储类型：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Ccode>memcache\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>\u003Ccode>mongodb\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>\u003Ccode>postgres\u003C\u002Fcode>\u003C\u002Fli>\n \u003Cli>\u003Ccode>filesystem\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Sessions vs JWT\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>对比方面\u003C\u002Fth>\n   \u003Cth>服务端存储型 Sessions\u003C\u002Fth>\n   \u003Cth>JWT\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>存储\u003C\u002Ftd>\n   \u003Ctd>服务端（内存、Redis 等）\u003C\u002Ftd>\n   \u003Ctd>客户端（token）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>撤销\u003C\u002Ftd>\n   \u003Ctd>简单，从存储中删除即可\u003C\u002Ftd>\n   \u003Ctd>相对困难，一般需要设置黑名单\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>可扩展性\u003C\u002Ftd>\n   \u003Ctd>需要共享存储\u003C\u002Ftd>\n   \u003Ctd>无状态\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>数据大小\u003C\u002Ftd>\n   \u003Ctd>服务端无限制\u003C\u002Ftd>\n   \u003Ctd>受令牌大小限制\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Web 应用一般优先用 session，HttpOnly 和 SameSite 机制可以一定程度上保障安全。JWT 主要面向微服务间认证。\u003C\u002Fp>\n\u003Cp>cookie 型 sessions 也是无状态的，有点类似 JWT。区别在于 cookie 内容都是加密的（如果使用了双密钥），客户端读不出来也改不了。而 JWT 的 payload 可以被任何人看到，只是改不了。cookie 对用户来说是不透明令牌，而 JWT 对客户端来说是自描述令牌。因此 JWT 中不适合存放任何敏感数据，连 \u003Ccode>\"user\": \"username\"\u003C\u002Fcode> 这样的信息也不要有（一般可以写成 \"uid\": \"abcd\"，避免直接暴露用户信息）。\u003C\u002Fp>\n\u003Cp>cookie 借助浏览器，天然获得 \u003Ccode>HttpOnly\u003C\u002Fcode>，可以防 XSS（跨站脚本攻击），但引入 CSRF 风险，需要设置 \u003Ccode>SameSite\u003C\u002Fcode> 来抵御。JWT 如果放 \u003Ccode>localStorage\u003C\u002Fcode>，XSS 能直接偷走，但请求靠 JS 手动加 \u003Ccode>Authorization\u003C\u002Fcode> 请求头，没有 CSRF 问题。\u003C\u002Fp>\n\u003Cp>要撤销 JWT，除了加黑名单（服务验证签名后，再到共享存储中查一下 token 在不在黑名单中。设置黑名单时，一般设置 TTL 为 token 的剩余时长。），一般还有双 Token 验证机制：长期的 refresh token 和短期的 access token，只有 refresh token 需要黑名单机制，access token 还是无状态的。（以后展开具体说，这里不再详述。）\u003C\u002Fp>\n\u003Cp>在移动 App 中，因为 App 没有类似浏览器的 cookie 机制，直接用 JWT。\u003C\u002Fp>\n\u003Cp>前后端分离的项目中，使用 sessions 和 JWT 都要注意跨域问题。\u003C\u002Fp>","前言 HTTP 是无状态的，sessions 让服务能在多个请求之间保存同一用户的数据；服务端通过 cookie 等机制识别用户，取回其先前存储的数据。 gin-contrib\u002Fsessions 中间件提供支持多种存储后端的 session 管理能力。 go get github.com\u002Fgin-contrib\u002Fsessions 参考：https:\u002F\u002Fgin-gonic.com\u002Fen\u002Fdocs\u002Fmiddleware\u002Fsession-management\u002F 概念 cookie 型 session：session 数据序列化并签名加密后，整个塞到浏览器 cookie。服务端无状态、天然支持水平扩容；缺点是数据可见体积受限（~4KB）、服务端无法主动吊销（只能靠删 cookie 或引入服务端黑名单）、每次请求都传输整份数据。 服务端存储型：cookie 里只放一个随机 session ID，数据存在服务端。这类后端能真正“服务端登出”（删掉\u002F过期掉 session 记录）。 cookie 型示例 package main import ( \"net\u002Fhttp\" \"time\" \"github.com\u002Fgin-contrib\u002Fsessions\" \"github.com\u002Fgin-contrib\u002Fsessions\u002Fcookie\" \"github.com\u002Fgin-gonic\u002Fgin\" ) var ( mockUsers = map[string]string{ \"zhangsan\": \"123456\", \"lisi\": \"654321\", } \u002F\u002F secretKey = \"your-secret-key\" hmacSecretKey = \"5KiinhGcvoXFmgOttvgMWd9ugadE0KVP\" blockSecretKey = \"OLZmfTWHGyjCfJoH1Y8SjbSY\" ) const sessionTTL = 60 * 5 func sessionOptions(maxAge int) sessions.Options { return sessions.Options{ Path: \"\u002F\", Domain: \"\", \u002F\u002F 设置为 \"\" 表示当前域名 MaxAge: maxAge, \u002F\u002F 单位为秒。0 表示不写Max-Age(浏览器会话cookie); -1 表示删除 cookie Secure: false, \u002F\u002F 生产走 HTTPS 时改 true HttpOnly: true, \u002F\u002F true 表示禁止 JS 读取 cookie SameSite: http.SameSiteStrictMode, \u002F\u002F 严格模式，仅在同源请求中发送 cookie } } func main() { r := gin.Default() \u002F\u002F Create cookie-based session store with a secret key \u002F\u002F 传单个密钥时, 只做签名认证，不加密cookie内容. 认证密钥建议 32 或 64 字节 \u002F\u002F 传一对密钥时，第一个用于认证（HMAC），第二个用于加密（AES-128, AES-192, AES-256）。加密密钥必须是16、24或32字节长度，对应 AES-128\u002F192\u002F256。 \u002F\u002F 多对密钥用于密钥轮换，第一对用于当前加密和认证，后续的用于解密和认证旧的cookie。Example: cookie.NewStore([]byte(\"newAuth\"), []byte(\"newEnc\"), []byte(\"oldAuth\"), []byte(\"oldEnc\")) store := cookie.NewStore([]byte(hmacSecretKey), []byte(blockSecretKey)) store.Options(sessionOptions(sessionTTL)) \u002F\u002F 全局设置 cookie 过期时间为 5 分钟 r.Use(sessions.Sessions(\"mysession\", store)) r.POST(\"\u002Flogin\", login) auth := r.Group(\"\u002F\", authRequired()) { auth.GET(\"\u002Fprofile\", profile) auth.POST(\"\u002Flogout\", logout) } r.Run(\":8080\") } func login(c *gin.Context) { session := sessions.Default(c) username := c.PostForm(\"username\") password := c.PostForm(\"password\") if username == \"\" || password == \"\" { c.JSON(http.StatusBadRequest, gin.H{\"error\": \"username or password is empty\"}) return } if mockUsers[username] != password { c.JSON(http.StatusUnauthorized, gin.H{\"error\": \"invalid username or password\"}) return } session.Set(\"user\", username) session.Set(\"expire_at\", time.Now().Add(time.Second*sessionTTL).Unix()) \u002F\u002F 设置过期时间 if err := session.Save(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{\"error\": \"save session failed\"}) return } c.JSON(http.StatusOK, gin.H{\"message\": \"logged in\"}) } func logout(c *gin.Context) { session := sessions.Default(c) session.Clear() session.Options(sessionOptions(-1)) \u002F\u002F 让浏览器删除 cookie if err := session.Save(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{\"error\": \"save session failed\"}) return } c.JSON(http.StatusOK, gin.H{\"message\": \"logged out\"}) } func profile(c *gin.Context) { session := sessions.Default(c) user := session.Get(\"user\").(string) c.JSON(http.StatusOK, gin.H{\"user\": user}) } func authRequired() gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) if session.Get(\"user\") == nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\"error\": \"unauthorized\"}) return } if expireAt, ok := session.Get(\"expire_at\").(int64); !ok || time.Now().Unix() > expireAt { session.Clear() session.Options(sessionOptions(-1)) session.Save() c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\"error\": \"expired session\"}) return } c.Next() } } session.Delete(key) 只删除一个 key 对应的条目 session.Clear() 清空所有条目。适用于登出时整体作废。 sessions.Options 中的 SameSite 用来控制跨域 cookie 行为，有 Lax, Strict 和 None 选项。 服务端内存型 服务端内存型就是把 session 存在服务的内存中，这种一般只适合本地单实例开发测试。服务一旦重启，所有 session 即失效，需要客户端重新登录。而且多个实例之间内存数据不共享，如果没有做粘性会话，可能会让客户端反复登录。 如果使用 gin-contrib\u002Fsessions，只需要在上面 cookie 型的基础上，将 store 替换掉即可。以下为需要的改动，其它的都不用改。 import ( \u002F\u002F 引入 memstore \"github.com\u002Fgin-contrib\u002Fsessions\u002Fmemstore\" ) func main() { \u002F\u002F 替换store类型 store := memstore.NewStore([]byte(hmacSecretKey), []byte(blockSecretKey)) } 替换成服务端内存型后，登出的代码虽然没改，但是行为有所变化。在 cookie 型中，登出只是让浏览器把 cookie 删了；换成服务端内存型后，session.Save() 会额外把内存里的会话记录删了，让登出变得真正吊销了。 Redis 后端型 类似上面的服务端内存型，只是把会话数据放到了 Redis 中，实现多实例之间共享会话数据，生产环境常用。 redis.NewStore() 的函数签名如下，其中 size 代表最大空闲连接数；network 应该为 tcp 或 udp；address 是 host:port 的格式。 func redis.NewStore(size int, network string, address string, username string, password string, keyPairs ...[]byte) (redis.Store, error) 代码改动点: import ( \"github.com\u002Fgin-contrib\u002Fsessions\u002Fredis\" ) func main() { \u002F\u002F 替换store类型 \u002F\u002F 第一个参数 store, err := redis.NewStore(10, \"tcp\", \"localhost:6379\", \"\", \"\", []byte(hmacSecretKey), []byte(blockSecretKey)) } 客户端请求示例 以 curl 为例: 登录 curl -X POST http:\u002F\u002F127.0.0.1:8080\u002Flogin -d 'username=zhangsan&password=123456' -v Note: Unnecessary use of -X or --request, POST is already inferred. * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 * using HTTP\u002F1.x > POST \u002Flogin HTTP\u002F1.1 > Host: 127.0.0.1:8080 > User-Agent: curl\u002F8.14.1 > Accept: *\u002F* > Content-Length: 33 > Content-Type: application\u002Fx-www-form-urlencoded > * upload completely sent off: 33 bytes \u003C HTTP\u002F1.1 200 OK \u003C Content-Type: application\u002Fjson; charset=utf-8 \u003C Set-Cookie: mysession=MTc4ODYxOTIyNHwyN2FJTXR2QW5fQkVEMTJXa3dCUm96b0FxMWhmT0trTk15TG9LZmdSbjFxZk1pYjVHUjJabUZDTTBYTTJ6emFReG9FS0ZEQmVvY3c5aUgxQTdwSEJKd3lqOFQ5YnRVYUd8gZKrbW95srVahjgHg3FVThw6B1AXbJFseRs-DMIcp_w=; Path=\u002F; Expires=Sat, 05 Sep 2026 14:45:24 GMT; Max-Age=300; HttpOnly; SameSite=Strict \u003C Date: Sat, 05 Sep 2026 14:40:24 GMT \u003C Content-Length: 23 \u003C * Connection #0 to host 127.0.0.1 left intact {\"message\":\"logged in\"} 请求 \u002Fprofile API # 不带cookie测试, 提示未认证 curl http:\u002F\u002F127.0.0.1:8080\u002Fprofile {\"error\":\"unauthorized\"} # 带 cookie 测试 curl http:\u002F\u002F127.0.0.1:8080\u002Fprofile -b 'mysession=MTc4ODYxOTIyNHwyN2FJTXR2QW5fQkVEMTJXa3dCUm96b0FxMWhmT0trTk15TG9LZmdSbjFxZk1pYjVHUjJabUZDTTBYTTJ6emFReG9FS0ZEQmVvY3c5aUgxQTdwSEJKd3lqOFQ5YnRVYUd8gZKrbW95srVahjgHg3FVThw6B1AXbJFseRs-DMIcp_w=; Path=\u002F;Expires=Sat, 05 Sep 2026 14:45:24 GMT; Max-Age=300; HttpOnly; SameSite=Strict' {\"user\":\"zhangsan\"} 登出 curl -X POST http:\u002F\u002F127.0.0.1:8080\u002Flogout -b 'mysession=MTc4ODYxOTIyNHwyN2FJTXR2QW5fQkVEMTJXa3dCUm96b0FxMWhmT0trTk15TG9LZmdSbjFxZk1pYjVHUjJabUZDTTBYTTJ6emFReG9FS0ZEQmVvY3c5aUgxQTdwSEJKd3lqOFQ5YnRVYUd8gZKrbW95srVahjgHg3FVThw6B1AXbJFseRs-DMIcp_w=; Path=\u002F; Expires=Sat, 05 Sep 2026 14:45:24 GMT; Max-Age=300; HttpOnly; SameSite=Strict' {\"message\":\"logged out\"} 再次请求 \u002Fprofile 会提示未认证。 补充 其它存储类型 除了 memstore 和 redis，gin-contrib\u002Fsessions 还支持以下存储类型： memcache mongodb postgres filesystem Sessions vs JWT 对比方面 服务端存储型 Sessions JWT 存储 服务端（内存、Redis 等） 客户端（token） 撤销 简单，从存储中删除即可 相对困难，一般需要设置黑名单 可扩展性 需要共享存储 无状态 数据大小 服务端无限制 受令牌大小限制 Web 应用一般优先用 session，HttpOnly 和 SameSite 机制可以一定程度上保障安全。JWT 主要面向微服务间认证。 cookie 型 sessions 也是无状态的，有点类似 JWT。区别在于 cookie 内容都是加密的（如果使用了双密钥），客户端读不出来也改不了。而 JWT 的 payload 可以被任何人看到，只是改不了。cookie 对用户来说是不透明令牌，而 JWT 对客户端来说是自描述令牌。因此 JWT 中不适合存放任何敏感数据，连 \"user\": \"username\" 这样的信息也不要有（一般可以写成 \"uid\": \"abcd\"，避免直接暴露用户信息）。 cookie 借助浏览器，天然获得 HttpOnly，可以防 XSS（跨站脚本攻击），但引入 CSRF 风险，需要设置 SameSite 来抵御。JWT 如果放 localStorage，XSS 能直接偷走，但请求靠 JS 手动加 Authorization 请求头，没有 CSRF 问题。 要撤销 JWT，除了加黑名单（服务验证签名后，再到共享存储中查一下 token 在不在黑名单中。设置黑名单时，一般设置 TTL 为 token 的剩余时长。），一般还有双 Token 验证机制：长期的 refresh token 和短期的 access token，只有 refresh token 需要黑名单机制，access token 还是无状态的。（以后展开具体说，这里不再详述。） 在移动 App 中，因为 App 没有类似浏览器的 cookie 机制，直接用 JWT。 前后端分离的项目中，使用 sessions 和 JWT 都要注意跨域问题。",6657,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":15,"meta":17,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":37},"#2563eb","16 \u002F 10",[7,8],{"targetType":8,"targetId":9,"likedByMe":39,"likeCount":40,"commentCount":40,"contentLikeCount":40,"contentCommentCount":40,"sourceLikeCount":40,"sourceCommentCount":40},false,0,[42,51,57,63,72,79,85,92],{"id":43,"kind":7,"title":44,"summary":45,"image":46,"href":47,"meta":48,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":49},"NEWS_ARTICLE:927","HelloCrab - 短视频开源爬虫，仅供学习参考","HelloCrab 基于 Avalonia、Playwright、AI与 FFmpeg 的跨平台桌面采集器，支持9大平台，以及 Android、iOS、Browser 远程控制端。 Made By ChatGPT &amp; Vincent with ❤ 平台 是否接入 哔哩哔哩 ✅ 抖音 ✅ 快手","https:\u002F\u002Fwww.cnblogs.com\u002Fhupo376787\u002Fp\u002FScreenshot\u002FWindows.jpg","\u002Fnews\u002F927","2026 · 软件开发",[50],"软件开发",{"id":52,"kind":7,"title":53,"summary":54,"image":14,"href":55,"meta":48,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":56},"NEWS_ARTICLE:929",".NET 异常处理的\"暗门\"：代码里写满 catch，你依然能抓住它——从一个 AI Agent 运行时的源码说起","一个健壮的系统，必然到处都是有意的 catch；异常被消化不等于问题不存在。 观测与韧性，是一个硬币的两面——降级逻辑保证系统不崩，FirstChance 保证你能看见它为什么降级。","\u002Fnews\u002F929",[50],{"id":58,"kind":7,"title":59,"summary":60,"image":14,"href":61,"meta":17,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":62},"NEWS_ARTICLE:928","架构师化繁为简，执行者化简为繁","新手改三天，你改三行——反而是你显得更不重要。因为化繁为简做得越纯熟，产出看起来越小。这篇聊聊两种能力的辩证关系，以及为什么'看不见'的那部分工作，恰恰是最难的部分。","\u002Fnews\u002F928",[7,8],{"id":64,"kind":7,"title":65,"summary":66,"image":67,"href":68,"meta":69,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":70},"NEWS_ARTICLE:930","基于 vLLM+Nginx 构建负载均衡推理集群","企业内部私有环境部署大模型推理集群时，很容易遇到流量调度混乱、节点负载失衡、会话上下文丢失、接口缺少鉴权防护等一系列问题，单 vLLM 推理节点难以支撑并发请求。本文基于 Ubuntu 22.04 系统环境，搭建 Nginx + vLLM-Semantic-Router + vLLM-Router","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1379525\u002F202609\u002F1379525-20260910165534385-2022408098.png","\u002Fnews\u002F930","2026 · 人工智能",[71],"人工智能",{"id":73,"kind":7,"title":74,"summary":75,"image":76,"href":77,"meta":69,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":78},"NEWS_ARTICLE:932","2026年AI编程工具大全，33个主流工具一次看懂","事情是这样的，前两天看到一张图，是某个社区官网的「支持的工具」清单，我数了数，整整31个AI编程工具。 两年前这份清单撑死5个，现在直接31个，而且我居然每一个都认识。。。 干脆整理成一篇，顺手把最近字节的TraeWork和豆包工作也补了进来，凑成33个。 今天给大家推荐一遍，每个工具说说它是干什么","https:\u002F\u002Fimage.kjdaohang.com\u002Fimg\u002F20260909210838518.png","\u002Fnews\u002F932",[71],{"id":80,"kind":7,"title":81,"summary":82,"image":14,"href":83,"meta":17,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":84},"NEWS_ARTICLE:931","SH 中文化样例数据使用手册","在数据库演示与 PoC 场景中，Oracle 自带的 SH 示例模式虽然经典，但英文维度数据往往让国内演示效果打折扣。笔者整理了一套方案：保留 SH 标准英文对象名，同时装载中文化维度数据，并按参数生成可复现的销售历史数据，方便个人测试与概念验证。 01 | 环境准备 使用前请确认满足以下条件： O","\u002Fnews\u002F931",[7,8],{"id":86,"kind":7,"title":87,"summary":88,"image":89,"href":90,"meta":48,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":91},"NEWS_ARTICLE:933","写给 C++ 工程师的 OpenClaw.NET 上手指南：用你熟悉的 C++ 思维，跑起一个生产级 AI Agent","它像一个「基于 boost.asio + REST 端点的常驻服务」——对外是 HTTP \u002F WebSocket \u002F 各 IM 的 webhook，对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent——只不过协程不用你手写 promise_type，内存不用你管 new\u002Fdelete。","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202609\u002F510-20260905070811424-1837144874.jpg","\u002Fnews\u002F933",[50],{"id":93,"kind":7,"title":94,"summary":95,"image":96,"href":97,"meta":69,"badge":10,"author":12,"stats":-1,"accent":35,"coverRatio":36,"tags":98},"NEWS_ARTICLE:935","[Agent Memory \u002F 强化学习] MemPO源码学习笔记 ---（1）--- 总体","[Agent Memory \u002F 强化学习] MemPO源码学习笔记 （1） 总体 目录[Agent Memory \u002F 强化学习] MemPO源码学习笔记 （1） 总体0x00 概要0x01 基础 &amp; 背景1.1 用RL训练记忆系统的要点1.2 主要难点1.3 主要思路1.4 RL训练方案1.","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1850883\u002F202609\u002F1850883-20260906190854637-986949885.jpg","\u002Fnews\u002F935",[71]]