基於 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」鏈接,點擊即可退出應用:

下篇教程我們將實現用戶介面的群組和主題增刪改查功能。