• 06-User Login
    • Session
      • 全局变量设置
      • 将 session 操作封装
      • ListenAndServe 修改
  • middleware实现登陆控制
  • 加入register
  • Links

    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

    • 目录
    • 上一节: 05-Database
    • 下一节: 07-Profile-Page-And-Avatar