歡迎關(guān)注我的專欄( つ??ω??)つ【人工智能通識(shí)】
【匯總】2019年4月專題
如何實(shí)現(xiàn)用戶自動(dòng)登錄?

上一篇文章,軟件技術(shù)-零基礎(chǔ)-Golang注冊(cè)驗(yàn)證與忘記密碼
人工智能通識(shí)-2019年3月專題匯總
Cookie
瀏覽器其實(shí)可以幫助網(wǎng)站記錄我們?yōu)g覽的信息,包括用戶名,密碼,或者上一次滾動(dòng)頁面的位置,或者任何網(wǎng)站開發(fā)者希望記錄的信息。
這些信息其實(shí)就是很多小文件,瀏覽器為每個(gè)網(wǎng)站配一個(gè)小文件,用來記錄用戶瀏覽信息,而到底要記錄什么,則由網(wǎng)站的開發(fā)者來決定。
這些小文件有個(gè)可愛的名字,就叫做Cookie小甜餅。
Token
如果當(dāng)用戶第一次登錄成功的時(shí)候,我們就把用戶名和密碼放在Cookie里面,然后每次頁面打開都自動(dòng)用script執(zhí)行post登錄,這樣可以嗎?
可以的。但把用戶密碼放在Cookie里面很不安全,隨便誰獲得了這個(gè)電腦都能從網(wǎng)頁里查看到Cookie,所以你絕對(duì)不希望網(wǎng)站開發(fā)者把你的銀行卡密碼放在Cookie里面。

另外一個(gè)方法就比較好些。
當(dāng)用戶登錄成功的時(shí)候,我們用Golang為用戶生成一個(gè)特殊的額唯一數(shù)字令牌Token,然后把這個(gè)數(shù)字Token放在Cookie里面,當(dāng)用戶把這個(gè)數(shù)字發(fā)送給Golang服務(wù)器程序的時(shí)候,我們?cè)儆眠@個(gè)數(shù)字找到對(duì)應(yīng)的用戶名和密碼,這樣我們就知道他又回來了。
這樣的數(shù)字我們之前在代碼里使用過,比如注冊(cè)和找回密碼時(shí)候返回的那個(gè)_id數(shù)字。
但還有一個(gè)問題,這個(gè)id是數(shù)據(jù)庫user里面固定的數(shù)字,如果用戶在新的電腦上重新用密碼登錄了,那么舊電腦和新電腦的Token就一樣的,而且能同時(shí)登錄,用戶只能跑回去舊電腦注銷才可以清除,以防止其他人冒用?!@很糟糕。
如果用戶每次手工密碼登錄,我們就為他生成一個(gè)新的Token,問題就解決了。
UUID
通用唯一識(shí)別碼(英語:Universally Unique Identifier,UUID),是用于計(jì)算機(jī)體系中以識(shí)別信息數(shù)目的一個(gè)128位標(biāo)識(shí)符,還有相關(guān)的術(shù)語:全局唯一標(biāo)識(shí)符(GUID)。
通俗說就是有個(gè)程序不斷生產(chǎn)字符串(或數(shù)字),每次生產(chǎn)的數(shù)字都不同,永遠(yuǎn)不會(huì)相同。
我們需要為每次用戶手工登錄創(chuàng)建一個(gè)獨(dú)一無二的UUID。
我們使用下面的命令安裝能夠生產(chǎn)uuid的模塊:
go get github.com/satori/go.uuid
go install github.com/satori/go.uuid
用法很簡(jiǎn)單,a, _ := uuid.NewV4()就能得到一串547d9f4b-05bd-4dc2-89d1-bab1c0f6ecd8這樣的代碼。
改進(jìn)login.go寫入Cookie
改進(jìn)后的func Login函數(shù)代碼如下:
//Login 注冊(cè)接口處理函數(shù)
func Login(w http.ResponseWriter, r *http.Request) {
ds := loginReqDS{}
json.NewDecoder(r.Body).Decode(&ds)
// //訪問數(shù)據(jù)集
dbc := tool.MongoDBCLient.Database("myweb").Collection("user")
//驗(yàn)證用戶郵箱是否與用戶名匹配
var u bson.M
dbc.FindOne(context.TODO(), bson.M{"Email": ds.Email}).Decode(&u)
if u["Pw"] == ds.Pw {
//創(chuàng)建token并寫入數(shù)據(jù)庫
uid, _ := uuid.NewV4()
uids := uid.String()
ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
du := bson.M{"Token": uids, "Id": u["_id"], "Ts": time.Now().Unix()}
ctoken.InsertOne(context.TODO(), du)
//返回id,并將token寫入cookie
expire := time.Now().AddDate(0, 1, 0)
c := http.Cookie{
Name: "Token",
Path: "/",
Value: uids,
HttpOnly: true,
Expires: expire,
}
w.Header().Set("Set-Cookie", c.String())
util.WWrite(w, 0, "登錄成功", u["_id"])
} else {
util.WWrite(w, 1, "郵箱與用戶名不匹配", nil)
}
return
}
注意幾點(diǎn):
- 我們把
token和_id的對(duì)應(yīng)關(guān)系存儲(chǔ)在token數(shù)據(jù)集里面了。 - 使用
http.Cookie創(chuàng)建要存儲(chǔ)的數(shù)據(jù),HttpOnly是限定只能用Golang服務(wù)器端修改,不能用網(wǎng)頁的script修改。 - Cookie必須注意
Path路徑和Expires過期時(shí)間的設(shè)置,否則可能導(dǎo)致只在/api路徑下有效(實(shí)際這只是個(gè)接口,真實(shí)瀏覽器并沒有這個(gè)路徑,所以導(dǎo)致Cookie刷新后就會(huì)消失)。 - 使用
w.Header().Set設(shè)置Cookie。 - 設(shè)置Cookie和返回信息數(shù)據(jù)沒有關(guān)系。
分離SetCookie.go
寫入Cookie這個(gè)還是比較啰嗦的,因?yàn)橐院髸?huì)一直使用,我們把它單獨(dú)出來放到util里面util/SetCookie.go,內(nèi)容如下:
package util
import (
"net/http"
"time"
)
//SetCookie 設(shè)置Cookie,默認(rèn)1月/路徑
func SetCookie(w http.ResponseWriter, k string, v string) {
exp := time.Now().AddDate(0, 1, 0)
path := "/"
SetCookieExt(w, k, v, exp, path, 0)
}
//DelCookie 刪除Cookie,MaxAge=-1
func DelCookie(w http.ResponseWriter, k string) {
exp := time.Now()
path := "/"
SetCookieExt(w, k, "", exp, path, -1)
}
//SetCookieExt 設(shè)置Cookie擴(kuò)展版
func SetCookieExt(w http.ResponseWriter, k string, v string, exp time.Time, path string, max int) {
c := http.Cookie{
Name: k,
Path: path,
Value: v,
HttpOnly: true,
Expires: exp,
MaxAge: max,
}
http.SetCookie(w, &c)
}
注意以下幾點(diǎn):
- 由于Golang不支持函數(shù)的參數(shù)默認(rèn)值(每個(gè)值必須設(shè)置),所以我們做了三個(gè)函數(shù),一個(gè)簡(jiǎn)化版
func SetCookie的只有3個(gè)參數(shù),另一個(gè)用來刪除Cookie的DelCookie只有2個(gè)參數(shù),還有一個(gè)擴(kuò)展版SetCookieExt有5個(gè)參數(shù)。 - 刪除一個(gè)Cookie只要把它的
MaxAge設(shè)置為小于0。雖然你仍然可以在瀏覽器中看到這個(gè)Cookie,但是由于已經(jīng)過期,所以讀取出來是nil空的,等于不存在。 -
http.SetCookie(w, &c)可以疊加多個(gè)Cookie,而w.Header().Set("Set-Cookie", c.String())只會(huì)執(zhí)行最后一個(gè)Cookie。
然后我們就可以修改login.go中的代碼:
//Login 注冊(cè)接口處理函數(shù)
func Login(w http.ResponseWriter, r *http.Request) {
ds := loginReqDS{}
json.NewDecoder(r.Body).Decode(&ds)
// //訪問數(shù)據(jù)集
dbc := tool.MongoDBCLient.Database("myweb").Collection("user")
//驗(yàn)證用戶郵箱是否與用戶名匹配
var u bson.M
dbc.FindOne(context.TODO(), bson.M{"Email": ds.Email}).Decode(&u)
if u["Pw"] == ds.Pw {
uid := u["_id"].(primitive.ObjectID).Hex()
//創(chuàng)建token并寫入數(shù)據(jù)庫
token, _ := uuid.NewV4()
tokens := token.String()
ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
du := bson.M{"Token": tokens, "Id": u["_id"], "Ts": time.Now().Unix()}
ctoken.InsertOne(context.TODO(), du)
//返回id,寫入Token和Uid
util.SetCookie(w, "Token", tokens)
util.SetCookie(w, "Uid", uid)
util.WWrite(w, 0, "登錄成功", u["_id"])
} else {
util.WWrite(w, 1, "郵箱與用戶名不匹配", nil)
}
return
}
注意這里我們寫入了兩個(gè)Cookie:Token和Uid。
其中uid(userId)使用uid := u["_id"].(primitive.ObjectID).Hex()把從數(shù)據(jù)庫中讀取的內(nèi)容轉(zhuǎn)成了字符string。
運(yùn)行代碼,在頁面上登錄之后就可以看到新增了兩個(gè)Cookie:
[圖片上傳失敗...(image-f37ea6-1554260822526)]
中間件MiddleWare.go
在app.go中,我們使用了文件服務(wù),http.Handle("/", http.FileServer(http.Dir(webDir))把所有沒明確指出處理服務(wù)的路徑都指向了文件服務(wù)。(api/...都是明確指出處理服務(wù)的)。
如果我們能夠在用戶每次打開新頁面.html的時(shí)候就自動(dòng)檢測(cè)他是否已經(jīng)登錄過,那么以后處理就容易很多。
我們目前的文件路徑處理是:
中間加一個(gè)事情,我們叫它中間件,就變?yōu)?
我們先修改app.go:
//文件服務(wù)器和中間件
fileHandler := http.FileServer(http.Dir(webDir))
http.Handle("/", ext.MiddleWare(fileHandler))
這里我們給原來的fileHandler加了一層外套ext.MiddleWare(fileHandler)。然后我們來看app/ext/MiddleWare.go,代碼如下:
package ext
import (
"app/tool"
"app/util"
"context"
"net/http"
"regexp"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
//MiddleWare 文件服務(wù)中間件
func MiddleWare(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//僅對(duì).html文件處理
htmlRe, _ := regexp.Compile(`^.+\.html[\?]*.*$`)
if !htmlRe.MatchString(r.URL.String()) {
h.ServeHTTP(w, r)
return
}
//獲取Token
token, _ := r.Cookie("Token")
if token == nil {
util.DelCookie(w, "Uid")
h.ServeHTTP(w, r)
return
}
tv := token.Value
if tv == "" {
util.DelCookie(w, "Uid")
h.ServeHTTP(w, r)
return
}
//如果token匹配就向Cookie添加"Uid"
ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
var t bson.M
ctoken.FindOne(context.TODO(), bson.M{"Token": tv}).Decode(&t)
uid := t["Id"]
if uid != nil {
uids := uid.(primitive.ObjectID).Hex()
util.SetCookie(w, "Uid", uids)
} else {
util.DelCookie(w, "Uid")
}
//文件服務(wù)
h.ServeHTTP(w, r)
})
}
注意幾點(diǎn):
- 我們的這個(gè)
func MiddleWare(h http.Handler) http.Handler可以看得出,進(jìn)來的參數(shù)是h http.Handler,返回的也是http.Handler,就是說吃進(jìn)來的和吐出來的是一樣類型。這樣我們?cè)?code>app.go里面才能確保fileHandler和ext.MiddleWare(fileHandler)類型一樣不會(huì)錯(cuò)。 - 我們使用了正則表達(dá)式
regexp.Compile(`^.+\.html[\?]*.*$`)來判斷是否是.html文件。只對(duì).html文件頁面做自動(dòng)登錄處理。 - 讀取Cookie的代碼是
r.Cookie("Token"),但要取得它的.Value才能用。 - 僅對(duì)檢測(cè)到匹配的
Token的時(shí)候增加寫入Uid,對(duì)于未檢測(cè)到或者不匹配的就刪除掉Uid。
添加autoLogin.go
我們來增加一個(gè)自動(dòng)登錄的接口api/autoLogin.go,每個(gè)需要自動(dòng)登錄檢查的頁面都可以調(diào)用這個(gè)地址,如果成功就返回用戶的郵箱信息,如果失敗就跳轉(zhuǎn)到login.html頁面。
package api
import (
"app/tool"
"app/util"
"context"
"encoding/json"
"net/http"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type autoLoginReqDS struct {
Email string
}
//AutoLogin 注冊(cè)接口處理函數(shù)
func AutoLogin(w http.ResponseWriter, r *http.Request) {
ds := autoLoginReqDS{}
json.NewDecoder(r.Body).Decode(&ds)
//直接信任Cookie中的Uid
uid, _ := r.Cookie("Uid")
//沒登錄返回空
if uid == nil || uid.Value == "" {
util.WWrite(w, 1, "自動(dòng)登錄失敗。", nil)
return
}
//登錄成功返回對(duì)象
var u bson.M
coll := tool.MongoDBCLient.Database("myweb").Collection("user")
idobj, err := primitive.ObjectIDFromHex(uid.Value)
if err != nil {
util.WWrite(w, 1, "自動(dòng)登錄Cookie.Uid異常。", nil)
return
}
coll.FindOne(context.TODO(), bson.M{"_id": idobj}).Decode(&u)
data := map[string]string{
"Email": u["Email"].(string),
"Uid": uid.Value}
datas, err := json.Marshal(data)
if err != nil {
util.WWrite(w, 1, "自動(dòng)登錄數(shù)據(jù)庫內(nèi)容異常。", nil)
return
}
util.WWrite(w, 0, "自動(dòng)登錄成功。", string(datas))
return
}
這個(gè)代碼沒有很特別的地方,注意最后我們利用json.Mashal返回了較復(fù)雜一些的數(shù)據(jù),稍后我們會(huì)在頁面上讀取這個(gè)內(nèi)容。
改進(jìn)MiddleWare.go
在上面的自動(dòng)登錄autoLogin.go中我們直接信任了Cookie里面的Uid。然而原則上前端網(wǎng)頁帶來的信息都是不可靠的,可以被偽造的。所以最好我們也應(yīng)該在autoLogin處理之前最好也用中間件驗(yàn)證一下這個(gè)Cookie里面的Uid是否可靠。
我們改進(jìn)MiddleWare.go:
package ext
import (
"app/tool"
"app/util"
"context"
"net/http"
"regexp"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
//MiddleWare 文件服務(wù)中間件
func MiddleWare(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//僅對(duì).html文件處理
htmlRe, _ := regexp.Compile(`^.+\.html[\?]*.*$`)
if !htmlRe.MatchString(r.URL.String()) {
h.ServeHTTP(w, r)
return
}
//檢查Cookie中的Uid是否合法
loginCheck(w, r)
//文件服務(wù)
h.ServeHTTP(w, r)
})
}
//MiddleWareAPI API中間件:檢查Uid和Token的合理性
func MiddleWareAPI(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//檢查Cookie中的Uid是否合法
loginCheck(w, r)
//API服務(wù)
next(w, r)
})
}
//loginCheck 檢查Cookie中的Uid是否合法
func loginCheck(w http.ResponseWriter, r *http.Request) {
//獲取Token
token, _ := r.Cookie("Token")
if token == nil {
util.DelCookie(w, "Uid")
return
}
tv := token.Value
if tv == "" {
util.DelCookie(w, "Uid")
return
}
//如果token匹配就向Cookie添加"Uid"
ctoken := tool.MongoDBCLient.Database("myweb").Collection("token")
var t bson.M
ctoken.FindOne(context.TODO(), bson.M{"Token": tv}).Decode(&t)
uid := t["Id"]
if uid != nil {
uids := uid.(primitive.ObjectID).Hex()
util.SetCookie(w, "Uid", uids)
} else {
util.DelCookie(w, "Uid")
}
}
注意幾點(diǎn):
- 我們把驗(yàn)證用戶登錄的方法單獨(dú)拉出來變?yōu)?code>loginCheck。
- 我們?cè)僭形募幚碇虚g件的基礎(chǔ)上新增了
API版本MiddleWareAPI。 -
MiddleWareAPI其實(shí)比較簡(jiǎn)單,它吃http.HandlerFunc,也返回http.HandlerFunc,只是中間我們插入了loginCheck(w,r)。
然后我們終于可以到app.go設(shè)置服務(wù)路徑了:
http.HandleFunc("/api/AutoLogin", ext.MiddleWareAPI(api.AutoLogin))
改進(jìn)index.html
我們來改一下index.html,讓首頁嘗試自動(dòng)登錄,如果登錄失敗就跳轉(zhuǎn)到登錄頁面,下面是index.html的完整代碼:
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<title>我的站點(diǎn)</title>
</head>
<body>
<div class="row justify-content-center" style="margin-top:100px;margin-bottom:20px">
<h4>~歡迎您來到我的網(wǎng)站~</h4>
</div>
<div class="row justify-content-center">
<div id='uEmail'>正在為您登錄</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>
<script type="text/javascript">
function autoLogin() {
$.post('/api/AutoLogin', function (res) {
obj = JSON.parse(res.Data);
if (obj && obj['Email']) {
$('#uEmail').html(obj['Email'])
}else{
$('#uEmail').html("自動(dòng)登錄失敗,正在為您跳轉(zhuǎn)...")
setTimeout(() => {
location='/page/login.html'
}, 1000);
}
}, 'json')
}
autoLogin()
</script>
</html>
注意以下幾點(diǎn):
- 我們?cè)诮Y(jié)尾自動(dòng)執(zhí)行了
autologin() - 因?yàn)镚olang傳過來的都是string,所以我們
obj = JSON.parse(res.Data)把string轉(zhuǎn)為對(duì)象,這樣就可以obj['Email']獲取數(shù)據(jù)了。 - 使用
location='/page/login.html'方法跳轉(zhuǎn)頁面。 - 使用
setTimeout(() => {...}, 1000)延遲1秒再跳轉(zhuǎn)。
好了,可以運(yùn)行測(cè)試了,正常的話如果還沒登錄(或者把Cookie刪掉了),那么首頁就會(huì)為你跳轉(zhuǎn)到登錄頁面,正常登陸之后,再回到首頁就可以看到自己的郵箱了:

小結(jié)
- Cookie就是瀏覽器為每個(gè)網(wǎng)站的開發(fā)者準(zhǔn)備的用于記錄用戶信息的小文件??梢杂肎olang直接操作Cookie。
- Token是我們?cè)谟脩裘看问止さ卿洉r(shí)候創(chuàng)建的唯一字符串,和用戶的Uid是對(duì)應(yīng)的,也對(duì)應(yīng)到數(shù)據(jù)庫中的條目。注意可能多個(gè)Token對(duì)應(yīng)一個(gè)Uid,但不可能多個(gè)Uid對(duì)應(yīng)同一個(gè)Token。
- 中間件概念可以讓我們?yōu)槎鄠€(gè)路徑處理服務(wù)插入同一個(gè)處理程序,比如我們?yōu)槊總€(gè).html文件服務(wù)都插入了驗(yàn)證Cookie中Token和Uid的功能,同樣我們也為
api/Autologin路徑插入了這個(gè)驗(yàn)證,如果需要的話任何一個(gè)api處理都可以先加上這個(gè)驗(yàn)證以確保Uid可靠性。 - 別忘了提及到Git再提交到Github。
雖然還有一些鏈接沒有添加,但似乎登錄注冊(cè)功能基本完成了。但還有一個(gè)嚴(yán)重缺陷,那就是我們一直把用戶的密碼反復(fù)的明文傳輸,如果被壞人中間截獲了就不好了,當(dāng)然,你的網(wǎng)站數(shù)據(jù)庫中直接明明白白記錄著這些重要的密碼,本身就是非常不負(fù)責(zé)的,下一篇我們介紹如何解決這個(gè)缺陷。
歡迎關(guān)注我的專欄( つ??ω??)つ【人工智能通識(shí)】
每個(gè)人的智能新時(shí)代
如果您發(fā)現(xiàn)文章錯(cuò)誤,請(qǐng)不吝留言指正;
如果您覺得有用,請(qǐng)點(diǎn)喜歡;
如果您覺得很有用,歡迎轉(zhuǎn)載~
END