The Missing Session Cookie While Using GoLang Gin

English 简体中文 Tiếng Việt
Summary

Developers using Gin may encounter a "missing session cookie" problem where sessions aren't accessible across routes and the cookie isn't set in the browser. This issue stems from calling session.Options() with only MaxAge, which inadvertently overwrites other critical cookie attributes like Path with their zero values. An empty Path, in particular, can cause some browsers to reject the cookie, preventing it from being stored. To resolve this, developers must explicitly define all necessary cookie options, including Path: "/", HttpOnly: true, Secure: true, and SameSite settings, when configuring session options to ensure proper cookie behavior and persistence.

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模块的设计产生了一些想法。

  1. 如果未设置/为空,为什么某些字段没有默认值?当创建会话结构时,这些字段确实有默认值。
  2. 它是否应该提供WithOptions模式,以便在必要时设置单个字段,因为有时我可能只想更改MaxAge字段。

希望这对遇到相同问题的用户有所帮助。

SESSION GO GOLANG COOKIE NOT WORKING GIN MISSING

  RELATED

  COMMENTS

0

No comment for this article.