JWT数字签名与token实现
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。什么时候你应该用JSON Web Token?Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。
JWT介绍
官方介绍
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
什么时候你应该用JSON Web Token ?
下列场景中使用JSON Web Token是很有用的:
- Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
- Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
博主讲解
JWT具体用来干什么的?
JSON Web Token (JWT) 就是用来保证数据不可篡改,鉴定消息无篡改;绝不是保证消息保密性的东西;这一点要特别清楚,首先清楚JWT具体用来干什么的,从而能够理解其设计原理;经过jwt加密的消息是公开可见的,是不可篡改的,因此jwt的消息体中payload
的数据不要放敏感数据,如果需要放敏感数据,请加密。例如payload
中可以存放用户名,密码的话需要加密后在存放;
明白了上述用途,再去了解原理才会融会贯通,下面讲解一下非对称加密;
非对称加密
非对称加密(如RSA)是通过公钥和私钥进行加密,解密的过程;私钥只能被一方所拥有,一般是服务器;公钥可以公开,任何人可以获取公钥;通过公钥加密的消息只能通过私钥解密,通过私钥加密的消息也只能通过公钥解密;但是这个过程不是线性的,使用公钥加密和私钥加密的密文是不一样的;
-
公钥加密,私钥解密
这种情况用于消息的加密,保证消息的安全性,即保证消息的不可见性,保密性,敏感性;一般是客户端向服务器发送消息时为了避免消息被泄露,使用服务器公开的公钥进行消息加密形成密文发给服务器,只有服务器使用私钥才能解密密文获得消息。
-
私钥加密,公钥解密
使用私钥加密,可以作为数字签名防止伪造,但是无法保证数据的不可见性;因为私钥加密的密文可以被其他用户使用公钥解密,其他用户也就可以获取消息,如果要避免敏感消息被盗取,需要将敏感消息加密后放在jwt的
payload
中。
小常识
遗憾的是,公钥算法速度非常慢,大约比对称算法慢 1000 倍。 使用它们来加密大量数据是不切实际的。 实际上,公钥算法用于加密 会话密钥。 对称算法 用于加密/解密大多数数据。由于对消息进行签名实际上会加密消息,因此使用公钥签名算法对大型消息进行签名是不切实际的。
JWT的数据结构
JWT其实就是一个很长的字符串,字符之间通过"."分隔符分为三个子串,各字串之间没有换行符。每一个子串表示了一个功能块,总共有三个部分:JWT头(header)、有效载荷(payload)、签名(signature),如下图所示:
JWT头
JWT头是一个描述JWT元数据的JSON对象,通常如下所示:
{"alg": "HS256","typ": "JWT"}
alg:表示签名使用的算法,默认为HMAC SHA256(写为HS256)
typ:表示令牌的类型,JWT令牌统一写为JWT
最后,使用Base64 URL算法将上述JSON对象转换为字符串,注意Base64 URL只是编码算法,不是加密算法,可以被解码得到数据。
有效载荷
有效载荷,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。
有效载荷部分规定有如下七个默认字段供选择:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,还可以自定义私有字段。
userid:100001
username:nick
email:123@qq.com
最后,同样使用Base64 URL算法将有效载荷部分JSON对象转换为字符串,注意Base64 URL只是编码算法,不是加密算法,可以被解码得到数据。
签名
签名实际上是一个使用私钥加密的过程,是对上面两部分数据通过指定的算法生成哈希,以确保数据不会被篡改。
首先需要指定一个密码(secret,可以服务器随机设置,相当于种子),该密码仅仅保存在服务器中,并且不能向用户公开。然后使用JWT头中指定的签名算法(默认情况下为HMAC SHA256),根据以下公式生成签名哈希:为了得到签名部分,你必须有编码过的header、编码过的payload、一个密码,签名算法是header中指定的那个,然对它们签名即可。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串 ,每个部分用"."分隔,就构成整个JWT对象。
xxxxx.yyyyy.zzzzz
,xxxxx是header经过Base64 URL编码后的字符串,yyyyy是payload经过Base64 URL编码后的字符串,zzzzz是对xxxxx.yyyyy经过私钥加密后得到的字符串;
JWT签名算法
JWT签名算法中,一般有两个选择:HS256和RS256。
HS256 (带有 SHA-256 的 HMAC )是一种对称加密算法, 双方之间仅共享一个密钥,没有公钥私钥之分。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。
RS256 (采用SHA-256 的 RSA 签名) 是一种非对称加密算法, 它使用公共/私钥对: JWT的提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。
token认证
在讲token认证之前,我们先回顾一下以前熟悉的seesion认证,Cookie-session 认证机制是通过浏览器带上来Cookie对象来与服务器端的session对象匹配来实现状态管理。第一次请求认证在服务端创建一个Session对象,同时在用户的浏览器端创建了一个Cookie对象;当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效。session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。最大的问题是session将用户信息存储在了服务器上,这样会导致服务器存储压力。而token相反,把认证签名存放在客户端,客户端访问只需要带上token令牌签名即可访问自己的账户;
JSON Web Tokens是如何工作的
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证。传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。 JWT与Session的差异 相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
-
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
-
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
-
Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。
基于Token的身份认证是如何工作的 基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
-
用户携带用户名和密码请求访问 ,第一次访问必须使用账号密码;
-
服务器校验用户凭据,验证账号密码;
-
应用提供一个token给客户端,通过后生成jwt数据xxxxx.yyyyy.zzzzz作为token;
-
客户端存储token,并且在随后的每一次请求中都带着它,token用于验证用户身份;
-
服务器校验token并返回数据,如何验证呢?只需要使用私钥对xxxxx.yyyyy进行加密,然后得到的结果与zzzzz对比即可,通过后对yyyyy进行base64解码获得用户id,即证明客户端是上一次访问的用户,且数据未被篡改;如果被篡改了,那么xxxxx.yyyyy加密不能得到zzzzz。
每一次请求都需要token,Token应该放在请求header中 ,我们还需要将服务器设置为接受来自所有域的请求。
其他
用Token的好处 - 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。 - 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!
还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。JWT与OAuth的区别 -OAuth2是一种授权框架 ,JWT是一种认证协议 -无论使用哪种方式切记用HTTPS来保证数据的安全性 -OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。
实战jwt
jjwt是一个提供JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。
导入依赖
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
实战案例
package com.example.test;
import cn.hutool.core.io.FileUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.SignatureException;
import org.junit.Test;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
public class JwtTest {
/**
* 对称加密算法
*/
@Test
public void test_HS256(){
String key = "8888"; //秘钥要大于3个字符,至少四个字符
// 封装jwt的head部信息
HashMap<String, Object> header = new HashMap<>();
// SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法
header.put("arg",SignatureAlgorithm.HS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可
header.put("typ","JWT"); //指定令牌类型
// 封装jwt的body部分
HashMap<String, Object> body = new HashMap<>();
body.put("account","15637283927");
body.put("phone","123456");
body.put("role","user");
// 生成JWT令牌
String token = Jwts.builder()
.setHeader(header)
.setClaims(body)
.setId("00001") //设置这个jwt的唯一标识,不写也可以
.signWith(SignatureAlgorithm.HS256,key) //使用公钥加密
.compact();
// 打印生成的jwt令牌token
System.out.println(token);
String[] split = token.split("\\.");
System.out.println("header:"+split[0]);
System.out.println("body:"+split[1]);
// 解析jwt令牌,使用服务器的私钥解密
Jwt res = null;
try {
res = Jwts
.parser()
.setSigningKey(key)
.parse(token);
Header header_parser = res.getHeader();
Object body_parser = res.getBody();
System.out.println(header_parser);
System.out.println(body_parser);
} catch (SignatureException e) {
System.out.println("秘钥错误!");
}
}
/**
* 生成公钥和私钥
* @param seedPassword 提供种子密码
* @return
* @throws Exception
*/
//生成自己的 秘钥/公钥 对
public byte[][] GeneratPubPriKey(String seedPassword) throws Exception{
// 使用RSA算法,获得RSA算法实例
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(seedPassword.getBytes());
// 使用种子密码初始化
keyPairGenerator.initialize(1024, secureRandom);
// 获得密码对,编码为byte类型
KeyPair keyPair = keyPairGenerator.genKeyPair();
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
//存入本地文件,可以使用最后面提供的代码读取秘钥文件,这里作为例子直接返回
// FileUtil.writeBytes(publicKeyBytes, "d:\\pub.key");
// FileUtil.writeBytes(privateKeyBytes, "d:\\pri.key");
byte[][] key = new byte[][]{publicKeyBytes,privateKeyBytes};
return key;
}
/**
* 非对称加密算法:实现的是私钥加密,公钥解密
* 一般用于身份识别,电子签名,确认加密的数据属于公钥下发方
* 能用你的公钥解密,那么数据一定是你的(一定使用你的私钥加密的)
* 用于识别服务器身份
*/
@Test
public void test_RS256() throws Exception {
// 封装jwt的head部信息
HashMap<String, Object> header = new HashMap<>();
// SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法
header.put("arg",SignatureAlgorithm.RS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可
header.put("typ","JWT"); //指定令牌类型
// 封装jwt的body部分
HashMap<String, Object> body = new HashMap<>();
body.put("account","mimiNick");
body.put("phone","123456");
body.put("role","user");
// 获取公钥和私钥
byte[][] keys = GeneratPubPriKey("8888888");
// 获取私钥对象
byte[] priKeyByte = keys[1];
PKCS8EncodedKeySpec spec2 = new PKCS8EncodedKeySpec(priKeyByte);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(spec2);
// 生成JWT令牌
String token = Jwts.builder()
.setHeader(header)
.setClaims(body)
.setId("00001") //设置这个jwt的唯一标识,不写也可以
.signWith(SignatureAlgorithm.RS256,privateKey) //使用私钥加密
.compact();
// 打印生成的jwt令牌token
System.out.println(token);
String[] split = token.split("\\.");
System.out.println("header:"+split[0]);
System.out.println("body:"+split[1]);
//获取公钥
byte[] publicByte = keys[0];
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicByte);
PublicKey publicKey = kf.generatePublic(spec);
// 解析jwt令牌,使用服务器的私钥解密
Jwt res = null;
try {
res = Jwts
.parser()
.setSigningKey(publicKey) //使用公钥解密
.parse(token);
Header header_parser = res.getHeader();
Object body_parser = res.getBody();
System.out.println(header_parser);
System.out.println(body_parser);
} catch (SignatureException e) {
System.out.println("秘钥错误!");
}
}
@Test
public void test_RS256_2() throws Exception {
// 封装jwt的head部信息
HashMap<String, Object> header = new HashMap<>();
// SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法
header.put("arg",SignatureAlgorithm.RS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可
header.put("typ","JWT"); //指定令牌类型
// 封装jwt的body部分
HashMap<String, Object> body = new HashMap<>();
body.put("account","mimiNick");
body.put("phone","123456");
body.put("role","user");
// 获取公钥和私钥
byte[][] keys = GeneratPubPriKey("8888888");
// 获取私钥对象
byte[] priKeyByte = keys[1];
PKCS8EncodedKeySpec spec2 = new PKCS8EncodedKeySpec(priKeyByte);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(spec2);
//获取公钥
byte[] publicByte = keys[0];
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicByte);
PublicKey publicKey = kf.generatePublic(spec);
// 生成JWT令牌
String token = Jwts.builder()
.setHeader(header)
.setClaims(body)
.setId("00001") //设置这个jwt的唯一标识,不写也可以
.signWith(SignatureAlgorithm.RS256,privateKey) //使用私钥加密
.compact();
// 打印生成的jwt令牌token
System.out.println(token);
String[] split = token.split("\\.");
System.out.println("header:"+split[0]);
System.out.println("body:"+split[1]);
// 解析jwt令牌,使用服务器的私钥解密
Jwt res = null;
try {
res = Jwts
.parser()
.setSigningKey(publicKey) //使用公钥解密
.parse(token);
Header header_parser = res.getHeader();
Object body_parser = res.getBody();
System.out.println(header_parser);
System.out.println(body_parser);
} catch (SignatureException e) {
System.out.println("秘钥错误!");
}
}
}
获取磁盘上的秘钥文件代码
//获取私钥
public PrivateKey getPriKey() throws Exception{
InputStream resourceAsStream =
this.getClass().getClassLoader().getResourceAsStream("pri.key");
DataInputStream dis = new DataInputStream(resourceAsStream);
byte[] keyBytes = new byte[resourceAsStream.available()];
dis.readFully(keyBytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(spec);
}
//获取公钥
public PublicKey getPubKey() throws Exception{
InputStream resourceAsStream =
this.getClass().getClassLoader().getResourceAsStream("pub.key");
DataInputStream dis = new DataInputStream(resourceAsStream);
byte[] keyBytes = new byte[resourceAsStream.available()];
dis.readFully(keyBytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)