
用戶郵箱地址有效性驗證
在上一節中,我們編寫了用于用戶注冊的代碼。但是當時并沒有為用戶的Email添加有效性驗證,導致Email被設置為任何字符串都能注冊成功。所以在本節初,我們首先來為Email添加一個有效性驗證。
首先還是來編寫測試數據,打開我們上一節創建的test_user_data.json,向其中添加一個測試用的用戶信息。
代碼清單3-1
models/test_user_data.json
[
.
.
. {"email":"email8@exam_ple.com","password": "password8","username": "username8"}
]
我們添加的這個新的用戶信息的Email是一個無效的郵箱地址。接著我們打開user_test.go,也向其中增加一個測試case。
代碼清單3-2
models/user_test.go
func TestUserCreate(t *testing.T) {
.
.
.err = users[7].Create() // 測試Email為無效的郵箱地址的情況if err == nil {t.Error("expected get an error but no error occured.")} else if err.Error() != "Invalid email address" {t.Errorf("expected get "Invalid email address" but got "%s"n", err.Error())}
}
然后打開user.go,我們向其中增加一個isValidEmail()方法用來檢測設置的用戶Email是否有效,然后再在向數據庫插入數據之前,檢查郵箱地址是否有效。如果無效的話,我們返回一個“Invalid email address”的錯誤。
代碼清單3-3
models/user.go
package modelsimport ("errors""regexp""time"
)type User struct {ID intEmail *string `gorm:"not null;unique_index"`Password *string `gorm:"not null"`Username *string `gorm:"not null;unique_index"`Message stringCreatedAt time.TimeUpdatedAt time.Time
}func (u *User) Create() (err error) {if u.Email != nil && !u.isValidEmail() {err = errors.New("Invalid email address")return err}if u.Password == nil {err = errors.New("Error occured when creating user")return err}plain := *u.Passwordencrypt := Encrypt(plain)u.Password = &encrypterr = DB.Create(&u).Errorif err != nil {err = errors.New("Error occured when creating user")}return
}func (u *User) isValidEmail() bool {pat := `(?i)A[w+-.]+@[a-zd-.]+.[a-z]+z`email := u.Emailok, _ := regexp.MatchString(pat, *email)return ok
}
在isValidEmail()方法里,我們使用了正則表達式去匹配Email屬性。如果能匹配上,則返回true,否則就會返回false。所使用的正則表達式的含義如下:

完成后我們新打開一個命令行工具,cd到models文件夾下,運行go test
C:Userssxu37GosrcGoWeb>cd models
C:Userssxu37GosrcGoWebmodels>go test
結果如下:
PASS
ok GoWeb/models 4.101s
測試Pass,說明我們添加的用于測試郵箱地址是否有效的代碼是正確的。
查找用戶
既然我們已經有了創建用戶的功能,接下來,我們就來實現簡單的用戶查找的功能。在創建用戶模型的時候,我們在數據庫中,將用戶的id列設置為了primary key,并且在email和username都創建了index,這就意味著,通過ID、Email和Username這三者我們都可以快速檢索到一個用戶的信息。
首先我們還是來編寫查找用戶的測試代碼。打開user_test.go,向其中增加下面三個函數。
代碼清單3-4
models/user_test
.
.
.
func TestFindUserByID(t *testing.T) {id1, id2 := 5, 4user1, err := FindUserByID(id1)if err == nil {t.Error("expected find no user but got:", user1)}user2, err := FindUserByID(id2)if err != nil {t.Error("got an unexpected error:", err)} else if user2.ID != id2 {t.Errorf("expected find user which id is %d but got user which id is: %dn", id2, user2.ID)} else if user2.Email != nil || user2.Password != nil {t.Error("expected do not get user email and password but got them now")}
}func TestFindUserByEmail(t *testing.T) {email1, email2 := "email6@exam.com", "email5@exam.com"user1, err := FindUserByEmail(email1)if err == nil {t.Error("expected find no user but got:", user1)}user2, err := FindUserByEmail(email2)if err != nil {t.Error("got an unexpected error:", err)} else if *user2.Email != email2 {t.Errorf("expected find user which email is "%s" but got user which email is: "%s"", email2, *user2.Email)}
}func TestFindUserByUsername(t *testing.T) {username1, username2 := "username6", "username5"user1, err := FindUserByUsername(username1)if err == nil {t.Error("expected find no user but got:", user1)}user2, err := FindUserByUsername(username2)if err != nil {t.Error("got an unexpected error:", err)} else if *user2.Username != username2 {t.Errorf("expected find user which username is "%s" but got user which username is: "%s"", username2, *user2.Username)} else if user2.Email != nil || user2.Password != nil {t.Error("expected do not get user email and password but got them now")}
}
在上面的測試代碼中,我們分別測試了通過ID、Email和Username來查找用戶的情況。因為這三個屬性在數據庫中都是唯一的,所以我們每條測試case最多只能找到一條數據。所以我們只測試了找到的這條數據的某一項值是否是我們所期待的值。如果是,就表明這條數據就是我們需要的數據,如果不是,說明我們查找用戶的功能代碼有問題。我們還檢查了查找到的數據中是否包含用戶的Email和Password,如果包含了的話也不能通過測試。
接下來,我們就來編寫查找用戶的代碼。因為在查找用戶之前,我們通常不會事先得到一個用戶的實例,所以我們直接將查找用戶的代碼作為函數來編寫的。另外,我們為FindUserByID()和FindUserByUsername()添加了Select()函數,對查詢到的數據進行篩選,不從數據庫中獲取用戶的email和password信息,以保證用戶賬號的安全。
代碼清單3-5
models/user.go
.
.
.
var QueryKey string = "id, username, message"
func FindUserByID(id int) (user User, err error) {DB.Where("id = ?", id).Select(QueryKey).Find(&user)if user.ID == 0 {err = errors.New("Cannot find such user")}return
}func FindUserByEmail(email string) (user User, err error) {DB.Where("email = ?", email).Find(&user)if user.ID == 0 {err = errors.New("Cannot find such user")}return
}func FindUserByUsername(username string) (user User, err error) {DB.Where("username = ?", username).Select(QueryKey).Find(&user)if user.ID == 0 {err = errors.New("Cannot find such user")}return
}
清空數據庫,然后打開命令行,cd到models目錄下,運行go test。
C:Userssxu37GosrcGoWebmodels>go test
.
.
.
PASS
ok GoWeb/models 3.781s
接下來,我們為查找用戶編寫一個接口。在我們的App中,我們不會將FindUserByEmail()公開。這個函數將只在App內部被使用。FindUserByUsername()可以作為通過用戶名查找用戶的功能被公開,但是我們目前并不打算先實現這個功能。我們首先實現通過ID來查找用戶,然后我們通過訪問類似“users/1”的路徑來獲取某個具體的用戶的信息。我們先編寫測試代碼。向users_controller_test.go中增加如下的測試代碼:
代碼清單3-6
controllers/users_controller_test.go
func TestFindUserByID(t *testing.T) {id := 1controller := UsersController{}user, err := controller.Show(id)if user.ID == 0 || err != nil {t.Error("expected to show user but error occured:", user, err)}
}
這個測試case很簡單,就是指定id為1,看能不能返回id為1的用戶。
接著我們就應用代碼,應用代碼也很簡單。
代碼清單3-7
controllers/users_controller.go
func (c *UsersController) BeforeActivation(b mvc.BeforeActivation) {middleware := func(ctx iris.Context) {ctx.Application().Logger()ctx.Next()}b.Handle("POST", "/users/new", "Create", middleware)b.Handle("GET", "/users/{id:int}", "Show", middleware)
}func (c *UsersController) Create(ctx iris.Context) (user models.User, err error) {...
}func (c *UsersController) Show(id int) (user models.User, err error) {user, err = models.FindUserByID(id)return
}
注意在設置路徑的時候,我們用/users/{id:int}的形式設置了一個動態路徑。int值會通過{id:int}作為id參數傳入到Show()方法中。加入我們訪問/users/1這個路徑,那么訪問就會被轉發到Show()方法并且參數為1,即Show(1)。
清空數據庫,cd到controllers目錄,運行go test
C:Userssxu37GosrcGoWebcontrollers>go test
.
.
.
PASS
ok GoWeb/controllers 3.124s
接著,我們再試一試接口。
代碼清單3-8
main_test.go
.
.
.func TestUsersShowRoute(t *testing.T) {app := weiboApp()e := httptest.New(t, app)request := e.Request("GET", "/users/1")response := request.Expect()response.Status(httptest.StatusOK)
}
C:Userssxu37GosrcGoWeb>go test
.
.
.
PASS
ok GoWeb 2.655s
現在我們不僅能創建用戶,還能根據用戶的ID查詢一個用戶。查詢到用戶之后,我們可以實現對這些用戶的資料進行更新和刪除。
但是僅僅這樣操作就會產生一個問題,任何用戶,只要知道其他用戶的ID,就可以隨意地向服務器發送請求,去更改、刪除其他用戶的信息。這明顯是很不安全的,所以我們需要為更新和刪除操作增加一點安全性。大多數App在進行更新和刪除操作的時候都需要有先進行登錄,有些嚴格的App還需要登錄的用戶擁有這些操作的權限才可以。在這里,我們實現只要用戶登錄就能對自己的賬號進行更新和刪除。
用戶登錄
那么接下來,我們來實現用戶登錄。用戶登錄功能的實現最簡單的方式就是利用session,通過下面幾個步驟來實現:
1. 客戶端通過request將用戶的郵箱和密碼提交到服務器。
2. 服務器取得request的數據后,會首先從數據庫讀取該用戶的信息,并判斷數據庫和request提交上來的信息是否一致。
3. 如果一致的話,服務器將該用戶的信息記錄到session里,并將session的標識返回給客戶端。
4. 客戶端收到session的標識之后將它添加到之后每一次request的header里,服務器通過識別request的header里的session標識,來判斷該客戶端是否已經登錄。
一般還會給服務器的session 設置一個有效期,一旦某個session生成后超過了一個固定期限即視為過期,過期的session也是無效的。
在上面的登錄過程中,我們User模型需要做的就是進行登錄驗證:對客戶端request過來的郵箱和密碼進行驗證,并返回一個布爾值來表示驗證結果是否通過。首先我們來編寫登錄驗證的測試代碼。打開models文件夾,新建一個test_auth_data.json,然后將下列測試數據保存到該文件中。
代碼清單3-9
models/test_auth_data.json
[{"email": "wrongemail4@example.com","password": "password4"},{"email": "email4@exam.com","password": "wrongpassword4"},{"email": "email4@exam.com","password": "password4"}
]
然后打開test_user.go,稍微修改一下setup()函數,然后向其中增加一個TestUserAuthenticate()函數。
代碼清單3-10
models/test_user.go
.
.
.
func setup(filename string) (users []User) {file, _ := os.Open(filename)defer file.Close()data, _ := ioutil.ReadAll(file)json.Unmarshal(data, &users)return users
}func TestUserCreate(t *testing.T) {users := setup("test_user_data.json")
.
.
.}func TestUserAuthenticate(t *testing.T) {users := setup("test_auth_data.json")if _, err := users[0].Authenticate(); err == nil {if err.Error() != "Invalid email or password" {t.Errorf("expected get "Invalid email or password" but got "%s"n", err.Error())}t.Error("expected authentication fail but it passed")}if _, err := users[1].Authenticate(); err == nil {if err.Error() != "Invalid email or password" {t.Errorf("expected get "Invalid email or password" but got "%s"n", err.Error())}t.Error("expected authentication fail but it passed")}if user, err := users[2].Authenticate(); err != nil {t.Error("expected authentication pass but it failed cause:", err)} else if user.ID != 3 {t.Errorf("expected user id to be 3 but got %dn", user.ID)}}
在這份測試代碼中,我們分別測試了三種情況:email不正確、password不正確和兩者都正確。
前兩種情況,我們除了驗證不通過以外,我們還得到了一個Invalid email or password的錯誤信息。只有email和password都正確的時候,我們確認驗證通過,此時錯誤為空。
接下來,我們就為User模型添加Authenticate()方法。
代碼清單3-11
models/user.go
.
.
.
func (u *User) Authenticate() (user User, err error) {user, err = FindUserByEmail(*u.Email)if err != nil {return}if user.ID == 0 || *user.Password != Encrypt(*u.Password) {user = User{}err = errors.New("Invalid email or password")return}return
}
登錄驗證的代碼中,我們通過FindUserByEmail()函數尋找數據庫中email和待驗證的用戶的Email一致的數據。將找到的數據的password和待驗證的用戶的Password進行比較。如果找不到用戶,或者找到的記錄和待驗證的用戶兩者的password不一致,那么我們就判定為驗證失敗,并返回一個Invalid email or password錯誤。如果既能找到記錄,而且password也一致,我們就判定為驗證通過。
登錄驗證的代碼就編寫完成,就可以進行測試了。我們首先還是清空數據庫:
DROP TABLE users;
然后打開命令行工具,cd到models目錄下,運行go test。
C:Userssxu37GosrcGoWebmodels>go test
.
.
.
PASS
ok GoWeb/models 4.417s
說明我們登錄驗證的代碼的行為符合預期。
接下來,我們還需要在控制器里面添加一個Login()方法,我們希望用這個Login()方法來處理用戶登錄。如果用戶登錄驗證成功,Login()方法會返回登錄成功的用戶信息。如果登錄驗證失敗,我們就返回從User#Authenticate()得到的錯誤。
首先我們還是來編寫測試代碼。打開users_controller_test.go,向里面添加下面的函數。
代碼清單3-12
controllers/users_controller_test.go
.
.
.
func TestUserLogin(t *testing.T) {app := iris.New()ctx := context.NewContext(app)// 向新創建的ctx中添加一個ResponseWriter用來寫入session的信息w := context.AcquireResponseWriter()hw := httptest.NewRecorder()w.BeginResponse(hw)ctx.ResetResponseWriter(w)// 向新創建的ctx中添加一個Request并將文件中的數據讀取到Request的Body中file, _ := os.Open("sample_login_user.json")defer file.Close()newRequest, _ := http.NewRequest("POST", "/login", nil)newRequest.ContentLength = 500newRequest.Body = filectx.ResetRequest(newRequest)// 創建一個UsersController的實例,并設置該實例的Session屬性controller := UsersController{}cookie := http.Cookie{Name: "sample_cookie_uuid", Value: ""}ctx.SetCookie(&cookie)sess := sessions.New(sessions.Config{Cookie: "weibo_app_cookie"})controller.Session = sess.Start(ctx)// 調用UsersController實例的Login()方法進行測試user, err := controller.Login(ctx)if err != nil {t.Error("expected no error, but an error occured:", err)}if user.ID != 1 {t.Errorf("expected returned user id to be 1, but got %dn:", user.ID)}id, _ := controller.Session.GetInt("userID")if id != 1 {t.Errorf("expected user id in session to be 1, but got %dn", id)}
}
在上面的代碼中,我們會從一個叫sample_login_user.json中讀取測試數據,然后用測試數據作為參數調用Login()方法模擬登錄。登錄后,我們首先檢查登錄過程有無err以及被登錄的用戶是否是我們的測試用戶,接著我們訪問Session,并用GetInt獲得Session里的userID字段。檢查Session的userID字段的值是否和測試數據的id是一致的。
接著我們還是在controllers目錄下,新建一個sample_login_user.json文件,用來編寫測試數據。
代碼清單3-13
controllers/sample_login_user.json
{"email": "email1@sample.com","password": "password1"
}
接下來,我們打開users_controller.go,為UsersController新增一個Session屬性和一個Login()方法:
代碼清單3-14
controllers/users_controller.go
package controllersimport ("goweb/models""github.com/kataras/iris""github.com/kataras/iris/mvc""github.com/kataras/iris/sessions"
)type UsersController struct {Session *sessions.Session
}
.
.
.
func (c *UsersController) Login(ctx iris.Context) (user models.User, err error) {if err = ctx.ReadJSON(&user); err != nil {ctx.StatusCode(iris.StatusBadRequest)return}if user, err = user.Authenticate(); err != nil {return}c.Session.Set("userID", user.ID)return
}
在Login()方法中,我們讀取客戶端request中的json格式的數據,然后將這些數據映射為一個User實例。調用這個User實例的Authenticate()方法進行驗證,如果驗證能夠通過,我們就將該用戶的ID記錄到session里,并且將驗證通過的用戶保存到前面我們聲明的全局變量里。
接下來,我們在main.go里將session注冊到WeiboApp里。
代碼清單3-15
main.go
package mainimport ("github.com/kataras/iris""github.com/kataras/iris/middleware/logger""github.com/kataras/iris/middleware/recover""github.com/kataras/iris/mvc""github.com/kataras/iris/sessions""goweb/controllers"
)func main() {app := weiboApp()app.Run(iris.Addr(":8080"))
}func weiboApp() *iris.Application {app := iris.New()app.Use(recover.New())app.Use(logger.New())weiboApp := mvc.New(app)expiresTime, _ := time.ParseDuration("168h")sess := sessions.New(sessions.Config{Cookie: "weibo_app_cookie", Expires: expiresTime})weiboApp.Register(sess.Start,)helloWorldController := new(controllers.HelloWorldController)usersController := new(controllers.UsersController)weiboApp.Handle(helloWorldController)weiboApp.Handle(usersController)return app
}
我們在創建session的時候,設置的session的名字“weibo_app_cookie”和過期的時間168個小時,即一個星期。
接下來,我們清空數據庫,打開命令行工具,cd到controllers目錄,運行go test
C:Userssxu37Go>cd srcgowebcontrollers
C:Userssxu37GosrcGoWebcontrollers>go test
.
.
.
PASS
ok GoWeb/controllers 8.059s
說明我們用戶登錄的代碼也是編寫正確的。接著我們只需要再測試一下”/login“這個路徑是否可以用”POST“方法訪問即可。
代碼清單3-16
main_test.go
.
.
.
func TestLoginRoute(t *testing.T) {app := weiboApp()e := httptest.New(t, app)request := e.Request("POST", "/login")request.WithJSON(map[string]interface{}{"email": "email1@example.com", "password": "password1"})response := request.Expect()response.Status(httptest.StatusOK)
}
清空數據庫,然后打開命令行工具,cd到項目根目錄下,運行go test
C:Userssxu37GosrcGoWeb>go test
.
.
.
PASS
ok GoWeb 3.684s
用戶登錄的功能我們也成功實現了。 我們也還是可以使用crul命令測試一下:
C:Userssxu37GosrcGoWeb>curl -i -X POST -d {""email"":""email1@example.co
m"",""password"":""password1""} “http://localhost:8080/login”

服務器按照預期返回了登錄成功的用戶信息。
在開始下一節之前,我們還是先把代碼push到Github上保管。
C:Userssxu37GosrcGoWeb>git add –A
C:Userssxu37GosrcGoWeb>git commit -m "user login"
C:Userssxu37GosrcGoWeb>git push