JWT與Golang

JWT基礎(chǔ)概念

JWT是 json web token的簡稱

其中的 token 是令牌的意思, 其實這個令牌實質(zhì)上是服務(wù)端生成的一段有規(guī)則的字符串

我們看看JWT官方自己對其的定義

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

我們提煉出重點信息:

  1. JWT是一個開放的標(biāo)準(zhǔn)
  2. jwt本身體積比較緊湊,所以傳輸速度比較快
  3. jwt可以放在URL中傳遞,可以放在請求頭中傳遞,可以放在請求體中傳遞
  4. jwt的有效載荷中包含一些自定義的有效信息,在某些場景中可以避免部分?jǐn)?shù)據(jù)庫查詢

JWT使用場景

  • 授權(quán) : jwt最常見的使用場景,用戶端首先登陸成功,從服務(wù)端獲取jwt(令牌),那么用戶后面的所有的請求中都應(yīng)該包含這個令牌,服務(wù)端通過這個令牌判斷允許用戶的權(quán)限和訪問的資源,服務(wù).

    基于這樣的特點可以做單點登錄(SingleSignOn易迹,SSO)

    基于此也可以做跨域認(rèn)證

  • 信息交換 : 通訊雙方通過jwt可以傳遞信息, 信息都是簽名之后,可以防止偽造

JWT的結(jié)構(gòu)

如下是一個標(biāo)準(zhǔn)的JWT

jwt.png

JWT 是有三部分組成的,每一部分之間通過 . (點號) 隔開

header 頭部

Payload 載荷

Signature 簽名

那么JWT的格式如下 :

header . Payload . Signature

頭部

標(biāo)頭是一個json對象通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法,例如HMAC SHA256或RSA。

{
  "alg": "HS256",
  "typ": "JWT"
}

這一部分的json內(nèi)容被 Base64Url 編碼之后成為第一部分

載荷

載荷也是一個json對象,是實際承載傳遞數(shù)據(jù)的部分,JWT提供了7個預(yù)定義好字段可以按需使用

iss (issuer):簽發(fā)人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發(fā)時間
jti (JWT ID):編號

除了這些預(yù)定義的字段,我們可以在payload中添加自己的'私貨' ,添加一些自己字段

{
    "name":"admin",
    "pwd":"123456"
}

有了有效載荷之后再對有效載荷進行 Base64Url 編碼 ,編碼之后的這部分就是 jwt 的第二部分

tips : 盡量不要將很重要和私密的信息放在其中,因為這部分解碼之后是可見的

簽名

簽名就是將前面的編碼之后header 和 編碼之后的 payload 再加上秘鑰secretKey 通過指定的加密算法創(chuàng)建出來的(簽名默認(rèn)算法是 HMAC SHA256)

創(chuàng)建簽名的公式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secretKey)

通常認(rèn)為簽名的作用是防數(shù)據(jù)被篡改

JWT工作原理

jwt1.png

一般而言我們攜帶JWT發(fā)送請求可以放在HTTP請求頭中的Authorization 中, 格式如下

Authorization: Bearer <token>

tips: 我們前面說了jwt 這個令牌其實也可以放在 HTTP的url中,也可以在HTTP請求的請求報文中

當(dāng) token在HTTP的請求頭中的 Authorization 中發(fā)送時可以解決 CORS 的問題

Golang使用JWT

模擬使用場景 :

  1. 新建http服務(wù),提供三個處理接口 /auth ,/home ,/list
  2. /auth 接口處理用戶登陸驗證,并返回 token (jwt)
  3. /home 接口處理登陸成功的用戶都能訪問的 home服務(wù)
  4. /list 接口處理登陸成功的用戶并且用戶權(quán)限是admin 才能訪問的 list服務(wù)

tips : 這里的http服務(wù)和JWT 的 Authorization server(認(rèn)證服務(wù)) 在同一服務(wù)器上,在實際開發(fā)中 Authorization server(認(rèn)證服務(wù)) 可以單獨部署

jwt3.png

代碼實現(xiàn)

Golang中有很多關(guān)于jwt的包,我們使用如下包

 # 安裝依賴包
 go get github.com/dgrijalva/jwt-go

JWT服務(wù)的關(guān)鍵代碼如下

// JWT中的payload中不要放重要數(shù)據(jù),因為這部分?jǐn)?shù)據(jù)通過Base64URL算法能反解出來
type Claims struct {
    // 自定義的`私有`數(shù)據(jù),在payload中
    UserAccount
    // jwt的標(biāo)準(zhǔn)的claims
    jwt.StandardClaims
}

// 生成token
func GetToken(name, password, role string) (string, error) {
    c := Claims{
        UserAccount: UserAccount{
            Username: name,
            Password: password,
            Role:     role,
        },
        StandardClaims: jwt.StandardClaims{
            // token過期時間
            ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
            // 簽發(fā)人
            Issuer:  "captain",
            // 主題
            Subject: "jwt test",
        },
    }
    // 生成token,默認(rèn)采用HMAC SHA256
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
    // 加上簽名(需要用到秘鑰),生成完整的token
    return token.SignedString(SecretKey)
}

// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
        return SecretKey, nil
    })
    if err != nil {
        return nil, err
    }
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    return nil, errors.New("invalid token")
}

此處我們不用框架,直接使用golang一些標(biāo)準(zhǔn)包構(gòu)建一個http服務(wù),并且集成JWT服務(wù)

http服務(wù)(包含JWT服務(wù))端完整代碼

jwt.go

package main

import (
    "encoding/json"
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "github.com/pkg/errors"
    "log"
    "net/http"
    "strings"
    "time"
)

const (
    // 定義token的有效時間
    TokenExpireDuration = time.Hour * 1
)

var SecretKey = []byte("123456")

// 請求的賬戶信息
type UserAccount struct {
    Username string `json:"username"`
    Password string `json:"password"`
    Role     string `json:"role"`
}

// 響應(yīng)給客戶端的數(shù)據(jù)
type ResponseToClient struct {
    Code    string      `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

// JWT中的payload中不要放重要數(shù)據(jù),因為這部分?jǐn)?shù)據(jù)通過Base64URL算法能反解出來
type Claims struct {
    // 自定義的`私有`數(shù)據(jù),在payload中
    UserAccount
    // jwt的標(biāo)準(zhǔn)的claims
    jwt.StandardClaims
}

// 生成token
func GetToken(name, password, role string) (string, error) {
    c := Claims{
        UserAccount: UserAccount{
            Username: name,
            Password: password,
            Role:     role,
        },
        StandardClaims: jwt.StandardClaims{
            // token過期時間
            ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
            // 簽發(fā)人
            Issuer:  "captain",
            Subject: "jwt test",
        },
    }
    // 生成token,默認(rèn)采用HMAC SHA256
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
    // 加上簽名(需要用到秘鑰),生成完整的token
    return token.SignedString(SecretKey)
}

// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
        return SecretKey, nil
    })
    if err != nil {
        return nil, err
    }
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    return nil, errors.New("invalid token")
}

func WriteToResponse(w http.ResponseWriter, code, message string, data interface{}) {
    var resp ResponseToClient
    resp.Code = code
    resp.Message = message
    resp.Data = data
    respJson, _ := json.Marshal(resp)
    // 設(shè)置響應(yīng)為json格式
    w.Header().Set("Content-Type", "application/json;charset=utf-8")
    fmt.Fprintf(w, "%v\n", string(respJson))
}

// 默認(rèn)處理函數(shù)
func defaultFunc(w http.ResponseWriter, r *http.Request) {

}

// 模擬用戶登陸接口,響應(yīng)json數(shù)據(jù)
// 目的是讓客戶端從服務(wù)器端獲取token
// 處理請求邏輯之后,響應(yīng)給客戶的數(shù)據(jù)中包含新生成的token值
func AuthFunc(w http.ResponseWriter, r *http.Request) {
    var user UserAccount
    // 讀取客戶端請求的類容
    buf := make([]byte, 2048)
    n, _ := r.Body.Read(buf)
    // debug 調(diào)試請求的內(nèi)容
    log.Println("json :", string(buf[:n]))
    // 將請求json數(shù)據(jù)解析出來
    err := json.Unmarshal(buf[:n], &user)
    // 如果解析錯誤,給客戶端提示
    if err != nil {
        WriteToResponse(w, "400", err.Error(), "")
        return
    }
    // debug 調(diào)試解析之后的內(nèi)容
    log.Println(user)
    // 模擬驗證賬戶登錄的邏輯(賬戶,密碼都正確)
    if user.Username == "admin" && user.Password == "123456" {
        tokenString, _ := GetToken(user.Username, user.Password, user.Role)
        WriteToResponse(w, "200", "success", map[string]string{"token": tokenString})
        return
    } else {
        WriteToResponse(w, "400", "Account error", "")
        return
    }
    // 通過命令行的 CURL 測試
    // curl -X POST -H -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"admin"}' http://127.0.0.1:8080/auth
}

// 模擬用戶登陸(獲取token)之后再請求某個接口,響應(yīng)json數(shù)據(jù)
// 請求時,在請求的數(shù)據(jù)中包含token
// 包含token的載體可以是請求的url,也可以是請求頭,也可以是請求體
func homeFunc(w http.ResponseWriter, r *http.Request) {
    // 此處我們模擬的token包含在請求頭信息中
    authorH := r.Header.Get("Authorization")
    if authorH == "" {
        WriteToResponse(w, "401", "request header Authorization is null", "")
        return
    }
    // 將獲取的Authorization 內(nèi)容通過分割出來
    authorArr := strings.SplitN(authorH, " ", 2)
    // debug
    log.Println(authorArr)
    // Authorization的字符串通常是 "Bearer" 開頭(可以理解為固定格式,標(biāo)識使用承載模式),然后一個空格 再加上token的內(nèi)容
    // Tips:  請求頭中Authorization的內(nèi)容直接是token也是可以的
    if len(authorArr) != 2 || authorArr[0] != "Bearer" {
        WriteToResponse(w, "402", "request header Authorization formal error", "")
        return
    }
    // 解析token這個字符串
    mc, err := ParseToken(authorArr[1])
    if err != nil {
        WriteToResponse(w, "403", err.Error(), "")
        return
    }
    // debug
    log.Println(mc)
    // 請求成功響應(yīng)給客戶端
    WriteToResponse(w, "200", "welcome to home", "")
}
func listFunc(w http.ResponseWriter, r *http.Request) {
    authorH := r.Header.Get("Authorization")
    if authorH == "" {
        WriteToResponse(w, "401", "request header Authorization is null", "")
        return
    }
    authorArr := strings.SplitN(authorH, " ", 2)
    // debug
    log.Println(authorArr)
    if len(authorArr) != 2 || authorArr[0] != "Bearer" {
        WriteToResponse(w, "402", "request header Authorization formal error", "")
        return
    }
    mc, err := ParseToken(authorArr[1])
    if err != nil {
        WriteToResponse(w, "403", err.Error(), "")
        return
    }
    // 模擬通過jwt確定權(quán)限的邏輯
    // 如果 用戶角色是admin,那么就能訪問該接口,否則就不允許
    if mc.Role == "admin" {
        WriteToResponse(w, "200", "列表數(shù)據(jù)", "")
        return
    }
    WriteToResponse(w, "404", "無權(quán)限訪問", "")
}
func main() {
    http.HandleFunc("/", defaultFunc)
    http.HandleFunc("/auth", AuthFunc)
    http.HandleFunc("/home", homeFunc)
    http.HandleFunc("/list", listFunc)
    fmt.Println("start http server and listen 8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("ListenAndServer err : ", err)
    }
}

# 運行jwt服務(wù)
$ go run jwt.go
start http server and listen 8080

服務(wù)測試

測試工具是直接使用命令行下的 curl命令工具進行的測試

tips: 當(dāng)然也可以使用其他的任何能發(fā)送http請求的工具進行測試(包括使用代碼編寫http客戶端)

測試登陸驗證服務(wù)

# 發(fā)送POST并攜帶json數(shù)據(jù)
curl -X POST -v -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"edit"}' http://127.0.0.1:8080/auth

# 響應(yīng)數(shù)據(jù)
> POST /auth HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.69.1
> Accept: */*
> Content-type:application/json
> Content-Length: 54
>
} [54 bytes data]
* upload completely sent off: 54 out of 54 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=utf-8
< Date: Sun, 05 Apr 2020 09:12:56 GMT
< Content-Length: 266
<
// 響應(yīng)的json數(shù)據(jù) 包含了token
{"code":"200","message":"success","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4NDUzMCwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.VNIk9nI8SStCMCI_QyJ8gLrUbOLNSQgeoVabjQFzMS0"}}


測試訪問 home 服務(wù)

# 無token請求
 curl -X POST -H "Content-type:application/json" http://127.0.0.1:8080/home
# 響應(yīng)
{"code":"401","message":"request header Authorization is null","data":""}

# 正確的請求
 curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/home

# 響應(yīng)
{"code":"200","message":"welcome to home","data":""}

測試訪問 list 服務(wù)

# 請求
 curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/list
 
# 響應(yīng)
{"code":"404","message":"無權(quán)限訪問","data":""}

參考資料

JSON Web Token 入門教程

jwt官網(wǎng)

sha256

CORS

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碗暗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泉蝌,更是在濱河造成了極大的恐慌锻霎,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件长已,死亡現(xiàn)場離奇詭異,居然都是意外死亡昼牛,警方通過查閱死者的電腦和手機术瓮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贰健,“玉大人胞四,你說我怎么就攤上這事×娲唬” “怎么了辜伟?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵氓侧,是天一觀的道長。 經(jīng)常有香客問我导狡,道長约巷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任旱捧,我火速辦了婚禮独郎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘枚赡。我一直安慰自己氓癌,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布贫橙。 她就那樣靜靜地躺著贪婉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卢肃。 梳的紋絲不亂的頭發(fā)上疲迂,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音践剂,去河邊找鬼鬼譬。 笑死,一個胖子當(dāng)著我的面吹牛逊脯,可吹牛的內(nèi)容都是我干的优质。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼军洼,長吁一口氣:“原來是場噩夢啊……” “哼巩螃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起匕争,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤避乏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后甘桑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拍皮,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年跑杭,在試婚紗的時候發(fā)現(xiàn)自己被綠了铆帽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡德谅,死狀恐怖爹橱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窄做,我是刑警寧澤愧驱,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布慰技,位于F島的核電站,受9級特大地震影響组砚,放射性物質(zhì)發(fā)生泄漏吻商。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一糟红、第九天 我趴在偏房一處隱蔽的房頂上張望手报。 院中可真熱鬧,春花似錦改化、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兄裂,卻和暖如春句旱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晰奖。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工谈撒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人匾南。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓啃匿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蛆楞。 傳聞我的和親對象是個殘疾皇子溯乒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容