JWT 简介

2018-02-13 Tuesday     Network, Security, GoLang


在程序开发中,用户认证授权是一个绕不过的重难点,以前的开发模式下,cookie 和 session 认证是主流,随着前后端分离的趋势,基于 Token 的认证方式成为主流。

而 JWT(RFC 7519) 是基于 Token 认证方式的一种机制,是实现单点登录认证的一种有效方法。

这里详细介绍其设计和使用方式。

JSON Web Token, JWT

JWT 定义了一个紧凑且自包含的方式,通过 JSON 对象安全地传输信息,这些信息可以通过数字签名进行验证和信任,可以使用 HMAC 算法或使用 RSA 的公私钥对来对 JWT 进行签名。

体积足够小,可以通过 URL、POST 参数或者使用 HTTP 头发送,而且其有效载荷包含有关用户的所有必需信息,从而可以避免多次查询数据库。

详细可以参考 jwt.io 中的相关入门介绍,如下仅仅介绍相关的概念。

结构

JWT 包含三个由点 . 分隔的部分:头部、有效载荷、签名,所以看起来基本上类似 xxx.yyy.zzz 的格式。

头部

头部通常由两部分组成:令牌的类型 (JWT) 和正在使用的散列算法 (例如HMAC SHA256 RSA)。

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

然后,这个 JSON 被 Base64Url 编码,形成 JWT 的第一部分。

有效载荷

第二部分是包含声明的有效载荷,有三种类型的声明 (Claims) :保留,公开和私有声明。

  • 保留的声明 (Reserved Claims),一组预先定义的声明,非强制性但推荐使用,例如 iss(发行人),exp(到期时间),sub(主题),aud(听众)等等。
  • 公开声明 (Public Claims),这些可以由使用JWT的人员任意定义。 但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或者将其定义为包含防冲突命名空间的URI。
  • 私有声明(Private Claims),:这是为了同意使用它们的各方之间共享信息而创建的自定义claims。
{
	"sub": "1234567890",
	"name": "John Doe",
	"admin": true
}

其中保留声明主要包含了如下几种:

sub   该JWT所面向的用户 
iss   该JWT的签发者 
iat   Issued AT 在什么时候签发的token
exp   Expires token什么时候过期 
nbf   Not Before token在此时间之前不能被接收处理
jti   JWT ID为web token提供唯一标识

签名

签名部分会计算 编码头部 编码有效载荷 中的内容,例如,如果想使用 HMAC SHA256 算法,签名将按以下方式创建:

HMACSHA256(
	base64UrlEncode(header) + "." +
	base64UrlEncode(payload),
	secret
)

签名用于验证 JWT 是否被修改。

加密算法

HS256 HS384 HS512
	HMAC   + SHA256/384/512

ES256 ES384 ES512
	ECDSA  + SHA256/384/512
RS256 RS384 RS512
	RSA    + SHA256/384/512
PS256 PS384 PS512
	RSAPSS + SHA256/384/512

如何工作

当用户使用自己的凭证 (Credentials) 成功登录时,将返回一个 JSON Web Token,并且必须保存在本地 (可以是本地存储中,也可以使用 Cookie),当然为了安全考虑,需要确认其有效时间。

无论何时用户想要访问受保护的路由或资源时,需要同时带上 JWT ,一般来说在 Authorization 头部的 Bearer 模式中,类似如下:

Authorization: Bearer <token>

这是一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中,服务器通过检查是否为有效的 JWT 判断其权限,由于 JWT 是独立的,所有必要的信息都在那里,减少了多次查询数据库的需求。

JWT 实现方案

JWT-GO

jwt-go

简单示例

如下是 HS256 的示例。

package main

import (
	"fmt"
	"time"

	"github.com/dgrijalva/jwt-go"
)

func main() {
	key := "It's your secret key"
	token := jwt.New(jwt.SigningMethodHS256)
	claims := make(jwt.MapClaims)
	claims["username"] = "Your Name"
	claims["iss"] = "Cargo JWT Builder"
	claims["aud"] = "www.cargo.com"
	claims["exp"] = time.Now().Add(time.Hour * time.Duration(1)).Unix()
	claims["iat"] = time.Now().Unix()
	token.Claims = claims

	tokenString, err := token.SignedString([]byte(key))
	if err != nil {
		fmt.Printf("[ERROR] Sign the token failed, %v\n", err)
		return
	}
	fmt.Printf("[ INFO] Got signed token '%v'\n", tokenString)

	newtoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(key), nil
	})

	if claims, ok := newtoken.Claims.(jwt.MapClaims); ok && newtoken.Valid {
		fmt.Printf("[ INFO] Got claims '%v'\n", claims)
	} else {
		fmt.Printf("[ERROR] Invalid claims '%v'\n", err)
	}
}

PyJWT

PyJWT 是一个用来编码和解码 JSON Web Tokens, JWT 的 Python 库,可以通过 pip install pyjwt 命令安装。

生成 Token

需要使用 PyJWT 的 encode() 方法,需要传入三个参数:

jwt.encode(payload, config.SECRET_KEY, algorithm='HS256')

上面代码的方法中传入了三个参数:A)payload 认证依据的主要信息;B) 密钥,这里是读取配置文件中的SECRET_KEY配置变量;C) 生成 Token 的算法。

注意,payload 是认证的依据,也是后续解析 token 后定位用户的依据,需要包含特定用户的特定信息,例如可以记录用户 ID 和登陆时间,其中 pyjwt 内置了几个声明:

exp: 过期时间
nbf: 表示当前时间在nbf里的时间之前,则Token不被接受
iss: token签发者
aud: 接收者
iat: 发行时间

RBAC

也就是 Role Based Access Control 基于角色的访问控制,RBAC 认为权限授权实际上是 Who、What、How 的问题,从而构成了访问权限三元组,也就是 “Who 对 What 进行 How 的操作”。

其它

关于 Token

近年来 RESTful API 开始风靡,使用 HTTP Header 来传递认证令牌似乎变得理所应当,而单页应用、前后端分离架构似乎正在促成越来越多的 WEB 应用放弃历史悠久的 Cookie-Session 认证机制,转而使用 JWT 来管理用户 Session。

水平扩展

在 Cookie-Session 方案中,Cookie 内仅包含一个 SessionID ,而诸如用户信息、授权列表等都保存在服务端的 Session 中,如果把 Session 中的认证信息都保存在 JWT 中,在服务端就没有保存 Session 的必要了。

因此,当服务端水平扩展的时候,就不用处理 Session 复制或者引入外部 Session 存储了。

这三个概念经常出现,而且容易混淆。

Cookie 总是保存在客户端中,是浏览器实现的一种数据保存方式,可以保存在内存或者磁盘中,前者由浏览器维护,关闭后就消失了,而后者通常会设置一个过期时间。

由服务器生成,发送给浏览器,浏览器把 Cookie 以 KV 形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该 Cookie 发送给服务器。

由于 Cookie 是存在客户端上的,所以浏览器加入了一些限制确保 Cookie 不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的 Cookie 数量是有限的。

Session

简单来说,客户端与服务器进行交互时,需要确认发送端是谁,为了做这种区分,服务器就要给每个客户端分配不同的 “身份标识”,然后客户端每次向服务器发请求的时候,都带上这个 “身份标识”,服务器就知道这个请求来自于谁了。

至于客户端怎么保存这个”身份标识”,可以有很多种方式,对于浏览器客户端,大家都默认采用 Cookie 的方式。

服务器使用 Session 把用户的信息临时保存在了服务器上,用户离开网站后 Session 会被销毁,这种用户信息存储方式相对 Cookie 来说更安全。

Token

也就是 “令牌” 是用户身份的验证方式,最简单的 Token 组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接 Token 请求服务器),也可以把不变的参数放进 Token,避免多次查库。

JWT 就是 Token 的一种实现方式。

参考

https://jwt.io/



如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^