JSON Web Token (JWT): 理解与应用

JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象的形式安全地传输信息。JWT通常用于身份验证和授权目的,因为它可以使用JSON对象在各方之间安全地传输信息

官网地址:https://jwt.io/

0.介绍

通俗地说,JWT的本质就是一个字符串 ,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。

0.1.JWT的应用场景

身份验证 : 当用户成功登录时,服务器会生成一个JWT并将其发送给客户端。客户端在后续请求中将JWT附加到HTTP请求头中,以此来证明用户的身份。

授权 : JWT中可以包含用户的权限信息,这样服务器可以根据这些信息决定用户是否被允许访问某些资源。

信息传递 : 除了用户的身份和权限外,JWT还可以用来携带其他有用的信息,如用户的偏好设置等。

在 java 中 常与 Spring Security 框架配合使用

JWT的认证流程如下:

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
  2. 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
  3. 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
  4. 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
  5. 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
  6. 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

0.2.优点

这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

  • 支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题

  • 无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力

  • 更适用CDN:可以通过内容分发网络请求服务端的所有资料

  • 更适用于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多

  • 无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御

1.JWT的结构

JWT是一种自包含的令牌格式。JWT由三个部分组成:头部、载荷和签名。

1.1.头部 (Header)

头部通常包含两个部分:

  • typ: 表示该令牌的类型,通常是"JWT"。
  • alg: 指定签名算法,例如 HMAC SHA-256 或 RSA。

头部通常以JSON格式书写,并经过Base64Url编码。

1.2.载荷 (Payload)

载荷包含了需要作为声明传输的信息。这些声明可以分为三类:

  • 标准声明 :由JWT规范定义的声明。
    • iss (issuer): 发行者。
    • sub (subject): 主题,通常是指用户ID。
    • aud (audience): 接收者,即令牌的预期受众。
    • exp (expiration time): 过期时间。
    • nbf (not before): 该时间之前不可使用。
    • iat (issued at): 发行时间。
    • jti (JWT ID): 一个唯一的标识符,用于防止重放攻击。
  • 私有声明:由发行者和接收者约定的声明,例如用户的角色或权限等。
  • 公共声明:虽然不是JWT规范的一部分,但可以在任何JWT中使用。

载荷也是经过Base64Url编码的。

1.3.签名 (Signature)

签名部分保证了JWT的完整性和安全性。签名通过将头部和载荷进行编码并使用指定的算法(如HMAC SHA-256 RSA或ECDSA)进行计算得到。签名确保了:

  • 令牌没有被篡改。
  • 令牌是由可信的一方发行的。

签名部分同样经过Base64Url编码。

1.4.JWS, JWK

JWS ,也就是JWT Signature,其结构就是在之前nonsecure JWT的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证jwt不能被他人随意篡改。我们通常使用的JWT一般都是JWS

为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。

加密的算法一般有2类:

  • 对称加密:secretKey指加密密钥,可以生成签名与验签
  • 非对称加密:secretKey指私钥,只用来生成签名,不能用来验签(验签用的是公钥)

JWT的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK

到目前为止,jwt的签名算法有三种:

  • HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
  • RSASSA【RSA签名算法(非对称)】 :(RS256/RS384/RS512)
  • ECDSA【椭圆曲线数据签名算法(非对称)】 :(ES256/ES384/ES512)

2.实现与工具

2.1.jjwt (Java JWT)

GitHub 仓库地址:https://github.com/jwtk/jjwt

这是一个基于Java的库,用于在JVM和Android平台上创建和验证JSON Web Tokens (JWTs)和JSON Web Keys (JWKs)。

它支持多种JOSE工作组的RFC规范:

  • RFC 7519: JSON Web Token (JWT)
  • RFC 7515: JSON Web Signature (JWS)
  • RFC 7516: JSON Web Encryption (JWE)
  • 等等
2.1.1.导入Maven依赖
xml 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>
2.1.2.测试代码
java 复制代码
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Key;
import java.util.Date;
import java.util.UUID;

@RestController
public class JwtController {

    // 密钥 : 实际开发时 应该从 配置文件 / 持久化存储中 获取
    private String secret;

    // token时效:24小时
    public static final long EXPIRE = 1000 * 60 * 60 * 24;


    @RequestMapping("/create")
    public String createToken(){

        // 生成一个随机的密钥 ID
        String keyId = UUID.randomUUID().toString();

        // 密钥
        secret = keyId;



        // 创建一个 JWT 令牌
        String jwt = Jwts.builder()

                // 设置JWT头部参数,指定令牌类型为JWT
                .setHeaderParam("typ", "JWT")
                // 设置JWT头部参数,指定签名算法为HS256
                .setHeaderParam("alg", "HS256")
                // 设置JWT过期时间,当前时间戳加上设定的过期时长
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                // 设置主题,通常是用户身份标识
                .setSubject("admin")
            
                // 添加角色声明
                .claim("role", "admin")
                // 设置用户ID为1
                .claim("id", 1)
                // 设置用户昵称为"王小二"
                .claim("nickname", "王小二")
            
                // 设置令牌签发时间
                .setIssuedAt(new Date())
                // 设置唯一标识符
                .setId( keyId )

                // 使用HS256算法和密钥签名
                .signWith(SignatureAlgorithm.HS256, secret.getBytes())


                // 将令牌压缩为紧凑形式的字符串
                .compact();

        return jwt;
    }


    @RequestMapping("/check")
    public  void checkToken(String jwt) {
        System.out.println("jwt = " + jwt);

        // 创建一个安全的密钥
        Key secureKey = Keys.hmacShaKeyFor(secret.getBytes());

        try {
            // 使用Jwts.parserBuilder()方法构建一个解析器,该解析器使用secureKey作为签名密钥
            // 然后使用这个解析器解析jwt字符串,获取到Claims对象,即JWT的主体部分
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(secureKey)
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody();


            // 打印解析后的 JWT 信息
            System.out.println("Subject: " + claims.getSubject());
            System.out.println("Role: " + claims.get("role", String.class));
            System.out.println("Issue Time: " + claims.getIssuedAt());
            System.out.println("JWT ID: " + claims.getId());
            System.out.println("user nickname: " + claims.get("nickname"));

            // 可以在这里添加更多逻辑来验证 JWT 的有效性,例如检查过期时间等
            if(claims.getExpiration()==null){
                System.out.println("过期时间不能为空 Expiration time cannot be null");
            }

        } catch (Exception e) {
            // 如果 JWT 无法验证,这里会捕获异常
            System.err.println("Invalid JWT: " + e.getMessage());
        }
    }
}

2.2.Nimbus

Nimbus JOSE + JWT 是一个非常强大的 Java 库,用于处理 JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), JSON Web Token (JWT) 和 OAuth 2.0 授权服务器。这个库是由 NimbusDS 开发的,并且广泛应用于身份验证和授权系统中。

2.2.1.导入依赖

首先,您需要在 Maven 项目中添加 json-jwt 的依赖:

xml 复制代码
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>9.40</version>
        </dependency>
2.2.2.测试代码
java 复制代码
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.text.ParseException;
import java.util.Date;
import java.util.UUID;

@RestController
public class JwtController {

    // 密钥 : 实际开发时 应该从 配置文件 / 持久化存储中 获取
    private String secret;

    @RequestMapping("/create")
    public String createToken() {
        try {
            // 生成一个随机的密钥 ID
            String keyId = UUID.randomUUID().toString();

            // 密钥
            secret = keyId;

            // 创建一个 HMAC 签名器
            MACSigner signer = new MACSigner(secret.getBytes());


            // 设置 JWT 的声明
            JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
            builder.subject("admin");
            builder.claim("role", "admin");
            builder.issueTime(new Date());
            builder.jwtID(keyId);
            // 构建 JWT Claims Set
            JWTClaimsSet claimsSet = builder.build();
            // 创建一个空的 SignedJWT 对象
            SignedJWT signedJWT = new SignedJWT(
                    new JWSHeader(JWSAlgorithm.HS256),
                    claimsSet);
            // 签名 JWT
            signedJWT.sign(signer);
            // 将 JWT 转换为紧凑形式
            String jwt = signedJWT.serialize();

            return jwt;
        } catch (JOSEException e) {
            e.printStackTrace();
        }
        return null;
    }


    @RequestMapping("/check")
    public void checkToken(String jwt) {
        try {
            System.out.println("jwt = " + jwt);
            // 解析 JWT
            SignedJWT signedJWT = SignedJWT.parse(jwt);
            // 验证 JWT
            MACVerifier verifier = new MACVerifier( secret.getBytes() );
            boolean isValid = signedJWT.verify(verifier);
            if (!isValid) {
                System.out.println("无效的 JWT 签名 Invalid JWT signature.");
                return;
            }
            // 获取 JWT Claims
            JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
            System.out.println("Subject: " + claimsSet.getSubject());
            System.out.println("Role: " + claimsSet.getStringClaim("role"));
            System.out.println("Issue Time: " + claimsSet.getIssueTime());
            System.out.println("JWT ID: " + claimsSet.getJWTID());

        } catch (JOSEException | ParseException e) {
            e.printStackTrace();
        }
    }
    
    /**
    * token 通过 header传递
    * 再通过 request 取出
    */
    @RequestMapping("/checkHeader")
    public void checkHeaderToken(HttpServletRequest request) {
        try {
            String jwt = request.getHeader("Authorization");

            System.out.println("jwt header=> " + jwt);
            // 解析 JWT
            SignedJWT signedJWT = SignedJWT.parse(jwt);
            // 验证 JWT
            MACVerifier verifier = new MACVerifier( secret.getBytes() );
            boolean isValid = signedJWT.verify(verifier);
            if (!isValid) {
                System.out.println("无效的 JWT 签名 Invalid JWT signature.");
                return;
            }
            // 获取 JWT Claims
            JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
            System.out.println("Subject: " + claimsSet.getSubject());
            System.out.println("Role: " + claimsSet.getStringClaim("role"));
            System.out.println("Issue Time: " + claimsSet.getIssueTime());
            System.out.println("JWT ID: " + claimsSet.getJWTID());

        } catch (JOSEException | ParseException e) {
            e.printStackTrace();
        }
    }
}
2.2.3.请求测试
text 复制代码
###
GET http://localhost:8080/create

生成token : eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyMzQ1MDgyOCwiaWF0IjoxNzIzMzY0NDI4LCJqdGkiOiI5Yzc3N2YwZC03NDU5LTQ3MTUtYmVkNy1mNWViYzJiNmMwOTgifQ.Y_P7L4gehl0kJwxTnwUxX8Yy502qrCHQ0hkxod58ly8

text 复制代码
###
GET http://localhost:8080/check?jwt=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyMzQ1MDgyOCwiaWF0IjoxNzIzMzY0NDI4LCJqdGkiOiI5Yzc3N2YwZC03NDU5LTQ3MTUtYmVkNy1mNWViYzJiNmMwOTgifQ.Y_P7L4gehl0kJwxTnwUxX8Yy502qrCHQ0hkxod58ly8

idea 控制台:

Subject: admin

Role: admin

Issue Time: Sun Aug 11 16:20:28 CST 2024

JWT ID: 9c777f0d-7459-4715-bed7-f5ebc2b6c098

text 复制代码
###
GET http://localhost:8080/checkHeader
Content-Type: application/json
Authorization: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcyMzQ1MDgyOCwiaWF0IjoxNzIzMzY0NDI4LCJqdGkiOiI5Yzc3N2YwZC03NDU5LTQ3MTUtYmVkNy1mNWViYzJiNmMwOTgifQ.Y_P7L4gehl0kJwxTnwUxX8Yy502qrCHQ0hkxod58ly8

idea 控制台:

Subject: admin

Role: admin

Issue Time: Sun Aug 11 16:20:28 CST 2024

JWT ID: 9c777f0d-7459-4715-bed7-f5ebc2b6c098

相关推荐
Mephisto.java27 分钟前
【大数据学习 | kafka高级部分】kafka的kraft集群
大数据·sql·oracle·kafka·json·hbase
Mephisto.java28 分钟前
【大数据学习 | kafka高级部分】kafka的文件存储原理
大数据·sql·oracle·kafka·json
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js