什么是 JWT?
JSON Web Token,通过数字签名的方式,以 JSON 对象为载体,在不同的服务终端之间安全的传输信 息。(将信息进行封装,以 JSON 的形式传递) JWT 有什么用? JWT 最常见的场景就是授权认证,一旦用户登录,后续每个请求都将包含 JWT,系统在每次处理用户请 求的之前,都要先进行 JWT 安全校验,通过之后再进行处理。
前后端传输数据时,一般是使用 session 或是 cookie,但是都相对有自己的弊端,JWT 是利用 token 来对用户身份进行验证的方式,具体流程是前端使用用户名和密码来登录,服务器收到请求后验 证用户名和密码,验证成功后,服务器会签发一个 token ,将 token 返回给前端,前端将收到的 token 存储起来,在每次请求资源时都必须携带 token,服务器收到请求,会先验证 token 是否有效,验证成 功就给前端返回请求的数据。
优势
这种基于 token 的认证方式相对于传统的 session 认证方式更节约服务器资源,token 在服务端不需要 存储 session 信息,因为 token 自身包含了所有登录用户的信息,所有可以减轻服务端压力。
JWT 的组成 JWT 由 3 部分组成,用.拼接
eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiI
sInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2NjU1NTAzNDIsImp0aSI6IjBiOWU3MmRmLTQ0NjQtNDV
kYy04ODMwLWU3NTJkMjg1OGQ0MCJ9.00-RQiPM_a9_0q03CrmcrOBMaLbcfWw85Mbc7xc4AtU
这三部分分别是:
Header
包含token 类型和加密算法的名称
{
//类型
'typ': 'jwt',
//算法
'alg': 'HS256'
}
将信息进行base64编码(转码)
Payload //载荷(存储有效信息)
包含标准中注册的声明、公共的声明、私有的声明,其实就是信息安全的分类,存储主要的信息 同时也需要将信息进行base64编码(转码)
{
"sub": '1234567890',
"name": 'john',
"amdin":true
}
Signature //签名
将转码后的 header 和 payload 用.进行拼接之后再用使用 HS256 进行加盐加密,就构成了 jwt 的第三 部分
其实就是由转码后的 header 和 payload 进行再次加密后的数据
//对转码后的 Header 和 Payload 用.进行拼接
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
//使用 HS256 进行加盐加密
var signature = HMACSHA256(encodedString, 'secret');
项目搭建
pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jdk1.8 以上要加-->
<!-- <dependency>-->
<!-- <groupId>javax.xml.bind</groupId>-->
<!-- <artifactId>jaxb-api</artifactId>-->
<!-- <version>2.3.1</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.sun.xml.bind</groupId>-->
<!-- <artifactId>jaxb-impl</artifactId>-->
<!-- <version>2.3.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.sun.xml.bind</groupId>-->
<!-- <artifactId>jaxb-core</artifactId>-->
<!-- <version>4.0.1</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>javax.activation</groupId>-->
<!-- <artifactId>activation</artifactId>-->
<!-- <version>1.1</version>-->
<!-- </dependency>-->
测试加密/解密
如何使用 jwt
加密
在这里我们测试一下 jwt 的加密和解密过程来了解 jwt
首先使用 JwtBuilder 来创建 jwt 对象 ,根据三部分先加入 jwt 的 header,使用.setHeaderParam() 这里使用链式编程增加参数,再加入载荷 payload 使用 .claim() 方法 还可以加入主题和有效时间(一 天的有效时间 System.currentTimeMillis() + time),id 用了 UUID
定义全局签名信息用于解密,使用 .sigWith(算法,签名key) 加密,调用 .compact() 进行拼接
输出 JWT 数据
import io.jsonwebtoken.*;
import javafx.scene.chart.PieChart;
import sun.awt.SunHints;
import java.security.Key;
import java.security.KeyException;
import java.util.Date;
import java.util.UUID;
public class Test {
private long time = 1000*60*60*24;
private String signature = "amdin";//签名信息(解密)
@org.junit.Test
public void jwt(){
JwtBuilder jwtBuilder = Jwts.builder();//用来构建jwt对象
String jwtToken = jwtBuilder
//Header
.setHeaderParam("typ","jwt")
.setHeaderParam("alg","HS256")
//Payload
.claim("username","tom")
.claim("role","admin")
.setSubject("admin-test")//主题
.setExpiration(new Date(System.currentTimeMillis() + time))//有效
时间 当前时间+一天 24h
.setId(UUID.randomUUID().toString())
//Signature
.signWith(SignatureAlgorithm.HS256,signature)
.compact();//拼接三部分
System.out.println(jwtToken);
}
@org.junit.Test
public void parse(){
String token =
"eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbi
IsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2NjU1NTAzNDIsImp0aSI6IjBiOWU3MmRmLTQ0NjQtND
VkYy04ODMwLWU3NTJkMjg1OGQ0MCJ9.00-RQiPM_a9_0q03CrmcrOBMaLbcfWw85Mbc7xc4AtU";
JwtParser jwtParser = Jwts.parser();
//解密 通过签名进行解析
Jws<Claims> claimsJws =
jwtParser.setSigningKey(signature).parseClaimsJws(token);
//把解析的数据存入对象中
Claims claims = claimsJws.getBody();
//从对象中取出数据
System.out.println(claims.get("username"));
System.out.println(claims.get("role"));
System.out.println(claims.getId()); //ID
System.out.println(claims.getSubject()); //签名
System.out.println(claims.getExpiration()); //有效期截至时间
}
}
对 token 进行解密
首先 使用 Jwts.parser() 对象的 .setSigningkey(签名key)解密 .parseClaimsJws(token)会把所有信息解 析成 Claims,Claims 相当于我们的各种数据,通过签名对 token 进行解析拿到 Claims ,调用 .getBody 方法把封装的数据放到 claims 对象当中,之后就可以使用 对象 .get 方法取出数据,从而将 jwttoken 中解析出我们的数据。
Spring boot + vue + Jwt
简单的小项目来了解 jwt 具体用法
作为java web中的一个令牌,就像之前测试加密时,用户在登录后,访问咱们的登录接口,登录接口判 断登录成功后把用户信息转为 jwtoken(jwt工具类,跟上面加密过程一样) 存入用户信息中,把整个 用户对象给到前端。
controller
@GetMapping("/login")
public User login(User user){
if (USERNAME.equals(user.getUsername()) &&
PASSWORD.equals(user.getPassword())){
//添加token
user.setToken(JwtUtil.createToken());
return user;
}
return null;
}
JwtUtil
private static long time = 1000*60*60*24;
private static String signature = "amdin";
public static String createToken(){
JwtBuilder jwtBuilder = Jwts.builder();//用来构建jwt对象
String jwtToken = jwtBuilder
//Header
.setHeaderParam("typ","jwt")
.setHeaderParam("alg","HS256")
//Payload
.claim("username","admin")
.claim("role","admin")
.setSubject("admin-test")//签名
.setExpiration(new Date(System.currentTimeMillis() + time))//有效
时间 当前时间+一天 24h
.setId(UUID.randomUUID().toString())
//Signature
.signWith(SignatureAlgorithm.HS256,signature)
.compact();//拼接三部分
return jwtToken;
}
前端拿到数据,从请求中拿到 JSON 进入 home 页面
this.$refs.loginForm.validate((valid) => {
if (valid) {
//点击登录后的一个入口
// alert('submit!');
//把全局this记录下来
let _this = this
//axios的get请求需要套{params:_this},需要把loginForm对象给到
后台
this.$axios.get('http://localhost:8080/login',
{params:_this.loginForm}).then(function (response) {
console.log(response.data)
//使用 localStorage 进行存储 access-admin(localStorage
只能存储字符串)
localStorage.setItem('accessadmin',JSON.stringify(response.data))//JSON.stringify JSON转字符串的方法
//去到登录成功的页面
_this.$router.replace({path:'/home'})
})
} else {
this.$message.error('请输入所有字段!');
return false;
}
});
登录后在请求中拿到用户数据(保证安全性不显示)
home页面,将 JSON 取出并还原存入全局变量 admin 中,就可以从 admin 中取出数据显示到页面上
created() {
this.admin = JSON.parse(window.localStorage.getItem('access-admin'))
}
登录成功后切换页面进行验证(因为每次跳转都需要向后台发请求,重复代码太多,把验证抽象出来, 实现代码复用,所以写到路由当中,每个页面的跳转都需要经过路由)俗称路由守卫,首先验证用户是 否登录,(取出 JSON 还原 判断)如果没有登录直接跳转到登录页面,接着检验token合法性,访问后 台/checkToken,把保存的 token 传到后台进行认证,后台返回布尔值,如果是 flase 提示校验失败跳 转到错误页面,正确直接往后运行
router.beforeEach((to,from,next)=>{
if (to.path.startsWith('/login')) {
window.localStorage.removeItem('access-admin')
next()
} else {
//验证是否登录
let admin = JSON.parse(window.localStorage.getItem('access-admin'))
if (!admin){
next({path:'/login'})
} else {
//校验token合法性
axios({
url:'http://localhost:8081/checkToken',
method:'get',
headers:{
token:admin.token//类似?传参
}
}).then((response)=>{
console.log(response.data)
//给一个布尔值,如果是flase,提示校验失败,给到错误页面
if (!response.data){
console.log('校验失败')
next({path:'/error'})
}
})
next()
}
}
})
后台验证
从 headers 中获取前端给的 token,进入工具类中进行验证
controller
@GetMapping("/checkToken")
public Boolean checkToken(HttpServletRequest request){
//因为前端是存入 headers 中
String token = request.getHeader("token");
return JwtUtil.checkToken(token);
}
判断 token 是否为空,使用 Jwts.parser()方法,类似之前的解密 token ,解析签名 signature ,解析后 拿到集合 获取 JwtToken ,不需要拿出数据,只需要判断解析是否异常,如果解析异常说明 token 失 效,如果有异常返回 false,反之则返回 true,给到前端
JwtUtil
public static boolean checkToken(String token){
if (token == null){
return false;
}
try {
//判断token是否有效,如果解析异常说明token无效
Jws<Claims> claimsJws =
Jwts.parser().setSigningKey(signature).parseClaimsJws(token);
} catch (Exception e) {
return false;
}
return true;
}
在前端 console.log(response.data) 中可以看出是否有效
验证,改变 token 有效时间
private static long time = 1000*5;