在 Web 应用开发中 Session 是在用户和服务器之间进行交换的非持久化交互信息。当用户登录时,可以在用户和服务器之间生成 Session ,然后来回交换数据,并在用户登出时销毁 Session 。 gorilla/sessions 软件包提供了易于使用的 Go 语言 Session 实现。该软件包提供了两种不同的实现。第一个是文件系统存储,它将每个会话存储在服务器的文件系统中。另一个是 Cookie 存储,它使用我们 上篇文章 讲的 SecureCookie 在客户端上存储会话。同时还提供了用户自定义 Session 存储实现的选项,我们可以根据应用的需求自己实现 Session 存储。因为我们的教程是学会使用为目的就不大费周章的去实现 MySQL 或者 Redis 版本的 Session 存储了,我们直接使用软件包提供的 Cookie 实现来完成本节的 Session 相关内容。
Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复 gohttp09 获取本文源代码
客户端使用 Cookie 管理用户 Session 较之在服务器进行用户的 Session 管理会有一些优势。客户端 Session 增加了应用程序的可伸缩性,因为所有的会话数据都存储在用户端,因此可以将用户的请求平衡到不同的远端服务器,也不必在服务器端对所有用户的会话进行统一管理,所以使用 Cookie 存储用户 Session 会更简单一些。
当然有优势就必定有劣势,客户端 Cookie 的整体大小是有限制的。目前, Google Chrome 浏览器将 Cookie 限制为 4096 个字节。
客户端会话还意味着无法终止会话,从而导致注销不完整。如果用户在退出前保存了 Cookie 中的会话信息,则他们可以使用该会话信息创建一个新的 Cookie ,然后继续使用该应用程序,为了最大程度地降低安全风险,我们可以将会话 Cookie 设置为在合理的时间内过期,使用加密后的 ScureCookie 存储数据,同时还要避免在其中存储敏感信息(即使是服务端管理 Session 也不应该存储类似密码这种敏感信息)。
总之在考虑使用客户端还是服务端存储用户 Session 时一定要根据应用的使用场景来选择,这一点很重要。
在开始编码前先来安装一下 gorilla/sessions 软件包,
$ go get github.com/gorilla/sessions
并简单看一下软件包功能特性的介绍
我们今天的示例代码是用 gorilla/sessions 提供的 CookieSessionStore 实现一个简单的系统登录功能。
我们会定义如下几个路由:
为了达到演示目的的同时减少文章中出现过多代码,我们不会做前端页面,通过命令行 cURL 直接请求上面几个 URL 验证我们的系统登录功能。
我们现在项目的 handler 目录下新建一个 user 子目录,用于存放使用到用户 Session 的处理程序
...
handler/
└── user/
└── init.go
└── login.go
└── logout.go
└── secret.go
...
main.go
其下的四个分别是包的初始化程序 init.go 以及存放上面说的三个路由处理程序的 .go 源文件。
我们把 Session 存储的初始化工作放在 user 包的 init 函数中,这样首次导入 user 包时即可完成相关的初始化工作。
package user
import "github.com/gorilla/sessions"
const (
//64位
cookieStoreAuthKey = "..."
//AES encrypt key必须是16或者32位
cookieStoreEncryptKey = "..."
)
var sessionStore *sessions.CookieStore
func init () {
sessionStore = sessions.NewCookieStore(
[]byte(cookieStoreAuthKey),
[]byte(cookieStoreEncryptKey),
)
sessionStore.Options = &sessions.Options{
HttpOnly: true,
MaxAge: 60 * 15,
}
}
// login.go
var sessionCookieName = "user-session"
func Login(w http.ResponseWriter, r *http.Request) {
session, err := sessionStore.Get(r, sessionCookieName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 登录验证
name := r.FormValue("name")
pass := r.FormValue("password")
_, err = logic.AuthenticateUser(name, pass)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// 在session中标记用户已经通过登录验证
session.Values["authenticated"] = true
err = session.Save(r, w)
fmt.Fprintln(w, "登录成功!", err)
}
登出我们这里就是简单的将 Session 中 authenticated 的值设置成了 false .
//logout.go
func Logout(w http.ResponseWriter, r *http.Request) {
session, _ := sessionStore.Get(r, sessionCookieName)
session.Values["authenticated"] = false
session.Save(r, w)
}
//secret.go
func Secret(w http.ResponseWriter, r *http.Request) {
session, _ := sessionStore.Get(r, sessionCookieName)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
fmt.Fprintln(w, "这里还是空空如也!")
}
// router.go
func RegisterRoutes(r *mux.Router) {
...
userRouter := r.PathPrefix("/user").Subrouter()
userRouter.HandleFunc("/login", user.Login).Methods("POST")
userRouter.HandleFunc("/secret", user.Secret)
userRouter.HandleFunc("/logout", user.Logout)
...
}
编写完上面的 Session 管理的功能后,重启服务器,然后使用 cURL 分别请求 URL 验证一下效果。
curl -XPOST -d 'name=Klein&password=123' \\
-c - http://localhost:8000/user/login
-c 选项表示将 Cookie 写入到后面的文件中,完整格式是 -c -
我们可以在下图里看到, Cookie 中的 user-session 存储的就是加密后的 Session 数据了
如果请求中不携带这个 Cookie 访问 /user/secret 会直接返回 HTTP 403 错误
那么接下来在使用 cURL 请求 /user/secret 时带上上面返回的 Cookie 值,看看请求是否能成功
curl --cookie "user-session=MTU4m..." http://localhost:8000/user/secret
Cookie 加密后的值太长了,搞得字儿好小, cURL 执行的结果显示服务器成功地响应了我们的请求。你们试验的时候换成自己生成的 Cookie 值请求就可以啦。
你们实践时也可以用 PostMan 代替 cURL 试验,不过感觉 PostMan 的返回不如 cURL 来的明显。