【Golang】Golang + jwt 實(shí)現(xiàn)簡(jiǎn)易用戶認(rèn)證

本文已同步發(fā)布到我的個(gè)人博客:https://glorin.xyz/2019/11/23/Golang-jwt-simple-auth/

前言

在開發(fā)app的時(shí)候,難免會(huì)需要有后臺(tái)API丢氢,Golang是一門非常適合開發(fā)后臺(tái)服務(wù)的高性能的語(yǔ)言傅联。在使用Golang開發(fā)后臺(tái)API的時(shí)候,經(jīng)常需要有用戶注冊(cè)疚察、登錄的功能蒸走,例如為了保存用戶數(shù)據(jù)、為了給不同用戶提供不同服務(wù)等貌嫡。本文便是介紹一種基于jwt的Golang的用戶認(rèn)證系統(tǒng)比驻。

目標(biāo)

我們的模板是實(shí)現(xiàn)三個(gè)API接口:

  • /api/account: 支持Post方法,用來注冊(cè)一個(gè)用戶
  • /api/account/accesstoken: 登錄功能
  • /api/account/me: 獲取用戶信息岛抄,具有認(rèn)證功能别惦,如果沒有用戶認(rèn)證信息(token),則獲取失敗夫椭,否則返回當(dāng)前用戶的信息步咪。

工具原料

  • Golang + Goland IDE(非必須,VSCode等等也可以)
  • jwt-go:一個(gè)go語(yǔ)言的jwt實(shí)現(xiàn)
  • github.com/gorilla/mux: go語(yǔ)言的一個(gè)路由組件益楼,用來提供http路由服務(wù)

實(shí)現(xiàn)步驟

搭建http服務(wù)猾漫,提供API

  1. 首先我們?cè)谧约旱膅o語(yǔ)言目錄下建立項(xiàng)目:golang-jwt-simple-auth(名字可自由決定),golang項(xiàng)目一般遵循golang的約定感凤,放在GOPATH(默認(rèn)是用戶目錄下的go文件夾)下面悯周,比如我的項(xiàng)目放在github上,那目錄就是
~/go/src/github.com/glorinli/golang-jwt-simple-auth
  1. 新建main.go問陪竿,作為程序的主入口禽翼,main.go內(nèi)容如下:
package main

import (
    "fmt"
    "github.com/glorinli/go-jwt-simple-auth/app"
    "github.com/glorinli/go-jwt-simple-auth/controllers"
    "log"
    "net/http"
    "os"

    "github.com/gorilla/mux"
)

func init() {
    log.SetPrefix("simple-auth")
}

func main() {
    // 新建路由器
    router := mux.NewRouter()

    // 注冊(cè)jwt認(rèn)證的中間件
    router.Use(app.JwtAuthentication)

    // 注冊(cè)路由
    router.HandleFunc("/api/account", controllers.CreateUser).Methods(http.MethodPost)
    router.HandleFunc("/api/account/accesstoken", controllers.Login).Methods(http.MethodGet)
    router.HandleFunc("/api/account/me", controllers.Me).Methods(http.MethodGet)

    // 獲取端口號(hào)
    port := os.Getenv("golang-jwt-simple-auth-port")
    if port == "" {
        port = "8001"
    }

    fmt.Println("Port is:", port)

    // 開始服務(wù)
    err := http.ListenAndServe(":"+port, router)
    if err != nil {
        fmt.Print("Fail to start server", err)
    }
}

代碼的作用在注釋中已經(jīng)說明了,關(guān)于mux庫(kù)的使用族跛,可以參考 http://www.gorillatoolkit.org/pkg/mux, 這里我們只要知道它起到一個(gè)路由器的作用闰挡,負(fù)責(zé)把一個(gè)api請(qǐng)求映射到一個(gè)方法上。

關(guān)鍵就在于

router.Use(app.JwtAuthentication)

這相當(dāng)與注冊(cè)了一個(gè)中間件礁哄,也可以理解為攔截器长酗,就是說所有的請(qǐng)求都會(huì)先經(jīng)過這個(gè)中間件攔截處理,于是我們便可以在里面處理認(rèn)證相關(guān) 的邏輯了桐绒,接下來就來說說這個(gè)JwtAuthentication夺脾。

JWT認(rèn)證實(shí)現(xiàn)

首先還是貼上JwtAuthentication的代碼:

package app

import (
    ... 省略
)

var JwtAuthentication = func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        // 只針對(duì)這個(gè)me接口開啟認(rèn)證
        needAuthPaths := []string{"/api/account/me"}
        requestPath := r.URL.Path

        var needAuth = false
        // 判斷是否是需要認(rèn)證的api,省略

        // 不需要認(rèn)證茉继,直接走下一步
        if !needAuth {
            next.ServeHTTP(w, r)
            return
        }

        // 從Header讀取Token
        tokenHeader := r.Header.Get("Authorization")

        // Token is missing
        if tokenHeader == "" {
            sendInvalidTokenResponse(w, "Missing auth token")
            return
        }

        tk := &models.Token{}

        token, err := jwt.ParseWithClaims(tokenHeader, tk, func(token *jwt.Token) (interface{}, error) {
            return []byte(os.Getenv("token_password")), nil
        })

        fmt.Println("Parse token error:", err)

        if err != nil {
            sendInvalidTokenResponse(w, "Invalid auth token: "+err.Error())
            return
        }

        // Token is invalid
        if !token.Valid {
            sendInvalidTokenResponse(w, "Token is not valid")
            return
        }

        // Auth ok
        fmt.Println("User:", tk.UserId)
        ctx := context.WithValue(r.Context(), "user", tk.UserId)
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

func sendInvalidTokenResponse(w http.ResponseWriter, message string) {
    response := u.Message(false, message)
    w.WriteHeader(http.StatusForbidden)
    w.Header().Set("Content-Type", "application/json")
    u.Respond(w, response)
}

這個(gè)函數(shù)的作用就是解析客戶端傳遞過來的token咧叭,將其解析為一個(gè)Token對(duì)象,Token對(duì)象的格式如下:

/*
JWT claims struct
*/
type Token struct {
    UserId uint
    jwt.StandardClaims
}

可以看到烁竭,除了jwt標(biāo)準(zhǔn)的數(shù)據(jù)菲茬,我們還添加了一個(gè)UserId字段,這是為了方便從Token確定用戶的id派撕。從這里我們也可以看出婉弹,Jwt Token是可以包含額外信息的。

關(guān)于這個(gè)Token是如何生程的腥刹,我們下面分析马胧。

注冊(cè)功能

返回去看main.go,我們發(fā)現(xiàn)注冊(cè)接口被綁定到一個(gè)函數(shù)上:

router.HandleFunc("/api/account", controllers.CreateUser).Methods(http.MethodPost)

來看這個(gè)CreateUser函數(shù)衔峰,位于authController.go中:

var CreateUser = func(w http.ResponseWriter, r *http.Request) {
    account := &models.Account{}

    err := json.NewDecoder(r.Body).Decode(account)

    if err != nil {
        utils.Respond(w, utils.Message(false, "Invalid info: "+err.Error()))
        return
    }

    utils.Respond(w, account.Create())
}

最終的實(shí)現(xiàn)是在account.Create()函數(shù)佩脊,位于account.go中:

func (account *Account) Create() map[string]interface{} {
    // 校驗(yàn) 省略

    // 將密碼做一個(gè)加密
    hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
    account.Password = string(hashedPassword)

    // 創(chuàng)建數(shù)據(jù)庫(kù)數(shù)據(jù)
    err := GetDB().Create(account).Error

    // ...

    // 創(chuàng)建Token
    tk := &Token{UserId: account.ID}
    token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk)
    tokenString, _ := token.SignedString([]byte(os.Getenv("token_password")))
    account.Token = tokenString

    account.Password = ""
    response := u.MessageWithData(true, "Account has been created", account)
    return response
}

關(guān)鍵就在于創(chuàng)建Token這一步,我們調(diào)用jwt.NewWithClaims來生成Token垫卤,參數(shù)有兩個(gè)威彰,第一個(gè)是簽名方法,采用HS256穴肘,第二個(gè)就是一個(gè)Token對(duì)象歇盼,這個(gè)對(duì)象即將被編碼到Token中,這也是我們?cè)谶M(jìn)行認(rèn)證的時(shí)候评抚,從Token解析出來的對(duì)象豹缀。

登錄功能

登錄功能與注冊(cè)功能類似伯复,只是把創(chuàng)建數(shù)據(jù)改為校驗(yàn)用戶名密碼。

運(yùn)行部署

我們可以直接在Goland中運(yùn)行程序邢笙,默認(rèn)會(huì)運(yùn)行在8081端口啸如,然后便可以用Postman或者curl調(diào)試相應(yīng)接口,這部分內(nèi)容請(qǐng)讀者自行研究氮惯。

注:筆者在mac os 10.15上叮雳,發(fā)現(xiàn)編譯時(shí)需要添加-ldflags "-w"參數(shù),否則會(huì)運(yùn)行失敗妇汗。

小結(jié)

本文介紹了如何使用Golang + jwt構(gòu)建一個(gè)建議的認(rèn)證系統(tǒng)帘不,可以讓大家對(duì)用戶認(rèn)證有一個(gè)基本的概念,詳細(xì)的代碼也已經(jīng)同步到github上杨箭,讀者們可以clone下來參考:https://github.com/glorinli/go-jwt-simple-auth

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寞焙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子告唆,更是在濱河造成了極大的恐慌棺弊,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擒悬,死亡現(xiàn)場(chǎng)離奇詭異模她,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)懂牧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門侈净,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人僧凤,你說我怎么就攤上這事畜侦。” “怎么了躯保?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵旋膳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我途事,道長(zhǎng)验懊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任尸变,我火速辦了婚禮义图,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘召烂。我一直安慰自己碱工,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怕篷,像睡著了一般历筝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上廊谓,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天漫谷,我揣著相機(jī)與錄音,去河邊找鬼蹂析。 笑死,一個(gè)胖子當(dāng)著我的面吹牛碟婆,可吹牛的內(nèi)容都是我干的电抚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼竖共,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蝙叛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起公给,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤借帘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后淌铐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肺然,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年腿准,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了际起。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吐葱,死狀恐怖街望,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弟跑,我是刑警寧澤灾前,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站孟辑,受9級(jí)特大地震影響哎甲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扑浸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一烧给、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧喝噪,春花似錦础嫡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)伯诬。三九已至,卻和暖如春巫财,著一層夾襖步出監(jiān)牢的瞬間盗似,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工平项, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赫舒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓闽瓢,卻偏偏與公主長(zhǎng)得像接癌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子扣讼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359