JWT
JWT全程叫 json web token,通过数字签名的方式,以json对象为载体,在不同的服务终端之间安全的传输信息。
JWT在前后端分离系统,或跨平台系统中,通过JSON形式作为WEB应用中的令牌(token),用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。
前端应用在访问后端应用时,会携带令牌(token),后端应用在接到令牌(token)后,会验证令牌(token)的合法性,从而决定前端应用是否能继续访问。
JWT还可以在系统之间进行信息传递,A系统通过令牌对B系统进行数据传输,在传输过程中,可以完成数据的加密,B系统拿到数据后,通过签名进行验证,从而判断信息是否有篡改
JWT的应用
JWT最常见的场景就是授权认证,一旦用户登录,后续每个请求都将包含JWT,系统在每次处理用户请求之前,都要先进行JWT安全校验,通过之后再进行处理。
1、授权
这是JWT最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源
2、信息交换
JWT是在各方之间安全地传输信息的好方法,可以验证传输内容是否遭到篡改。
JWT和session的比较
- 传统的session认证有如下的弊端:
我们知道HTTP本身是一种无状态的协议,这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,认证通过后HTTP协议不会记录下认证后的状态,那么下一次请求时,用户还要再一次进行认证,因为根据HTTP协议,我们并不知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在用户首次登录成功后,在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这是传统的基于session认证的过程
然而,传统的session认证有如下的问题:
- 登录用户的信息,都会保存在服务器的session中,随着用户的增多,服务器的开销会明显增大。
- session 是存放在服务器物理内存中,所以在分布式架构模式下,这种方式显示会失效,也可以利用session共享机制或者把session存放入redis中解决
- 非浏览器的客户端:手机移动端等不适用,因为session依赖于cookie,而移动端经常没有cookie
- 基于cookie的,cookie无法做到跨域,所以session的认证也无法跨域,对单点登录不适用,如果cookie被截获,用户就会容易受到跨站请求伪造的攻击
- JWT认证:
JWT认证基于令牌,该令牌存储在客户端,但认证由服务器端进行,解决服务器内存占用问题,当用户提交用户名和密码时,在服务器端认证通过后,会生成token令牌。然后将令牌响应给客户端浏览器。
客户端浏览器会在本地存储令牌,在客户端再次请求服务器接口时,每次都会携带JWT,在服务器验证通过后,再继续访问服务器资源
JWT的优点
1、简洁,可以通过URL、POST参数或HTTP header发送,因为数据量小,传输速度快
2、自包含,负载(属于JWT的一部分)中包含了用户所需要的信息,不需要在服务器端保存会话信息,不占服务器内存,也避免了多次查询数据库,特别适用于分布式微服务
3、因为token是以json加密的形式保存在客户端的,所以JWT可以跨语言使用,原则上任何WEB形式都支持
JWT的认证流程
1、前端将用户名和密码发送到后端服务器,后端服务器对用户和密码验证通过后,将用户信息作为JWT payload负载,将其与头部进行Base64编号拼接后签名,形成JWT。形成的JWT本质上就是一个形如lll.zzz.xxx的字符串。
2、后端将JWT字符串作为登陆成功的返回结果返回给客户端,前端可以将返回结果保存在localStorage,退出登陆时,前端删除保存的JWT即可。
3、前端在每次请求时将JWT放入HTTP Header中Authorization位
4、后端检查是否存在,如果验证JWT有效,后端就可以使用JWT中包含的用户信息。
JWT的组成
JWT其实就是一段字符串,由标头、有效负载、签名这三部分组成,用 . 拼接
- 标头
Header{ // 标头
‘typ’ :’ JWT’, //表示token类型
‘alg’ :’ HS256’ // 表示签名算法
}
它会使用 Base64编码组成JWT结构的第一部分。
- 有效负载
Payload // 有效负载
{
‘userCode’:’ 43435’ ,
‘name’ : ‘john’ ,
‘phone’ : ‘13950498765’
}
用于存储主要信息,使用 Base64编码组成JWT结构的第二部分。由于该信息是可以被解析的,所以,在信息中不要存放敏感信息。
- 签名
Signature签名
前面两部分都使用Base64进行编码,前端可以解开知道里面的信息
Signature需要使用编码后的header和payload以及我们提供的一密钥
然后使用header中指定的签名算法进行签名,以保证JWT没有被篡改过
使用Signature签名可以防止内容被篡改。如果有人对头部及负载内容解码后进行修改,再进行编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的
JWT的使用
- 首先创建Maven工程,导入依赖
<!-- JWT依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.13.0</version>
</dependency>
- 书写JWT工具类
/**
* JWT工具类
*/
public class JWTUtil {
/**
* 创建JWT
* @return JWT字符串
*/
public String createToken(Map<String,String> map){
// JWT 过期时间设置
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE , 30);
// 创建JWT -- 使用JWT.create()方法进行创建
// 此时builder对象中默认设置了header -- 标头,类型默认为JWT
JWTCreator.Builder builder = JWT.create();
// 将信息写入有效载荷中,payload中
for (String key : map.keySet()){
// 通过 withClaim方法传入输入到payload中
builder.withClaim(key,map.get(key));
}
// 利用builder 和签名创建token 同时利用Calendar类设置过期时间
String token = builder.withExpiresAt(calendar.getTime()).
sign(Algorithm.HMAC256("bibibi!"));
return token;
}
/**
* 根据token和键解码信息
* @param token token
* @param key 键
* @return 键对应的值
*/
public String verify(String token,String key){
// 创建解码对象 利用JWT签名去完成解码对象的创建
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("bibibi!")).build();
// decodedJWT 就包含了JWT解码信息
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// 利用键,得到存放在有效负载 payload中的数据
String value = decodedJWT.getClaim(key).asString();
return value;
}
- 此时就可以应用JWT了
例如如下代码,做一个简单的模拟登陆效果。
- 后端应用
@Controller
@RequestMapping("jwt")
public class JWTController {
/**
* 模拟登陆
* @param userName 用户名
* @param userPwd 用户密码
* @return 登陆信息
*/
@RequestMapping("login")
@ResponseBody
public String login(String userName, String userPwd, HttpServletResponse response){
if (userName.equals("zhang3") && userPwd.equals("123456")){
// 此时拥有该用户 则利用该用户信息创建JWT
Map map = new HashMap<>();
map.put("userName",userName);
map.put("userPwd",userPwd);
String token = new JWTUtil().createToken(map);
// 通过响应头发送token 到客户端
response.setHeader("token",token);
return "ok";
}
return "no";
}
@RequestMapping("/getLoginUser")
@ResponseBody
public String getLoginUser(HttpServletRequest request){
String token = request.getHeader("token");
System.out.println(token);
// 利用工具类对其进行解码
String userName= new JWTUtil().verify(token,"userName");
String userPwd = new JWTUtil().verify(token,"userPwd");
return "userName="+userName+"&userPwd="+userPwd;
}
}
- 客户端
<body>
<form action="#">
用户名:<input type="text" class="user" id="userName"><br>
用户名:<input type="text" class="user" id="userPwd"><br>
<input type="button" value="登陆" onclick="login()">
<input type="button" value="获取登陆对象" onclick="getLoginUser()" >
</form>
<script>
function login(){
let user = document.getElementsByClassName("user")
axios.get("jwt/login",{
params:{
userName:user[0].value,
userPwd:user[1].value
}
}).then(res =>{
console.log(res)
// 保存JWT信息 localStorage
if (res.data =="ok") {
localStorage.setItem("token", res.headers.token)
}else {
alert("用户名或密码错误")
}
})
}
/*
获取登陆对象
*/
function getLoginUser(){
// 读取localStorage的信息,以请求头的方式,发送给服务器
let token = localStorage.getItem("token")
// 添加配置, 在请求头加入JWT的信息
let config = {
headers:{
"token":token
}
}
axios.get("/jwt/getLoginUser",config).then(res => {
console.log(res)
})
}
</script>
</body>