Gin là một framework web được sử dụng rộng rãi trong Go, cung cấp các tính năng mạnh mẽ để đơn giản hóa việc phát triển website. Trong số nhiều khả năng của nó, xử lý session và cookie là một khía cạnh quan trọng để xây dựng các ứng dụng web hoạt động tốt.
Bài viết này làm nổi bật một hành vi trong framework Gin có thể không ngay lập tức rõ ràng hoặc trực quan—vấn đề cookie session bị thiếu.
Vậy, chính xác vấn đề cookie session bị thiếu là gì? Nó xảy ra khi một session được tạo và lưu trong một route nhưng không thể truy cập được trong các route khác. Ngoài ra, cookie session dự kiến với khóa được chỉ định không có mặt hoặc được gửi đến trình duyệt. Dưới đây là cái nhìn kỹ hơn về cách vấn đề này biểu hiện.
Chúng ta về cơ bản có hai trang: /signin và /stock. Trang /signin chỉ là một form đăng nhập đơn giản với hai trường: tên người dùng và mật khẩu. Khi người dùng nhập chúng và gửi form, dữ liệu yêu cầu sẽ được gửi đến API backend và sau khi xác minh tên người dùng và mật khẩu, một session với tên người dùng sẽ được tạo và lưu.
session := sessions.Default(c)
session.Set("username", username)
session.Options(sessions.Options{MaxAge: 60 * 2}) // 2 phút cho mục đích thử nghiệm
session.Save()
Sau khi đăng nhập, người dùng sẽ được chuyển hướng đến trang /stock hiển thị một số thông tin chứng khoán (không quan trọng), nhưng nó cần có dữ liệu session khả dụng (người dùng cần đăng nhập trước) để các API không bị lạm dụng khi truy cập. Trong API /stock, dữ liệu session sẽ được đọc như
func AuthMiddleware() gin.HandlerFunc {
fmt.Println("Khởi tạo middleware xác thực")
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()
}
}
Đây là một middleware và nó sẽ được chạy khi mọi yêu cầu đến. Bản thân kho lưu trữ session được khởi tạo khi máy chủ khởi động.
// Khởi tạo kho lưu trữ cookie
store := cookie.NewStore([]byte("secret"))
// Sử dụng middleware session
r.Use(sessions.Sessions("mysecurity", store))
Ban đầu tất cả những điều này trông khá tiêu chuẩn và nên hoạt động tốt. Mã cũng có khắp nơi trên web. Tuy nhiên, trong khi thử nghiệm, trang /stock không bao giờ bị chuyển hướng và cookie session không được đặt đúng trong trình duyệt, vì không thể tìm thấy cookie với tên được chỉ định ở trên.
Để gỡ lỗi, trước tiên tôi đã xác nhận rằng quá trình đăng nhập chính nó ổn. Tên người dùng và mật khẩu được nhận chính xác bởi API backend và các hàm session.Set()
và session.Save()
được gọi. Chỉ là cookie không được đặt đúng.
Vấn đề là gì? Vấn đề thực sự nằm ở dòng
session.Options(sessions.Options{MaxAge: 60 * 2})
Với cuộc gọi hàm này, tùy chọn cookie được đặt với tùy chọn được chỉ định. max-age được đặt, nhưng chỉ vậy thôi. Các phần tử khác của cookie HTTP bị bỏ qua, chẳng hạn như PATH
. Với PATH trống, một số trình duyệt (ít nhất là Microsoft Edge) không chấp nhận cookie và do đó sẽ không lưu nó.
SetCookie: mysecurity=MTczMjcwNzQUJFQUVRQUFBcV80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQW9BQ0hCcGEyVXdNREF5fDuc3NUMqZ5fjluUzVQbDCSpiN-NaHO5BgwXkXCyi01x; Expires=Wed, 27 Nov 2024 11:41:53 GMT; Max-Age=600
Theo triển khai của Gin, hành vi này khi đặt cookie là điều được mong đợi, vì tôi đã gọi session.Options()
về cơ bản ghi đè toàn bộ sessions.Options
cũng chứa các trường khác.
type Options struct {
Path string
Domain string
// MaxAge=0 có nghĩa là không có thuộc tính 'Max-Age' nào được chỉ định.
// MaxAge<0 có nghĩa là xóa cookie ngay bây giờ, tương đương với 'Max-Age: 0'.
// MaxAge>0 có nghĩa là thuộc tính Max-Age hiện diện và được cho theo giây.
MaxAge int
Secure bool
HttpOnly bool
// rfc-draft để ngăn chặn CSRF: https://tools.ietf.org/html/draft-west-first-party-cookies-07
// tham khảo: https://godoc.org/net/http
// https://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/
SameSite http.SameSite
}
Trong trường hợp này, Path
hiện đang trống. Sau đó, khi tạo cookie HTTP thực tế và gửi nó. Logic của việc tạo chuỗi cookie là
// Save thêm một session duy nhất vào phản hồi.
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))
}
...
}
Để khắc phục sự cố, tôi cần đặt các trường khác khi đặt tùy chọn.
session.Options(sessions.Options{
MaxAge: 60 * 2, // Điều này đặt session hết hạn trong 600 giây (10 phút)
// Thêm các cài đặt này để đảm bảo hành vi cookie chính xác:
Path: "/", // Làm cho cookie khả dụng cho tất cả các đường dẫn
HttpOnly: true, // Bảo vệ chống lại XSS
Secure: true, // Cho HTTPS
SameSite: http.SameSiteStrictMode,
})
Sau điều này, khởi động lại và chạy luồng một lần nữa và nó hoạt động. Cookie trông giống như
SetCookie: mysecurity=MTczMjcwNzA3MHxEWDhFUVRQUFBcV80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQW9BQ0hCcGEyVXdNREF5fMXu2tVBJMAYJbOxou9ZVNYMuTuU1H7XrV7KU2cQ3S6x; Path=/; Expires=Wed, 27 Nov 2024 11:41:10 GMT; Max-Age=600; HttpOnly; Secure; SameSite=Strict
Các trường Path
và các trường khác hiện diện.
Mặc dù vấn đề đã được khắc phục. Tại sao giải pháp ban đầu lại không hoạt động? Hoặc ít nhất là tôi mong đợi nó hoạt động. Điều này dẫn đến một số suy nghĩ về thiết kế của mô-đun session
trong Gin.
- Tại sao không có giá trị mặc định cho một số trường nếu không được đặt/trống? Khi cấu trúc session được tạo, mặc dù có giá trị mặc định cho các trường đó.
- Nó có nên cung cấp mẫu WithOptions có thể đặt từng trường khi cần thiết, vì đôi khi tôi chỉ muốn thay đổi trường
MaxAge
.
Hy vọng điều này sẽ giúp những người đã gặp phải vấn đề tương tự.