JWT Token 入门篇
这里记录下自己学习jwt的过程。
什么是JWT
JSON Web Token(缩写 JWT),是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的自定义格式。用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519
jwt令牌的优点:
1.jwt基于json,非常方便解析。
2.可以在令牌中自定义丰富的内容,易扩展。
3.通过非对称加密算法及数字签名技术,jwt防止篡改,安全性高。
4.资源服务使用jwt可不依赖认证服务即可完成授权。
jwt令牌的缺点:
jwt令牌较长,占存储空间比较大。
JWT组成部分
一个jwt实际就是一个字符串,它由三部分组成:头部、载荷和签名。
头部(Header)
头部用于描述关于该jwt的最基本信息,例如其类型(即JWT)以及签名所用的算法(如:HMAC SHA256或RSA)等。这也可以被表示成一个JSON对象。
{
"alg": "HS256",
"typ": "JWT"
}
- alg:签名的算法,这里使用的算法是HS256算法。
- typ:是类型。
用Base64对这个JSON编码就能得到JWT的第一部分,这里下面代码会写。
载荷(Payload)
第二部分是载荷,就是存放有效信息的地方。这些有效信息包含三个部分:
- 标准中注册的声明(建议但不强制使用)
iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须要大于签发时间
nbf:定义在什么时间之间,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- 公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分可以在客户端解密。 - 私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如下面举例中的name属于自定义的claim。这些claim跟JWT标准规定的claim区别在于:jwt规定的claim,jwt的接收方在拿到jwt之后,都知道怎么对这些标准的claim进行验证;而private claim不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
其中sub和iat是标准的声明,name是自定义的声明(私有的或者公共的),也可以用Base64对这个JSON编码来得到JWT的第二部分
签证、签名(Signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header(base64后的)
- payload(base64后的)
- secret(盐,一定要保密)
这个部分需要base64加密后的header和base64加密后payload使用.连接组成的字符串,然后通过header中声明的加密方式加盐组合加密,然后就构成了jwt的第三部分。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务器的私钥,在任何场景都不应该泄露出去。一旦客户端得知这个secret,那就意味着是可以自我签发jwt了。
什么是JJWT
JJWT是一个提供端到端的JWT创建和验证的Java库。永久免费和开源(Apache License,版本2.0),JJWT很容易使用和理解,它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
快速入门
创建springboot工程,引入依赖,依赖如下:
<?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.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hjl</groupId>
<artifactId>jjwt-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jjwt-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
官网显示java的有6个不同的依赖,这里为什么要用io.jsonwebtoken这个版本,是因为官网显示的这个支持的算法多,以及人用的多,最新的是0.11.1,我这里用的是0.9.0版本
在test下面测试,示例如下:
代码如下:
生成jwt token代码
package com.hjl.jjwtdemo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.Base64Codec;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
class JjwtDemoApplicationTests {
@Test
void contextLoads() {
}
/**
* 生成jwt,因为没有设置过期时间,所以是永久有效的
*/
@Test
public void testJwt(){
//jwt三部分构成第一个是头部,第二个是载荷,第三个是签名
JwtBuilder jwtBuilder= Jwts.builder()
.setId("888") //唯一Id{"jti":"888"}
.setSubject("Rose") //接收的用户{"sub":"Rose"}
.setIssuedAt(new Date()) //签发时间 {"iat":""}
.signWith(SignatureAlgorithm.HS256,"xxxx"); //签名算法及秘钥,签名算法是{"alg":"HS256"},秘钥是xxxx
//签发token
String token=jwtBuilder.compact();
System.out.println("token="+token);
//通过BASE64解析jwt生成的token
String[] a=token.split("\\.");
//头部值里面只有你是用的什么算法,如:HS256
System.out.println("jwt头部值:"+ Base64Codec.BASE64.decodeToString(a[0]));
//载荷值里面就是你设置的唯一Id,接收的用户和签发时间
System.out.println("jwt载荷值:"+ Base64Codec.BASE64.decodeToString(a[1]));
//这个会乱码,因为你没有通过秘钥来解析值
System.out.println("jwt签名值:"+ Base64Codec.BASE64.decodeToString(a[2]));
}
}
然后我们点击这个,如下图:
执行结果如下,token就是我们生成的jwt token值,下面的就是通过Base64解码获得的值,这里会发现签名是乱码,这是正常的:
解析jwt token代码:
在testJwt()方法下面在写上这段代码,来解析jwt
/**
* 解析token
*/
@Test
public void testParseToken(){
//上一个方法生成的token
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA3ODQ3NjY0fQ.zgpT3QV5q-kC-RsEjV73jQtYCnTT9ICHgxsZgYPMjy8";
//解析token,获取Claims,就是载荷中的值
Claims claims = (Claims) Jwts.parser()
.setSigningKey("xxxx") //秘钥 通过这个秘钥来解析
.parse(token)
.getBody();
System.out.println("jti="+claims.getId());
System.out.println("sub="+claims.getSubject());
System.out.println("iat="+claims.getIssuedAt());
}
获取结果如下,发现值是对的,是自己设置的值:
生成jwt(失效时间),设置过期时间
在testJwt里面的方法里面改一下就好,代码如下:
/**
* 生成jwt(失效时间),设置过期时间
*/
@Test
public void testJwtHasExpire(){
//当前时间
long date=System.currentTimeMillis();
//失效时间 1分钟
long exp=date+60*1000;
//jwt三部分构成第一个是头部,第二个是载荷,第三个是签名
JwtBuilder jwtBuilder= Jwts.builder()
.setId("888") //唯一Id{"jti":"888"}
.setSubject("Rose") //接收的用户{"sub":"Rose"}
.setIssuedAt(new Date()) //签发时间 {"iat":""}
.signWith(SignatureAlgorithm.HS256,"xxxx") //签名算法及秘钥{"alg":"HS256"}
.setExpiration(new Date(exp)); //设置失效时间
//签发token
String token=jwtBuilder.compact();
System.out.println("token="+token);
//解析jwt生成的token
String[] a=token.split("\\.");
//头部值里面只有你是用的什么算法,如:HS256
System.out.println("jwt头部值:"+ Base64Codec.BASE64.decodeToString(a[0]));
//载荷值里面就是你设置的唯一Id,接收的用户和签发时间
System.out.println("jwt载荷值:"+ Base64Codec.BASE64.decodeToString(a[1]));
//这个会乱码
System.out.println("jwt签名值:"+ Base64Codec.BASE64.decodeToString(a[2]));
}
执行结果如下,这里就是多了个过期时间,当时间到了后,这个token就过期不可用了,结果和testJwt返回结果一样:
解析token(失效时间)
这里的代码和testParseToken()代码是一样的,如下:
/**
* 解析token(失效时间)
*/
@Test
public void testParseTokenHasExpire(){
//上一个方法生成的token
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA3ODQ4NTU1LCJleHAiOjE2MDc4NDg2MTV9.Q3DxON-xEKYZt_VX5Sxar-e6nuMdFIynCE-y2OFZPec";
//解析token,获取Claims,就是载荷中的值
Claims claims = (Claims) Jwts.parser()
.setSigningKey("xxxx") //秘钥 通过这个秘钥来解析
.parse(token)
.getBody();
System.out.println("jti="+claims.getId());
System.out.println("sub="+claims.getSubject());
System.out.println("iat="+claims.getIssuedAt());
}
当过期时间到了后,在执行的结果如下:
会说这个jwt已经过期了,报错如下:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2020-12-13T16:36:55Z. Current time: 2020-12-13T16:38:46Z, a difference of 111422 milliseconds. Allowed clock skew: 0 milliseconds.
生成jwt(自定义声明载荷值)
代码如下:
/**
* 生成jwt(自定义声明载荷值)
*/
@Test
public void testJwtHasEnhancer(){
//jwt三部分构成第一个是头部,第二个是载荷,第三个是签名
JwtBuilder jwtBuilder= Jwts.builder()
.setId("888") //唯一Id{"jti":"888"}
.setSubject("Rose") //接收的用户{"sub":"Rose"}
.setIssuedAt(new Date()) //签发时间 {"iat":""}
.signWith(SignatureAlgorithm.HS256,"xxxx") //签名算法及秘钥{"alg":"HS256"}
//自定义申明
.claim("name","Jack") //通过claim来自定义载荷值,这里是属性为name,值为jack
.claim("sex","男");
//.addClaims(map) //这里是通过map集合来自定义值,将值放到map里面添加
//签发token
String token=jwtBuilder.compact();
System.out.println("token="+token);
//解析jwt生成的token
String[] a=token.split("\\.");
//头部值里面只有你是用的什么算法,如:HS256
System.out.println("jwt头部值:"+ Base64Codec.BASE64.decodeToString(a[0]));
//载荷值里面就是你设置的唯一Id,接收的用户和签发时间
System.out.println("jwt载荷值:"+ Base64Codec.BASE64.decodeToString(a[1]));
//这个会乱码
System.out.println("jwt签名值:"+ Base64Codec.BASE64.decodeToString(a[2]));
}
执行结果如下:
解析token(自定义申明载荷值)
代码如下:
/**
* 解析token(自定义申明载荷值)
*/
@Test
public void testParseTokenHasEnhancer(){
//上一个方法生成的token
String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjA3ODQ5MDA4LCJuYW1lIjoiSmFjayIsInNleCI6IueUtyJ9.hcPlDsy0v8MN0CuBS__SC-PbsJgT1gBa1qyRGP_jMpg";
//解析token,获取Claims,就是载荷中的值
Claims claims = (Claims) Jwts.parser()
.setSigningKey("xxxx") //秘钥 通过这个秘钥来解析
.parse(token)
.getBody();
System.out.println("jti="+claims.getId());
System.out.println("sub="+claims.getSubject());
System.out.println("iat="+claims.getIssuedAt());
System.out.println("name="+claims.get("name"));
System.out.println("sex="+claims.get("sex"));
}
执行结果如下:
好了到这里,我的学习笔记就做完了!