@[TOC](JWT(Json Web Token))
1.简介
JWT全称 Json·Web·Token,是一个开放标准(RFC·7519),它定义了一种紧凑的,自包含的方式,用于作为JSON对象在各方之间安全的传输信息。该信息可以被验证和信任,因为它是数字签名的.
JWT是目前最流行的跨域身份解决方案。
2.使用场景
下列场景中使用JWT是很有用的:
2.1 Authorization(授权)
这是使用JWT的常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松的跨域使用。
跨域:
比如你的网站有几个不同的后台的域名,你在a域名下登录,登录信息记录到cookie,那么后面访问a域名才会带cookie,其他域名是不会带cookie的。
浏览器的同源策略:
同源:如果两个 URL 的 protocol、port (如果有指定的话)和 host 都相同的话,则这两个URL 是同源。
举例:
假如现在有一个url:http://store.company.com/dir/page.html
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 同源 | 只有路径 |
http://store.company.com/dir/inner/another.html | 同源 | 只有路径 |
https://store.company.com/secure.html | 失败 | 协议不同 |
http://store.company.com:81/dir/etc.html | 失败 | 端⼝不同 |
http://news.company.com/dir/other.html | 失败 | 主机不同 |
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
因为有了浏览器同源策略的限制,所以有了跨域问题。
单点登录( SSO):Single Sign On:比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,⽤户只需要登录一次就可以访问所有相互信任的应用系统。
2.2 Information Exchange(信息交换)
对于安全的在各方之间传输信息⽽言,Json·Web·Token无疑是一种很好的方式。因为JWT可以被签名,例如,⽤公钥/私钥配对,你可以确定发送人就是他们所说的那个。另外,由于签名是使用头和有效负载计算的,您还可以验证内容有没有被篡改。
3.如何解决单点登录问题(集群为例)
3.1 方案1:Nginx负载均衡做iphash,同一个用户的请求永远只给到同一个后台服务器。
不足之处:(1)⼀个服务有若干个用户,有的用户活跃,向后端发起请求的次数比较频繁,(2)而有的用户不活跃,那么基于这种iphash的解决方案的话就势必会造成负载不均衡,(3)也就是说有的tomcat服务服务器压⼒大,而有的tomcat服务器压⼒不大,造成资源分配不均的情况。(4)其次就是假如有一个tomcat服务挂了,那么对于有一些用户来说,这个系统就挂了,失去了集群的意义。
3.2 方案2:基于tomcat广播的session复制
不足之处:每⼀个tomcat都需要维护⼀个大的session,会造成内存资源紧张。
解决⽅案:使用JWT能够完美的解决上述问题
4.Token VS Session
4.1 基于session的身份认证
- HTTP协议是无状态的,也就是说,如果已经认证了一个用户,那么下一次请求的时候,服务器不知道我是谁,须再次认证。
- 传统的做法是将已经认证过的用户信息存储到服务器上,比如session。用户下次请求的时候带着sessionId,然后服务器检查用户是否已经认证过。
这种基于服务器的身份认证方式存在一些问题:
- Sessions:每次用户认证通过以后,服务器需要创建一条记录来保存用户信息,通常是在内存中。那么随着认证通过的用户越来越多,服务器在这里的开销就会越来越大。
- Scalability:由于session是在内存中的,这就带来一些扩展性的问题。
- CORS:当我们想要扩展我们的应用,当我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
- CSRF:用户很容易受到CSRF的攻击
CSRF:跨站请求伪造(早期的攻击方式)
- 假设一个网站用户Bob可能正在浏览聊天论坛,而同时另一个用户Alice也在此论坛中,并且后者刚发布了一个具有Bob银行链接的图片信息
- 正常情况下,Bob点击图片链接访问银行,银⾏会让Bob登录
- 假如Bob在此之前刚好登录过了银行,浏览器中还有cookie,那么此时银⾏可能认为点击这个图片访问银⾏是Bob发出的,所以会正常给出响应。
- 假如这个链接是一个Alice伪造的转账的请求,那么Bob可能就会收到经济损失
4.2 基于Token的身份认证
基于Token的身份认证,在服务端不需要存储用户的登录信息,大概流程如下:
- 客户端使⽤用户名、密码请求登录
- 服务器收到请求去验证用户名和密码
- 验证成功之后服务端会签发一个token,再把这个token发送给客户端
- 客户端收到token以后可以把它存储起来,存到客户端内存或者其他地⽅
- 客户端每次向服务器请求资源的时候需要带着服务器签发的token
- 服务端收到请求,然后去验证客户端请求⾥面带着的token,如果验证成功,就向客户端返回请求的数据
4.3 对比
- 两者都可以存储用户信息,然而,session是把用户信息保存在服务端的,而JWT是把用户信息保存在客户端的,当然,也可以保存到服务端,甚至保存到数据库中。
- Session方式存储用户信息最大的问题在于要暂用服务器⼤量的内存,增加服务器器的开销。而基于token的方式将用户状态分散到了各个客户端中,可以明显的减轻服务端的内存压力。
- session的状态存储在服务端,客户端只有sessionId,而token的状态是存储在客户端
4.4 使用Token的好处:
- ⽆状态和可拓展性:Token 存储在客户端,完全无状态,可拓展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。
- 安全:Token不是cookie。每次请求的时候token都会被发送,可以作为请求参数发送,可以放在请求头里面发送,也可以放在cookie里面被发送。即使在你的实现中将token存储到客户端的cookie中,这个cookie也只是一种存储机制,而⾮身份认证机制。没有基于会话的信息可以操作,因为我们没有会话。
5.JWT token的基本格式
5.1 Header,Payload,Signature
JSON·Web·Token由三部分组成,他们之间用圆点(·)连接,这三部分分别是:
- Header
- Payload
- Signature
5.2 Header由两部分信息组成:
- type:声明类型,这里是jwt
- alg:声明加密的算法 通常直接使用 HMAC SHA256
5.3 Payload就是存放有效信息的地方(不强制)
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的⼀方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- claim:jwt存放信息的地方
5.4 Signature就是签名信息
因此,一个典型的JWT看起来是这个样子的:
xxxxxxxx·yyyyyyyyyy·zzzzzzzzzzz
具体如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3bGd6cyIsImV4cCI6MTU4Nzk3MzY1N
ywidXNlciI6Ijk2MkYxODkwNTVFMzRFNzVERjVGMzQ0QTgxODNCODdGIn0.APehq9dxRiilgTOGyuz
9qtZxvPDIJ5QIIVUCLYeX1QE
6.项目中如何使用
- 项目已经整合好了JWT,并且给我们提供了现有的工具类进⾏操作:
com.mall.user.utils.JwtTokenUtils
- 如何使⽤现有的工具创建JWT?
String token = JwtTokenUtils.builder().msg("xxx").build().creatJwtToken();
- 如何解析JWT呢?
String msg = JwtTokenUtils.builder().token(token).build().freeJwt();
7.所有代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>basic_skillTest</artifactId>
<groupId>com.gy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.gy</groupId>
<artifactId>jwt</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.3</version>
</dependency>
</dependencies>
</project>
AESUtil
/**
* @Author:gaoyuan
* @Description:
* @DateTime:2021/4/20 14:24
**/
@Slf4j
public class AESUtil {
//加密或解密内容
@Setter
private String content;
//加密密钥
private String secret;
public AESUtil(String content) {
this.content = content;
this.secret = "iwofnoadnsa922342mnjaolkdsao9423242niosadopa_a02402sad";
}
/**
* 加密
* @return 加密后内容
*/
public String encrypt () {
Key key = getKey();
byte[] result = null;
try{
//创建密码器
Cipher cipher = Cipher.getInstance("AES");
//初始化为加密模式
cipher.init(Cipher.ENCRYPT_MODE,key);
//加密
result = cipher.doFinal(content.getBytes("UTF-8"));
} catch (Exception e) {
log.info("aes加密出错:"+e);
}
//将二进制转换成16进制
StringBuffer sb = new StringBuffer();
for (int i = 0; i < result.length; i++) {
String hex = Integer.toHexString(result[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 解密
* @return 解密后内容
*/
public String decrypt () {
//将16进制转为二进制
if (content.length() < 1)
return null;
byte[] result = new byte[content.length()/2];
for (int i = 0;i< content.length()/2; i++) {
int high = Integer.parseInt(content.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(content.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
Key key = getKey();
byte[] decrypt = null;
try{
//创建密码器
Cipher cipher = Cipher.getInstance("AES");
//初始化为解密模式
cipher.init(Cipher.DECRYPT_MODE,key);
//解密
decrypt = cipher.doFinal(result);
} catch (Exception e) {
log.info("aes解密出错:"+e);
}
assert decrypt != null;
return new String(decrypt);
}
/**
* 根据私钥内容获得私钥
*/
private Key getKey () {
SecretKey key = null;
try {
//创建密钥生成器
KeyGenerator generator = KeyGenerator.getInstance("AES");
//初始化密钥
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(secret.getBytes());
generator.init(128,random);
//生成密钥
key = generator.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return key;
}
public static void main(String[] args) {
AESUtil aesUtil=new AESUtil("Hello");
String ec=aesUtil.encrypt();
System.out.println(ec);
System.out.println(new AESUtil(ec).decrypt());
}
}
JwtTokenUtils
@Slf4j
@Builder
public class JwtTokenUtils {
/**
* 传输信息,必须是json格式
*/
private String msg;
/**
* 所验证的jwt
*/
@Setter
private String token;
private final String secret="324iu23094u598ndsofhsiufhaf_+0wq-42q421jiosadiusadiasd";
public String creatJwtToken () {
msg = new AESUtil(msg).encrypt();//先对信息进行aes加密(防止被破解) AES 对称加密
String token = null;
try {
token = JWT.create()
.withIssuer("zs").withExpiresAt(DateTime.now().plusDays(1).toDate())
.withClaim("user", msg)
.sign(Algorithm.HMAC256(secret));
} catch (Exception e) {
throw e;
}
log.info("加密后:" + token);
return token;
}
public static void main(String[] args) {
// String msg = "你好,JWT";
// JwtTokenUtils tokenCreateor = JwtTokenUtils.builder().msg(msg).build();
// String token = tokenCreateor.creatJwtToken();
// System.out.println(token);
String token = "pyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6cyIsImV4cCI6MTYxODkxMDc5MiwidXNlciI6IjgxMzQ4ODBFMUUyNjVBODE1MDc2MDc0NEQ2MDQyNkUyIn0.JlwuzwOnvqwTPpxCFDx7sMv6f-q0sYoimt4U4XRUAyA";
String s = JwtTokenUtils.builder().token(token).build().freeJwt();
System.out.println(s);
}
/**
* 解密jwt并验证是否正确
*/
public String freeJwt () {
DecodedJWT decodedJWT = null;
try {
//使用hmac256加密算法
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))
.withIssuer("zs")
.build();
decodedJWT = verifier.verify(token);
log.info("签名人:" + decodedJWT.getIssuer() + " 加密方式:" + decodedJWT.getAlgorithm() + " 携带信息:" + decodedJWT.getClaim("user").asString());
} catch (Exception e) {
log.info("jwt解密出现错误,jwt或私钥或签证人不正确");
//throw new ValidateException(SysRetCodeConstants.TOKEN_VALID_FAILED.getCode()
// ,SysRetCodeConstants.TOKEN_VALID_FAILED.getMessage());
throw new RuntimeException(" jwt 信息获取失败");
}
//获得token的头部,载荷和签名,只对比头部和载荷
String [] headPayload = token.split("\\.");
//获得jwt解密后头部
String header = decodedJWT.getHeader();
//获得jwt解密后载荷
String payload = decodedJWT.getPayload();
if(!header.equals(headPayload[0]) && !payload.equals(headPayload[1])){
//throw new ValidateException(SysRetCodeConstants.TOKEN_VALID_FAILED.getCode(),SysRetCodeConstants.TOKEN_VALID_FAILED.getMessage());
throw new RuntimeException(" jwt 信息获取失败");
}
return new AESUtil(decodedJWT.getClaim("user").asString()).decrypt();
}
}
结果:
创建jwt:
15:09:21.436 [main] INFO com.jwt.JwtTokenUtils - 加密后:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.eyJpc3MiOiJ6cyIsImV4cCI6MTYxODk4ODk2MSwidXNlciI6IjgxMzQ4ODBFMUUyNjVBODE1MDc2MDc0NEQ2MDQyNkUyIn0
.6x-Z973YmWy_ostDoGhiMV0JNoX8Jzs6Iu6jcfkSxfs
解密;