Gin 是Go语言中广泛使用的Web框架,它提供了强大的功能来简化网站开发。在其众多功能中,处理会话和Cookie是构建功能性Web应用程序的关键方面。
这篇文章重点介绍了Gin框架中一种可能并不明显或直观的行为——丢失的会话Cookie问题。
那么,丢失的会话Cookie问题究竟是什么?它发生在会话在一个路由中创建并在其中保存,但在其他路由中却无法访问时。此外,带有指定键的预期会话Cookie不存在或未发送到浏览器。以下是此问题如何表现的更详细说明。
我们基本上有两个页面:/signin 和 /stock。/signin页面只是一个简单的登录表单,包含两个字段:用户名和密码。一旦用户输入并提交表单,请求数据将被发送到后端API,并且一旦用户名和密码被验证,将创建一个包含用户名的会话并保存。
session := sessions.Default(c)
session.Set("username", username)
session.Options(sessions.Options{MaxAge: 60 * 2}) // 测试目的,2分钟
session.Save()
登录后,用户将被重定向到/stock页面,该页面显示一些股票信息(不重要),但它需要会话数据可用(用户需要先登录),以便在访问时不会滥用API。在/stock API中,会话数据将像这样读取
func AuthMiddleware() gin.HandlerFunc {
fmt.Println("初始化身份验证中间件")
return func(c *gin.Context) {
session := sessions.Default(c)
username := session.Get("username")
if username == nil && c.Request.URL.Path != "/signin" && !strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.Redirect(http.StatusTemporaryRedirect, "signin")
c.Abort()
return
}
c.Next()
}
}
这是一个中间件,每次请求到来时都会运行。会话存储本身在服务器启动时初始化。
// 初始化Cookie存储
store := cookie.NewStore([]byte("secret"))
// 使用会话中间件
r.Use(sessions.Sessions("mysecurity", store))
最初所有这些看起来都相当标准并且应该可以正常工作。这段代码在网上也很常见。但是,在测试过程中,/stock页面从未被重定向,并且会话Cookie未在浏览器中正确设置,因为找不到上面指定的名称的Cookie。
为了调试,我首先确认登录过程本身没有问题。用户名和密码被后端API正确接收,并且session.Set()
和session.Save()
函数被调用。只是Cookie没有正确设置。
问题是什么?问题实际上在于这一行
session.Options(sessions.Options{MaxAge: 60 * 2})
通过此函数调用,Cookie选项将设置为指定的选项。设置了max-age,仅此而已。HTTP Cookie的其他元素被省略,例如PATH
。如果PATH为空,某些浏览器(至少Microsoft Edge)不会接受Cookie,因此不会保存它。
SetCookie: mysecurity=MTczMjcwNzQUJFQUVRQUFBcV80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQW9BQ0hCcGEyVXdNREF5fDuc3NUMqZ5fjluUzVQbDCSpiN-NaHO5BgwXkXCyi01x; Expires=Wed, 27 Nov 2024 11:41:53 GMT; Max-Age=600
根据Gin的实现,这种设置Cookie的行为是预期的,因为我已经调用了session.Options()
,它基本上会覆盖整个sessions.Options
,其中也包含其他字段。
type Options struct {
Path string
Domain string
// MaxAge=0 表示未指定“Max-Age”属性。
// MaxAge<0 表示立即删除Cookie,相当于“Max-Age: 0”。
// MaxAge>0 表示存在Max-Age属性,并以秒为单位给出。
MaxAge int
Secure bool
HttpOnly bool
// rfc-draft 用于防止CSRF:https://tools.ietf.org/html/draft-west-first-party-cookies-07
// 参考:https://godoc.org/net/http
// https://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/
SameSite http.SameSite
}
在这种情况下,Path
现在为空。稍后在创建实际的HTTP Cookie并发送它时。组合Cookie字符串的逻辑是
// Save 将单个会话添加到响应中。
func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
session *Session) error {
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
return nil
}
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: options.Path,
Domain: options.Domain,
MaxAge: options.MaxAge,
Secure: options.Secure,
HttpOnly: options.HttpOnly,
SameSite: options.SameSite,
}
}
func (c *Cookie) String() string {
...
if len(c.Path) > 0 {
b.WriteString("; Path=")
b.WriteString(sanitizeCookiePath(c.Path))
}
...
}
为了纠正这个问题,我需要在设置选项时设置其他字段。
session.Options(sessions.Options{
MaxAge: 60 * 2, // 这将会话设置为在600秒(10分钟)后过期
// 添加这些设置以确保正确的Cookie行为:
Path: "/", // 使Cookie可用于所有路径
HttpOnly: true, // 防止XSS
Secure: true, // 用于HTTPS
SameSite: http.SameSiteStrictMode,
})
之后,重新启动并再次运行流程,它就可以工作了。Cookie看起来像这样
SetCookie: mysecurity=MTczMjcwNzA3MHxEWDhFUVRQUFBcV80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQW9BQ0hCcGEyVXdNREF5fMXu2tVBJMAYJbOxou9ZVNYMuTuU1H7XrV7KU2cQ3S6x; Path=/; Expires=Wed, 27 Nov 2024 11:41:10 GMT; Max-Age=600; HttpOnly; Secure; SameSite=Strict
Path
和其他字段都存在。
虽然问题已解决。为什么原来的解决方案不起作用?或者至少我希望它能工作。这导致我对Gin中session
模块的设计产生了一些想法。
- 如果未设置/为空,为什么某些字段没有默认值?当创建会话结构时,这些字段确实有默认值。
- 它是否应该提供WithOptions模式,以便在必要时设置单个字段,因为有时我可能只想更改
MaxAge
字段。
希望这对遇到相同问题的用户有所帮助。