軟件技術(shù)-零基礎(chǔ)-Golang操作Cookie

歡迎關(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:TokenUid。
其中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)登錄過,那么以后處理就容易很多。

我們目前的文件路徑處理是:
用戶請(qǐng)求 \to FileServer文件服務(wù)處理
中間加一個(gè)事情,我們叫它中間件,就變?yōu)?
用戶請(qǐng)求 \to 中間件MiddleWare處理 \to FileServer文件服務(wù)處理

我們先修改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里面才能確保fileHandlerext.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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容