Go Web 编程--SecureCookie 实现客户端 Session 管理

Web 应用开发中 Session 是在用户和服务器之间进行交换的非持久化交互信息。当用户登录时,可以在用户和服务器之间生成 Session,然后来回交换数据,并在用户登出时销毁 Sessiongorilla/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 中存储用户 SessionCookie-Name 设置成了 user-session
  • 登录验证就是简单的用户名和密码查找匹配的用户,在之前的文章应用数据库应用 ORM 两篇文章中有在 MySQL 数据库中创建 users 表,并介绍了怎么使用 ORM 操作数据库,没有看过的同学可以回看一下。
  • 登录验证成功后在 Sessionauthenticated 中标记了用户已通过认证。session.Values 是类型 map[interface{}]interface{} 的别名,所以可以往其中存储任意类型的数据。

实现登出#

登出我们这里就是简单的将 Sessionauthenticated 的值设置成了 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 获取本文源代码

前文回顾#

Go Web 编程–如何确保 Cookie 数据的安全传输

Go Web 编程–应用 ORM

Go Web 编程–应用 ORM

五分钟用 Docker 快速搭建 Go 开发环境

深入学习用 Go 编写 HTTP 服务器

本作品采用《CC 协议》,转载必须注明作者和本文链接
公众号:网管叨 bi 叨 | Golang、Laravel、Docker、K8s 等学习经验分享
未填写
文章
113
粉丝
368
喜欢
487
收藏
317
排名:34
访问:20.4 万
私信
所有博文
社区赞助商