jwt教程

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

未完待续

JWT是什么?

JWT是JSON Web Token的缩写,即JSON Web令牌

<a href="https://tools.ietf.org/html/rfc7519" target="_blank">JWT规范</a>中对其所作的描述是:

JSON Web令牌(JWT)是一种紧凑的、URL安全的方式,用来表示要在双方之间传递的“声明”。JWT中的声明被编码为JSON对象,用作JSON Web签名(JWS)结构的有效内容或JSON Web加密(JWE)结构的明文,使得声明能够被:数字签名、或利用消息认证码(MAC)保护完整性、加密。

JWT的声明(Claims)就是一小段信息,用“键-值”对表示。

想要详细了解<a href="https://tools.ietf.org/html/rfc7515" target="_blank">JSON Web签名(JWS)</a>和<a href="https://tools.ietf.org/html/rfc7516" target="_blank">JSON Web加密(JWE)</a>,可以自行去IETF的网站查阅规范,下文中我会简单的介绍它们。

JWT的构成

JWT由三部分组成:

  • Header:头部,即JOSE Header
  • Claims:声明,即JWS Paylaod
  • Signature:签名,即JWS Signature

JWT由这三部分组成,每一部分都是使用base64url编码的,并使用句点(.)连接起来。这里使用base64url编码而不是普通的base64,是因为base64编码会产生+/,这两个字符在URL中是有特殊意义的,会导致JWT不是URL安全的

下面以<a href="http://jwt.io" target="_blank">JWT.io</a>首页的一个例子介绍JWT的组成。再用Golang通过这些JSON对象生成JWT,最后用<a href="http://github.com/dgrijalva/jwt-go" target="_blank">jwt-go</a>包比对生成的JWT。

JWT标准并没有规定必须清除JSON结构中开头结尾的空白符和换行,但是为了消除歧义,一般在使用JSON对象时不用换行,并去掉多余的空白符,这会在我们的代码中有所体现。

为了方便查看,下面展示代码时使用的都是格式化后的JSON对象。

头部(JOSE Header)

JSOEJSON Object Signing and Encryption,即JSON对象签名与加密的缩写。

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

示例中给出了两个声明:

  • typ: (Type)类型。在JOSE Header中这是个可选参数,但这里我们需要指明类型是JWT
  • alg: (Algorithm)算法,必须是JWS支持的算法,算法列表可以在<a href="https://tools.ietf.org/html/rfc7518" target="_blank">JSON Web算法(JWA)</a>。这里指定算法为HS256

例子中只列举了两个声明,更多的声明和其具体定义可以到<a href="https://tools.ietf.org/html/rfc7515" target="_blank">JSON Web签名(JWS)</a>中查看。

Golang代码:

...
header := []byte(`{
  "typ": "JWT",
  "alg": "HS256"
}`)

buffer := new(bytes.Buffer)
//去掉多余的换行和空白符
json.Compact(buffer, header)
//Base64URL编码
jwtHeader := base64.URLEncoding.EncodeToString(buffer.Bytes())
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
fmt.Println(jwtHeader)
...

上述代码片段会输出eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,这就是编码后的JWT头部。

声明(JWT Claims)

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

例子中给的是一个注册的声明(sub),和两个私有的声明(nameadmin)。

注册的、公开的、私有的

在一个声明集当中,一般会有如下注册的声明名字

  • iss: (Issuer)签发者
  • iat: (Issued At)签发时间,用Unix时间戳表示
  • exp: (Expiration Time)过期时间,用Unix时间戳表示
  • aud: (Audience)接收该JWT的一方
  • sub: (Subject)该JWT的主题
  • nbf: (Not Before)不要早于这个时间
  • jti: (JWT ID)用于标识JWT的唯一ID

上面的声明都是可选的,但是一般都达成共识,
注册的声明是在IANA中注册的,
公开的声明要保证不引起命名冲突
私有的声明可以使用

Golang代码:

...
claims := []byte(`{
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
}`)

buffer := new(bytes.Buffer)
json.Compact(buffer, claims)
jwtClaims := base64.URLEncoding.EncodeToString(buffer.Bytes())
fmt.Println(jwtClaims)
...

上述代码片段会输出eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9,这就是编码后的JWT声明。

签名(Signature)

按照头部中指定的,我们要使用HS256算法对上面的编码后的字符串进行签名。
头部和声明用.号连接起来:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

我们要做的就是对这个字符串进行签名。

Golang代码:

...
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
s := strings.Join([]string{jwtHeader, jwtClaims}, ".")
//HS256算法,key是"secret"
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write([]byte(s))
expectedMAC := mac.Sum(nil)
//TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
signature := strings.TrimRight(base64.URLEncoding.EncodeToString(expectedMAC), "=")
fmt.Println(signature)
...

上述代码输出TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ,这就是这个JWT的签名。

将头部、声明、签名用.号连在一起就得到了我们要的JWT。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

验证

...
//定义
type MyCustomClaims struct {
    Sub   string `json:"sub"`
    Name  string `json:"name"`
    Admin bool   `json:"admin"`
}
//实现Claims接口
func (m MyCustomClaims) Valid() error {
    return nil
}

mySigningKey := []byte("secret")

claims2 := MyCustomClaims{
    "1234567890",
    "John Doe",
    true,
}


token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims2)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v\n", ss, err)
if ss == s {
    fmt.Println("OK")
}
...
// Encode JWT specific base64url encoding with padding stripped
func EncodeSegment(seg []byte) string {
    return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
}

// Decode JWT specific base64url encoding with padding stripped
func DecodeSegment(seg string) ([]byte, error) {
    if l := len(seg) % 4; l > 0 {
        seg += strings.Repeat("=", 4-l)
    }
    return base64.URLEncoding.DecodeString(seg)
}

不安全的JWT

签名为空的JWT

创建JWT

按一下步骤创建:

对UTF-8的八进制序列进行Base64url编码

一些可以应用JWT的案例

注意:下面的例子设计并不完善,甚至存在漏洞。这里仅仅是展示JWT的用途。不要将例子直接用于生产环境。

验证用户

签发JWT

1.客户端发送带有用户名、密码的表单到服务器;
2.服务器验证用户名密码后,将user_id作为JWT Claims中的一个声明,生成JWT;
3.将签发的JWT作为cookies的内容发送给用户。

这里要注意,JWT作为cookies的一部分,本质上还是cookies,所以还是要遵循一般的安全原则,防止XSS等攻击手段。

验证请求

1.客户端发送带有JWT的请求到服务器;
2.服务器从JWT中提取信息;
3.验证JWT是否合法(签名是否正确、令牌是否过期、请求时间在nbf之前还是之后、签发人是否被接受、服务器是否是真正的接受者等);
4.从声明中取出user_id

和session的区别

session需要在服务器中存储标记用户的信息,比如session_id,而JWT则需要。

JWT在服务器端需要一定量的计算,而session方式一般不需要。

在分布式系统中,使用Session的方式,需要在多台服务器之间session id,增加了服务器的内存和IO压力。而JWT方式则免去了同步的麻烦。因为用户的状态已经存储在客户端中了,虽然增加了一些计算开销,但是与IO开销比起来,还是要好很多的。

单点登录

Set-Cookie: jwt=header.claims.signature; HttpOnly; max-age=980000; domain=.yourdomain.com

我们将域名设置为顶级域名(域名前要加.),这样yourdomain.com*.yourdomain.com都能接收这个cookies了。

免登陆退订订阅邮件功能

我们的邮箱中经常会收到一些订阅邮件,有一些

一些有用的链接

<a href="http://jwt.io" target="_blank">JWT.io</a>
<a href="https://www.sitepoint.com/using-json-web-tokens-node-js/" target="_blank">Using JSON Web Tokens with Node.js</a>


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

本文来自:简书

感谢作者:sladeliu

查看原文:jwt教程

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

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