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
Copied!

全局变量设置

在 controller/g.go 中设置将要用到的全局变量 sessionName, store,
Tip: 设置全局变量其实是方便以后的更新,如果以后sessionName要改成 flask-mega,只要跑到全局设置的地方修改,不用到每个函数中修改,而且还容易改错。
store 初始化的时候可以设置 secret-key,这里直接 hard code 了,其实安全点的做法可以设置在配置文件里,这里就这样偷懒了吧
controller/g.go
1
package controller
2
3
import (
4
"html/template"
5
6
"github.com/gorilla/sessions"
7
)
8
9
var (
10
homeController home
11
templates map[string]*template.Template
12
sessionName string
13
store *sessions.CookieStore
14
)
15
16
func init() {
17
templates = PopulateTemplates()
18
store = sessions.NewCookieStore([]byte("something-very-secret"))
19
sessionName = "go-mega"
20
}
21
22
// Startup func
23
func Startup() {
24
homeController.registerRoutes()
25
}
Copied!

将 session 操作封装

操作函数基本上就是 GetSession, SetSession, ClearSession 所有语言说道Session基本上就实现这三个基本的,后面属于自由发挥
controller/utils.go
1
...
2
3
// session
4
5
func getSessionUser(r *http.Request) (string, error) {
6
var username string
7
session, err := store.Get(r, sessionName)
8
if err != nil {
9
return "", err
10
}
11
12
val := session.Values["user"]
13
fmt.Println("val:", val)
14
username, ok := val.(string)
15
if !ok {
16
return "", errors.New("can not get session user")
17
}
18
fmt.Println("username:", username)
19
return username, nil
20
}
21
22
func setSessionUser(w http.ResponseWriter, r *http.Request, username string) error {
23
session, err := store.Get(r, sessionName)
24
if err != nil {
25
return err
26
}
27
session.Values["user"] = username
28
err = session.Save(r, w)
29
if err != nil {
30
return err
31
}
32
return nil
33
}
34
35
func clearSession(w http.ResponseWriter, r *http.Request) error {
36
session, err := store.Get(r, sessionName)
37
if err != nil {
38
return err
39
}
40
41
session.Options.MaxAge = -1
42
43
err = session.Save(r, w)
44
if err != nil {
45
return err
46
}
47
48
return nil
49
}
Copied!
Tip: 这里 clearSession 的操作是通过设置 MaxAge 为 负数来完成的。

ListenAndServe 修改

main.go
1
...
2
controller.Startup()
3
4
http.ListenAndServe(":8888", context.ClearHandler(http.DefaultServeMux))
Copied!
这样我们就支持了 session 我们在 controller home中来使用
vm/login.go
1
...
2
3
// CheckLogin func
4
func CheckLogin(username, password string) bool {
5
user, err := model.GetUserByUsername(username)
6
if err != nil {
7
log.Println("Can not find username: ", username)
8
log.Println("Error:", err)
9
return false
10
}
11
return user.CheckPassword(password)
12
}
Copied!
controller/home.go
1
...
2
3
func (h home) registerRoutes() {
4
http.HandleFunc("/", indexHandler)
5
http.HandleFunc("/login", loginHandler)
6
http.HandleFunc("/logout", logoutHandler)
7
}
8
9
...
10
11
func loginHandler(w http.ResponseWriter, r *http.Request) {
12
...
13
14
if !vm.CheckLogin(username, password) {
15
v.AddError("username password not correct, please input again")
16
}
17
18
if len(v.Errs) > 0 {
19
templates[tpName].Execute(w, &v)
20
} else {
21
setSessionUser(w, r, username)
22
http.Redirect(w, r, "/", http.StatusSeeOther)
23
}
24
}
25
}
26
27
func logoutHandler(w http.ResponseWriter, r *http.Request) {
28
clearSession(w, r)
29
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
30
}
Copied!
上面的代码完成了在 login 成功后设置 session,也加入了 logoutHanler
现在我们运行程序后,点击login 之后会发现,session go-mega
本小节 Diff

middleware实现登陆控制

一般正常的需要登陆的网站,如果你要访问主页面,如果你没有登陆过,跳转到登陆界面,如果之前登陆过,则直接跳转到你要访问的页面。
现在我们访问根目录http://127.0.0.1/是不受登陆控制的,如果我们要给它加上必须登陆后才能访问,要怎么处理呢?
答案就是加上 middleware 中间层去判断是否存在session,类似于 Python 中的装饰器的作用
controller/middle.go
1
package controller
2
3
import (
4
"log"
5
"net/http"
6
)
7
8
func middleAuth(next http.HandlerFunc) http.HandlerFunc {
9
return func(w http.ResponseWriter, r *http.Request) {
10
username, err := getSessionUser(r)
11
log.Println("middle:", username)
12
if err != nil {
13
log.Println("middle get session err and redirect to login")
14
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
15
} else {
16
next.ServeHTTP(w, r)
17
}
18
}
19
}
Copied!
在路由上加入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
8
func indexHandler(w http.ResponseWriter, r *http.Request) {
9
tpName := "index.html"
10
vop := vm.IndexViewModelOp{}
11
username, _ := getSessionUser(r)
12
v := vop.GetVM(username)
13
templates[tpName].Execute(w, &v)
14
}
15
...
Copied!
现在我们访问到 indexHandler 的时候 middleAuth 保证是有session的,所以取出session,根据取出的user来获取viewmodel响应的,我们的 vm 也要调整
主要是将 User区分下,比如 CurrentUser 以后就代表登陆的用户,也就是Session的User,例如ProfileUser 则可以是你查看的任何人
vm/g.go
1
package vm
2
3
// BaseViewModel struct
4
type BaseViewModel struct {
5
Title string
6
CurrentUser string
7
}
8
9
// SetTitle func
10
func (v *BaseViewModel) SetTitle(title string) {
11
v.Title = title
12
}
13
14
// SetCurrentUser func
15
func (v *BaseViewModel) SetCurrentUser(username string) {
16
v.CurrentUser = username
17
}
Copied!
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
}
Copied!
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
...
Copied!
templates/content/index.html
1
{{define "content"}}
2
<h1>Hello, {{.CurrentUser}}!</h1>
3
4
{{range .Posts}}
5
<div><p>{{ .User.Username }} says: <b>{{ .Body }}</b></p></div>
6
{{end}}
7
{{end}}
Copied!
然后运行,输入 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
11
<p>Have account? <a href="/login">Click to Login!</a></p>
12
13
{{if .Errs}}
14
<ul>
15
{{range .Errs}}
16
<li>{{.}}</li>
17
{{end}}
18
</ul>
19
{{end}}
20
{{end}}
Copied!
在 login 的页面上加入 register 链接
templates/content/login.html
1
...
2
<p>New User? <a href="/register">Click to Register!</a></p>
3
...
Copied!
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
}
Copied!
vm/register.go
1
package vm
2
3
import (
4
"log"
5
6
"github.com/bonfy/go-mega-code/model"
7
)
8
9
// RegisterViewModel struct
10
type RegisterViewModel struct {
11
LoginViewModel
12
}
13
14
// RegisterViewModelOp struct
15
type RegisterViewModelOp struct{}
16
17
// GetVM func
18
func (RegisterViewModelOp) GetVM() RegisterViewModel {
19
v := RegisterViewModel{}
20
v.SetTitle("Register")
21
return v
22
}
23
24
// CheckUserExist func
25
func CheckUserExist(username string) bool {
26
_, err := model.GetUserByUsername(username)
27
if err != nil {
28
log.Println("Can not find username: ", username)
29
return true
30
}
31
return false
32
}
33
34
// AddUser func
35
func AddUser(username, password, email string) error {
36
return model.AddUser(username, password, email)
37
}
Copied!
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
14
func checkUsername(username string) string {
15
return checkLen("Username", username, 3, 20)
16
}
17
18
func checkPassword(password string) string {
19
return checkLen("Password", password, 6, 50)
20
}
21
22
func checkEmail(email string) string {
23
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4}) 06-User-Login - go-mega , email); !m {
24
return fmt.Sprintf("Email field not a valid email")
25
}
26
return ""
27
}
28
29
func checkUserPassword(username, password string) string {
30
if !vm.CheckLogin(username, password) {
31
return fmt.Sprintf("Username and password is not correct.")
32
}
33
return ""
34
}
35
36
func checkUserExist(username string) string {
37
if !vm.CheckUserExist(username) {
38
return fmt.Sprintf("Username already exist, please choose another username")
39
}
40
return ""
41
}
42
43
// checkLogin()
44
func checkLogin(username, password string) []string {
45
var errs []string
46
if errCheck := checkUsername(username); len(errCheck) > 0 {
47
errs = append(errs, errCheck)
48
}
49
if errCheck := checkPassword(password); len(errCheck) > 0 {
50
errs = append(errs, errCheck)
51
}
52
if errCheck := checkUserPassword(username, password); len(errCheck) > 0 {
53
errs = append(errs, errCheck)
54
}
55
return errs
56
}
57
58
// checkRegister()
59
func checkRegister(username, email, pwd1, pwd2 string) []string {
60
var errs []string
61
if pwd1 != pwd2 {
62
errs = append(errs, "2 password does not match")
63
}
64
if errCheck := checkUsername(username); len(errCheck) > 0 {
65
errs = append(errs, errCheck)
66
}
67
if errCheck := checkPassword(pwd1); len(errCheck) > 0 {
68
errs = append(errs, errCheck)
69
}
70
if errCheck := checkEmail(email); len(errCheck) > 0 {
71
errs = append(errs, errCheck)
72
}
73
if errCheck := checkUserExist(username); len(errCheck) > 0 {
74
errs = append(errs, errCheck)
75
}
76
return errs
77
}
78
79
// addUser()
80
func addUser(username, password, email string) error {
81
return vm.AddUser(username, password, email)
82
}
Copied!
我们顺便将各种后端验证(check function) 都移到了 controller/utils.go,这样便于以后的扩展,而且 controller/home.go 也相应简化了
controller/home.go
1
package controller
2
3
import (
4
"log"
5
"net/http"
6
7
"github.com/bonfy/go-mega-code/vm"
8
)
9
10
type home struct{}
11
12
func (h home) registerRoutes() {
13
http.HandleFunc("/logout", middleAuth(logoutHandler))
14
http.HandleFunc("/login", loginHandler)
15
http.HandleFunc("/register", registerHandler)
16
http.HandleFunc("/", middleAuth(indexHandler))
17
}
18
19
func indexHandler(w http.ResponseWriter, r *http.Request) {
20
tpName := "index.html"
21
vop := vm.IndexViewModelOp{}
22
username, _ := getSessionUser(r)
23
v := vop.GetVM(username)
24
templates[tpName].Execute(w, &v)
25
}
26
27
func loginHandler(w http.ResponseWriter, r *http.Request) {
28
tpName := "login.html"
29
vop := vm.LoginViewModelOp{}
30
v := vop.GetVM()
31
32
if r.Method == http.MethodGet {
33
templates[tpName].Execute(w, &v)
34
}
35
if r.Method == http.MethodPost {
36
r.ParseForm()
37
username := r.Form.Get("username")
38
password := r.Form.Get("password")
39
40
errs := checkLogin(username, password)
41
v.AddError(errs...)
42
43
if len(v.Errs) > 0 {
44
templates[tpName].Execute(w, &v)
45
} else {
46
setSessionUser(w, r, username)
47
http.Redirect(w, r, "/", http.StatusSeeOther)
48
}
49
}
50
}
51
52
func registerHandler(w http.ResponseWriter, r *http.Request) {
53
tpName := "register.html"
54
vop := vm.RegisterViewModelOp{}
55
v := vop.GetVM()
56
57
if r.Method == http.MethodGet {
58
templates[tpName].Execute(w, &v)
59
}
60
if r.Method == http.MethodPost {
61
r.ParseForm()
62
username := r.Form.Get("username")
63
email := r.Form.Get("email")
64
pwd1 := r.Form.Get("pwd1")
65
pwd2 := r.Form.Get("pwd2")
66
67
errs := checkRegister(username, email, pwd1, pwd2)
68
v.AddError(errs...)
69
70
if len(v.Errs) > 0 {
71
templates[tpName].Execute(w, &v)
72
} else {
73
if err := addUser(username, pwd1, email); err != nil {
74
log.Println("add User error:", err)
75
w.Write([]byte("Error insert database"))
76
return
77
}
78
setSessionUser(w, r, username)
79
http.Redirect(w, r, "/", http.StatusSeeOther)
80
}
81
}
82
}
83
84
func logoutHandler(w http.ResponseWriter, r *http.Request) {
85
clearSession(w, r)
86
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
87
}
Copied!
运行程序,我们就有了注册页面了
06-03
本小节 Diff

Links