06-User Login

这章我将告诉你如何创建一个用户登录子系统。

你在第四章中学会了如何创建用户登录表单,在第五章中学会了运用数据库。本章将教你如何结合这两章的主题来创建一个简单的用户登录系统。

本章的GitHub链接为: Source, Diff,
Zip

Session

不知道 Go 有没有第三方库实现类似 Flask-Login 这样的登陆辅助(我也没有好好去找),不过我们知道原理,基本上就是用 Session 实现的,对 Session 不了解的同学可以看下 Session and Cookie

这里我们又要引入一个第三方package来实现这个Session(如果不引入第三方库,自己处理还满麻烦的,所以就选择easy way)

  1. $ go get -v github.com/gorilla/sessions

全局变量设置

在 controller/g.go 中设置将要用到的全局变量 sessionName, store,

Tip: 设置全局变量其实是方便以后的更新,如果以后sessionName要改成 flask-mega,只要跑到全局设置的地方修改,不用到每个函数中修改,而且还容易改错。

store 初始化的时候可以设置 secret-key,这里直接 hard code 了,其实安全点的做法可以设置在配置文件里,这里就这样偷懒了吧

controller/g.go

  1. package controller
  2. import (
  3. "html/template"
  4. "github.com/gorilla/sessions"
  5. )
  6. var (
  7. homeController home
  8. templates map[string]*template.Template
  9. sessionName string
  10. store *sessions.CookieStore
  11. )
  12. func init() {
  13. templates = PopulateTemplates()
  14. store = sessions.NewCookieStore([]byte("something-very-secret"))
  15. sessionName = "go-mega"
  16. }
  17. // Startup func
  18. func Startup() {
  19. homeController.registerRoutes()
  20. }

将 session 操作封装

操作函数基本上就是 GetSession, SetSession, ClearSession 所有语言说道Session基本上就实现这三个基本的,后面属于自由发挥

controller/utils.go

  1. ...
  2. // session
  3. func getSessionUser(r *http.Request) (string, error) {
  4. var username string
  5. session, err := store.Get(r, sessionName)
  6. if err != nil {
  7. return "", err
  8. }
  9. val := session.Values["user"]
  10. fmt.Println("val:", val)
  11. username, ok := val.(string)
  12. if !ok {
  13. return "", errors.New("can not get session user")
  14. }
  15. fmt.Println("username:", username)
  16. return username, nil
  17. }
  18. func setSessionUser(w http.ResponseWriter, r *http.Request, username string) error {
  19. session, err := store.Get(r, sessionName)
  20. if err != nil {
  21. return err
  22. }
  23. session.Values["user"] = username
  24. err = session.Save(r, w)
  25. if err != nil {
  26. return err
  27. }
  28. return nil
  29. }
  30. func clearSession(w http.ResponseWriter, r *http.Request) error {
  31. session, err := store.Get(r, sessionName)
  32. if err != nil {
  33. return err
  34. }
  35. session.Options.MaxAge = -1
  36. err = session.Save(r, w)
  37. if err != nil {
  38. return err
  39. }
  40. return nil
  41. }

Tip: 这里 clearSession 的操作是通过设置 MaxAge 为 负数来完成的。

ListenAndServe 修改

main.go

  1. ...
  2. controller.Startup()
  3. http.ListenAndServe(":8888", context.ClearHandler(http.DefaultServeMux))

这样我们就支持了 session 我们在 controller home中来使用

vm/login.go

  1. ...
  2. // CheckLogin func
  3. func CheckLogin(username, password string) bool {
  4. user, err := model.GetUserByUsername(username)
  5. if err != nil {
  6. log.Println("Can not find username: ", username)
  7. log.Println("Error:", err)
  8. return false
  9. }
  10. return user.CheckPassword(password)
  11. }

controller/home.go

  1. ...
  2. func (h home) registerRoutes() {
  3. http.HandleFunc("/", indexHandler)
  4. http.HandleFunc("/login", loginHandler)
  5. http.HandleFunc("/logout", logoutHandler)
  6. }
  7. ...
  8. func loginHandler(w http.ResponseWriter, r *http.Request) {
  9. ...
  10. if !vm.CheckLogin(username, password) {
  11. v.AddError("username password not correct, please input again")
  12. }
  13. if len(v.Errs) > 0 {
  14. templates[tpName].Execute(w, &v)
  15. } else {
  16. setSessionUser(w, r, username)
  17. http.Redirect(w, r, "/", http.StatusSeeOther)
  18. }
  19. }
  20. }
  21. func logoutHandler(w http.ResponseWriter, r *http.Request) {
  22. clearSession(w, r)
  23. http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
  24. }

上面的代码完成了在 login 成功后设置 session,也加入了 logoutHanler

现在我们运行程序后,点击login 之后会发现,session go-mega

06-User-Login - 图1

本小节 Diff

middleware实现登陆控制

一般正常的需要登陆的网站,如果你要访问主页面,如果你没有登陆过,跳转到登陆界面,如果之前登陆过,则直接跳转到你要访问的页面。

现在我们访问根目录http://127.0.0.1/是不受登陆控制的,如果我们要给它加上必须登陆后才能访问,要怎么处理呢?

答案就是加上 middleware 中间层去判断是否存在session,类似于 Python 中的装饰器的作用

controller/middle.go

  1. package controller
  2. import (
  3. "log"
  4. "net/http"
  5. )
  6. func middleAuth(next http.HandlerFunc) http.HandlerFunc {
  7. return func(w http.ResponseWriter, r *http.Request) {
  8. username, err := getSessionUser(r)
  9. log.Println("middle:", username)
  10. if err != nil {
  11. log.Println("middle get session err and redirect to login")
  12. http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
  13. } else {
  14. next.ServeHTTP(w, r)
  15. }
  16. }
  17. }

在路由上加入session 控制

  1. ...
  2. func (h home) registerRoutes() {
  3. http.HandleFunc("/", middleAuth(indexHandler))
  4. http.HandleFunc("/login", loginHandler)
  5. http.HandleFunc("/logout", middleAuth(logoutHandler))
  6. }
  7. func indexHandler(w http.ResponseWriter, r *http.Request) {
  8. tpName := "index.html"
  9. vop := vm.IndexViewModelOp{}
  10. username, _ := getSessionUser(r)
  11. v := vop.GetVM(username)
  12. templates[tpName].Execute(w, &v)
  13. }
  14. ...

现在我们访问到 indexHandler 的时候 middleAuth 保证是有session的,所以取出session,根据取出的user来获取viewmodel响应的,我们的 vm 也要调整

主要是将 User区分下,比如 CurrentUser 以后就代表登陆的用户,也就是Session的User,例如ProfileUser 则可以是你查看的任何人

vm/g.go

  1. package vm
  2. // BaseViewModel struct
  3. type BaseViewModel struct {
  4. Title string
  5. CurrentUser string
  6. }
  7. // SetTitle func
  8. func (v *BaseViewModel) SetTitle(title string) {
  9. v.Title = title
  10. }
  11. // SetCurrentUser func
  12. func (v *BaseViewModel) SetCurrentUser(username string) {
  13. v.CurrentUser = username
  14. }

vm/index.go

  1. ...
  2. // GetVM func
  3. func (IndexViewModelOp) GetVM(username string) IndexViewModel {
  4. u1, _ := model.GetUserByUsername(username)
  5. posts, _ := model.GetPostsByUserID(u1.ID)
  6. v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *posts}
  7. v.SetCurrentUser(username)
  8. return v
  9. }

templates/_base.html

  1. ...
  2. <div>
  3. Blog:
  4. <a href="/">Home</a>
  5. {{if .CurrentUser}}
  6. <a href="/logout">Logout</a>
  7. {{else}}
  8. <a href="/login">Login</a>
  9. {{end}}
  10. </div>
  11. ...

templates/content/index.html

  1. {{define "content"}}
  2. <h1>Hello, {{.CurrentUser}}!</h1>
  3. {{range .Posts}}
  4. <div><p>{{ .User.Username }} says: <b>{{ .Body }}</b></p></div>
  5. {{end}}
  6. {{end}}

然后运行,输入 http://127.0.0.1/ 会直接跳转到登陆页面;在正确输入用户名、密码正确登陆后,右上角变成了 Logout

06-02

本小节 Diff

加入register

templates/content/register.html

  1. {{define "content"}}
  2. <h1>Register</h1>
  3. <form action="/register" method="post" name="register">
  4. <p><input type="text" name="username" value="" placeholder="Username"></p>
  5. <p><input type="text" name="email" value="" placeholder="Email"></p>
  6. <p><input type="password" name="pwd1" value="" placeholder="Password"></p>
  7. <p><input type="password" name="pwd2" value="" placeholder="Password"></p>
  8. <p><input type="submit" name="submit" value="Register"></p>
  9. </form>
  10. <p>Have account? <a href="/login">Click to Login!</a></p>
  11. {{if .Errs}}
  12. <ul>
  13. {{range .Errs}}
  14. <li>{{.}}</li>
  15. {{end}}
  16. </ul>
  17. {{end}}
  18. {{end}}

在 login 的页面上加入 register 链接

templates/content/login.html

  1. ...
  2. <p>New User? <a href="/register">Click to Register!</a></p>
  3. ...

addUser 的调用是 vm调用model的,controller 调用 vm的,所以各层都要建立 AddUser函数

model/user.go

  1. ...
  2. // AddUser func
  3. func AddUser(username, password, email string) error {
  4. user := User{Username: username, Email: email}
  5. user.SetPassword(password)
  6. return db.Create(&user).Error
  7. }

vm/register.go

  1. package vm
  2. import (
  3. "log"
  4. "github.com/bonfy/go-mega-code/model"
  5. )
  6. // RegisterViewModel struct
  7. type RegisterViewModel struct {
  8. LoginViewModel
  9. }
  10. // RegisterViewModelOp struct
  11. type RegisterViewModelOp struct{}
  12. // GetVM func
  13. func (RegisterViewModelOp) GetVM() RegisterViewModel {
  14. v := RegisterViewModel{}
  15. v.SetTitle("Register")
  16. return v
  17. }
  18. // CheckUserExist func
  19. func CheckUserExist(username string) bool {
  20. _, err := model.GetUserByUsername(username)
  21. if err != nil {
  22. log.Println("Can not find username: ", username)
  23. return true
  24. }
  25. return false
  26. }
  27. // AddUser func
  28. func AddUser(username, password, email string) error {
  29. return model.AddUser(username, password, email)
  30. }

controller/utils.go

  1. ...
  2. // Login Check
  3. func checkLen(fieldName, fieldValue string, minLen, maxLen int) string {
  4. lenField := len(fieldValue)
  5. if lenField < minLen {
  6. return fmt.Sprintf("%s field is too short, less than %d", fieldName, minLen)
  7. }
  8. if lenField > maxLen {
  9. return fmt.Sprintf("%s field is too long, more than %d", fieldName, maxLen)
  10. }
  11. return ""
  12. }
  13. func checkUsername(username string) string {
  14. return checkLen("Username", username, 3, 20)
  15. }
  16. func checkPassword(password string) string {
  17. return checkLen("Password", password, 6, 50)
  18. }
  19. func checkEmail(email string) string {
  20. if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, email); !m {
  21. return fmt.Sprintf("Email field not a valid email")
  22. }
  23. return ""
  24. }
  25. func checkUserPassword(username, password string) string {
  26. if !vm.CheckLogin(username, password) {
  27. return fmt.Sprintf("Username and password is not correct.")
  28. }
  29. return ""
  30. }
  31. func checkUserExist(username string) string {
  32. if !vm.CheckUserExist(username) {
  33. return fmt.Sprintf("Username already exist, please choose another username")
  34. }
  35. return ""
  36. }
  37. // checkLogin()
  38. func checkLogin(username, password string) []string {
  39. var errs []string
  40. if errCheck := checkUsername(username); len(errCheck) > 0 {
  41. errs = append(errs, errCheck)
  42. }
  43. if errCheck := checkPassword(password); len(errCheck) > 0 {
  44. errs = append(errs, errCheck)
  45. }
  46. if errCheck := checkUserPassword(username, password); len(errCheck) > 0 {
  47. errs = append(errs, errCheck)
  48. }
  49. return errs
  50. }
  51. // checkRegister()
  52. func checkRegister(username, email, pwd1, pwd2 string) []string {
  53. var errs []string
  54. if pwd1 != pwd2 {
  55. errs = append(errs, "2 password does not match")
  56. }
  57. if errCheck := checkUsername(username); len(errCheck) > 0 {
  58. errs = append(errs, errCheck)
  59. }
  60. if errCheck := checkPassword(pwd1); len(errCheck) > 0 {
  61. errs = append(errs, errCheck)
  62. }
  63. if errCheck := checkEmail(email); len(errCheck) > 0 {
  64. errs = append(errs, errCheck)
  65. }
  66. if errCheck := checkUserExist(username); len(errCheck) > 0 {
  67. errs = append(errs, errCheck)
  68. }
  69. return errs
  70. }
  71. // addUser()
  72. func addUser(username, password, email string) error {
  73. return vm.AddUser(username, password, email)
  74. }

我们顺便将各种后端验证(check function) 都移到了 controller/utils.go,这样便于以后的扩展,而且 controller/home.go 也相应简化了

controller/home.go

  1. package controller
  2. import (
  3. "log"
  4. "net/http"
  5. "github.com/bonfy/go-mega-code/vm"
  6. )
  7. type home struct{}
  8. func (h home) registerRoutes() {
  9. http.HandleFunc("/logout", middleAuth(logoutHandler))
  10. http.HandleFunc("/login", loginHandler)
  11. http.HandleFunc("/register", registerHandler)
  12. http.HandleFunc("/", middleAuth(indexHandler))
  13. }
  14. func indexHandler(w http.ResponseWriter, r *http.Request) {
  15. tpName := "index.html"
  16. vop := vm.IndexViewModelOp{}
  17. username, _ := getSessionUser(r)
  18. v := vop.GetVM(username)
  19. templates[tpName].Execute(w, &v)
  20. }
  21. func loginHandler(w http.ResponseWriter, r *http.Request) {
  22. tpName := "login.html"
  23. vop := vm.LoginViewModelOp{}
  24. v := vop.GetVM()
  25. if r.Method == http.MethodGet {
  26. templates[tpName].Execute(w, &v)
  27. }
  28. if r.Method == http.MethodPost {
  29. r.ParseForm()
  30. username := r.Form.Get("username")
  31. password := r.Form.Get("password")
  32. errs := checkLogin(username, password)
  33. v.AddError(errs...)
  34. if len(v.Errs) > 0 {
  35. templates[tpName].Execute(w, &v)
  36. } else {
  37. setSessionUser(w, r, username)
  38. http.Redirect(w, r, "/", http.StatusSeeOther)
  39. }
  40. }
  41. }
  42. func registerHandler(w http.ResponseWriter, r *http.Request) {
  43. tpName := "register.html"
  44. vop := vm.RegisterViewModelOp{}
  45. v := vop.GetVM()
  46. if r.Method == http.MethodGet {
  47. templates[tpName].Execute(w, &v)
  48. }
  49. if r.Method == http.MethodPost {
  50. r.ParseForm()
  51. username := r.Form.Get("username")
  52. email := r.Form.Get("email")
  53. pwd1 := r.Form.Get("pwd1")
  54. pwd2 := r.Form.Get("pwd2")
  55. errs := checkRegister(username, email, pwd1, pwd2)
  56. v.AddError(errs...)
  57. if len(v.Errs) > 0 {
  58. templates[tpName].Execute(w, &v)
  59. } else {
  60. if err := addUser(username, pwd1, email); err != nil {
  61. log.Println("add User error:", err)
  62. w.Write([]byte("Error insert database"))
  63. return
  64. }
  65. setSessionUser(w, r, username)
  66. http.Redirect(w, r, "/", http.StatusSeeOther)
  67. }
  68. }
  69. }
  70. func logoutHandler(w http.ResponseWriter, r *http.Request) {
  71. clearSession(w, r)
  72. http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
  73. }

运行程序,我们就有了注册页面了

06-03

本小节 Diff