1.什么是JWT
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任,JWT可以使用HMAC算法或使用RSA的公钥私钥对来签名,防止被篡改。
- 官网 https://jwt.io/
- 标准 https://tools.ietf.org/html/rfc7519
优点:
- jwt基于ison,非常方便解析
- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法及数字签名技术,JwT防止算改,安全性高。
- 资源服务使用JwT可不依赖认证服务即可完成授权。
缺点: JWT令牌较长,占存储空间比较大.
2.JWT组成
一个 JWT 实际上就是一个字符串,它由三部分组成,第一部分我们称它为头部(header) , 第二部分我们称其为载荷(payload) ,第三部分是签证(signature)
2.1 头部
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{ 'typ': 'JWT', 'alg': 'HS256' }
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。
JDK中提供了非常方便的BASE64Encoder和BASE64Decoder,用它们可以非常方便的完成基于BASE64的编码和解码。
2.2载荷
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个载荷:
{ "sub": "123456", "name": "MoYu", "admin": true }
然后将其进行base64加密,得到Jwt的第二部分:
ewogICJzdWIiOiAiMTIzNDU2IiwKICAibmFtZSI6ICJNb1l1IiwKICAiYWRtaW4iOiB0cnVlCn0=
提示:不要放一些 敏感信息
2.3 签证
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret (一定要保密)
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
作者:Dearmadman
链接:https://www.jianshu.com/p/576dbf44b2ae
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewogICJzdWIiOiAiMTIzNDU2IiwKICAibmFtZSI6ICJNb1l1IiwKICAiYWRtaW4iOiB0cnVlCn0=.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
3.JWT-dome
3.1 生成token
1.创建一个maven项目:
(该项目选用的Mavend的QuickStart版本,注意别选错)
2.使其成为SpringBoot项目、导入依赖
pom.xml 中加入
spring-boot-starter-parentorg.springframework.boot2.4.4
org.springframework.bootspring-boot-starter-webio.jsonwebtokenjjwt0.9.1org.springframework.bootspring-boot-starter-testtestorg.junit.vintagejunit-vintage-engine
3.创建启动类
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } }
4.创建测试类
@SpringBootTest public class JwtTest { @Test public void testJwt(){ JwtBuilder jwtBuilder = Jwts.builder() //设置id,{“jti”:“888”} .setId("888") //签发用户,{"sub":"MoYu"} .setSubject("MoYu") //签发时间,{"iat":"xxxxx"} .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256, "xxxx"); //生成token String token = jwtBuilder.compact(); System.out.println(token); String[] split = token.split("\\."); //头部 System.out.println(Base64Codec.BASE64.decodeToString(split[0])); //载荷 System.out.println(Base64Codec.BASE64.decodeToString(split[1])); //签名 乱码 System.out.println(Base64Codec.BASE64.decodeToString(split[2])); } }
运行测试:
将生成的jwt令牌在jwt官网查看:
3.2 解析token
@Test public void parseToken(){ String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJNb1l1IiwiaWF0IjoxNjE4MTE5MDkwfQ.-GSkCkgflTs6IQHmWQjrgY6YD0hOgRBmFwG64x5mgdk"; Claims claims = Jwts.parser() .setSigningKey("xxxx") .parseClaimsJws(token) .getBody(); System.out.println("jti:"+claims.getId()); System.out.println("sub:"+claims.getSubject()); System.out.println("iat:"+claims.getIssuedAt()); }
3.3 token过期校验
创建一个带过期时间exp的token令牌
// 生成token(带过期时间) @Test public void testJwtHasExp(){ long now = System.currentTimeMillis(); //过期时间 long exp = now + 60 * 1000; JwtBuilder jwtBuilder = Jwts.builder() //设置id,{“jti”:“888”} .setId("888") //签发用户,{"sub":"MoYu"} .setSubject("MoYu") //签发时间,{"iat":"xxxxx"} .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256, "xxxx") //设置过期时间,{“exp”:"xxxx"} .setExpiration(new Date(exp)); //生成token String token = jwtBuilder.compact(); System.out.println(token); String[] split = token.split("\\."); //头部 System.out.println(Base64Codec.BASE64.decodeToString(split[0])); //载荷 System.out.println(Base64Codec.BASE64.decodeToString(split[1])); //签名 乱码 System.out.println(Base64Codec.BASE64.decodeToString(split[2])); }
对应的修改token解析方法:
// 解析token(带过期时间) @Test public void parseTokenHasExp(){ String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJNb1l1IiwiaWF0IjoxNjE4MTE5NzY2LCJleHAiOjE2MTgxMTk4MjZ9.T5vAI5KK2QYY7W7qqWZXYn9JHHZ9kW6IdtTFg_hX98Q"; Claims claims = Jwts.parser() .setSigningKey("xxxx") .parseClaimsJws(token) .getBody(); System.out.println("jti:"+claims.getId()); System.out.println("sub:"+claims.getSubject()); System.out.println("iat:"+claims.getIssuedAt()); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println("过期时间:"+format.format(claims.getExpiration())); System.out.println("签发时间:"+format.format(claims.getIssuedAt())); System.out.println("当前时间:"+format.format(new Date())); }
过期时间内:
过期时间外:
3.4 token自定义申明
对于token的自定义申明有两种方式:
- .addClaims() ,参数类型为 Map
- .claim("xxxx","xxxx") ,这个方法可以进行逐条添加
生成带有自定义申明的toke令牌:
// 生成token(自定义申明) @Test public void testJwtHasClaims(){ JwtBuilder jwtBuilder = Jwts.builder() //设置id,{“jti”:“888”} .setId("888") //签发用户,{"sub":"MoYu"} .setSubject("MoYu") //签发时间,{"iat":"xxxxx"} .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256, "xxxx") //自定义申明 参数是map // .addClaims(); .claim("logo","xxx,jpg") .claim("一句话","这只是一句话"); //生成token String token = jwtBuilder.compact(); System.out.println(token); String[] split = token.split("\\."); //头部 System.out.println(Base64Codec.BASE64.decodeToString(split[0])); //载荷 System.out.println(Base64Codec.BASE64.decodeToString(split[1])); //签名 乱码 System.out.println(Base64Codec.BASE64.decodeToString(split[2])); }
进行对带有自定义申明的token令牌进行解析:
// 解析token(自定义申明) @Test public void parseTokenHasClaims(){ String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJNb1l1IiwiaWF0IjoxNjE4MTIwNTg0LCJsb2dvIjoieHh4LGpwZyIsIuS4gOWPpeivnSI6Iui_meWPquaYr-S4gOWPpeivnSJ9.m44gqMFO863jI5B2qjb9yXvaRJsIRYcfdEI3KiJgW1M"; Claims claims = Jwts.parser() .setSigningKey("xxxx") .parseClaimsJws(token) .getBody(); System.out.println("jti:"+claims.getId()); System.out.println("sub:"+claims.getSubject()); System.out.println("iat:"+claims.getIssuedAt()); System.out.println("logo:"+claims.get("logo")); System.out.println("一句话:"+claims.get("一句话")); }
3.5 刷新token令牌
刷新token令牌本质上就是,对之前的token令牌中的签发时间进行更新,从而生成新的token令牌
@Test public void parseTokenRefresh(){ String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJNb1l1IiwiaWF0IjoxNjE4MTIwNTg0LCJsb2dvIjoieHh4LGpwZyIsIuS4gOWPpeivnSI6Iui_meWPquaYr-S4gOWPpeivnSJ9.m44gqMFO863jI5B2qjb9yXvaRJsIRYcfdEI3KiJgW1M"; Claims claims = Jwts.parser() .setSigningKey("xxxx") .parseClaimsJws(token) .getBody(); //更新签发时间 JwtBuilder jwtBuilder = Jwts.builder() .setId(claims.getId()) .setSubject(claims.getSubject()) .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256, "xxxx"); String refresh_token = jwtBuilder.compact(); System.out.println("更新前:"+token); System.out.println("更新后:"+refresh_token); }