简介
1、JWT介绍
JWT(Json Web Token)是一种可以跨域的认证方案。
构成:
- 头部Header:头部包含了两部分,token 类型和采用的加密算法(可为none,后端应限制加密算法,不以这里为准)。
- 载荷Payload:这部分才是重要的,可以自定义信息保存在此。
- 签名Signature:使用编码后的header和payload以及我们提供的一个密钥,然后使用header中指定的签名算法进行签名,签名的作用是保证JWT没有被篡改过,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
这三部分均用base64进行编码,并使用 . 进行分隔。一个典型的 JWT 格式的 token 类似xxxxx.yyyyy.zzzzz。
JWT对三个部分都使用的是Base64进行编码,但是Base64是可逆的,所以注意在Payload部分不要携带敏感信息。
特点:
- 简洁(Compact) : 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
- 自包含(Self-contained) :负载中包含了所有用户所需要的信息,避免了多次查询数据库。
核心优势:无状态
- 省事,解析完token把信息放在处理链路的上下文,不用去redis或者database去取了。
- 省时,用每个请求验证token慢一点点的代价,换来不用去存储取的优势。
2、JWT和Session对比
存储id的差异:
Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。
Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。
使用Session的缺点:
- 占用服务端内存;
- 扩展性差(跨域难解决,不利于分布式);
- 基于Cookie,容易受到CSRF(跨站请求伪造)攻击。
JWT使用流程
- 前端通过Web表单将用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为 JWT-Payload(负载),将其与头部分别进行Base64编码拼接后签名形成一个JWT。形成的JWT就是一个形同111.zzz.xxx的字符串。
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
代码实现
1、添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
2、生成token
private static final String ISSUER = "签发者";
/**
* 创建JWT令牌
* @param userId
* @return
* @throws Exception
*/
public static String generateToken(Long userId) throws Exception {
//使用Algorithm的RSA256的加密算法
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(),RSAUtil.getPrivateKey());
//日历类
Calendar claendar = Calendar.getInstance();
//设置日历为当前时间
claendar.setTime(new Date());
//设置过期时间及单位
claendar.add(Calendar.SECOND,30);
//创建JWT token令牌,设置KeyId、签发者、过期时间、加密算法
return JWT.create().withKeyId(String.valueOf(userId))
.withIssuer(ISSUER)
.withExpiresAt(claendar.getTime())
.sign(algorithm);
}
3、token认证
/**
* token验证方法
* @param token
* @return
*/
public static Long verifyToken(String token) {
try {
//此处也是使用RSA加解密算法,因为获取token使用了RSA的加密算法,这里也需要使用对应的解密算法
Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(),RSAUtil.getPrivateKey());
//创建JWT认证者
JWTVerifier verifier = JWT.require(algorithm).build();
//获取解密后的JWT
DecodedJWT verify = verifier.verify(token);
//获取JWT的keyId,也就是这里的userId
String userId = verify.getKeyId();
return Long.valueOf(userId);
}catch (TokenExpiredException e){
throw new ConditionException("555", "token过期!");
}catch (Exception e) {
throw new ConditionException("非法用户token!");
}
}