跟其他很多Web应用一样,ChitChat既拥有任何人都可以访问的公开页面,也拥有用户在登录账号之后才能看见的私人页面。
当一个用户成功登录以后,服务器必须在后续的请求中标示出这是一个已登录的用户。为了做到这一点,服务器会在响应的首部中写入一个cookie
,而客户端在接收这个cookie
之后则会把它存储到浏览器里面。代码清单2-4展示了authenticate
处理器函数的实现代码,这个函数定义在route_auth.go
文件中,它的作用就是对用户的身份进行验证,并在验证成功之后向客户端返回一个cookie
。
func authenticate(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
user, _ := data.UserByEmail(r.PostFormValue("email"))
if user.Password == data.Encrypt(r.PostFormValue("password")) {
session := user.CreateSession()
cookie := http.Cookie {
Name: "_cookie",
Value: session.Uuid,
HttpOnly: true,
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", 302)
} else {
http.Redirect(w, r, "/login", 302)
}
}
注意,代码清单2-4中的authenticate
函数使用了两个我们尚未介绍过的函数,一个是data.Encrypt
,而另一个则是data.UserbyEmail
。因为本节关注的是ChitChat论坛的访问控制机制而不是数据处理方法,所以本节将不会对这两个函数的实现细节进行解释,但这两个函数的名字已经很好地说明了它们各自的作用:data.UserByEmail
函数通过给定的电子邮件地址获取与之对应的User
结构,而data.Encrypt
函数则用于加密给定的字符串。本章稍后将会对data包作更详细的介绍,但是在此之前,让我们回到对访问控制机制的讨论上来。
在验证用户身份的时候,程序必须先确保用户是真实存在的,并且提交给处理器的密码在加密之后跟存储在数据库里面的已加密用户密码完全一致。在核实了用户的身份之后,程序会使用User
结构的CreateSession
方法创建一个Session
结构,该结构的定义如下:
type Session struct {
Id int
Uuid string
Email string
UserId int
CreatedAt time.Time
}
Session
结构中的
Email
字段用于存储用户的电子邮件地址
UserId
字段则用于记录用户表中存储用户信息的行的ID
。Uuid
字段存储的是一个随机生成的唯一ID,这个ID是实现会话机制的核心,服务器会通过cookie
把这个ID存储到浏览器里面,并把Session结构中记录的各项信息存储到数据库中。
在创建了Session结构之后,程序又创建了Cookie结构:
cookie := http.Cookie{
Name: "_cookie",
Value: session.Uuid,
HttpOnly: true,
}
cookie的名字是随意设置的,而cookie的值则是将要被存储到浏览器里面的唯一ID。因为程序没有给cookie设置过期时间,所以这个cookie就成了一个会话cookie,它将在浏览器关闭时自动被移除。此外,程序将HttpOnly
字段的值设置成了true
,这意味着这个cookie只能通过HTTP或者HTTPS访问,但是却无法通过JavaScript等非HTTP API进行访问。
在设置好cookie之后,程序使用以下这行代码,将它添加到了响应的首部里面:
http.SetCookie(writer, &cookie)
在将cookie存储到浏览器里面之后,程序接下来要做的就是在处理器函数里面检查当前访问的用户是否已经登录。为此,我们需要创建一个名为session的工具(utility)函数,并在各个处理器函数里面复用它。代码清单2-5展示了session函数的实现代码,跟其他工具函数一样,这个函数也是在util.go
文件里面定义的。再提醒一下,虽然程序把工具函数的定义都放在了util.go
文件里面,但是因为util.go
文件也隶属于main
包,所以这个文件里面定义的所有工具函数都可以直接在整个main
包里面调用,而不必像data.Encrypt
函数那样需要先引入包然后再调用。
代码清单2-5 util.go
文件中的session工具函数
func session(w http.ResponseWriter, r *http.Request)(sess data.Session, err error){
cookie, err := r.Cookie("_cookie") if err == nil {
sess = data.Session{Uuid: cookie.Value}
if ok, _ := sess.Check(); !ok {
err = errors.New("Invalid session")
}
}
return
}
为了从请求中取出cookie,session函数使用了以下代码:
cookie, err := r.Cookie("_cookie")
如果cookie不存在,那么很明显用户并未登录;相反,如果cookie存在,那么session
函数将继续进行第二项检查——访问数据库并核实会话的唯一ID是否存在。第二项检查是通过data.Session
函数完成的,这个函数会从cookie
中取出会话并调用后者的Check
方法:
sess = data.Session{Uuid: cookie.Value}
if ok, _ := sess.Check(); !ok {
err = errors.New("Invalid session")
}
在拥有了检查和识别已登录用户和未登录用户的能力之后,让我们来回顾一下之前展示的index
处理器函数,代码清单2-6中被加粗的代码行展示了这个处理器函数是如何使用session
函数的。
代码清单2-6 index
处理器函数
func index(w http.ResponseWriter, r *http.Request) {
threads, err := data.Threads(); if err == nil {
_, err := session(w, r)//加粗部分
public_tmpl_files := []string{"templates/layout.html",
"templates/public.navbar.html",
"templates/index.html"}
private_tmpl_files := []string{"templates/layout.html",
"templates/private.navbar.html",
"templates/index.html"}
var templates *template.Template
if err != nil {
templates = template.Must(template.ParseFiles(private_tmpl_files...))
} else {
templates = template.Must(template.ParseFiles(public_tmpl_files...))
}
templates.ExecuteTemplate(w, "layout", threads)
}
}
通过调用session
函数可以取得一个存储了用户信息的Session
结构,不过因为index
函数目前并不需要这些信息,所以它使用空白标识符(blank identifier)(_)忽略了这一结构。index
函数真正感兴趣的是err
变量,程序会根据这个变量的值来判断用户是否已经登录,然后以此来选择是使用public导航条还是使用private导航条。
好的,关于ChitChat应用处理请求的方法就介绍到这里了。本章接下来会继续讨论如何为客户端生成HTML,并完整地叙述之前没有说完的部分。