Go Jwt使用和源碼學習

Jwt概念

JWT(JSON Web Token)是一個很是輕巧的規範。這個規範容許咱們使用JWT在用戶和服務器之間傳遞安全可靠的信息。
一個JWT由3個部分組成:頭部(header)、載荷(payload)、簽名(signature)。
這三個部分又是由一個分隔符「.」 分割開的。git

header

用戶說明簽名的加密算法等,大概以下:github

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

payload 結構是一個json或者說是map對象
目前有一個相對標準的payload格式web

  • sub: 該JWT所面向的用戶
  • iss: 該JWT的簽發者
  • iat(issued at): 在何時簽發的token
  • exp(expires): token何時過時
  • nbf(not before):token在此時間以前不能被接收處理
  • jti:JWT ID爲web token提供惟一標識

固然你也能夠不用這些字段,能夠本身隨意定義。redis

signature

簽名是由頭部和荷載加上一串祕鑰,通過頭部聲明的加密算法加密獲得的。由於這個祕鑰只有服務端知道,可是這個祕鑰一旦泄漏了後果是很嚴重的。算法

使用

通常使用方法,則是在登陸的時候生成一個token返回到客戶端。客戶端則能夠放到header或者cookie中。每次請求數據的時候帶上這個token,而服務端則去驗證token是否正確,由於jwt中的祕鑰只有服務器知道一旦這個token被別人修改過及時修改過再使用base64編碼替換也是能夠被發現的,由於簽名是把header和payload加起來再麼祕鑰加密的。以下圖:
image
這樣作有幾個好處:數據庫

  • 能夠減小請求數據庫的次數,不須要每次數據接口請求都去訪問數據庫驗證用戶的有效性
  • 能夠設置過時時間,在payload中有一個字段叫exp。這個字段能夠設置過時時間,若是服務端發現過時則須要中心登陸或者驗證身份。
  • 在荷載(payload)中實際上是能夠做爲客戶端服務器端的信息交換,可是通常不會用這樣的操做
  • 服務器不保存session狀態,更適合分佈式的系統構建。每一個請求不用經過hash打到固定的機器上。

可是也帶來了一些問題:json

  • 由於服務器不保存狀態,jwt狀態是遊離狀態那麼服務器就不能主動的註銷。在到期以前這個token始終有效。通常的解決方法則是使用redis記錄token,每次請求判斷下若是redis中不存在則過時或者不合法。
  • JWT中的祕鑰一旦被泄漏出去,那麼任何人均可以冒充別人請求數據了。

Go 使用Jwt 實現驗證

簡單的用Gin實現一個http服務端, 一個login接口若是帳號密碼正確,則爲客戶端添加cookie。
第二個接口則是請求數據接口,經過auth中間件來驗證cookie中的token是否爲以前服務端發出去的那個token,這個只有服務端能驗證,由於服務端擁有祕鑰。
這個是最簡單的實現,沒有加上上面說的redis驗證。數組

package main

import (
    "fmt"
    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
    "github.com/gomodule/redigo/redis"
    "time"
)

const (
    SecretKey = "I have login"
)

var redisCoon redis.Conn

func main() {
    router := gin.Default()
    router.GET("/login", loginHandler)
    router.Use(authMiddleware)
    router.GET("/getData", getData)
    router.Run(":2323")
}

//驗證token中間件
func authMiddleware(ctx *gin.Context) {
    //從cookie中獲取token
    if tokenStr, err := ctx.Cookie("token"); err == nil {
        //獲取驗證以後的結果
        token, err := parseToken(tokenStr)
        if err != nil {
            ctx.JSON(200, "token verify error")
        }
        //若是驗證結果是false直接返回token錯誤我 若是成功則繼續下一個handler
        if token.Valid {
            ctx.Next()
        } else {
            ctx.JSON(200, "token verify error")
            ctx.Abort()
        }
    } else {
        ctx.JSON(200, "no token")
        ctx.Abort()
    }
}

func getData(ctx *gin.Context) {
    ctx.JSON(200, "data")
}

func loginHandler(ctx *gin.Context) {
    user := ctx.Query("user")
    pwd := ctx.Query("pwd")

    if user == "peter" && pwd == "pwd" {
        token := CreateToken(user, pwd)
        //ctx.Header("Authorization", token)
        ctx.SetCookie("token", token, 10, "/", "localhost", false, true)
        ctx.JSON(200, "ok")
    } else {
        ctx.JSON(200, "user is not exit")
    }
}

func parseToken(s string) (*jwt.Token, error) {
    fn := func(token *jwt.Token) (interface{}, error) {
        return []byte(SecretKey), nil
    }
    return jwt.Parse(s, fn)
}

//建立token
func CreateToken(user, pwd string) string {
    token := jwt.New(jwt.SigningMethodHS256)

    claims := make(jwt.MapClaims)
    claims["user"] = user
    // 這邊的pwd 不該該放到claims 荷載中不該該有機密的數據
    claims["pwd"] = pwd
    token.Claims = claims
    if tokenString, err := token.SignedString([]byte(SecretKey)); err == nil {
        return tokenString
    } else {
        return ""
    }
}

源碼

其實源碼邏輯挺簡單的,就是把上述流程簡單的實現。安全

1. jwt主要對象和接口的定義

其中的SigningMethod接口是主要簽名的方法,在jwt中有幾個預置的簽名方法。
其實若是咱們本身寫一個類而且實現了這個接口,其實也是能夠自定義簽名方法。服務器

// token結構
type Token struct {
    Raw       string                 // 保存原始token解析的時候保存
    Method    SigningMethod          // 保存簽名方法 目前庫裏有HMAC  RSA  ECDSA
    Header    map[string]interface{} // jwt中的頭部
    Claims    Claims                 // jwt中第二部分荷載,Claims是一個藉口
    Signature string                 // jwt中的第三部分 簽名
    Valid     bool                   // 記錄token是否正確
}

type Claims interface {
    Valid() error
}

// 簽名方法 全部的簽名方法都會實現這個接口
// 具體能夠參考https://github.com/dgrijalva/jwt-go/blob/master/hmac.go
type SigningMethod interface {
    // 驗證token的簽名,若是有限返回nil
    Verify(signingString, signature string, key interface{}) error
    // 簽名方法 接受頭部和荷載編碼事後的字符串和簽名祕鑰
    // 在hmac中key必須是Key must be []byte
    // 在rsa中key 必須是*rsa.PrivateKey 對象
    Sign(signingString string, key interface{}) (string, error)
    // 返回加密方法的名字 好比'HS256'
    Alg() string
}

// 新建token
func New(method SigningMethod) *Token {
    return NewWithClaims(method, MapClaims{})
}

func NewWithClaims(method SigningMethod, claims Claims) *Token {
    // 組成token
    return &Token{
        Header: map[string]interface{}{
            "typ": "JWT",
            "alg": method.Alg(),
        },
        Claims: claims,
        Method: method,
    }
}
2. 建立簽名

建立簽名的邏輯很清晰,下面的註釋中已經很清楚了。

// 傳入 key 返回token或者error
func (t *Token) SignedString(key interface{}) (string, error) {
    var sig, sstr string
    var err error
    // 生成jwt的前兩部分string
    if sstr, err = t.SigningString(); err != nil {
        return "", err
    }
    // 根據不一樣的簽名method 生成簽名字符串
    if sig, err = t.Method.Sign(sstr, key); err != nil {
        return "", err
    }
    return strings.Join([]string{sstr, sig}, "."), nil
}

// 生成jwt的頭部和荷載的string
func (t *Token) SigningString() (string, error) {
    var err error
    parts := make([]string, 2)
    // 建立一個字符串數組
    for i, _ := range parts {
        var jsonValue []byte
        if i == 0 {
            // 把header部分轉成[]byte
            if jsonValue, err = json.Marshal(t.Header); err != nil {
                return "", err
            }
        } else {
            // 把荷載部分部轉成[]byte
            if jsonValue, err = json.Marshal(t.Claims); err != nil {
                return "", err
            }
        }
        // 爲簽名編碼
        parts[i] = EncodeSegment(jsonValue)
    }
    // 用'.'號拼接兩部分而後返回
    return strings.Join(parts, "."), nil
}
2. 驗證簽名

有了建立token,就必定有驗證token。這個操做通常在服務端的中間件完成。在上面的例子中也能夠看到。

// 解析方法的回調函數 方法返回祕鑰 能夠根據不一樣的判斷返回不一樣的祕鑰
type Keyfunc func(*Token) (interface{}, error)

func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
    return new(Parser).Parse(tokenString, keyFunc)
}

func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
    return new(Parser).ParseWithClaims(tokenString, claims, keyFunc)
}

func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
    // 解析tokenstring 根據'.' 風格以後用base64反編碼以後組成 token對象
    token, parts, err := p.ParseUnverified(tokenString, claims)
    if err != nil {
        return token, err
    }

    // 判斷parse裏的validmethods 是否爲空 不爲空則循環調用
    if p.ValidMethods != nil {
        var signingMethodValid = false
        var alg = token.Method.Alg()
        for _, m := range p.ValidMethods {
            if m == alg {
                signingMethodValid = true
                break
            }
        }
        if !signingMethodValid {
            // signing method is not in the listed set
            return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid)
        }
    }

    // 調用keyfunc 返回祕鑰 方法從以前的調用注入的方法
    var key interface{}
    if keyFunc == nil {
        // keyFunc was not provided.  short circuiting validation
        return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable)
    }
    if key, err = keyFunc(token); err != nil {
        // keyFunc returned an error
        if ve, ok := err.(*ValidationError); ok {
            return token, ve
        }
        return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
    }

    vErr := &ValidationError{}

    // 判斷是否須要驗證claims
    if !p.SkipClaimsValidation {
        // valid 方法中會判斷 過時時間、簽發人、生效時間 若是沒有這3個字段則不判斷
        if err := token.Claims.Valid(); err != nil {

            if e, ok := err.(*ValidationError); !ok {
                vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}
            } else {
                vErr = e
            }
        }
    }

    // 驗證jwt中第三部分 簽名 調用的是簽名方法定義的verify方法
    token.Signature = parts[2]
    if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
        vErr.Inner = err
        vErr.Errors |= ValidationErrorSignatureInvalid
    }
    // 設置valid字段
    if vErr.valid() {
        token.Valid = true
        return token, nil
    }

    return token, vErr
}

總結

上面的源碼,只是主要的流程。jwt中還有不少代碼上面兵沒有列出來,好比rsa,ecdsa的具體實現、claims.go裏面也有不少邏輯的判斷。有興趣的話能夠再深刻研究。

相關文章
相關標籤/搜索