jwt教程

未完待续

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>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容