JWT简介

1.简介

JWT(JSON Web Token)是一个开放标准(RFC 7519),它将用户信息存储在经过加密的JSON格式的字符串中,在各方之间安全地传输信息,进行身份认证。简单来说,JWT就是token的一种实现形式,是目前最流行的跨域身份验证解决方案之一。使用token进行身份认证的一般流程如下:

  1. 用户输入用户名和密码进行登录
  2. 服务端收到登录请求后,对接收到的用户名和密码进行验证
  3. 验证通过后,服务端会生成一个token返回给客户端
  4. 客户端在收到返回的token后存储起来
  5. 之后客户端在向服务端发起请求时携带服务端返回的token
  6. 服务端在收到非登录请求后,会拿到请求中的token并进行验证,如果验证通过,就正常返回数据给客户端

2.JWT结构

JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。
1.Header
JWT Header是一个JSON对象,用来描述JWT元数据。其中,alg属性表示签名使用的算法,默认为HMAC SHA256;typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

{
  "alg": "HS256",
  "typ": "JWT"
}

2.Payload
Payload,即有效载荷,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据, JWT指定七个默认字段供选择。这些预定义的字段并不要求强制使用。

{
    "iss": "发行人",
    "exp": "到期时间",
    "sub": "主题",
    "aud": "用户",
    "nbf": "在此之前不可用",
    "iat": "发布时间",
    "jti": "JWT ID用于标识该JWT"
}

除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,例如用户名等非敏感信息。但绝不能将用户密码存入载荷中,因为一般情况下JWT只采用了Base64,没有进行加密,相当于裸奔。
3.Signature
signature是对上面两部分数据进行签名得到的,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,从而确保数据不会被篡改。需要指定一个密钥(secretKey),该密钥仅保存在分发签名的服务器中。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)。
H M A C S H A 256 ( b a s e 64 U r l E n c o d e ( h e a d e r ) + " . " + b a s e 64 U r l E n c o d e ( p a y l o a d ) , s e c r e t ) HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
JWT中生成signature的部分源码如下:

private String sign() throws SignatureGenerationException {
    String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
    String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
    byte[] signatureBytes = this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));
    String signature = Base64.encodeBase64URLSafeString(signatureBytes);
    return String.format("%s.%s.%s", header, payload, signature);
}

3.JWT分类

JWT大致可以分为三类:

  • nonsecure JWT:未经过签名
  • JWS:经过加密生成签名的JWT
  • JWE:payload部分经过加密的JWT

在日常开发中,我们主要用的是JWS,也就是生成签名的JWT;而加密的算法可以根据加密形式分为两类:

  • 对称加密:密钥可以生成签名也能验签
    • 进行对称加密的算法有:HMAC(哈希消息验证码)
  • 非对称加密:有私钥和公钥,私钥只能用来生成签名,用公钥来进行验签
    • 进行非对称加密的算法有:RSASSA(RSA签名算法)、ECDSA(椭圆曲线数据签名算法)

对称加密 JWT 和非对称加密 JWT 的比较

  1. 安全性: 非对称加密 JWT 更安全,因为私钥不需要共享,而对称加密 JWT 需要确保密钥的安全性。
  2. 复杂度: 对称加密 JWT 更简单高效,而非对称加密 JWT 需要管理公钥和私钥的配对,处理复杂度较高。
  3. 用途: 对称加密 JWT 适用于简单的场景,非对称加密 JWT 适用于更高安全要求的场景。

4.JWT使用

在Java中常用的JWT库有java-jwt和jjwt-root,下面以java-jwt举例

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

4.1对称加密

@Component
public class JwtUtils {
    // 签名密钥
    private static final String SECRET_KEY = "!MBGA&";

    /**
     * 生成JWT
     * @param payLoad 载荷
     * @param expiration 失效时间
     * @return JWT
     */
    public String generateToken(Map<String, String> payLoad, Date expiration) {
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        JWTCreator.Builder jwtBuilder = JWT.create();
        if (!CollectionUtils.isEmpty(payLoad)) {
            payLoad.forEach(jwtBuilder::withClaim);
        }
        jwtBuilder.withExpiresAt(expiration);
        return jwtBuilder.sign(algorithm);
    }

    /**
     * 解析token
     * @param token jwt
     * @return 解析结果
     */
    public DecodedJWT decodeToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        return jwtVerifier.verify(token);
    }

    /**
     * 验证token
     * @param token token
     * @param claimMap 验证载荷
     * @return 验证结果
     */
    public boolean verifyToken(String token, Map<String, String> claimMap) {
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        Verification verification = JWT.require(algorithm);
        if (!CollectionUtils.isEmpty(claimMap)) {
            claimMap.forEach(verification::withClaim);
        }
        JWTVerifier jwtVerifier = verification.build();
        jwtVerifier.verify(token);
        return true;
    }
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTTest {

    @Autowired
    private JwtUtils jwtUtils;
    
    @Test
    public void testGenerateToken() {
        Map<String, String> payloadMap = new HashMap<>();
        payloadMap.put("userId", "007");
        payloadMap.put("userName", "zhangsan");
        String token = jwtUtils.generateToken(payloadMap, new Date(System.currentTimeMillis() + 1000 * 5));
        System.out.println(token);
        System.out.println("验证结果1:" + jwtUtils.verifyToken(token, null));
        System.out.println("验证结果2:" + jwtUtils.verifyToken(token, payloadMap));
    }
}

执行结果如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6InpoYW5nc2FuIiwiZXhwIjoxNzE3MDUwNTI0LCJ1c2VySWQiOiIwMDcifQ.oDEfhh2Ece8U2LVgzLwKejIdq9RrcFqG7DgYgYrGOb4
验证结果1:true
验证结果2:true

若在超时后再进行校验,则会报token已失效:

@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTTest {

    @Autowired
    private JwtUtils jwtUtils;
    
    @Test
    public void testGenerateToken() throws InterruptedException {
        Map<String, String> payloadMap = new HashMap<>();
        payloadMap.put("userId", "007");
        payloadMap.put("userName", "zhangsan");
        String token = jwtUtils.generateToken(payloadMap, new Date(System.currentTimeMillis() + 1000 * 5));
        System.out.println(token);
        Thread.sleep(1000 * 10);
        System.out.println("验证结果1:" + jwtUtils.verifyToken(token, null));
        System.out.println("验证结果2:" + jwtUtils.verifyToken(token, payloadMap));
    }
}
com.auth0.jwt.exceptions.TokenExpiredException: The Token has expired on Thu May 30 14:30:20 CST 2024.

4.2非对称加密

由于非对称加密需要用公钥解析签名进行验签,因此还需要一对公钥和私钥;使用非对称加密的算法有ECDSA和RSASSA,下面以RSASSA进行举例。
在使用 RSA 算法生成 JWT(JSON Web Token)时,通常使用私钥进行签名(不是加密),这是因为 JWT 的签名部分需要保证数据的完整性和来源可信性,而私钥可以确保签名是由持有私钥的一方生成的,从而确保JWT的创建者是合法的,且在传输过程中没有被篡改,从而提供了数字签名的验证功能。在这个过程中,私钥只能由签发方持有,公钥由验证方持有。
例如,Alice 想发送一条带有数字签名的消息给 Bob:

  • Alice 生成一对密钥,并将公钥发送给 Bob。
  • Alice 使用自己的私钥对消息进行签名,然后将消息和签名一起发送给 Bob。
  • Bob 使用 Alice 的公钥验证签名,以确保消息的来源和完整性。
@Component
public class JwtUtils {
    // Base64编码的公钥、私钥
    private static final String RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtG4t8uCjjkWUijuFxCP2SX7MtrreQRIdow4YrzGutVMqh/YJYDXsAY3nvOQnhYCOh3OBmQ8OCLsg+cM7s3sB+GHW7NRTRRPsY5NzwQFGWhAKer5pC++TAj8PgL36Jm+u9TtMSweBnzhMycedyHO6aNwnXGF3fMmx+YZIzJCzWD7TayJsOmTdAb3Oj4sUF+1SwAsXjFIuHUz00bqAI2WlaW9sTpKLZVrzSGxo/UkmTzrBXRkC7Dcnroa6Q7RrbcvQ/aXaufnqw+D1rJh4pmH34/L7FOVbVcb+iEWsL8MNDSq2jS5c2Ch4SEbtQEikRbRiyw4M2lLY0z96RYXl1Ob4MQIDAQAB";
    private static final String RSA_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0bi3y4KOORZSKO4XEI/ZJfsy2ut5BEh2jDhivMa61UyqH9glgNewBjee85CeFgI6Hc4GZDw4IuyD5wzuzewH4Ydbs1FNFE+xjk3PBAUZaEAp6vmkL75MCPw+Avfomb671O0xLB4GfOEzJx53Ic7po3CdcYXd8ybH5hkjMkLNYPtNrImw6ZN0Bvc6PixQX7VLACxeMUi4dTPTRuoAjZaVpb2xOkotlWvNIbGj9SSZPOsFdGQLsNyeuhrpDtGtty9D9pdq5+erD4PWsmHimYffj8vsU5VtVxv6IRawvww0NKraNLlzYKHhIRu1ASKRFtGLLDgzaUtjTP3pFheXU5vgxAgMBAAECggEAfGV6SHmYSf6u6p8fGpuwsfs6KMGtei+DP2f4nNEkIt+z8wkubaa/kFLF8vVaSjDYE4sPTveSNXLOyWRRRO0J7rWF+MJqprWVxRFkPS0j0/Q7RWUCB7ilImdXyOsyDE3z0h+P+iqys5OdXYDCx/ECFEm02XxZZS+qBN8QUflxrOR44zE0cpvf5U3rNHbNeEKPYqvHkr1khdn02VwSGexRSiFGfcl9R5WGBzhpXYJWoca+CADGKdokjW0d7L4iv7E8Djf4gdfV/+oZSGkVK7u3khEz3ejWUCFiwvLdUvieXiw7T0RUBFgsVbFdoPObvfWqWevXI7mpqvgLTvQ4VSRSAQKBgQD1hdPCoaba6a5G+IREKZc548yEyTIeDW6QWWkKtGwX6O6Jz5yqvconGmT6G1kHMLoX3tyzUdRqE1YkmKMWn2YPYWQfsf+5HdQbnp6RRJX/PHo6aNsVY/qcT41h0NWUzBpBIgaNIHMwvnERw/1bes21zldoqLVQVUSppu8RAzu/dwKBgQC8IULs1qfIy6dePUUrZ1shWhifdjl+o3VERRhIhupw6QKahh8+HDfhxfhKf2yQDDSyeYdKSPqXTvE+29xNkQD50UPePTAWUY/489rqKjjqbzfP/0blKyMe7v0AuorshYlsvdTv8L/hv7IH+3ig/NeUmq8cfarfrDPMl1b4qft/lwKBgQDxuGH1esUVPX9xK/a8TN4wBlAyYwrOf5bc0soR2fOZzqAaWaX6i4Pc36WOfyI1bAmquqU6flPMY2EqAoR+H0yR0+aJNet4Sx+qWY3vo0Cx2s7TdqxK8PWossGVDc9ZAWDPwzoD83C1CqmzPevQBqVEWvO/fGmVv30sMceoTjCinQKBgQCU9q1chXejmgZzq4Y6oQNavFFk6qMJ8HopTaC1xQab0xT5sBvK/WMORcidjJo00UVk0K/clT6/UoXZpROi459nMFlR/xc1hO3ATa7Y3gflMZ16+AJ18ZtEoi35Rrw8Hly7mvCh8Pqu0TAx+9//cVVuWLExmRwVMfBuHqOCEG6RDwKBgEoIRzGrO7Mv1Hp8cwmd0I0AD1wKGsyIhaSybCFipzfE/or4c07YqLaqe71PCw2jL+y8iJi3sfzinyj+N8cof5PkkI3vaJ1pwsBzVL1U7zD7LU634QaEowyvnso73i2E9O5GJOjNuZePZr38DR3HXhpr2C58LxELHPQhKkhGBcKF";

    /**
     * 生成公私钥对
     */
    public void createRSAKey() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
        String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
        System.out.println(publicKeyBase64);
        System.out.println(privateKeyBase64);
    }

    /**
     * 生成非对称token
     * @param payloadMap 载荷
     * @param expiration 有效时间
     * @return token
     */
    public String generateAsymmetryToken(Map<String, String> payloadMap, Date expiration) {
        RSA rsa = new RSA(RSA_PRIVATE_KEY, RSA_PUBLIC_KEY);
        Algorithm algorithm = Algorithm.RSA256((RSAPrivateKey) rsa.getPrivateKey());
        JWTCreator.Builder jwtBuilder = JWT.create();
        if (!CollectionUtils.isEmpty(payloadMap)) {
            payloadMap.forEach(jwtBuilder::withClaim);
        }
        jwtBuilder.withExpiresAt(expiration);
        return jwtBuilder.sign(algorithm);
    }

    /**
     * 解析非对称token
     * @param token jwt
     * @return 解析结果
     */
    public DecodedJWT decodeAsymmetryToken(String token) {
        RSA rsa = new RSA(RSA_PRIVATE_KEY, RSA_PUBLIC_KEY);
        Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) rsa.getPublicKey());
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        return jwtVerifier.verify(token);
    }
    
    /**
     * 验证非对称token
     * @param token jwt
     * @param claimMap 验证载荷
     * @return 验证结果
     */
    public boolean verifyAsymmetryToken(String token, Map<String, String> claimMap) {
        RSA rsa = new RSA(RSA_PRIVATE_KEY, RSA_PUBLIC_KEY);
        Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) rsa.getPublicKey());
        Verification verification = JWT.require(algorithm);
        if (!CollectionUtils.isEmpty(claimMap)) {
            claimMap.forEach((k, v) -> verification.withClaim(k, v));
        }
        JWTVerifier jwtVerifier = verification.build();
        jwtVerifier.verify(token);
        return true;
    }

}
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTTest {
    @Autowired
    private JwtUtils jwtUtils;

    @Test
    public void testGenerateToken() throws InterruptedException, NoSuchAlgorithmException {
        Map<String, String> payloadMap = new HashMap<>();
        payloadMap.put("userId", "007");
        payloadMap.put("userName", "zhangsan");
        Date expiration = new Date(System.currentTimeMillis() + 1000 * 5);
        String token = jwtUtils.generateAsymmetryToken(payloadMap, expiration);
        System.out.println(token);
        //Thread.sleep(1000 * 10);
        System.out.println("验证结果1:" + jwtUtils.verifyAsymmetryToken(token, null));
        System.out.println("验证结果2:" + jwtUtils.verifyAsymmetryToken(token, payloadMap));
    }
}

执行结果如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyTmFtZSI6InpoYW5nc2FuIiwiZXhwIjoxNzE3MDUyOTA5LCJ1c2VySWQiOiIwMDcifQ.jiVFjX1WQiaJiMBjT1gTCENKq4D3e6RuvkP9bv0LDcz9yw9DxPkzh9loR69zK9BQWOREIFPUe3OONiljmfJmJJtg69Qry98QmoiCeUCLWkWFWU1Jh0UvUL0jze5eRXk0lP8_-LvN0fXJCStzhf4LJWS-0hxr8-v8KkLsOL9RAzq5k5cceenINrPtCRMgYJFpmWRPFEbMrtFSGJREZJzb3I_yAHspYMNwU-R_vIx9OfkRp60rtD7WifC2rxVWuXtf_SZMkws1yIVbMsYzg-nI-mCeWqVjaEVYnzWFml6Y4Uh7ZQ4-5WfctzPTAydQSNy3wX6W5FSXwPTvyZ13k-zk2Q
验证结果1:true
验证结果2:true

非对称加密也广泛应用在安全通信方面,此时公钥由发送方持有,私钥由接收方持有,例如:
Alice 想给 Bob 发送一条加密信息:

  • Bob 生成一对密钥,并将公钥发送给 Alice。
  • Alice 使用 Bob 的公钥加密信息,然后发送给 Bob。
  • Bob 使用自己的私钥解密信息。

无论是在数字签名还是安全通信中,私钥都必须保密,只能由密钥对的所有者持有使用;而公钥可以公开分发,需要进行加密通信或验证签名的一方都可以持有公钥。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值