JWT

全称为JSON Web Token,即JSON Web令牌,用于前端和后端之间以JSON对象安全的传递数据,提供了多种数字加密的算法,通过JSON作为Web中的令牌,通常用来做安全校验、授权

在传统的方式中,通常使用session进行验证,首次登陆时将认证信息放到session中,即在客户端和服务器中都保存一份登录的信息,下次访问时客户端将登录信息发送到服务器,服务器根据登录信息进行验证

问题:一般认证的session为了相应的速度,在服务器进行存储时都是存储在缓存中(Redis等),随着用户量变大,会造成服务器的内存压力变大,基于cookie的认证都可以进行伪造请求,从而达到攻击的目的

image-20220521195033054

JWT的令牌只存储在客户端而不存储在服务端

image-20220521195251809

image-20220521195457241

在这个令牌中包含了一些用户信息,避免了session信息的内存占用

构成

令牌分为三部分,每个部分可以看作是一个JSON对象:

  • 标头header

    • 由两部分组成,分别是令牌的类型(jwt)和签名的算法,HS256是默认并且推荐使用的签名算法

      • 对应的json

        {
            "typ": "jwt",
            "alg": "HS256等签名算法"
        }
        
      • 为了保证不被一眼看出来,会将标头使用Base64进行编码,编码的结果是一串字符串,使用此格式进行的编码可以做一个解码,所以Base64不能称为加密

  • 有效载荷payload

    • 其中包含着声明,通常是用户信息或者其他的数据声明同样也会采用Base64进行编码
    • 例如可以把用户名、角色、id等用到的信息放入到这一部分
    • 尽量不要放敏感信息,例如密码等
  • 签名signature,读音为ˈsɪɡnətʃər

    • 是将header的Base64编码.payload的Base64编码的字符串加上salt之后,经过HS256等指定的签名算法进行加密的生成的
    • salt永远只能自己知道,通过这个salt验证签名的合法性,而这个salt可以看作是一个私钥,通过headerpayload取出来加上salt使用签名算法进行加密后的结果可以看作是一个公钥
    • 后端要进行验签,即将headerpayload取出来加上salt使用签名算法进行加密和第三部分进行对比,只要有一个部分更改了,验证都会失败

由这三部分组成一个字符串,中间用.作为分隔header.payload.signature

整合到Java

所支持的依赖列表

引入依赖

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.2</version>
</dependency>

生成Token:

  • String token = JWT.create()
            .withClaim("username", "admin") // 放入到payload中的值,可以有多个
            .withClaim("isAdmin", true)
            .withExpiresAt(calendar.getTime()) // 设置过期时间
            .sign(Algorithm.HMAC256("Salt")); // 签名算法,需要包含salt
    
    • claim中文为宣称、声明,读音为kleɪm
    • expire中文为失效,终止,读音为ɪkˈspaɪər

通过Tokensalt验证令牌:

  • 如果token过期或者验证失败,将会在第4行的代码中抛出异常

  • JWTVerifier verifier = JWT.require(Algorithm.HMAC256("salt"))
            .build();
    //可以得到解码后的内容,如果解码失败,将会抛出异常
    DecodedJWT decodedJWT = verifier.verify("token");// 填入token
    
    System.out.println("decodedJWT.getExpiresAt() = " + decodedJWT.getExpiresAt());
    
    decodedJWT.getClaims().forEach((k, v) -> {
        System.out.println("key = " + k + ", value = " + v);
    });
    System.out.println("decodedJWT.getPayload() = " + decodedJWT.getPayload());
    System.out.println("decodedJWT.getSubject() = " + decodedJWT.getSubject());
    System.out.println("decodedJWT.getHeader() = " + decodedJWT.getHeader());
    System.out.println("decodedJWT.getToken() = " + decodedJWT.getToken());
    
    • verifier中文为验证器,读音为ˈvɛrəˌfaɪər
    • verify中文为验证,读音为ˈverɪfaɪ
  • 也可以不验证,直接进行取出值

    DecodedJWT decode = JWT.decode("eyJ0eXAiOiJKV1QiLCJpc1VzZXIiOnRydWUsImFsZyI6IkhTMjU2IiwidXNlcm5hbWUiOiJnb29kIn0.eyJleHAiOjE2NTMyMTE4MDd9.cWSuHHlwvY_T9jDAwhoREc5mn8wz36c9TOZNgCTNmWs");
    decode
            .getClaims()
            .forEach((k, v) -> {
                System.out.println("key = " + k + ", value = " + v);
            });
    System.out.println("decode.getPayload() = " + decode.getPayload());
    System.out.println("decode.getHeader() = " + decode.getHeader());
    System.out.println("decode.getExpiresAt() = " + decode.getExpiresAt());
    System.out.println("decode.getSignature() = " + decode.getSignature());
    
  • key的类型为Stringvalue的类型为JsonNodeClaim

  • 对于每个value,可以通过as类型的方式取出相应类型的值

image-20220522180334829

封装工具类

  • 生成token
  • 验证token
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

public class TokenUtil {

    private static final String SECRET = "afjk##UHGBHJ8894@/*-*-+";

    private TokenUtil() {

    }

    public static String getToken(Map<String, ?> payload) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, 3);
        return JWT.create()
                .withPayload(payload)
                .withExpiresAt(calendar.getTime())
                .sign(Algorithm.HMAC256(SECRET));
    }

    public static DecodedJWT verifyToken(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET))
                .build()
                .verify(token);
    }
}

也可以整合SpringBoot,配合拦截器进行使用:

image-20220524190119195

通常会将token放到请求头中

Q.E.D.


念念不忘,必有回响。