Go Jwt使用和源码学习

大二小的宝 · · 885 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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加起来再么秘钥加密的。如下图:
image
这样做有几个好处:

  • 可以减少请求数据库的次数,不需要每次数据接口请求都去访问数据库验证用户的有效性
  • 可以设置过期时间,在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里面也有很多逻辑的判断。有兴趣的话可以再深入研究。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:大二小的宝

查看原文:Go Jwt使用和源码学习

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

885 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传