本文将从Web应用程序处理请求时需要用户信息,同时HTTP又是无状态协议这个矛盾点出发。从该问题出发,简单描述了解决该问题的Token
机制,进而引出Cookie
的实现方案。
基于此我们将详细描述Cookie
的规范,然后详细描述具体的实现方式,进一步描述Gin
框架对Cookie
操作提供的API
,最终提供了一个详细的代码实现。
我们还将详细描述Gin
框架提供API
的实现原理,帮助用户更好得使用这两个API
。
在 如何使用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
的解析。
这里我们对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
的信息,然后基于此实现后续的流程。
回看上述流程,主要分为两个大部分: 客户端和服务器端。在客户端部分,关键任务包括保存浏览器返回的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
应用程序。
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; charset=utf-8
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头部的形式返回。
往客户端返回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
字段的值,然后将解析的结果正常返回客户端。
下面我们来搭建一个基于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是否满足预期。
这个是一个简单的代码示例,比如用户身份认证机制等,则需要自行完善,这里不再完整展示。
下面将简单描述gin.Context
对象中SetCookie
方法和Cookie
方法的实现原理,帮助读者更好使用这两个API
。
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响应体头部当中去,然后返回给客户端。
在调用 Cookie()
方法时,系统会首先检查请求头部中是否包含名为 Cookie
的字段。如果该字段不存在,则返回空字符串。
如果请求头部中包含 Cookie
字段,同时Cookie
的name
为调用Cookie()
方法指定的值,则系统会解析该字段并将其转换为一个 http.Cookie
对象。这个对象包含了所有的 Cookie
属性,例如名称、值、路径、过期时间、域名等等。最后,返回转换后的http.Cookie
对象中值,大概流程如下:
总的来说,Cookie()
方法的实现原理比较简单,它只是通过查找 HTTP 请求头部中的 Cookie 信息,并将其转换为 http.Cookie
对象来获取请求中特定 Cookie 值。
在本文中,我们深入探讨了Web应用程序在处理用户信息时所面临的挑战,特别是在HTTP协议作为无状态协议的背景下。我们从这一矛盾出发,介绍了解决方案中的Token
机制,并引出了基于Cookie
的实现方案。
我们详细阐述了Cookie
的规范,包括服务端如何发送Cookie
以及客户端如何在请求中携带Cookie
信息。
我们进一步深入探讨了具体的实现方式,并介绍了Gin
框架提供的API
,这些API
使得在服务端按照Cookie
规范发送和解析Cookie变得更加容易。通过一个实际的代码示例,我们演示了如何使用这些API来构建一个基于Cookie实现用户身份验证的Web应用程序。
在探讨API
的使用之余,我们也深入剖析了Gin框架提供的API的实现原理,为读者提供了更深层次的理解。
基于此,完成了对Gin中Cookie支持的介绍,希望对你有所帮助。