Go Web编程--SecureCookie实现客户端Session管理
source link: https://studygolang.com/articles/27135
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
在 Web
应用开发中 Session
是在用户和服务器之间进行交换的非持久化交互信息。当用户登录时,可以在用户和服务器之间生成 Session
,然后来回交换数据,并在用户登出时销毁 Session
。 gorilla/sessions
软件包提供了易于使用的 Go
语言 Session
实现。该软件包提供了两种不同的实现。第一个是文件系统存储,它将每个会话存储在服务器的文件系统中。另一个是 Cookie
存储,它使用我们 上篇文章 讲的 SecureCookie
在客户端上存储会话。同时还提供了用户自定义 Session
存储实现的选项,我们可以根据应用的需求自己实现 Session
存储。因为我们的教程是学会使用为目的就不大费周章的去实现 MySQL
或者 Redis
版本的 Session
存储了,我们直接使用软件包提供的 Cookie
实现来完成本节的 Session
相关内容。
Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复 gohttp09
获取本文源代码
使用Cookie存储用户Session的优缺点
客户端使用 Cookie
管理用户 Session
较之在服务器进行用户的 Session
管理会有一些优势。客户端 Session
增加了应用程序的可伸缩性,因为所有的会话数据都存储在用户端,因此可以将用户的请求平衡到不同的远端服务器,也不必在服务器端对所有用户的会话进行统一管理,所以使用 Cookie
存储用户 Session
会更简单一些。
当然有优势就必定有劣势,客户端 Cookie
的整体大小是有限制的。目前, Google Chrome
浏览器将 Cookie
限制为 4096
个字节。
客户端会话还意味着无法终止会话,从而导致注销不完整。如果用户在退出前保存了 Cookie
中的会话信息,则他们可以使用该会话信息创建一个新的 Cookie
,然后继续使用该应用程序,为了最大程度地降低安全风险,我们可以将会话 Cookie
设置为在合理的时间内过期,使用加密后的 ScureCookie
存储数据,同时还要避免在其中存储敏感信息(即使是服务端管理 Session
也不应该存储类似密码这种敏感信息)。
总之在考虑使用客户端还是服务端存储用户 Session
时一定要根据应用的使用场景来选择,这一点很重要。
安装gorilla/sessions
在开始编码前先来安装一下 gorilla/sessions
软件包,
$ go get github.com/gorilla/sessions
并简单看一下软件包功能特性的介绍
- 方便地设置签名(也可以选择加密)的
Cookie
。 - 自带将会话存储在
Cookie
或服务端文件系统中的SessionStore
实现。 - 支持Flash消息:读取即销毁的会话数据。
- 支持方便地切换会话数据的持久化方式。
- 为不同的
Session
存储提供统一的接口和基础设施。
演示用户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存储
我们把 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
。 - 登录验证就是简单的用户名和密码查找匹配的用户,在之前的文章 应用数据库 和 应用 ORM 两篇文章中有在
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) }
使用Session认证用户
//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管理功能
编写完上面的 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
获取本文源代码
前文回顾
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK