命令记不住

登陆

Web 编程基础 Cookie

cookie

1.GO的cookie

cookie 的出现是为了解决 HTTP 协议的无状态性的。客户端通过 HTTP 协议与服务器通信,多次请求之间无法记录状态。服务器可以在响应中设置 cookie,客户端保存这些 cookie。然后每次请求时都带上这些 cookie,服务器就可以通过这些 cookie 记录状态,辨别用户身份等。

整个计算机行业的收入都建立在 cookie 机制之上,广告领域更是如此

上面的说法虽然有些夸张,但是可见 cookie 的重要性

我们知道广告是互联网最常见的盈利方式。其中有一个很厉害的广告模式,叫做联盟广告。最常见的就是,刚在百度上搜索某个关键字,然后打开淘宝或京东后发现相关的商品已经被推荐到首页或边栏。这是由于这些网站组成了广告联盟,只要加入它们,就可以共享用户浏览器的cookie数据

Go 中 cookie 使用 http.Cookie 结构表示,在 net/http 包中定义

// src/net/http/cookie.go
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string
}
  • Name/Value : cookie 的键值对,都是字符串类型
  • 没有设置 Expires 字段的 cookie 被称为会话 cookie 或临时 cookie,这种 cookie 在浏览器关闭时就会自动删除。设置了Expires 字段的 cookie 称为持久 cookie,这种 cookie 会一直存在,直到指定的时间来临或手动删除;
  • HttpOnly 字段设置为 true 时,该 cookie 只能通过 HTTP 访问,不能使用其它方式操作,如JavaScript。提高安全性

注意 :

Expires 和 MaxAge 都可以用于设置 cookie 的过期时间。 Expires 字段设置的是 cookie 在什么时间点过期,而 MaxAge 字段表示 cookie 自创建之后能够存活多少秒。虽然 HTTP 1.1 中废弃了Expires ,推荐使用 MaxAge 代替。但是几乎所有的浏览器都仍然支持Expires ;而且微软的IE6/IE7/IE8 都不支持 MaxAge 。所以为了更好的可移植性,可以只使用 Expires 或同时使用这两个字段。

cookie 需要通过响应的首部发送给客户端。浏览器收到 Set-Cookie 首部时,会将其中的值解析成cookie 格式保存在浏览器中。下面我们来具体看看如何设置 cookie:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", setCookie)
	if err := http.ListenAndServe(":8082", nil); err != nil {
		log.Fatal(err)
	}

}

func setCookie(w http.ResponseWriter, r *http.Request) {
	c1 := &http.Cookie{
		Name:     "name",
		Value:    "lianshi",
		HttpOnly: true,
	}
	c2 := &http.Cookie{
		Name:     "age",
		Value:    "18",
		HttpOnly: true,
	}
	w.Header().Set("Set-Cookie", c1.String())
	w.Header().Add("Set-Cookie", c2.String())
}

运行程序,打开浏览器输入 localhost:8080/set_cookie ,在浏览器开发者工具,切换到Application(应用)标签,查看 cookie。在左侧 Cookies 下点击测试的 URL,右侧即可显示我们刚刚设置的 cookie

上面构造 cookie 的代码中,有几点需要注意:

  • 首部名称为 Set-Cookie
  • 首部的值需要是字符串,所以调用了 Cookie 类型的 String 方法将其转为字符串再设置
  • 设置第一个 cookie 调用 Header 类型的 Set 方法,添加第二个 cookie 时调用 Add 方法。 Set 会将同名的键覆盖掉。如果第二个也调用 Set 方法,那么第一个 cookie 将会被覆盖

为了使用的快捷, net/http 包还提供了SetCookie 方法。方法如下:

func setCookie2(w http.ResponseWriter, r *http.Request) {
    c1 := &http.Cookie {
Name: "name",
Value: "lianshi",
HttpOnly: true,
}
    c2 := &http.Cookie {
Name: "age",
Value: "18",
HttpOnly: true,
}
http.SetCookie(w, c1)
http.SetCookie(w, c2)
}
mux.HandleFunc("/set_cookie2", setCookie2)

如果收到的响应中有 cookie 信息,浏览器会将这些 cookie 保存下来。只有没有过期,在向同一个主机发送请求时都会带上这些 cookie。在服务端,我们可以从请求的 Header 字段读取 Cookie 属性来获得cookie:

func getCookie(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Host:", r.Host)
	fmt.Fprintln(w, "Cookies:", r.Header["Cookie"])
}
mux.HandleFunc("/get_cookie", getCookie)

第一次启动服务器,请求 localhost:8080/get_cookie 时,没有 cookie 信息

先请求一次 localhost:8080/set_cookie ,然后再次请求 localhost:8080/get_cookie ,浏览器就将 cookie 传过来了

r.Header["Cookie"] 返回一个切片,这个切片又包含了一个字符串,而这个字符串又包含了客户端发送的任意多个 cookie。如果想要取得单个键值对格式的 cookie,就需要解析这个字符串。 为此, net/http 包在 http.Request 上提供了一些方法使我们更容易地获取 cookie

func GetCookie(w http.ResponseWriter, r *http.Request) {
	name, err := r.Cookie("name")
	if err != nil {
		fmt.Fprintln(w, "cannot get cookie of name")
	}
	cookies := r.Cookies()
	fmt.Fprintln(w, name)
	fmt.Fprintln(w, cookies)
}
mux.HandleFunc("/get_cookies", getCookies2)
  • Cookie 方法返回以传入参数为键的 cookie,如果该 cookie 不存在,则返回一个错误
  • Cookies 方法返回客户端传过来的所有 cookie

有⼀点需要注意, cookie 是与主机名绑定的,不考虑端口。我们上面查看 cookie 的图中有一列Domain 表示的就是主机名。可以这样来验证一下,创建两个服务器,一个绑定在 8080 端口,一个绑定在 8081 端口,先请求 localhost:8080/set_cookie 设置 cookie,然后请求 localhost:8081/get_cookie

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
)

func main() {
	mux1 := http.NewServeMux()
	mux1.HandleFunc("/Set-Cookie", SetCookie)
	mux1.HandleFunc("/Get-Cookie", GetCookie)
	server1 := &http.Server{
		Addr:    ":8083",
		Handler: mux1,
	}
	mux2 := http.NewServeMux()
	mux2.HandleFunc("/Set-Cookie", SetCookie)
	mux2.HandleFunc("/Get-Cookie", GetCookie)
	server2 := &http.Server{
		Addr:    ":8081",
		Handler: mux2,
	}
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		if err := server1.ListenAndServe(); err != nil {
			log.Fatal(err)
		}

	}()
	go func() {
		defer wg.Done()
		if err := server2.ListenAndServe(); err != nil {
			log.Fatal(err)
		}
	}()
	wg.Wait()
}
func GetCookie(w http.ResponseWriter, r *http.Request) {
	name, err := r.Cookie("name")
	if err != nil {
		fmt.Fprintln(w, "cannot get cookie of name")
	}
	cookies := r.Cookies()
	fmt.Fprintln(w, name)
	fmt.Fprintln(w, cookies)
}
func SetCookie(w http.ResponseWriter, r *http.Request) {
	c1 := &http.Cookie{
		Name:     "name",
		Value:    "lianshi",
		HttpOnly: true,
	}
	c2 := &http.Cookie{
		Name:     "age",
		Value:    "18",
		HttpOnly: true,
	}
	w.Header().Set("Set-Cookie", c1.String())
	w.Header().Add("Set-Cookie", c2.String())
}

发送给端口 8081 的请求同样可以获取 cookie

上面代码中,不能直接在主 goroutine 中依次 ListenAndServe 两个服务器。因为 ListenAndServe只有在出错或关闭时才会返回。在此之前,第二个服务器永远得不到机会运行。所以,我创建两个goroutine 各自运行一个服务器,并且使用 sync.WaitGroup 来同步。否则,主 goroutine 运行结束之后,整个程序就退出了

2.梳理一下net/http代码的执行流程

  • 首先调用Http.HandleFunc
    按顺序做了几件事:

    • 调用DefaultServeMux的HandleFunc
    • 调用DefaultServeMux的Handle
    • 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则
  • 其次调用http.ListenAndServe(":8080", nil)
    按顺序做了几件事情:

    • 实例化Server
    • 调用Server的ListenAndServe()
    • 调用net.Listen("tcp", addr)监听端口
    • 启动一个for循环,在循环体中Accept请求
    • 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进入服务go c.serve()
    • 读取每个请求的内容w, err := c.readRequest()
    • 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler), handler就设置为DefaultServeMux
    • 调用handler的ServeHttp
    • 在这个例子中,下面就进入到DefaultServeMux.ServeHttp
    • 根据request选择handler,并且进入到这个handler的ServeHTTP

3.Go Web 的基本形式如下

package main

import (
	"fmt"
	"log"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World")
}

type greetingHandler struct {
	Name string
}

func (h greetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %s", h.Name)
}
func main() {
	mux := http.NewServeMux()
	// 注册处理器函数
	mux.HandleFunc("/hello", helloHandler)
	// 注册处理器
	mux.Handle("/greeting/golang", greetingHandler{Name: "Golang"})
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

4.cookie代码

login.html

<html>
<head>
<title>Go Web</title>
</head>
<body>
<form action="/form?lang=cpp&name=ls" method="post"
enctype="application/x-www-form-urlencoded">
<label>Form:</label>
<input type="text" name="lang" />
<input type="text" name="age" />
<button type="submit">提交</button>
</form>
</body>
</html>

main

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

func main(){
    //当用户访问/login的时候,执行login函数
    http.HandleFunc("/login",loginHandler)
    http.HandleFunc("/home",homeHandler)

    if err := http.ListenAndServe(":8080",nil); err !=nil {
        log.Fatal(err)
    }
}

func loginHandler(w http.ResponseWriter,r * http.Request){
    if r.Method == http.MethodPost{
        username := r.FormValue("username")
        password := r.FormValue("password")
        //对数据进行校验
        fmt.Println(username,password)
        if username == "xiaop" && password == "123456"{
            //登陆成功
            //创建cookie数据
            c1 :=&http.Cookie{
                Name:  "login",
                Value: "true",
                HttpOnly: true,
                //默认关闭浏览器的cookie就失败
                MaxAge: 10,
            }
            //把cookie添加到响应头中
            //w.Header().Set("Set-Cookie",c1.String())
            http.SetCookie(w,c1)
            w.Write([]byte("登陆成功"))
        }else {
            w.Write([]byte("登陆失败"))
        }
    } else {
        b,_ :=ioutil.ReadFile("./login.html")
        w.Write(b)
    }
}

func homeHandler(w http.ResponseWriter,r *http.Request){
    c,err := r.Cookie("login")
    if err !=nil{
        //从当前请求中取不到这个cookie数值
        fmt.Println(err)
        //2.如果不是登陆用户就跳转到登陆页面
        //返回一个跳转到登陆页面的响应
        w.Header().Set("Location","http://127.0.0.1:8080/login")
        w.WriteHeader(http.StatusFound) //必须作为修改响应头的最后一步
        return
    }
    fmt.Println(c.Value)
    fmt.Println("个人中心页面")
}
推荐阅读