基于 Go 语言开发在线论坛(四):通过 Cookie + Session 实现用户认证
- 2020 年 4 月 2 日
- 筆記
1、编写全局辅助函数
在此之前,我们现在 handlers
目录下创建一个 helper.go
文件,用于定义一些全局辅助函数(主要用在处理器中):
package handlers import ( "errors" "fmt" "github.com/xueyuanjun/chitchat/models" "html/template" "net/http" ) // 通过 Cookie 判断用户是否已登录 func session(writer http.ResponseWriter, request *http.Request) (sess models.Session, err error) { cookie, err := request.Cookie("_cookie") if err == nil { sess = models.Session{Uuid: cookie.Value} if ok, _ := sess.Check(); !ok { err = errors.New("Invalid session") } } return } // 解析 HTML 模板(应对需要传入多个模板文件的情况,避免重复编写模板代码) func parseTemplateFiles(filenames ...string) (t *template.Template) { var files []string t = template.New("layout") for _, file := range filenames { files = append(files, fmt.Sprintf("views/%s.html", file)) } t = template.Must(t.ParseFiles(files...)) return } // 生成响应 HTML func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) { var files []string for _, file := range filenames { files = append(files, fmt.Sprintf("views/%s.html", file)) } templates := template.Must(template.ParseFiles(files...)) templates.ExecuteTemplate(writer, "layout", data) } // 返回版本号 func Version() string { return "0.1" }
目前提供了版本信息,判断用户是否登录,HTML 模板的解析与生成等逻辑,我们将 HTML 模板解析与生成逻辑提取出来,主要是为了避免重复编写类似的模板代码,比如现在,我们可以将 handlers/index.go
中的 Index
方法改写如下:
func Index(w http.ResponseWriter, r *http.Request) { threads, err := models.Threads(); if err == nil { generateHTML(w, threads, "layout", "navbar", "index") } }
是不是看起来简单多了,更重要的是提高了代码的复用性。
在 session
函数中,通过从请求中获取指定 Cookie 字段里面存放的 Session ID,然后从 Session 存储器(这里存储驱动是数据库)查询对应 Session 是否存在来判断用户是否已认证,如果已认证则返回的 sess
不为空。
2、用户认证相关处理器
1)编写处理器代码
接下来,在 handlers
目录下创建一个 auth.go
来存放用户认证相关处理器:
package handlers import ( "fmt" "github.com/xueyuanjun/chitchat/models" "net/http" ) // GET /login // 登录页面 func Login(writer http.ResponseWriter, request *http.Request) { t := parseTemplateFiles("auth.layout", "navbar", "login") t.Execute(writer, nil) } // GET /signup // 注册页面 func Signup(writer http.ResponseWriter, request *http.Request) { generateHTML(writer, nil, "auth.layout", "navbar", "signup") } // POST /signup // 注册新用户 func SignupAccount(writer http.ResponseWriter, request *http.Request) { err := request.ParseForm() if err != nil { fmt.Println("Cannot parse form") } user := models.User{ Name: request.PostFormValue("name"), Email: request.PostFormValue("email"), Password: request.PostFormValue("password"), } if err := user.Create(); err != nil { fmt.Println("Cannot create user") } http.Redirect(writer, request, "/login", 302) } // POST /authenticate // 通过邮箱和密码字段对用户进行认证 func Authenticate(writer http.ResponseWriter, request *http.Request) { err := request.ParseForm() user, err := models.UserByEmail(request.PostFormValue("email")) if err != nil { fmt.Println("Cannot find user") } if user.Password == models.Encrypt(request.PostFormValue("password")) { session, err := user.CreateSession() if err != nil { fmt.Println("Cannot create session") } cookie := http.Cookie{ Name: "_cookie", Value: session.Uuid, HttpOnly: true, } http.SetCookie(writer, &cookie) http.Redirect(writer, request, "/", 302) } else { http.Redirect(writer, request, "/login", 302) } } // GET /logout // 用户退出 func Logout(writer http.ResponseWriter, request *http.Request) { cookie, err := request.Cookie("_cookie") if err != http.ErrNoCookie { fmt.Println("Failed to get cookie") session := models.Session{Uuid: cookie.Value} session.DeleteByUUID() } http.Redirect(writer, request, "/", 302) }
上述代码中定义了用户注册、登录、退出相关业务逻辑,非常简单,和 Laravel 认证脚手架生成的默认认证相关控制器非常相似。
2)用户注册
用户注册逻辑比较简单,无非是填写注册表单(Signup
处理器方法),提交注册按钮将用户信息保存到数据库(SignupAccount
处理器方法)。
3)用户登录
接下来,服务端会将用户重定向到登录页面(Login
处理器方法),用户填写登录表单后,就可以通过 Authenticate
处理器方法执行认证操作。
用户认证是基于 Cookie + Session 实现的,Session 的数据结构如下所示:
type Session struct { Id int Uuid string Email string UserId int CreatedAt time.Time }
通过 Uuid
字段可以唯一标识这个 Session,因此可以看作是对外可见的全局 Session ID,在客户端 Cookie 存储的 Session ID 也是这个 Uuid
。当用户认证成功之后,就会创建 Session,有了 Session 之后,就可以创建 Cookie 并写到响应中:
cookie := http.Cookie{ Name: "_cookie", Value: session.Uuid, HttpOnly: true, } http.SetCookie(writer, &cookie)
这样,下次用户访问在线论坛页面就会在请求头中带上包含 Session ID 的 Cookie,服务端通过解析这个 Uuid 并查询 Session 存储器(这里存储驱动是数据库)判断该用户 Session 是否存在,如果存在则用户认证通过,也就是前面辅助函数 session
所做的事情。
4)用户退出
上述 Cookie 未设置过期时间,所以生命周期和 Session 一致,当浏览器关闭时,Cookie 就自动删除,下次打开浏览器需要重新认证。
最后用户退出处理器方法 Logout
方法则是方便用户主动退出,当用户点击退出按钮,可以执行该处理器方法销毁当前用户 Session 和认证 Cookie,并将用户重定向到首页。
3、用户认证相关视图模板
定义好认证处理器后,我们来编写与认证相关的视图模板,主要是登录页面和注册页面,在 views
目录下新增 login.html
编写登录页面:
{{ define "content" }} <form class="form-signin center" role="form" action="/authenticate" method="post"> <h2 class="form-signin-heading"> <i class="fa fa-comments-o"> ChitChat </i> </h2> <input type="email" name="email" class="form-control" placeholder="Email address" required autofocus> <input type="password" name="password" class="form-control" placeholder="Password" required> <br/> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <br/> <a class="lead pull-right" href="/signup">Sign up</a> </form> {{ end }}
然后创建 signup.html
编写注册页面:
{{ define "content" }} <form class="form-signin" role="form" action="/signup_account" method="post"> <h2 class="form-signin-heading"> <i class="fa fa-comments-o"> ChitChat </i> </h2> <div class="lead">Sign up for an account below</div> <input id="name" type="text" name="name" class="form-control" placeholder="Name" required autofocus> <input type="email" name="email" class="form-control" placeholder="Email address" required> <input type="password" name="password" class="form-control" placeholder="Password" required> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button> </form> {{ end }}
此外,我们还为登录和注册页面定义了单独的布局模板 auth.layout.html
:
{{ define "layout" }} <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ChitChat</title> <link href="/static/css/bootstrap.min.css" rel="stylesheet"> <link href="/static/css/font-awesome.min.css" rel="stylesheet"> <link href="/static/css/login.css" rel="stylesheet"> </head> <body> <div class="container"> {{ template "content" . }} </div> <!-- /container --> <script src="/static/js/jquery-2.1.1.min.js"></script> <script src="/static/js/bootstrap.min.js"></script> </body> </html> {{ end }}
以上视图模板已经在认证处理器方法中引用。
4、注册用户认证路由
最后,我们需要在 routes/routes.go
中注册用户认证相关路由:
// 定义所有 Web 路由 var webRoutes = WebRoutes{ ... // 其他路由 { "signup", "GET", "/signup", handlers.Signup, }, { "signupAccount", "POST", "/signup_account", handlers.SignupAccount, }, { "login", "GET", "/login", handlers.Login, }, { "auth", "POST", "/authenticate", handlers.Authenticate, }, { "logout", "GET", "/logout", handlers.Logout, }, }
5、测试用户认证功能
这样一来,我们就可以重启应用并访问用户注册页面 http://localhost:8080/signup
进行注册了:

注册成功后,页面会跳转到登录页面 http://localhost:8080/login
:

输入刚才填写的注册邮箱和密码,点击「SIGN IN」按钮登录成功后,页面跳转到首页。
我们还没有对首页做额外的认证判断和处理,所以此时显示的页面效果和之前一样,为了区别用户认证与未认证状态,我们可以基于认证状态渲染不同的导航模板,对于认证用户,渲染 auth.navbar
模板,对于未认证用户,还是保持和之前一样,为此,我们需要在 views 目录下新增 auth.navbar.html
视图:
{{ define "navbar" }} <div class="navbar navbar-default navbar-static-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/"> <i class="fa fa-comments-o"></i> ChitChat </a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a href="/">Home</a></li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="/logout">Logout</a></li> </ul> </div> </div> </div> {{ end }}
同时还要修改 handlers.Index
处理器方法实现:
func Index(writer http.ResponseWriter, request *http.Request) { threads, err := models.Threads(); if err == nil { _, err := session(writer, request) if err != nil { generateHTML(writer, threads, "layout", "navbar", "index") } else { generateHTML(writer, threads, "layout", "auth.navbar", "index") } } }
再次重启应用,刷新首页,导航条的展示效果就不一样了:

此时显示的是「Logout」链接,点击即可退出应用:

下篇教程我们将实现用户界面的群组和主题增删改查功能。