一,什么是jwt
jsontoken,在各方之间以json对象安全的传送信息.此信息可以验证和信任因为它是数字签名的
从分布式认证流程中,我们不难发现,这中间起最关键作用的就是
token,token的安全与否,直接关系到系统的
健壮性,这里我们选择使用JWT来实现token的生成和校验。
JWT,全称JSON Web Token,官网地址https://jwt.io,是
一款出色的分布式身份校验方案。可以生成token,也可
以解析检验token。
二,jwt能做什么
1. 授权
这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
2.信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名〈例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
三.为什么是jwt
基于传统的Session认证
- 认证方式
传统使用session就是前端cookie保存一个sessionid,下次发送请求会自动携带 - 认证流程
- 暴露问题
- session保存在内存中,用户越多,内存负载越大
- 分布式应用的话限制了负载均衡能力
- cookie被截获容易收到跨站请求伪造攻击
- 在前后端分离系统中更加痛苦
- 也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session 每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是
sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。
认证流程
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名形成一个JWT。形成的JWT就是一个形同lll.ZZZ.xxx的字符串。 token head.payload.singurater
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
jwt优势
- 简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快-自包含( Self-contained)︰负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
四,jwt结构是什么
令牌组成
token string --> header.payload.singnature
- 标头(header)
- 有效负载(payload)
- 签名(singnature)
header
- 标头通常由两部分组成︰令牌的类型(即JWT)和所使用的签名算法,例如HNAC SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。
- 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
{ "alg" : "HS256", "typ" : "JWT" }
payload
- 令牌的第二部分,称为有效负载,其中包含声明.声明是有关实体(通常是用户)和其他数据的声明.同样的他会使用Base64编码组成JWT的第二部分
{ "sub" : "123", "name" : "jhon" "admin" : "admin" }
Signature
- 前面两部分都是使用Base64 进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的 header和 payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过
- 如:
HMACSHA256(base64UrlEncode( header) + “.” + base64Ur1Encode(payload) , secret);
JWT 第三部分是签名。是这样生成的,首先需要指定一个 secret,该 secret 仅仅保存在服务器中,保证不能让其他用户知道。这个部分需要 base64URL 加密后的 header 和 base64URL 加密后的 payload 使用 . 连接组成的字符串,然后通过header 中声明的加密算法 进行加盐secret组合加密,然后就得出一个签名哈希,也就是Signature,且无法反向解密。
所以前两个部分基本上是明文,第三个部分是使用密钥和前面两部分进行加密后的验证信息
这部分的生成,是对前面两个部分的编码结果,按照头部指定的方式进行加密
比如:头部指定的加密方法是HS256,前面两部分的编码结果是eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9
则第三部分就是用对称加密算法HS256对字符串eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9进行加密,当然你得指定一个秘钥,比如shhhhh
HS256(`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9`, "shhhhh") // 得到:BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc
令牌的验证
令牌在服务器组装完成后,会以任意的方式发送到客户端
客户端会把令牌保存起来,后续的请求会将令牌发送给服务器
而服务器需要验证令牌是否正确,如何验证呢?
首先,服务器要验证这个令牌是否被篡改过,验证方式非常简单,就是对header+payload用同样的秘钥和加密算法进行重新加密
然后把加密的结果和传入jwt的signature进行对比,如果完全相同,则表示前面两部分没有动过,就是自己颁发的,如果不同,肯定是被篡改过了。
传入的header.传入的payload.传入的signature 新的signature = header中的加密算法(传入的header.传入的payload, 秘钥) 验证:新的signature == 传入的signature
意思是我前面的两部分和密钥的加密结果和传入的第三部分进行比对
证为没有被篡改后,服务器可以进行其他验证:比如是否过期、听众是否满足要求等等,这些就视情况而定了
注意:这些验证都需要服务器手动完成,没有哪个服务器会给你进行自动验证,当然,你可以借助第三方库来完成这些操作
放在一起
- 输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAMNL)相比,它更紧凑。
- 简洁(Compact)
可以通过URL,POST参数或者在 HTTP header发送,因为数据量小,传输速度快自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
五,jwt使用
载荷(playload)
载荷就是存放有效信息的地方。这些有效信息包含三个部分:
(1)标准中注册的声明(建议但不强制使用)
Copyiss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
(2)公共的声明
Copy公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
例如:
Copy{"id":"123456","name":"MoonlightL","sex":"male"}
初步使用
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.9</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>jwt</artifactId> <version>0.0.1-SNAPSHOT</version> <name>jwt</name> <description>jwt</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
@Test void contextLoads() { HashMap<String,Object> map = new HashMap<>(); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND,2000); System.out.println("时间"+calendar.getTime()); String token = JWT.create().withHeader(map)//header .withClaim("userid", 21) //payload .withExpiresAt(calendar.getTime())//指定令牌过期时间 .sign(Algorithm.HMAC256("111"));//签名 System.out.println(token); } @Test public void test(){ JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("111")).build(); //生成验证器 DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODgyMTU1ODMsInVzZXJpZCI6MjF9.Q1tbjmbNVLzdJbWTGHKJjpHw5dXeyfsBrQp8TFC0yZs"); Integer integer = verify.getClaim("userid").asInt(); System.out.println(integer); }
5.1 添加依赖
Copy<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.5</version> <scope>runtime</scope> </dependency>
5.2 编码
Copyimport java.security.Key; import java.util.Date; import java.util.UUID; import org.junit.Test; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; public class JWTTest { @Test public void testJWT() { Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); System.out.println("=============创建 JWT==========="); Date now = new Date(); JwtBuilder builder= Jwts.builder() .setId(UUID.randomUUID().toString()) // 载荷-标准中注册的声明 .setSubject("admin") // 载荷-标准中注册的声明 .setIssuedAt(now) // 载荷-标准中注册的声明,表示签发时间 .claim("id", "123456") // 载荷-公共的声明 .claim("name", "MoonlightL") // 载荷-公共的声明 .claim("sex", "male") // 载荷-公共的声明 .signWith(key); // 签证 String jwt = builder.compact(); System.out.println("生成的 jwt :" +jwt); System.out.println("=============解析 JWT==========="); try { Jws<Claims> result = Jwts.parser().setSigningKey(key).parseClaimsJws(jwt); // 以下步骤随实际情况而定,只要上一行代码执行不抛异常就证明 jwt 是有效的、合法的 Claims body = result.getBody(); System.out.println("载荷-标准中注册的声明 id:" + body.getId()); System.out.println("载荷-标准中注册的声明 subject:" + body.getSubject()); System.out.println("载荷-标准中注册的声明 issueAt:" + body.getIssuedAt()); System.out.println("载荷-公共的声明的 id:" + result.getBody().get("id")); System.out.println("载荷-公共的声明的 name:" + result.getBody().get("name")); System.out.println("载荷-公共的声明的 sex:" + result.getBody().get("sex")); } catch (JwtException ex) { // jwt 不合法或过期都会抛异常 ex.printStackTrace(); } } }
执行结果:
Copy=============创建 JWT=========== 生成的 jwt :eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI3ZjZmZjRlMC04YjM5LTQyYjUtOGRkNS0xN2M4ZjM5ZmZhNzMiLCJzdWIiOiJhZG1pbiIsImlhdCI6MTU0MzIwNTI4OSwiZXhwIjoxNTQzMjA1MzQ5LCJpZCI6IjEyMzQ1NiIsIm5hbWUiOiJNb29ubGlnaHRMIiwic2V4IjoibWFsZSJ9.BtEi-GCj5mCunXD_g0Cra7CSE_bMxhTzlOELWKc17I8 =============解析 JWT=========== 载荷-标准中注册的声明 id:7f6ff4e0-8b39-42b5-8dd5-17c8f39ffa73 载荷-标准中注册的声明 subject:admin 载荷-标准中注册的声明 issueAt:Mon Nov 26 12:08:09 CST 2018 载荷-公共的声明的 id:123456 载荷-公共的声明的 name:MoonlightL 载荷-公共的声明的 sex:male
注意:加密和解密 JWT 必须是同一个 Key 对象
注意:解密 JWT 时,必须要抓取 JwtException 异常,只要抓取到该异常说明该 JWT 不可用了
六,jwt工具类
public class JWTUtils { public static final String SING="111"; /** * @explain 生成token * @create 2023-07-01 20:28 * @author linghanwu * @param * @return **/ public static String getToken(Map<String,String> map){ Calendar instance = Calendar.getInstance(); instance.add(Calendar.DATE,7); JWTCreator.Builder builder = JWT.create(); map.forEach((k,v)->builder.withClaim(k,v)); return builder.withExpiresAt(instance.getTime()) //设置过期时间 .sign(Algorithm.HMAC256(SING)); } /** * @explain 验证token合法性 * @create 2023-07-01 21:18 * @author linghanwu * @param * @return **/ public static DecodedJWT verify(String token){ DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token); return verify; } }
七,jwt整合springboot
创建过滤器
public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取请求头中的令牌 String token = request.getHeader("token"); Map<String,Object> map = new HashMap<>(); try { JWTUtils.verify(token); // 验证Token return true; } catch (TokenExpiredException e) { map.put("state", false); map.put("msg", "Token已经过期"); } catch (SignatureVerificationException e){ map.put("state", false); map.put("msg", "签名错误"); } catch (AlgorithmMismatchException e){ map.put("state", false); map.put("msg", "加密算法不匹配"); } catch (Exception e) { e.printStackTrace(); map.put("state", false); map.put("msg", "无效token"); } String json = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); return false; } }
引入过滤器
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/users/**"); } }
八,jwt的缺点
- 安全性没法保证,所以 jwt 里不能存储敏感数据。因为 jwt 的 payload 并没有加密,只是用 Base64 编码而已。
- 无法中途废弃。因为一旦签发了一个 jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的 jwt 过期后重新签发新的 jwt。
- 续签问题。当签发的 jwt 保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,否则当 jwt有效期到了就会导致用户需要重新登录。那么怎么为 jwt 续签呢?最简单粗暴就是每次签发新的 jwt,但是由于过于暴力,会影响性能。如果要优雅一点,又要引入 Redis 解决,但是这又把无状态的 jw t硬生生变成了有状态的,违背了初衷。
rceptor(new JWTInterceptor()) .addPathPatterns(“/“) .excludePathPatterns(”/users/”); } }
# 八,jwt的缺点 - 安全性没法保证,所以 jwt 里不能存储敏感数据。因为 jwt 的 payload 并没有加密,只是用 Base64 编码而已。 - 无法中途废弃。因为一旦签发了一个 jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的 jwt 过期后重新签发新的 jwt。 - 续签问题。当签发的 jwt 保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,否则当 jwt有效期到了就会导致用户需要重新登录。那么怎么为 jwt 续签呢?最简单粗暴就是每次签发新的 jwt,但是由于过于暴力,会影响性能。如果要优雅一点,又要引入 Redis 解决,但是这又把无状态的 jw t硬生生变成了有状态的,违背了初衷。