传送门
Oauth2系列7:授权码和访问令牌的颁发流程是怎样实现的?
什么是JWT令牌
官方网站地址:JSON Web Tokens - jwt.io
如果接触过jwt,可能没有看过这个网址,但是应该见过下面这个图(很多网上的文章都会引用);这个地址是用来解析jwt_token结构体的工具
所以,就正式来看下官方给的定义:
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
翻译过来大致就是
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。
从上面可以看到jwt令牌定义成3部分:HEADER,PAYLOAD,VERIFY SIGNATURE,它是怎么划分的呢?贴下官网的例子
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
从这个字符串可以看到,一个jwt令牌分为3段,用.分隔,按照HEADER,PAYLOAD,VERIFY SIGNATURE顺序拼接的
HEADER
{
"alg": "HS256",
"typ": "JWT"
}
表示装载令牌类型和算法等信息,是 JWT 的头部。其中,typ 表示第二部分 PAYLOAD 是 JWT 类型,alg 表示使用 HS256 对称签名的算法
PAYLOAD
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
表示是 JWT 的数据体,代表了一组数据。其中,sub(令牌的主体,一般设为资源拥有者的唯一标识)、iat(令牌颁发的时间戳)是 JWT 规范性的声明,代表的是常规性操作。除了这几个之外,还可以定义:
- exp(令牌的过期时间戳)
- iss(令牌签发者)
- aud(令牌接收者)
更多的细节可以在JSON Web Token (JWT)查看
不过,在一个 JWT 内可以包含一切合法的 JSON 格式的数据,也就是说,PAYLOAD 表示的一组数据允许我们自定义声明。 比如:name(用户名称)。因为PAYLOAD里面往往是填充的业务数据,所以可以不局限于上面的几个标准字段。而且通常业务系统是会直接从这里获取数据的,所以有些字段是双方约定好的带有业务含义的,比如"age","birthday"等等。
VERIFY SIGNATURE
表示对 JWT 信息的签名。那么,它有什么作用呢?其实就是做验签:需要对其进行加密签名处理,而 SIGNATURE 就是对信息的签名结果,当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。我们可能认为,有了 HEADER 和 PAYLOAD 两部分内容后,就可以让令牌携带信息了,似乎就可以在网络中传输了,但是在网络中传输这样的信息体是不安全的,因为你在“裸奔”。
JWT令牌更像是一种协议
虽然名字叫jwt令牌,其它感觉把它作为一种协议感觉理合理些。
就是jwt跟oauth2是同一级别的,都是可以用于授权!
那么为什么有了oauth这种成熟且强大的授权协议,还会出现jwt呢?
回顾一下前面的Oauth2系列7:授权码和访问令牌的颁发流程是怎样实现的?
里面提到了令牌的生成及使用:一般的令牌格式是一个字符串,对于它没有什么格式化的要求;而且客户端拿到令牌之后,需要获取其它资源信息,需要通过token到资源服务器去再次获取。
就是说:
- 令牌的存储它是在授权服务器,客户端只拿到一个token的字符串标识,是不知道token背后具体的业务数据的
- 所有需要的业务数据,必须通过token到授权服务器再次换取
那么,有没有一种方式,让用户拿到token之后,直接获取业务数据呢?这样授权流程更简单,也没有了授权服务器存储的要求,为此JWT应运而生!
JWT令牌的使用场景
在刚才的官网下面有一个警告提示,也反应了它的使用场景:
- jwt令牌是一种凭据,可用于资源授权
- 同oauth2一样,令牌是需要保护的,防止泄露
- 令牌不会存储,在客户端进行验证
至此,JWT的应用场景呼之欲出,可以单独用做授权,也可以做为oauth2的令牌发放!
利用jwt可以带来几个好处:
- 因为jwt本身是结构化生成的,包含业务数据,所以避免了额外的存储及一些系统交互,有点类似时间换空间的思想(因为客户端要解析jwt令牌,是通过算法计算,会消耗时间)。
- 可以提高系统的伸缩性,因为jwt令牌不用存储,在分布式系统下,如果用它做会话管理,就天然无状态的;并且从授权服务器的角度,因为不用存储及交互,也减轻了它的压力,都分摊到客户端去了
- 再一个就是jwt本身的保密性,因为里面要求验签,也不用做额外的安全处理
但是,肯定是有它的局限性:
- jwt令牌一旦发放,是无法撤销的。虽然在jwt令牌的PAYLOAD里面设置exp(令牌的过期时间戳),但是无论多短,在未过期之前,该令牌都无法主动让它失效。一旦发生泄露,将没有任何补救措施。
- 基于上,这也是有些场景用jwt令牌做登录,登录成功发放jwt令牌,但是如果用户主动退出,是没有办法做的,只能用类似黑名单的方式记录下这些令牌,但是这又退化到oauh令牌了,违背了设计的初衷
JWT令牌的实现
官方网站地址:JSON Web Token Libraries - jwt.io
提供了不同语言,不同的实现
找一个JAVA版本的jjwt来试试
安装maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
生成令牌
public static void main(String[] args)
{
// 生成jwt令牌
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jwtToken = Jwts.builder().setSubject("Joe").signWith(key).compact();
// 检验jwt令牌
String name = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwtToken).getBody().getSubject();
System.out.println("subject:" + name);
}
直接用jjwt库完成jwt令牌的生成与检验很简单。一般来说,生成是在授权服务器,也就是
// 生成jwt令牌
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jwtToken = Jwts.builder().setSubject("Joe").signWith(key).compact();
而检验一般是客户端里面实现的,即
// 检验jwt令牌
String name = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwtToken).getBody().getSubject();
这样,就涉及密钥,即上面的Key,这个理论上是授权服务器颁发,下发给客户端,需要加密传输。即生成+检验要用同样的Key
令牌格式
打印一下刚才生成的token:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.u3U3CnkpdJyPqmwcrLt50yxozKVupV-piuVHpWqwPqw
可以发现明显的分为3段,用.分隔,并且将它放到jwt的工具网站里面
可以直观的看到完整的数据结构了!
对称加密
JJwt也支持公/私钥这种密钥对的方式,如下
// 密钥对
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
// 私钥加密
String jwtToken2 = Jwts.builder().setSubject("James").signWith(keyPair.getPrivate()).compact();
// 检验jwt令牌,公钥解密
String name2 = Jwts.parserBuilder().setSigningKey(keyPair.getPublic()).build().parseClaimsJws(jwtToken2).getBody().getSubject();
System.out.println("subject:" + name2);
-
Keys.keyPairFor(SignatureAlgorithm.RS256),工具方法生成密钥对
- 私钥加密
- 公钥解密
同上操作,打印一下刚才生成的token:
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJKYW1lcyJ9.ZfiWiXCcmYBtskj_zZrxsPD8_4-ADQGd2C6Sn1lsTE-47eg6a2jY8H_uc8BPJ8AQXwHYxGw75EOir8tco4Q3xVXHXaRxMcuLsp7B4HULc6CujwRjCvFJhyBe2IjADHWVWRgQ4zKOsmX2poUQnOiQ5BgBVR2GUbxcSCEZ5L649jc43mJHHyuzPs6IMUOhqzpliAcxVg5Fh-AtVsxneYWYks9wXlRIqYarGxtgR5LgSrGs3fTfALMZy-qYsSjHm0VaBg9vSOWmSfgne8xVdsy2Z6FF7K9RNES1e5_XKaY6k0vfPvVnYDMIlOW6BAgbxHclgiEy43vHbNglaM4mIcoh7Q
令牌检验
如果Key错误,理论上是通过异常的方式,把上面例子的key在检验之前,随便修改一下运行
Exception in thread "main" io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:399)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:529)
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:589)
at io.jsonwebtoken.impl.ImmutableJwtParser.parseClaimsJws(ImmutableJwtParser.java:173)
at com.tw.tsm.jwt.JjwtDemo.main(JjwtDemo.java:19)
如果抛出这个异常SignatureException就表示,jwt令牌不合法
更多的用法可以研究API了