1、简介
JWT全称叫json web token,通过数字签名的⽅式,以json对象为载体,在不同的服务终端之间 安全的传输信息。
- JWT在前后端分离系统,或跨平台系统中,通过JSON形式作为WEB应⽤中的令牌,⽤于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。
- 前端应⽤在访问后端应⽤时,会携带令牌,后端应⽤在接到令牌后,会验证令牌的合法性。从⽽决定前端应⽤是否能继续访问。
- JWT还可以系统之间进⾏信息传递,A系统通过令牌对B系统进⾏数据传输,在传输过程中,可以完成数据的加密,B系统拿到数据后,通过签名进⾏验证,从⽽判断信息是否有篡改。
- JWT就是一个签名验证的框架,负责将用户信息进行编码加密,编码加密后再将加密后的数据返回给浏览器。浏览器下一次请求只需要带上这个令牌即可。服务器端就可以根据这个令牌来验证用户的身份信息。
- JWT是一种客户端浏览器和服务器之间传递安全信息的一种声明规范
2、JWT的应用
JWT最常⻅的场景就是授权认证,⼀旦⽤户登录,后续每个请求都将包含JWT,系统在每次处理⽤户请求之前,都要先进⾏JWT安全校验,通过之后再进⾏处理。
1:授权
这是JWT最常⻅⽅案,⼀旦⽤户登录,每个后续请求将包括JWT,从⽽允许⽤户访问该令牌允许的路由,服务和资源。
2:信息交换
JWT是在各⽅之间安全地传输信息的好⽅法,可以验证传输内容是否遭到篡改。
3、jwt和session的区别:
1、jwt可以隐藏数据、安全系数更高(安全性更好)
2、jwt更适合分布式/微服务系统
3、session是将数据存储在应用服务器里的,当用户量较大的时候,会造成服务资源的浪费。
4、session是存在单台服务器上的,如果涉及到分布式应用,那么因为每一台服务器上存储的session不一致导致用户状态丢失等问题。
5、jwt所产生的验证字符串(token)是不会存储在服务器的。
专业术语
1、每个⽤户经过我们的应⽤认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证⽤户的增多,服务器内存开销会明显增⼤。
2、⽤户认证之后,服务端使⽤session保存认证信息,那么要取到认证信息,只能访问同⼀台服务器,才能拿到授权的资源。这样在分布式应⽤上,就需要实现session共享机制,不⽅便集群应⽤。
3,JWT认证基于令牌,该令牌存储在客户端。但认证由服务器端进⾏,解决服务器内存占⽤问题。当⽤户提交⽤户名和密码时,在服务器端认证通过后,会⽣成token令牌。然后将令牌响应给客户端浏览器。
4,客户端浏览器会在本地存储令牌。在客户端再次请求服务器接⼝时,每次都会携带JWT,在服务器验证通过后,再继续访问服务器资源。
4、JWT优势
1、简洁明了,可以通过URL、POST参数或Http header发送,因为数据量⼩,传输速度快。用户只需要关心密钥安全性问题。
2、⾃包含,jwt的负载可以传递一些⽤户基本的信息,不需要在服务器端保存会话信息(不需要再将数据保存在服务器里),不占服务器内存,也避免了多次查询数据库,特别适⽤于分布式微服务。
3、因为(jwt)token是以json加密的形式保存在客户端的,所以JWT是可以跨语⾔使⽤(如:python、go),原则上任何WEB形式都⽀持。很多编程语言都可以使用jwt进行授权。
5、JWT认证流程
1、首先由客户端浏览器发送登录请求,服务器收到登录请求后进行用户身份的验证。(账户、密码的验证),验证失败,将失败信息回执给浏览器(用户名/密码错误),验证通过,会进行jwt加密授权。
2、jwt可以将用户信息存放在其内部结构里(负载中(payload)),将header(头部)和负载进行Base64编号拼接后进行签名(加密)签名时需要提供自定的一串密钥(密钥是jwt里重要的一环,密钥决定用户数据是否能够被篡改)。最后返回一个token令牌(形成JWT)。
3、后端服务器再将令牌(JWT字符串)作为登录成功的返回结果发送给客户端浏览器,客户端浏览器将返回结果存在localStorage对象里。
4、客户端浏览器下一次请求只需要将令牌(JWT)放在在header里的的Authorization位。
5、后端检查是否存在,如果验证令牌(JWT)有效,后端就可以使⽤JWT中包含的⽤户信息。
6、JWT组成结构
jwt是由三个部分组成,JWT其实就是⼀段字符串,由3部分组成,⽤ . (点)拼接
JWT字符串示例
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2M jc5NzA5NDUsInVzZXIiOiJ7XCJpZFwiOjMsXCJuYW1lXCI6XCL lkInlkIlcIixcInB3ZFwiOlwiMzMzXCJ9In0.NvGdUjFLJWj_ ZzhY9Qp--NkZgK1QGQtQjiCB7lEsFTg
1:标头:header——
一般不需要手动改设置。
JWT会自动进行设置。
头部主要分为两部分:typ:"JWT" 加密类型 alg:"hs256"签名算法。
Header{ //标头 ‘typ’:’JWT’, 表示token类型 ‘alg’:’HS256’ 表示签名算法 }
2:负载:payload——
负载一般设置的是用户基本数据。⽤于存储用户的主要信息,使⽤ Base64编码组成JWT结构的第⼆部分。由于该信息是可以被解析的,所以, 在信息中不要存放敏感信息
{name:"用户名",phoneNumber:"13878654547"};
Payload //有效负载 { "userCode":"43435", "name":"john", "phone’:"13950497865" }
3:签名:signature——
将编码过后的表头header和负载payload这两部分数据进行加密(加密需要使用到一个自定义的密钥。)
注意:
前⾯两部分都使⽤Base64进⾏编码,前端可以解开知道⾥⾯的信息, Signature需要使⽤编码后的header和payload以及我们提供的⼀密钥,然后使⽤header中指定的签名算法进⾏签名,以保证JWT没有被篡改过。
使⽤Signature签名可以防⽌内容被篡改。如果有⼈对头部及负载内容解码后进⾏修改,再进⾏编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不⼀样的。如果要对新的头部和负载进⾏签名,在不知道服务器加密时⽤的密钥的话,得出来的签名也是不⼀样的
7、JAVA里使用JWT
1、首先环境搭建,引入jwt的依赖jar包
<!--jwt的依赖--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.13.0</version> </dependency>
2、JWT创建、生成token
package com.lovo.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import static java.security.KeyRep.Type.SECRET; /** * JWT工具类 */ public class JwtUtils { //自定义一个密钥————签名时使用 public static String signature = "lovo"; /** * 获取生成token,使用jwt产生token 令牌 * @param paramMap 参数对象(token携带的信息) * @return token字符串 */ public static String getToken(Map<String,String> paramMap){ //创建一个日历对象 Calendar calendar = Calendar.getInstance(); //指定token过期时间为30分钟 calendar.add(Calendar.MINUTE,30); //设置header————标头————jwt默认会设置标头 //JWTCreator.Builder:初始化jwt构建对象 JWTCreator.Builder builder = JWT.create(); //设置负载payload //paramMap.keySet():得到map里的所有key值 /*for (String key : paramMap.keySet()) { builder.withClaim(key,paramMap.get(key)); }*/ paramMap.forEach((k,v) -> builder.withClaim(k,v)); // builder.withExpiresAt:设置token过期时间 // 指定签名算法:sign(Algorithm.HMAC256(signature)) String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(signature)); return token; } //编写代码测试生成的tokenn public static void main(String[] args) { Map<String,String> paramMap = new HashMap<>(); paramMap.put("userName","faker"); String token = JwtUtils.getToken(paramMap); System.out.println(token); //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImZha2VyIiwiZXhwIjoxNjY5MzY5MDM4fQ.SuIG1D4ti_bufG6uKjCvNxx-Gnzo4CUR-Fkl0GGps0M } }
3、验证令牌(解码)
//自定义一个密钥————签名时使用 public static String signature = "chw"; /** * 验证令牌(解码) * @param token * @return */ public static String verify(String token,String key){ //1、创建(获取)验证对象(解码对象) //require():给验证对象提供自定义的密钥 //build():创建验证对象 String value = ""; try{JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(signature)).build(); //2、verify():验证token //得到解码对象 DecodedJWT decodedJWT = jwtVerifier.verify(token); //3、从解码对象里获取负载里的数据 //getClaim():从负载中获取数据 value = decodedJWT.getClaim(key).asString(); //捕获令牌失效的异常 }catch (TokenExpiredException e){ e.printStackTrace(); return "令牌已经失效"; } return value; } public static void main(String[] args) { Map<String,String> paramMap = new HashMap<>(); paramMap.put("userName","faker"); paramMap.put("phoneNum","13534788685"); // String token = JwtUtils.createToken(paramMap); String tokenStr = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwaG9uZU51bSI6IjEzNTM0Nzg4Njg1IiwidXNlck5hbWUiOiJmYWtlciIsImV4cCI6MTY2OTYwNTA4N30.VMqDuRnjyBfteGcCVkPtm6plxZ7KmbmnjTGsZa7Ne-M"; // System.out.println(token); //如果没有对应的key值,则返回null //如果token过期,则抛出TokenExpiredException异常 String value = JwtUtils.verify(tokenStr, "userName"); System.out.println(value); }
4、JWT的发送和接收
首先书写控制器
@RestController @RequestMapping("/users") public class UserController { @RequestMapping("/login") public String userLogin(String userName, String passWord, HttpServletResponse response){ //验证用户的数据(账号和密码) if ("faker".equals(userName)&&"123456".equals(passWord)){ //验证通过,给用户授权 Map<String,String> paramMap = new HashMap<>(); paramMap.put("userName",userName); paramMap.put("passWord",passWord); //获取token令牌 String token = JwtUtils.createToken(paramMap); //在应⽤控制器Controller中,通过响应头发送给客户端。 response.addHeader("token",token); /*所以同名的响应头可以有多个,每调用一次 addHeader() 方法添加一个响应头, 而调用 setHeader() 方法后会覆盖且只保留一个同名的响应头。*/ return "success"; } return "error"; } }
书写HTML界面验证效果
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="js/axios.min.js"></script> </head> <body> <form> 账号:<input type="text" id="userName"><br> 密码:<input type="password" id="passWord"><br> <input type="button" value="登录" onclick="userLogin()"> </form> </body> <script> function userLogin(){ //post请求传递单个参数的方式 let dataParam = new URLSearchParams(); dataParam.append("userName",document.getElementById("userName").value); dataParam.append("passWord",document.getElementById("passWord").value); axios.post('/users/login',dataParam).then(response=>{ //请求成功获取token令牌 if ("success" == response.data){ let token = response.headers.token; //把令牌保存在localStorage对象里 localStorage.setItem("token",token); location.href="pet.html"; }else{ alert("用户名或密码有误!") } }); //sessionStorage和localStorage区别和联系 //两者都是客户端保存数据所使用的对象 //但是sessionStorage保存的数据会失效。一旦浏览器关闭 //sessionSrorage里保存的数据将被清空,它是会话级别的存储 //localStorage它会长久的存在与浏览器里。不会因为浏览器窗口关闭而清空, //只有清楚浏览器缓存后才会清空。 //这两个对象一般都是存储一些字符串信息。常常和json字符串一起使用。 //具体选用哪个对象作为token令牌存储,根据业务需求来定 } </script> </html>
新增一个跳转界面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>宠物界面</title> <script src="js/axios.min.js"></script> </head> <body> <img src="img/pic1.jpg" style="width: 100px;height: 100px"> <a href="JavaScript:void(0)">购买</a> <img src="img/pic2.jpg" style="width: 100px;height: 100px"> <a href="JavaScript:void(0)">购买</a> <img src="img/pic3.jpg" style="width: 100px;height: 100px"> <a href="JavaScript:void(0)">购买</a> </body> <script> window.onload = function (){ let config = { headers: { 'token': localStorage.getItem("token") } } axios.post('/pets/petList',null,config).then(response=>{ if (response.data == "success"){ //填充用户数据 alert("页面数据加载成功!") }else { alert("会话超时!请重新登录!"); location.href = "login.html"; } }); } </script> <style> a { position: absolute; margin-top: 100px; margin-left: -70px; } </style> </html>
控制器
@RestController @RequestMapping("/pets") public class PetController { @RequestMapping("/petList") public String petList(HttpServletRequest request){ //从请求头里获取到令牌token String token = request.getHeader("token"); //验证令牌token Object value = JwtUtils.verify(token,"userName"); if (value instanceof Boolean){ return "error"; } String userName = value.toString(); if (userName != null && !"".equals(userName)) { return "success"; }else { return "error"; } } }
8、Token的工具类
封装了一个Token的工具类,大家有需要可以使用。
传入token的是一个登录用户对象的JSON字符串,因为JSON支持多门语言,利于复用。
/** * Token工具类 */ public class TokenUtils { /*public static void main(String[] args) { UUID uuid = UUID.randomUUID(); System.out.println(uuid); }*/ //设置过期时间,60分钟 private static final long EXPIRE_TIME=1000*60*60; //设置一个自定义的密钥————签名时使用 private static final String TOKEN_SECRET="23ff6755-3e65-4c8a-9896-2e79157eaba7"; /** * 生成token(通过用户对象的json字符串) * @param userJson * @return token字符串 */ public static String createToken(String userJson) { //过期时间 Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); //私钥及加密算法 Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); //设置头信息 HashMap<String, Object> header = new HashMap<>(2); header.put("typ", "JWT"); header.put("alg", "HS256"); //附带username和userID生成签名 //设置header————标头————jwt默认会设置标头 //JWTCreator.Builder:初始化jwt构建对象 JWTCreator.Builder builder = JWT.create(); String token= builder.withHeader(header) //设置负载payload .withClaim("info",userJson) //过期时间 .withExpiresAt(date) //密钥和加密算法 .sign(algorithm); return token; } //解密 public static String verifierToken(String token){ String value = ""; try { //私钥及加密算法 Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); //1、创建(获取)验证对象(解码对象) //require():给验证对象提供自定义的密钥 //build():创建验证对象 JWTVerifier verifier = JWT.require(algorithm).build(); //2、verify():验证token //得到解码对象 DecodedJWT jwt = verifier.verify(token); //3、从解码对象里获取负载里的数据 //getClaim():从负载中获取数据(取token中的信息) value=jwt.getClaim("info").asString(); //捕获令牌失效的异常 }catch (Exception e){ e.printStackTrace(); return "令牌已经失效"; } return value; } }
扩展——雪花算法工具类(生成雪花ID)
public class SnowIdUtils { /** * 私有的 静态内部类 */ private static class SnowFlake { /** * 内部类对象(单例模式) */ private static final SnowFlake SNOW_FLAKE = new SnowFlake(); /** * 起始的时间戳 */ private final long START_TIMESTAMP = 1557489395327L; /** * 序列号占用位数 */ private final long SEQUENCE_BIT = 12; /** * 机器标识占用位数 */ private final long MACHINE_BIT = 10; /** * 时间戳位移位数 */ private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT; /** * 最大序列号 (4095) */ private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT); /** * 最大机器编号 (1023) */ private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT); /** * 生成id机器标识部分 */ private long machineIdPart; /** * 序列号 */ private long sequence = 0L; /** * 上一次时间戳 */ private long lastStamp = -1L; /** * 构造函数初始化机器编码 */ private SnowFlake() { //模拟这里获得本机机器编码 long localIp = 4321; //localIp & MAX_MACHINE_ID最大不会超过1023,在左位移12位 machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT; } /** * 获取雪花ID */ public synchronized long nextId() { long currentStamp = timeGen(); //避免机器时钟回拨 while (currentStamp < lastStamp) { // //服务器时钟被调整了,ID生成器停止服务. throw new RuntimeException(String.format("时钟已经回拨. Refusing to generate id for %d milliseconds", lastStamp - currentStamp)); } if (currentStamp == lastStamp) { // 每次+1 sequence = (sequence + 1) & MAX_SEQUENCE; // 毫秒内序列溢出 if (sequence == 0) { // 阻塞到下一个毫秒,获得新的时间戳 currentStamp = getNextMill(); } } else { //不同毫秒内,序列号置0 sequence = 0L; } lastStamp = currentStamp; //时间戳部分+机器标识部分+序列号部分 return (currentStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence; } /** * 阻塞到下一个毫秒,直到获得新的时间戳 */ private long getNextMill() { long mill = timeGen(); // while (mill <= lastStamp) { mill = timeGen(); } return mill; } /** * 返回以毫秒为单位的当前时间 */ protected long timeGen() { return System.currentTimeMillis(); } } /** * 获取long类型雪花ID */ public static long uniqueLong() { return SnowFlake.SNOW_FLAKE.nextId(); } /** * 获取String类型雪花ID */ public static String uniqueLongHex() { return String.format("%016x", uniqueLong()); } public static void main(String[] args) { for(int i=0;i<10;i++) { System.out.println(SnowIdUtils.uniqueLong()); } } }