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

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

前言

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

目標(biāo)

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

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

工具原料

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

實現(xiàn)步驟

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

  1. 首先我們在自己的go語言目錄下建立項目:golang-jwt-simple-auth(名字可自由決定),golang項目一般遵循golang的約定,放在GOPATH(默認(rèn)是用戶目錄下的go文件夾)下面,比如我的項目放在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()

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

    // 注冊路由
    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)

    // 獲取端口號
    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庫的使用,可以參考 http://www.gorillatoolkit.org/pkg/mux, 這里我們只要知道它起到一個路由器的作用,負(fù)責(zé)把一個api請求映射到一個方法上。

關(guān)鍵就在于

router.Use(app.JwtAuthentication)

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

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

首先還是貼上JwtAuthentication的代碼:

package app

import (
    ... 省略
)

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

        // 只針對這個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)
}

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

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

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

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

注冊功能

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

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

來看這個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())
}

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

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

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

    // 創(chuàng)建數(shù)據(jù)庫數(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ù)有兩個,第一個是簽名方法,采用HS256,第二個就是一個Token對象,這個對象即將被編碼到Token中,這也是我們在進(jìn)行認(rèn)證的時候,從Token解析出來的對象。

登錄功能

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

運(yùn)行部署

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

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

小結(jié)

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

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

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