58

Go Jwt使用和源码学习

 4 years ago
source link: https://www.tuicool.com/articles/jYVfYrQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Jwt概念

JWT(JSON Web Token)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一个JWT由3个部分组成:头部(header)、载荷(payload)、签名(signature)。

这三个部分又是由一个分隔符“.” 分割开的。

header

用户说明签名的加密算法等,大概如下:

{

"typ": "JWT",

"alg": "HS256"

}

payload

payload 结构是一个json或者说是map对象

目前有一个相对标准的payload格式

  • sub: 该JWT所面向的用户
  • iss: 该JWT的签发者
  • iat(issued at): 在什么时候签发的token
  • exp(expires): token什么时候过期
  • nbf(not before):token在此时间之前不能被接收处理
  • jti:JWT ID为web token提供唯一标识

当然你也可以不用这些字段,可以自己随意定义。

signature

签名是由头部和荷载加上一串秘钥,经过头部声明的加密算法加密得到的。因为这个秘钥只有服务端知道,但是这个秘钥一旦泄漏了后果是很严重的。

使用

一般使用方法,则是在登录的时候生成一个token返回到客户端。客户端则可以放到header或者cookie中。每次请求数据的时候带上这个token,而服务端则去验证token是否正确,因为jwt中的秘钥只有服务器知道一旦这个token被别人修改过及时修改过再使用base64编码替换也是可以被发现的,因为签名是把header和payload加起来再么秘钥加密的。如下图:

1460000019214864

这样做有几个好处:

  • 可以减少请求数据库的次数,不需要每次数据接口请求都去访问数据库验证用户的有效性
  • 可以设置过期时间,在payload中有一个字段叫exp。这个字段可以设置过期时间,如果服务端发现过期则需要中心登录或者验证身份。
  • 在荷载(payload)中其实是可以作为客户端服务器端的信息交换,但是一般不会用这样的操作
  • 服务器不保存session状态,更适合分布式的系统构建。每个请求不用通过hash打到固定的机器上。

但是也带来了一些问题:

  • 因为服务器不保存状态,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里面也有很多逻辑的判断。有兴趣的话可以再深入研究。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK