在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
并简单看一下软件包功能特性的介绍
Cookie
。Cookie
或服务端文件系统中的SessionStore
实现。Session
存储提供统一的接口和基础设施。我们今天的示例代码是用gorilla/sessions
提供的CookieSessionStore
实现一个简单的系统登录功能。
我们会定义如下几个路由:
/user/login
用户登录验证,验证成功后在用户Session
数据中标记用户是已验证的。/user/logout
用户登出,会在Session
中标记用户是未认证的。/user/secret
通过用户Session
判断用户是否已认证,未认证返回403 Forbidden
错误。为了达到演示目的的同时减少文章中出现过多代码,我们不会做前端页面,通过命令行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) }
Cookie
中存储用户Session
的Cookie-Name
设置成了user-session
。MySQL
数据库中创建users
表,并介绍了怎么使用ORM
操作数据库,没有看过的同学可以回看一下。Session
的authenticated
中标记了用户已通过认证。session.Values
是类型map[interface{}]interface{}
的别名,所以可以往其中存储任意类型的数据。登出我们这里就是简单的将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, "这里还是空空如也!") }
Session
中存储的数据值都是接口类型的,所以使用时要先对其进行类型断言session.Values["authenticated"].(bool)
authenticated
的值不为true
或者是从Session
中获取不到对应的值,这里直接返回HTTP 403 Forbidden
错误。// 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 -<file_name>
,短横线后不带文件名表示把Cookie
写入到标准输出中。
我们可以在下图里看到,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
来的明显。
Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp09
获取本文源代码