一、什么是JWT?
jwt全称为jsonwebtoken,它是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息.此信息可以验证和信任,因为它是数字签名的.jwt可以使用HMAC算法或RSA或ECDSA的公钥/私钥对进行签名.
二、JWT的作用
1、授权
这是使用JWT的最常见方案,一旦用户登录,每个后续请求将包含JWT,从而允许用户访问该令牌允许的路由,服务和资源,单点登录时当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用.
2、信息交换
JWT是在各方之间安全的传输信息的好方法,因为可以对JWT进行签名,使用表头和有效负载以及自定义标签计算,因此不仅可以验证请求方是否可访问,还可以验证内容是否被篡改.
三、为什么要使用JWT?
1、传统的Session登录验证方式通常需要将用户id或用户姓名以及权限等信息存储在Session中便于后续业务开发,这样做的代价就是会消耗大量服务器的内存资源,因为在浏览器访问服务器时,若请求中携带了SeesionId,则服务器会根据这个SessionId来查询对应的Session,若查找成功,则从中提取信息,若查找为空,则创建一个Session,并将id存入cookie中响应给浏览器.因此在浏览器与服务器中都需要消耗资源存储Session,若在线用户量较大,则内存压力很大,此时通常会选择构建服务器集群的方式来缓解压力(当然也有其他方式).
2、如上所述,构建集群缓解内存压力,但此时又会引出一个新的问题,不同服务器之间的Session如何共享呢?因为如果Session不共享,那么在A服务器登录后,下次请求时被分配到B服务器,那么B服务器中并不存在登陆后所创建的Session,难道要用户再次登录吗?这肯定不合理,因此需要实现Session共享,此时一般都会选择利用缓存中间件,如Redis来记录登录状态,利用key-value数据存储,此方法较为可行,读取速度也快,也能有效设置过期时间,但如果Redis宕机了呢?这时有人可能会提出Redis集群的方式来保证可靠性,但如果极端情况下,都宕机了怎么办,系统就直接宣告崩溃吗?
(当然也有其他方式,例如:
(1)、将存储在服务端的session保存在客户端cookie中,按照自定义的加密规则将session数据加密保存到cookie中:
优点:服务器压力减小,同时能够解决在多个服务器之间session不共享的问题。
缺点:不同的浏览器对cookie容量有大小限制;客户端每次请求都需要通过cookie发送session数据给服务器,导致网络请求占用高。
该方案不适合高访问量:带宽性能消耗大。
(2)、Session存入数据库中实现共享
建议使用内存表Heap进行存储
优点:安全性好。
缺点:(1)自己需要实现session淘汰逻辑;
(2)进行高并发读写时,数据库性能将会成为瓶颈,且容易出现表锁。
(3)、基于NFS的Session共享
多台运行服务器共享保存session文件的磁盘。
优点:实现简单,仅需将共享目录服务器mount到各频道服务器的本地session目录。
缺点:NFS依托于复杂的安全机制和文件系统,因此并发效率不高,尤其对于session这类高并发读写的小文件, 会由于共享目录服务器的io-wait过高,最终拖累前端WEB应用程序的执行效率。
)
3、CSRF(跨站伪造请求攻击)很难解决
session是基于cookie进行用户识别的,cookie如果被截获,用户就会很容易收到跨站请求伪造的攻击,用户信息容易泄露,同时也可能对系统造成破坏.
4、移动端设备session特性
移动端设备访问网站时,IOS/Android用原生接口发请求最大的特点是每一次请求创建一个会话,那么session用于登录验证自然也就无用了.
为了解决上述问题,JWT应运而生.JWT所生成的令牌是存储在客户端的,这样解决了服务端内存消耗以及信息共享的问题,同时也解决了CSRF攻击隐患,因为服务端可以判断令牌内容是否被篡改,对于移动端设备session特性,都不采用session了,自然不用考虑这个问题.是不是感觉JWT验证非常nice呢!
四、JWT的结构
1、JWT令牌组成
(1)、表头(Header)
(2)、有效负载(Payload)
(3)、签名(Signature)
因此,JWT通常如下所示:xxxxx.yyyyy.zzzz Header.Payload.Signature
2、Header
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法.它会使用Base64编码组成JWT结构的一部分,默认情况下一般为{ “alg":"HS256", "typ":"JWT"},需要注意的是,Base64仅仅是一种编码,因此也可以解码,所以它并不是一种加密过程.
3、Payload
令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常是用户)和其他数据的声明.同样的,它也会使用Base64编码组成JWT结构的第二部分,例如{ "userid":"1","username":"zhangan"},因此在服务端构建jwt时,可以使用哈希表传参.但负载并没有采取加密,所以不应放置用户密码等敏感信息.
4、Signature
前面两部分都是使用Base64进行编码的,即前端可以解析信息,Signature需要使用编码后header和payload以及我们提供的一个密钥,然后使用header指定的签名算法进行签名,签名的作用是保证JWT没有被篡改过.
签名目的:最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务端就会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的.如果要对新的头部和负载签名,在不知道服务器加密时用的密钥的话,的出来的签名也是不一样的.
服务端返回的内容为三个由点分割的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准相比,它更为紧凑与简洁,可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度快,负载中也可以包含用户信息,避免了多次查询数据库.
五、创建方式
1、引入jwt依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、封装工具类
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
@Component
public class JWTUtils {
private static final String SIGNAL="!TEST%2002@YUSHEN&&XIAOYU#login";
public String getToken(Map<String,Object> map) {
JwtBuilder jwtBuilder= Jwts.builder();//获取JWT构造器
String token=jwtBuilder.setIssuedAt(new Date()) //设置jwt生成时间
.setClaims(map) //可以存放用户姓名、id此类便于后续业务开发的信息,不要存放密码等敏感信息
.setExpiration(new Date(System.currentTimeMillis()+5000)) //设置token有效期
.signWith(SignatureAlgorithm.HS256,SIGNAL) //设置token加密方式和密钥
.compact(); //生成token字符串
return token;
}
public Claims check(String token){
if(null==token||"".equals(token)) throw new RuntimeException("无token");
//获取jwt解析器
JwtParser parser = Jwts.parser();
parser.setSigningKey(SIGNAL);//指定解析密钥
Jws<Claims> claimsJws = parser.parseClaimsJws(token);
return claimsJws.getBody();
}
}
3、一般JWT验证会配合拦截器使用