一、什么是JWT
JWT(JSON WEB TOKEN),通过数字签名的方式,以json对象为载体,在不同的服务终端之间安全的传输信息,用来解决传统session的弊端。
JWT在前后端分离系统,或跨平台系统中,通过JSON形式作为WEB应用中的令牌(token),用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。
token一般都是用来认证的,比如我们系统中常用的用户登录token可以用来认证该用户是否登录。jwt也是经常作为一种安全的token使用。
二、为什么要用JWT?
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
传统的session认证有如下的问题:
- 每个用户经过我们的应用认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证用户的增多,服务器内存开销会明显增大;
- 用户认证之后,服务端使用session保存认证信息,那么要取到认证信息,只能访问同一台服务器,才能拿到授权的资源。这样在分布式应用上,就需要实现session共享机制,不方便集群应用;
- 因为session是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
自此, 可以看到了JWT认证的优势:
- 简洁,可以通过URL、POST参数或Http header发送,因为数据量小,传输速度快;
- .自包含,负载(属于JWT的一部分)中包含了用户所需要的信息,不需要在服务器端保存会话信息,不占服务器内存,也避免了多次查询数据库,特别适用于分布式微服务;
-
.因为token是以json加密的形式保存在客户端的,所以JWT可以跨语言使用,原则上任何WEB形式都支持。
三、JWT认证流程
- 前端将用户名和密码发送到后端服务器。后端服务器对用户名和密码验证通过后,将用户信息作为JWT Payload负载,将其与头部进行Base64编号拼接后签名,形成JWT。形成的JWT本质上就是一个形如lll.zzz.xxx的字符串;
- 后端将JWT字符串作为登陆成功的返回结果返回给客户端。前端可以将返回结果保存在localStorage,退出登陆时,前端删除保存的JWT即可;
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位;
- 后端检查是否存在,如果验证JWT有效,后端就可以使用JWT中包含的用户信息。
四、JWT的组成
JWT其实就是一段字符串,由标头(Header)、有效载荷(Payload)和签名(Signature)这三部分组成,用 . 拼接。在传输的时候,会将JWT的三部分分别进行Base64编码后用.
进行连接形成最终传输的字符串。
1.Header
它会使用 Base64编码组成JWT结构的第一部分。
Header{ //标头
‘typ’:’JWT’, 表示token类型
‘alg’:’HS256’ 表示签名算法
}
2.Payload
用于存储主要信息,使用 Base64编码组成JWT结构的第二部分。由于该信息是可以被解析的,所以,在信息中不要存放敏感信息。
Payload //有效负载
{
‘userCode’:’43435’,
‘name’:’john’,
‘phone’:'13950497865'
}
3.Signature
前面两部分都使用Base64进行编码,前端可以解开知道里面的信息, Signature需要使用编码后的header和payload以及我们提供的一密钥,然后使用header中指定的签名算法进行签名,以保证JWT没有被篡改过。
使用Signature签名可以防止内容被篡改。如果有人对头部及负载内容解码后进行修改,再进行编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
五、JWT的使用
1.JWT的环境搭建
在pom.xml文件中导入依赖:
<!-- JWT依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.13.0</version>
</dependency>
<!-- 添加web启动器坐标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.创建JWT工具类Util
-
JWT的创建
/**
* 创建JWT
* @param map
* @return
*/
public String creatJWT(Map<String,String> map){
//设置Jwt的超时时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE,30);
//创建JWT,使用JWT.creat()方法进行创建
//此时builder对象中默认设置了header--表头,默认为JWT
JWTCreator.Builder builder = JWT.create();
//将信息写入有效载荷中payload
for (String key:map.keySet()){
//通过withClaim方法传入传输到payload中
builder.withClaim(key,map.get(key));
}
//利用builder和签名创健token,同时利用Calender类设置过期时间
String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256("lovo"));
return token;
}
- JWT的解码
/**
* 通过token和键解码信息
* @param token
* @param key
* @return
*/
public String verify(String token,String key){
//创建解码对象,利用JWT签名去完成解码对象的创建
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("lovo")).build();
//包含了JWT解码信息
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//利用键得到存放在有效负载payload的数据
String value = decodedJWT.getClaim(key).asString();
return value;
}
- 工具类测试
public static void main(String[] args) {
Util util = new Util();
Map map = new HashMap();
map.put("name","tom");
map.put("pwd","123");
System.out.println(util.creatJWT(map));
String token = util.creatJWT(map);
String value = util.verify(token,"pwd");
// System.out.println(value);
}
3.书写控制类Controller,用于JWT的发送
@Controller
public class JWTController {
/**
* 模拟登录
* @param name
* @param pwd
* @param response
* @return
*/
@RequestMapping("login")
@ResponseBody
public String login(String name, String pwd, HttpServletResponse response) {
if (name.equals("java") && pwd.equals("123")) {
//此时拥有该用户,则利用该用户的信息创建JWT
Map map = new HashMap();
map.put("name", name);
map.put("pwd", pwd);
String token = new Util().creatJWT(map);
//通过响应头发送给客户端
response.setHeader("token", token);
return "ok";
}
return "no";
}
@RequestMapping("getLogin")
@ResponseBody
public String getLogin(HttpServletRequest request){
String token = request.getHeader("token");
//利用工具对其进行解码
String name = new Util().verify(token,"name");
String pwd = new Util().verify(token,"pwd");
return "name="+name+"&pwd="+pwd;
}
}
4.在resource目录下书写user.html,用于JWT的接收
客户端通过axios接收响应头,并保存在sessionStorage中。
客户端会再次请求,读取localStorage的JWT信息,以请求头的方式,发送给服务器。
服务器从请求头中得到jwt字符串,解析后,得到数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="axios.min.js"></script>
</head>
<body>
<form action="#">
用户名:<input type="text" id="name"><br>
密码:<input type="text" id="pwd"><br>
<input type="button" value="登录" onclick="login()">
<input type="button" value="获取登录对象" onclick="getLogin()">
</form>
<script>
function login(){
let nameObj = document.getElementById("name")
let pwdObj = document.getElementById("pwd")
axios.get('/login',{
params:{
name:nameObj.value,
pwd:pwdObj.value
}
}).then(res=>{
console.log(res)
if (res.data==='ok'){
localStorage.setItem("token",res.headers.token)
}else {
alert("用户名或密码错误!")
}
})
}
/**
* 获取登录对象
*/
function getLogin(){
//读取localStorage的信息,以请求头的方式,发送给服务器
let token = localStorage.getItem("token");
//添加配置,在请求头中加入JWT的信息
let config = {
headers:{"token":token}
}
axios.get('/getLogin',config).then(res=>{
console.log(res)
})
}
</script>
</body>
</html>
5.MainServer测试
@SpringBootApplication
public class MainServer {
public static void main(String[] args) {
SpringApplication.run(MainServer.class,args);
}
}
测试结果: