1. 引言
本文将从Web应用程序处理请求时需要用户信息,同时HTTP又是无状态协议这个矛盾点出发。从该问题出发,简单描述了解决该问题的Token
机制,进而引出Cookie
的实现方案。
基于此我们将详细描述Cookie
的规范,然后详细描述具体的实现方式,进一步描述Gin
框架对Cookie
操作提供的API
,最终提供了一个详细的代码实现。
我们还将详细描述Gin
框架提供API
的实现原理,帮助用户更好得使用这两个API
。
2. 问题引入
在 如何使用Gin搭建一个Go Web应用程序 一文中,我们已经了解了如何使用Gin
搭建一个简单的Web应用程序。然而,在现实的Web应用程序中,大部分功能都是需要用户的身份信息才能处理。举例来说,在一个视频网站查看用户最近观看记录,如果缺少用户身份信息,此时将无法对请求进行处理。
但是HTTP协议的设计,是无状态的,也就是每次请求都是独立的。基于此,应该有一套机制,能够在用户身份认证成功后,给用户分配一个Token
,后续用户在每次请求时,都携带上该Token
,使得服务器能够从请求中获取用户信息,解决HTTP无状态问题。大概流程如下:
上面流程中,需要服务端按照某个协议,向客户端返回Token
;客户端通过该协议,成功解析出服务端返回的Token
,然后在每次请求中携带该Token
。然后服务器端再根据协议,从中解析出Token
信息,获取请求用户信息。
当前常用的有Cookie
,Jwt
,OAuth2.0
等标准,其各有优缺点。其中Cookie
是一种存储在客户端浏览器中的数据。服务端可以通过设置HTTP响应头将Token
存储在Cookie
当中,并在后续请求中从Cookie
中读取Token
。而JWT
则是一种基于JSON格式的安全令牌,可用于在客户端和服务端之间传递信息。
之前,我们在 一文读懂Cookie 中,已经了解Cookie
的相关内容。基于此,我们这次使用Cookie
来实现上述所说的流程,按照Cookie
的规范来实现Token
的返回和请求中Token
的解析。
3. 实现
3.1 Cookie规范说明
这里我们对HTTP协议中的Cookie
规范再补充一下,这里分为两部分,第一部分是服务端如何向客户端发送 Cookie
,第二部分是客户端向服务端发送请求时如何携带Cookie
信息。
对于服务端向客户端发送Cookie
的手段,HTTP协议存在一个Set-Cookie
的头部字段,服务器可以通过Set-Cookie
头部字段将Cookie
发送给客户端。例如下面这个例子:
Set-Cookie: username=abc; expires=Wed, 09 Jun 2023 10:18:14 GMT; path=/
在这个例子中,服务器设置了一个名为username
的Cookie
,它的值是abc
,过期时间是2023年6月9日,路径为/
。浏览器在接收到该Cookie
时,便将其保存起来。
客户端请求时携带Cookie
的方式,则是通过HTTP协议中的Cookie
头部字段,客户端可以通过该头部字段携带信息给服务器端,比如下面这个例子:
Cookie: sessionid=1234
在这个例子中,HTTP请求中携带了一个name
为sessionid
,value
为 1234
的 Cookie
。当服务器端接收到该HTTP
请求后,从中解析出Cookie
的信息,然后基于此实现后续的流程。
3.2 实现说明
回看上述流程,主要分为两个大部分: 客户端和服务器端。在客户端部分,关键任务包括保存浏览器返回的Cookie
信息以及在请求时携带Cookie
信息给服务器。对于服务器端,则是在通过身份校验之后,能够按照规范客户端返回Cookie
,并在接收到请求时,能够正确解析出请求中的 Cookie
信息,识别出用户信息。
对于客户端部分,在浏览器接收到HTTP响应时,如果响应体中有Set-Cookie
头部字段,浏览器会自动保存Cookie
信息;客户端发起请求时,需要将 Cookie
信息传递给服务器。此时浏览器会自动携带通过校验的Cookie
。如果通过校验,此时会在HTTP请求头中携带Cookie
信息给服务端,下面是一个大概的校验流程:
在整个流程中,客户端保存Token
信息和在请求时携带Token
信息这两部分工作,浏览器已经帮我们实现了。剩下的工作集中在服务端的,主要涉及按照Cookie
的规范给客户端返回用户标识,并在接收到客户端请求时从HTTP请求中读取Cookie
以获取到用户的信息。与Cookie
相关的详细内容可以参考文章一文读懂Cookie。
因此下面我们需要做的两件事情,其一,服务器需要按照Cookie
的规范往客户端发送Cookie
的内容;其次,服务器在处理请求时,需要从HTTP请求头中读取出Cookie
的信息,成功识别用户身份。
Gin
框架中提供了一些API
,能够帮助我们在服务端,按照Cookie
规范给客户端发送Cookie
信息,同时也有API
能够帮助我们解析Cookie
的信息。下面我们先来了解相关的API
,然后再基于这些API
,搭建一个能够自动识别用户信息的 Web
应用程序。
3.3 API说明
3.3.1 SetCookie
gin.Context
对象中的 SetCookie
方法用于向客户端返回响应的同时,在Set-Cookie
头部携带Cookie
信息。下面是该方法的详细说明:
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
name
:cookie 的名称(必须)。value
:cookie 的值(必须)。maxAge
:cookie 的过期时间,以秒为单位。如果为负数,则表示会话 cookie(在浏览器关闭之后删除),如果为零,则表示立即删除 cookie(可选,默认值为-1)。path
:cookie 的路径。如果为空字符串,则使用当前请求的 URI 路径作为默认值(可选,默认值为空字符串)。domain
:cookie 的域名。如果为空字符串,则不设置域名(可选,默认值为空字符串)。secure
:指定是否仅通过 HTTPS 连接发送 cookie。如果为 true,则仅通过 HTTPS 连接发送 cookie;否则,使用 HTTP 或 HTTPS 连接都可以发送 cookie(可选,默认值为 false)。httpOnly
:指定 cookie 是否可通过 JavaScript 访问。如果为 true,则无法通过 JavaScript 访问 cookie;否则,可以通过 JavaScript 访问 cookie(可选,默认值为 true)。
在处理函数中,通过调用SetCookie
方法,便可以向客户端发送一个HTTP cookie。这里举一个代码示例,来帮助读者更好得理解该API
,下面举一个代码示例,如下:
func main() {
router := gin.Default()
router.GET("/set-cookie", func(c *gin.Context) {
c.SetCookie("user", "john", 3600, "/", "", false, true)
c.String(http.StatusOK, "cookie set successfully")
})
router.Run(":8080")
}
在这个示例中,使用 SetCookie
方法设置一个名为user
的 cookie。这个 cookie 的值是john
,在 1 小时后过期。该代码还设置了路径为“/”以及HttpOnly属性为true。
下面启动该服务器,客户端向服务端发送请求,请求路径为/set-cookie
,上面的处理函数将会被执行,然后我们来看其响应内容:
# 1. 发送请求
curl -i http://localhost:8080/set-cookie
# 2. 返回响应
HTTP/1.1 200 OK
Content-Type: text/plain;
Set-Cookie: user=john; Path=/; Max-Age=3600; HttpOnly
Date: Sun, 20 Aug 2023 07:39:15 GMT
Content-Length: 23
cookie set successfully
查看上面第6行,可以看到,我们通过SetCookie
方法,成功设置了一个Cookie
,然后以在HTTP头部的形式返回。
3.1.2 Cookie方法
往客户端返回Cookie
后,浏览器会将Cookie
保存起来,然后在下次请求时将Cookie
跟随请求一起发送给服务器端。
在HTTP无状态协议的情况下,我们使用Cookie
来识别用户信息,此时服务器端需要正确解析出HTTP 头部中Cookie
的信息,Gin
框架中的gin.Context
提供了Cookie
方法,方便我们获取到Cookie
的信息。下面是该方法的定义说明:
func (c *Context) Cookie(name string) (string, error)
使用Cookie
方法可以获取指定名称的Cookie值,如果不存在指定名字的Cookie
,此时将会返回错误。下面给一个简单示例代码的说明:
func main() {
router := gin.Default()
// 定义路由
router.GET("/cookie", func(c *gin.Context) {
// 获取名为 "username" 的 cookie
cookie, err := c.Cookie("username")
if err != nil {
// 如果 cookie 不存在,则返回错误信息
c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
return
}
// 在响应中返回 cookie 值
c.JSON(http.StatusOK, gin.H{"username": cookie})
})
router.Run(":8080")
}
在上述示例中,我们定义了一个 /cookie
路由,使用 c.Cookie("username")
方法来获取名为 username
的 Cookie 值。如果 Cookie 不存在,则返回一个错误响应。否则,我们将在响应中返回 Cookie 的值。
下面我们通过curl
命令来对/cookie
请求,通过 -b
标识来携带cookie
值:
# -v, --verbose 这个参数会打开curl的详细模式,输出一些额外的信息,包括HTTP请求和响应头信息。
curl -b -v -b "username=hello cookie;" http://localhost:8080/cookie
下面我们来看具体的请求体和响应体的内容:
GET /cookie HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.79.1
Accept: */*
Cookie: username=hello cookie;
可以看到,我们请求体携带了Cookie
字段,Cookie
的名称为 username
,我们前面服务器端便是尝试获取名为 username
的 Cookie,下面我们看请求的响应体,看是否成功解析了HTTP 请求 Cookie的内容:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 20 Aug 2023 08:12:45 GMT
Content-Length: 27
{"username":"hello cookie"}
可以看到,服务端程序通过Cookie
方法成功解析了HTTP请求头部中Cookie
字段的值,然后将解析的结果正常返回客户端。
3.4 代码实现
下面我们来搭建一个基于Cookie
实现用户身份验证的Web
应用程序,首先需要一个登录页面,用于验证用户身份信息,验证通过后,我们将通过Cookie
给客户端返回一个 Token
。
同时,我们还需要创建一个页面,需要验证用户身份信息,在验证过程中,我们会检查用户请求中是否携带Cookie
,同时Cookie
中携带的数据是否正确,基于此实现用户身份的验证。下面是一个简单代码的示例:
func main() {
route := gin.Default()
route.GET("/login", func(c *gin.Context) {
// HTTP 响应中携带 Cookie
// Set cookie {"label": "ok" }, maxAge 30 seconds.
c.SetCookie("label", "ok", 30, "/", "localhost", false, true)
c.String(200, "Login success!")
})
route.GET("/home", func(c *gin.Context) {
// 获取 name = label 的 Cookie 的 value
if cookie, err := c.Cookie("label"); err == nil {
// 判断 Cookie的value 是否满足预期
if cookie == "ok" {
c.JSON(200, gin.H{"data": "Your home page"})
}
} else {
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden with no cookie"})
}
})
route.Run(":8080")
}
首先是一个/login
请求路由,通过SetCookie
方法给客户端返回Cookie
,基于此返回用户Token
。
然后/home
路由的处理,则是通过gin.Context
中Cookie
方法获取到HTTP请求头部中Cookie
的信息 ,然后验证Cookie
中的value是否满足预期。
这个是一个简单的代码示例,比如用户身份认证机制等,则需要自行完善,这里不再完整展示。
4. 原理
下面将简单描述gin.Context
对象中SetCookie
方法和Cookie
方法的实现原理,帮助读者更好使用这两个API
。
4.1 SetCookie方法
SetCookie
方法的实现原理如下,首先,SetCookie
方法会创建一个http.Cookie
对象,并设置其名称、值、路径、域名、过期时间等属性。例如,以下代码创建了一个名为sessionid
的Cookie
:
cookie := &http.Cookie{
Name: "sessionid",
Value: "1234",
Expires: time.Now().Add(24 * time.Hour),
Path: "/",
Domain: "",
Secure: false,
HttpOnly:true,
}
接下来,将上述Cookie
对象转换为字符串格式,并设置到HTTP响应头的Set-Cookie
字段中。代码实现如下:
func SetCookie(w ResponseWriter, cookie *Cookie) {
if v := cookie.String(); v != "" {
w.Header().Add("Set-Cookie", v)
}
}
这里第三行将Cookie
存储到Header
对象当中,Header
是专门用于存储HTTP响应头部的信息。调用Add
方法时,会根据指定的Key
,在 Header
对象中查找相应的值列表。如果这个键不存在,则会在 Header
对象中创建一个新的值列表;否则,会在已有的值列表末尾添加新的值,大概流程如下:
在返回HTTP响应时,会遍历Header
对象,填充HTTP响应头部信息,然后返回给客户端,比如上面Header
生成的HTTP响应头部如下:
Set-Cookie: v1
Set-Cookie: v2
Agent: Windows
SetCookie
方法便是通过上述所说流程,将Cookie
的信息设置到HTTP响应体头部当中去,然后返回给客户端。
4.2 Cookie方法
在调用 Cookie()
方法时,系统会首先检查请求头部中是否包含名为 Cookie
的字段。如果该字段不存在,则返回空字符串。
如果请求头部中包含 Cookie
字段,同时Cookie
的name
为调用Cookie()
方法指定的值,则系统会解析该字段并将其转换为一个 http.Cookie
对象。这个对象包含了所有的 Cookie
属性,例如名称、值、路径、过期时间、域名等等。最后,返回转换后的http.Cookie
对象中值,大概流程如下:
总的来说,Cookie()
方法的实现原理比较简单,它只是通过查找 HTTP 请求头部中的 Cookie 信息,并将其转换为 http.Cookie
对象来获取请求中特定 Cookie 值。
5. 总结
在本文中,我们深入探讨了Web应用程序在处理用户信息时所面临的挑战,特别是在HTTP协议作为无状态协议的背景下。我们从这一矛盾出发,介绍了解决方案中的Token
机制,并引出了基于Cookie
的实现方案。
我们详细阐述了Cookie
的规范,包括服务端如何发送Cookie
以及客户端如何在请求中携带Cookie
信息。
我们进一步深入探讨了具体的实现方式,并介绍了Gin
框架提供的API
,这些API
使得在服务端按照Cookie
规范发送和解析Cookie变得更加容易。通过一个实际的代码示例,我们演示了如何使用这些API来构建一个基于Cookie实现用户身份验证的Web应用程序。
在探讨API
的使用之余,我们也深入剖析了Gin框架提供的API的实现原理,为读者提供了更深层次的理解。
基于此,完成了对Gin中Cookie支持的介绍,希望对你有所帮助。