Cookie使用详解
目录
理论知识
前言
HTTP Cookie(也叫Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie
并在下次同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie
使基于无状态的HTTP协议记录稳定的状态信息成为了可能。
Cookie 主要用于以下三个方面
- 会话状态管理
- 用户登录状态
- 购物车
- 游戏分数
- 个性设置
- 用户自定义设置
- 主题
- 浏览器行为跟踪
- 跟踪分析用户行为
Cookie 曾用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在出现了许多现代存储API。由于服务器指定Cookie后,浏览器的每次请求都会携带Cookie数据,会带来额外的性能开销。目前新型的浏览器API已经开始允许开发者直接将数据存储到本地,如使用localStorage
、sessionStorage
或indexedDB
。
创建Cookie
服务器收到HTTP请求后,服务器可以在相应标头里面添加一个或多个Set-Cookie
选项。浏览器收到响应后通常会保存下Cookie,并将其放在HTTP Cookie
标头内,向同一服务器发出请求时一起发送。可以指定域和路径设置额外的限制,以限制cookie
发送的位置。
简单的说就是后端服务器可以在响应标头里面加上
Set-Cookie
这个特殊字段以及其值,让浏览器保存下来。
Set-Cookie
和Cookie
标头
服务器使用Set-Cookie
响应头部向用户代理(一般指浏览器)发送Cookie信息。一个简单的Cookie
可能像这样子:
Set-Cookie: <cookie-name>=<cookie-value>
cookie 的属性介绍
name 、value
Cookie的内容信息,以 name=value
的形式进行存储,区分大小写。
domain
domain
属性指定浏览器发送HTTP请求时,哪些域名要附带这个Cookie。
可以设置domain
为当前服务器域名或者父域名.如果未设置,则该Cookie
值只能在发向该域名的请求时被携带。如果设置了domain
,则发向该域以及子域的请求都会带上该Cookie
.由于历史原因,存在前缀点的domain
,有前缀点表示允许子域请求携带该cookie
,无前缀点则不允许。目前规范规定忽略前缀点。
path
path
指定浏览器发出HTTP请求时,哪些路径要附带这个Cookie
。如果配置的时候设置了path,那么在该path下的子路径都允许携带该cookie
。
例如,设置Path=/docs
,则以下地址都会匹配:
/docs
/docs/
/docs/Web
但是这些请求路径不会匹配以下地址:
/
/docsets
/api/docs
Expires 、Max-Age
这两个字段用于定义Cookie
的生命周期。Expires
是一个绝对时间,如果没有设置这个属性或者设置为null
,则表示这是一个Session Cookie
.需要注意的是,浏览器是根据本地时间与Expires
对此判断是否过期,而本地时间是可能变动的,所以无法保证Cookie
一定会在服务器指定的时间过期。
Max-Age
是一个相对时间,是在Cookie
失效之前需要经过的秒数。秒数为0或-1将会使Cookie
直接过期。
那么要是当Expires
和Max-Age
同时被设置时,Max-Age
的值将头衔生效。如果都没有指定,那这个Cookie
就是Session Cookie
,一旦用户关闭浏览器,这个Cookie
就会被删除。有些浏览器提供了绘画回复功能,这种情况下即使关闭了浏览器,Session Cookie
也会被保留下来,就好像浏览器从来没有关闭一样,这会导致Cookie
的生命周期无限期延长。
SameSite
SameSite Cookie允许服务器要求某个cookie在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF).总共有三个值:Strict,Lax,None。现在默认为Lax,为None时必须具有Secure
属性。
Secure,HttpOnly
Secure
属性限制该Cookie
只有在HTTPS协议下,才会被发送到服务器。若当前协议时HTTP,则浏览器会忽略服务器发来的Secure
属性。
HttpOnly
属性指定该Cookie
无法通过javaScript脚本获取到,防止恶意脚本的攻击。
Cookie与跨域、安全
在发送跨域AJAX请求时,Cookie
默认不会被发送。如果需要允许跨域请求携带Cookie
,需要在发送请求的时候设置withCredentials
请求头,服务端需要在请求相应中配置CORS
相关头部(Allow-cross-with-credentials:true)。
特殊的情况是,如果AJAX请求配置了withCredentials
,即使响应没有配置CORS
响应标头而被浏览器拦截,响应中的set-cookie
也会生效,可以成功种上Cookie
知识点小结
请求会携带Cookie
的条件
domain
跟path
属性与请求的域名路径匹配,Cookie
才会被携带- 如果有
Secure
属性,请求协议必须时HTTPS - 跨域AJAX请求不会携带
Cookie
,如果需要携带需要配置withCredentials
请求头以及CORS响应头。 - 如果请求跨站了,还要判断
SameSite
属性是否满足发送条件。
实践
相关配置修改
这里讲的环境为Windows系统下
直接快捷键win + r
,将C:\Windows\System32\drivers\etc\hosts
输入其中。选择确定并选择用记事本打开。
在其中的空白行输入:
127.0.0.1 ywhabc.com
表示ywhabc.com
映射了127.0.0.1
.
设置的这个域名,与后面代码中设置Cookie时的
domain
属性相关
现在我们就可以在本机上通过访问ywhabc.com
而达到访问127.0.0.1
达到相同的效果。
代码实践
为了更好的理解,这个运行的机制。采用GO gin框架使用cookie写一个简单的模拟用户登录的功能。
案例目标功能:
- 未登录时,无法成功进行其它请求
- 有登录功能
- 有登出功能
- 用户登录后半小时内无请求操作,取消其登录
采用的工具:
- go gin 框架
- redis (基于docker创建)
操作Redis部分代码
这个之前一篇博客有记录过,不细讲。下面没有对错误抛出进行正确处理。
package main
import (
"context"
"fmt"
"sync"
"time"
"github.com/go-redis/redis/v8"
)
var (
ctx = context.Background()
linkRedisMethod sync.Once
DbRedis *redis.Client
)
func init() {
initRedis()
}
// 连接redis数据库
// 且只会执行一次
func initRedis() {
linkRedisMethod.Do(func() {
//连接数据库
DbRedis = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379", // 对应的ip以及端口号
Password: "", // 数据库的密码
DB: 0, // 数据库的编号,默认的话是0
})
// 连接测活
_, err := DbRedis.Ping(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println("连接Redis成功")
})
}
// 登录成功时写入登录标记
func InsertLoginFlag(key, value string, timeout time.Duration) {
err := DbRedis.Set(ctx, key, value, timeout).Err()
if err != nil {
fmt.Println(err.Error())
}
fmt.Println("insert successfully")
}
// 检查用户是否登录
func CheckLoginFlag(key string) int {
_, err := DbRedis.Get(ctx, key).Result()
if err == redis.Nil {
return 0
} else if err != nil {
return -1
}
return 1
}
// 用户登出时删除登录记录
func DelLoginFlag(key string) {
err := DbRedis.Del(ctx, key)
if err != nil {
fmt.Println(err)
}
}
// 更新用户的登录状态
func UpdateLoginFlag(key string) {
DbRedis.Expire(ctx, key, time.Minute*30)
}
路由部分主体代码
这部分代码主要是用了中间件对用户登录状态进行验证,阻止用户进行非法请求。同时登录、登出、登录成功的测试部分包括了对cookie的设置和验证。
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// 检验用户是否登录的中中间件
func authorVriFMiddle(c *gin.Context) {
if c.Request.URL.Path == "/api/login" {
c.Next()
return
}
cookieKey, err := c.Cookie(LOGINNAME)
// 若有cookie就去数据库中检查确认是否登录
loginState := CheckLoginFlag(cookieKey)
if err != nil || loginState == 0 || loginState == -1 {
c.JSON(http.StatusUnauthorized, gin.H{"message": "访问未授权"})
c.Abort()
} else {
// 如果收到已登录用户的请求,那么就更新已登录用户的登录状态
if c.Request.URL.Path != "/api/logout" {
UpdateLoginFlag(cookieKey)
}
c.Next()
}
}
// 用于检验未登录能否获得数据
func testIsLogin(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "Hello",
})
}
// 用户登录接口
func UserLogin(c *gin.Context) {
data := make(map[string]interface{})
c.BindJSON(&data)
userName := data["username"].(string)
// password := data["password"].(string)
// Do something to check password
// 向数据库中写入已登录标志, 过期时间为半小时
InsertLoginFlag("login-"+userName, "ok", time.Minute*30)
c.SetCookie(LOGINNAME, "login-"+userName, 110000, "/api", ".ywhabc.com", false, false)
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
})
}
// 用户登出接口
func UserLogout(c *gin.Context) {
cookieKey, _ := c.Cookie(LOGINNAME)
DelLoginFlag(cookieKey)
c.SetCookie(LOGINNAME, "", -1, "/api", ".ywhabc.com", false, false)
c.JSON(http.StatusOK, gin.H{
"msg": "成功登出",
})
}
func main() {
router := gin.Default()
t := router.Group("/api")
t.Use(authorVriFMiddle)
t.GET("/test", testIsLogin)
t.POST("/login", UserLogin)
t.GET("/logout", UserLogout)
router.Run(":886")
}
实验过程记录
下面给出用Apifox
模拟用户登录的截图记录(按操作顺序给出)
非法请求数据
用户登录
登录后请求数据
用户登出
验证登出
其它
其实我们是可以看到,保存在浏览器上的Cookie
的.例如Chorme浏览器。
然后搜索一下
小结
本来是最近要做个cas登录项目的,里面用到了Cookie相关东西,所以先学习一下这个部分。