功能和背景介紹
在專案的登入功能中,如果在登入時發現使用者名稱和密碼在使用者表中不存在,會自動將使用者名稱和密碼儲存在使用者表中,建立一個新的使用者。
因此,除了使用手機號和驗證碼登入以外,還支援使用使用者名稱、密碼進行登入。
如果首次使用手機號和驗證碼進行登入,會預設將手機號作為使用者名稱建立新的使用者,將使用者結構體物件的資料儲存在資料庫中。
因此,我們有必要建立使用者表。
使用者資料結構體定義
在專案中,使用結構體定義使用者資料結構。結構體定義如下所示:
type Member struct {
Id int64 `xorm:"pk autoincr" json:"id"`
UserName string `xorm:"varchar(20)" json:"user_name"`
Mobile string `xorm:"varchar(11)" json:"mobile"`
Password string `xorm:"varchar(255)" json:"password"`
RegisterTime int64 `xorm:"bigint" json:"register_time"`
Avatar string `xorm:"varchar(255)" json:"avatar"`
Balance float64 `xorm:"double" json:"balance"`
IsActive int8 `xorm:"tinyint" json:"is_active"`
City string `xorm:"varchar(10)" json:"city"`
}
通過定義Member結構體,表示應用的使用者資訊。通過TAG中的xorm來指定結構體在資料庫表中的約束。
ORM對映
通過engine.Sync2方法將Member同步對映成為資料庫中的member表:
err = engine.Sync2(new(model.Member),
new(model.SmsCode))
if err != nil {
return nil,err
}
插入資料
當使用者獲取完驗證碼,並填寫驗證碼以後,使用者點選登入,會發起登入請求。因此,我們需要來完成登入相關的邏輯操作和處理。使用者手機號碼和驗證碼登入的介面是api/login_sms,因此我們在已經建立的MemberController中解析簡訊驗證碼介面。如下所示:
func (mc *MemberController) Router(engine *gin.Engine) {
...
//傳送手機驗證碼
engine.GET("/api/sendcode", mc.sendSmsCode)
//手機號和簡訊登入
engine.OPTIONS("/api/login_sms", mc.smsLogin)
}
在MemberController中建立smsLogin方法完成使用者手機號和密碼登入的邏輯,詳細實現如下:
//簡訊登入
func (mc *MemberController) smsLogin(context *gin.Context) {
var smsParam param.SmsLoginParam
err := toolbox.Decode(context.Request.Body, &smsParam)
fmt.Println(err.Error())
fmt.Println(context.PostForm("phone"))
fmt.Println(context.Query("code"))
if err != nil {
toolbox.Failed(context, "引數解析錯誤")
return
}
us := service.NewMemberService()
member := us.SmsLogin(smsParam)
if member != nil {
toolbox.Success(context, member)
return
}
toolbox.Failed(context, "登入失敗")
}
使用者服務層
在MemberService.go檔案中,編寫SmsLogin方法完成手機號和密碼登入。
func (msi *MemberService) SmsLogin(param param.SmsLoginParam) *model.Member {
dao := dao.NewMemberDao()
sms := dao.ValidateSmsCode(param.Phone, param.Code)
if sms == nil || time.Now().Unix()-sms.CreateTime > 300 {
return nil
}
member := dao.QueryByPhone(param.Phone)
if member != nil {
return member
}
user := model.Member{}
user.UserName = param.Phone
user.Mobile = param.Phone
user.RegisterTime = time.Now().Unix()
user.Id = dao.InsertMember(user)
return &user
}
在MemberService中,首先驗證手機號和驗證碼是否正確。如果通過了手機號和驗證碼的驗證,通過手機號查詢使用者是否已經存在。如果使用者記錄不存在,則建立新的使用者記錄並儲存到資料庫中,如果使用者記錄已經存在,則表示登入成功,返回使用者資訊。
資料庫操作的MemberDao實現如下
在MemberDao中,實現使用者模組的資料庫操作。
首先是手機驗證碼驗證功能,如下所示:
func (md *MemberDao) ValidateSmsCode(phone string, code string) *model.SmsCode {
var sms model.SmsCode
if err := md.Where(" phone = ? and code = ? ", phone, code).Find(&sms); err != nil {
toolbox.Error(err.Error())
}
return &sms
}
其次是根據手機號查詢使用者資料庫表中是否存在手機號對應的使用者,如下所示:
func (md *MemberDao) QueryByPhone(phone string) *model.Member {
var member model.Member
if err := md.Where(" phone = ? ", phone).Find(&member); err != nil {
toolbox.Error(err.Error())
}
return &member
}
最後,對於新手機號,新建使用者,插入到資料庫中:
func (md *MemberDao) InsertMember(member model.Member) int64 {
result, err := md.InsertOne(&member)
if err != nil {
toolbox.Error(err.Error())
}
return result
}
跨域
我們專案是使用gin開發一個介面專案,前端是使用vue+webpack進行開發和編譯執行的。
可以通過如下命令執行為大家提供的前端工程程式碼,在前端專案的根目錄下執行:
npm run dev
在瀏覽器中訪問http://localhost:8080即可進入應用的首頁,切換到使用者登入介面。
同時後端程式的執行埠是8090。
當使用我們上面兩節課已經開發完成的手機號+驗證碼的方式進行使用者登入時。會發現遇到一個問題,如下圖所示:
之前我們已經開發完成了手機號+驗證碼登入的功能,並且使用Postman已經測試成功了,為什麼現在在瀏覽器中會出現這個問題呢?
跨域訪問的問題
先了解一下什麼是跨域訪問。
在瀏覽器中的任意一個頁面地址,或者訪問後臺的api介面url,其實都包含幾個相同的部分:
/*
* 1、通訊協議:又稱protocol,有很多通訊協議,比如http, tcp/ip協議等等。
* 2、主機:也就是常說的host。
* 3、埠:即服務所監聽的埠號。
* 4、資源路徑:埠號後面的內容即是路徑。
*/
當在一個頁面中發起一個新的請求時,如果通訊協議、主機和埠,這三部分內容中的任意一個與原頁面的不相同,就被稱之為跨域訪問。
如,在gin介面專案中,前端使用nodejs開發,執行在8080埠,我們訪問的應用首頁是:http://localhost:8080。 在使用gin框架開發的api專案中,服務端的監聽埠為8090。
一個埠數8080,一個是8090,兩者埠不同,因此按照規定,發生了跨域訪問。
OPTIONS請求
如上文所述,前端vue開發的功能,使用axios傳送POST登入請求。在請求時發生了跨域訪問,因此瀏覽器為了安全起見,會首先發起一個請求測試一下此次訪問是否安全,這種測試的請求型別為OPTIONS,又稱之為options嗅探,同時在header中會帶上origin,用來判斷是否有跨域請求許可權。
然後伺服器相應Access-Control-Allow-Origin的值,該值會與瀏覽器的origin值進行匹配,如果能夠匹配通過,則表示有跨域訪問的許可權。
跨域訪問許可權檢查通過,會正式傳送POST請求。
服務端設定跨域訪問
可以在gin服務端,編寫程式進行全域性設定。通過中介軟體的方式設定全域性跨域訪問,用以返回Access-Control-Allow-Origin和瀏覽器進行匹配。
在服務端編寫跨域訪問中介軟體,詳細內容如下:
func Cors() gin.HandlerFunc {
return func(context *gin.Context) {
method := context.Request.Method
origin := context.Request.Header.Get("Origin")
var headerKeys []string
for k, _ := range context.Request.Header {
headerKeys = append(headerKeys, k)
}
headerStr := strings.Join(headerKeys, ",")
if headerStr != "" {
headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
} else {
headerStr = "access-control-allow-origin, access-control-allow-headers"
}
if origin != "" {
context.Writer.Header().Set("Access-Control-Allow-Origin", "*")
context.Header("Access-Control-Allow-Origin", "*") // 設定允許訪問所有域
context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
context.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
context.Header("Access-Control-Max-Age", "172800")
context.Header("Access-Control-Allow-Credentials", "false")
context.Set("content-type", "application/json") //// 設定返回格式是json
}
if method == "OPTIONS" {
context.JSON(http.StatusOK, "Options Request!")
}
//處理請求
context.Next()
}
}
其中的Access-Control-Allow-Origin的設定,表示允許進行跨域訪問,*表示可以訪問所有域。同時,通過Header方法進行了其他的設定。
最後context.Next()是中介軟體使用的標準用法,表示繼續處理請求。
伺服器設定跨域呼叫
在main函式中,呼叫編寫好的跨域訪問。呼叫如下:
func main(){
...
app := gin.Default()
app.Use(Cors())
...
}
/*
呼叫app.Use方法,設定跨域訪問
*/
功能演示
伺服器設定好跨域訪問以後,重新啟動伺服器api程式,並在瀏覽器端重新訪問。可以看到正常傳送了OPTIONS嗅探後,正常傳送了POST請求。如下圖所示: