JWT

 

1. 介绍

  JWT(JSON Web Token)是一种非常轻量级的规范;使我们能在用户和服务器之间传递安全可靠的消息。

  JWT本质上是一个袖珍的安全令牌,允许我们以一种结构化的方式,在网络上安全的传输数据。

 

2. 构成

  一个JWT实际上就是一个字符串,由三部分组合成:头部、载荷与签名。

  1) 头部(header)是JWT的第一部分,通常是一个JSON对象;包含两个主要部分:令牌的类型(通常是JWT)和所使用的签名算法(比:HMAC SHA256或者RSA)。头部告诉接受方如何验证这个令牌。

{"typ":"JWT","alg":"HS256"} 
下面有{"typ":"JWT","alg":"HS256"}编码后的结果:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

  PS:表示JWT的类型是JWT,签名算法是HS256(HMAC SHA-256)。

  编码后的字符串是通过将JSON对象 {"typ":"JWT","alg":"HS256"} 进行Base64 URL编码而得到的。在这个编码后的字符串中,. 被编码为 _+ 被编码为 -/ 被编码为 .,这样可以确保编码后的字符串在URL中安全传输。

  2) 载荷(Payload)是JWT的第二部分,也是一个JSON对象,包含一系列声明。这些声明是关于实体(如用户)及其他数据的声明。载荷是JWT中存储实际数据的部分,可以包含用户ID、角色、权限的数据。

  存储有效信息的地方。包含三部分:

  部分一:标准中注册的声明(建议但不强制,JWT规范默认的属性)

    iss:JWT的签发者(issuer);

    sub:当前令牌的描述说明(subject);

    aud:接收JWT的一方(audience);

    exp:JWT的过期时间,必须大于签发时间(expiration time);

    nbf:定义在什么时间之前,该JWT都是不可用的(not before);

    iat:JWT的签发时间(issued at);

    jti:JWT的唯一身份标识,用于避免重放攻击(JWT ID);

  部分二:公共的声明:可以添加任何信息。通常用于添加用户的相关信息和其他业务需求的必要信息。但不建议添加敏感信息,因为信息可在客户端解密。

  部分三:私有的声明:私有声明是提供者和消费者共同定义的声明,通常用于自定义信息。一般不建议存放敏感信息,因为base64是对称解密,意味着该部分信息可归类为明文信息。

      与标准声明不同的是,私有声明不会被验证,除非明确告知接收方要对这些声明要进行验证和验证规则。

        这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。
     这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,
     都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。 定义一个payload: {"sub":"1234567890","name":"John Doe","admin":true} base64加密结果是:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 载荷中只有标准声明是参与校验的,公共和私有是不参与校验的。

  3) 签名(Signature)是JWT的第三部分,由头部、载荷经过Base64编码后的字符串,及一个私钥或盐(secret)共同组成。签名用于验证JWT的真实性和完整性,确保令牌在传输过程中没有被篡改。

  签名 = {base64(header) + base64(payload) + 私钥(盐) } +HS256算法;

  
  https://base64.us/

 

3. JWT代码

  1) 新建一个boot项目

  导入依赖:

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
<!-- javax.xml.bind.DatatypeConverter 在Java9 版本后加 ->-->
 <dependency>
     <groupId>javax.xml.bind</groupId>
     <artifactId>jaxb-api</artifactId>
     <version>2.3.0</version>
</dependency>

<!-- 或 ->-->

 <dependency>
     <groupId>javax.xml.bind</groupId>
     <artifactId>jaxb-api</artifactId>
     <version>2.3.0</version>
</dependency
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

  javax.xml.bind:jaxb-api 是 Java Architecture for XML Binding (JAXB) API 的依赖。JAXB 是 Java 中用于 XML 数据与 Java 类之间相互转换的标准 API。 

  若不报错可不加。

 

  2) 加密&解密测试

    // 加密测试
    @Test
    void encryption() {

        JwtBuilder builder= Jwts.builder()      // 用于生成JWT
                .setId("2024")             // 设置唯一编号
                .setSubject("JWT")       // 设置主题  可以是JSON数据
                .setIssuedAt(new Date())  // 设置签发日期
                .setExpiration(new Date(new Date().getTime() + 7 * 24 * 3600 * 1000L))  // 过期时间 当前时间 + 七天的毫秒数
                .signWith(SignatureAlgorithm.HS256,"buildJWT");   // 设置签名 使用HS256算法,并设置SecretKey(字符串)
        // 构建 并返回一个字符串
        System.out.println("结果:" + builder.compact() );
        // eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyMDI0Iiwic3ViIjoiSldUIiwiaWF0IjoxNzA4Njk1NTM3LCJleHAiOjE3MDkzMDAzMzd9.CB37ieSRyB0309Ywdv8F7LktR_AzgVGWee7fw23j9t0
     // 头部(token类型、签名加密方式).载荷.签名(包含算法)
    } // 解密测试 @Test public void decrypt(){ String compactJwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyMDI0Iiwic3ViIjoiSldUIiwiaWF0IjoxNzA4Njk1NTM3LCJleHAiOjE3MDkzMDAzMzd9.CB37ieSRyB0309Ywdv8F7LktR_AzgVGWee7fw23j9t0"; Claims claims = Jwts.parser(). setSigningKey("buildJWT"). parseClaimsJws(compactJwt). getBody(); System.out.println(claims); // {jti=2024, sub=JWT, iat=1708695537, exp=1709300337} }

  

  3) 生成工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;

public class JwtUtil {
    // token 有效期
    public static final Long JWT_TTL = 3600000L;    // 60 * 60 * 1000  一个小时

    // Jwt令牌信息
    public static final String JWT_KEY = "Java_JWT";

    // 创建token方法
    public static String createJWT(String id, String subject, Long ttlMillis) {     // id, 信息, 过期时间
        // 指定算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 当前系统时间
        long nowMillis = System.currentTimeMillis();
        //令牌签发时间
        Date now = new Date(nowMillis);
        // 如果令牌有效期为null,则默认设置有效期1小时
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        // 令牌过期时间设置
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        // 生成秘钥
        SecretKey secretKey = generalKey();

        // 封装Jwt令牌信息
        JwtBuilder builder = Jwts.builder()
                .setId(id)                    // 唯一的ID
                .setSubject(subject)          // 主题  可以是JSON数据
                .setIssuer("admin")          // 签发者
                .setIssuedAt(now)             // 签发时间
                .signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙
                .setExpiration(expDate);      // 设置过期时间
        // builder.addClaims(map); 可以放一些额外信息;
        return builder.compact();
    }

    /**
     * 生成加密 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes()); // 转为字节码
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    /**
     * 解析令牌数据
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

}

  4) 测试

    public static void main(String[] args) {

        // 加密
        String encryptionToken = JwtUtil.createJWT("007", "学Java的bei", 7 * 24 * 3600 * 1000L);
        System.out.println("加密:" + encryptionToken);

        // 加密:eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDciLCJzdWIiOiLlraZKYXZh55qEYmVpIiwiaXNzIjoiYWRtaW4iLCJpYXQiOjE3MDg2OTc5MDEsImV4cCI6MTcwOTMwMjcwMX0.W0D-u0KcIG1Uiz8WN12s0xYR1f3abdzeM5iFrHd8umc

        System.out.println("·····································································");
        
        // 解密
        String decryptToken = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwMDciLCJzdWIiOiLlraZKYXZh55qEYmVpIiwiaXNzIjoiYWRtaW4iLCJpYXQiOjE3MDg2OTc0MjUsImV4cCI6MTcwOTMwMjIyNX0.HxE5aoGLAPxXJIjdcEynJ6d1v_KAwnC0I4G7R89YDKw";
        try {
            System.out.println("解密:" + parseJWT(decryptToken));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        // 解密:{jti=007, sub=学Java的bei, iss=admin, iat=1708697425, exp=1709302225}
        
    }

 

  4. 注册、登录

  1) 注册:使用用户名正则和密码正则,判断用户名格式是否通过,用户名是否被注册;判断密码格式是否通过并通过MD5加密。

public Result register(User user) {

        // 判断用户名格式与是否被注册
        String username = user.getUsername();
        // 判断用户名是否被注册
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        if (userMapper.selectOne(queryWrapper) != null) {
            return Result.error("此用户名已被注册.");
        }

        // 判断用户名格式
        if (!RegexUtil.checkUsername(username)) {
            return Result.error("用户名格式有误.");
        }


        // 判断密码格式
        String userPassword = user.getPassword();
        if (!RegexUtil.checkPassword(userPassword)) {
            return Result.error("密码格式有误.");
        }

        // 密码加密
        user.setPassword(MD5Util.MD5Encode(user.getPassword(),"utf-8"));
        int insertUser = userMapper.insert(user);

        return insertUser > 0 ? Result.ok("注册成功.") : Result.error("注册失败.");
    }

  MD5:https://www.cnblogs.com/warmNest-llb/p/18031009
  正则:https://www.cnblogs.com/warmNest-llb/p/18031247

  2) 登录
  未生成JWT(Token)前:

    /**
     * 登录
     * @param username
     * @param password
     * @return
     */
    @Override
    public Result login(String username, String password) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        queryWrapper.eq("password", MD5Util.MD5Encode(password,"utf-8"));
        User userMatch = userMapper.selectOne(queryWrapper);
        return userMatch != null ? Result.ok("登录成功.") : Result.error("用户名或密码有误.");
    }

  生成JWT(Token)后:

    /**
     * 登录
     * @param username
     * @param password
     * @return
     */
    @Override
    public Result login(String username, String password) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        queryWrapper.eq("password", MD5Util.MD5Encode(password,"utf-8"));
        User userMatch = userMapper.selectOne(queryWrapper);
        // 加上JWT(token)
        userMatch.setPassword(null);
        if (userMatch != null) {
            String jsonString = JSON.toJSONString(userMatch);
            String token = JwtUtil.createJWT(userMatch.getId().toString(), jsonString, 7 * 24 * 2600 * 1000L);
            return Result.ok(token);
        }
        return Result.error("用户名或密码有误.");
    }

 

  5. JWT的工作原理

   基于JWT的认证和授权机制,通过在用户登录后生成并返回JWT,在每次请求中携带JWT,并在服务器端验证和解析JWT来实现用户身份认证和授权。

   简单说:登录时等成JWT(token),返回给前端,前端拿到后放到请求头,前端的每次请求都可以带token到请求头;判断是否带上token,并解析(带-进,不带-禁);每次访问都由拦截器拦截。

 


  具体看 JWT整合Spring Boot:https://www.cnblogs.com/warmNest-llb/p/18031456

  token、jwt 和 jwt刷新token:https://www.cnblogs.com/warmNest-llb/p/18108105

 

 

 

 

 

posted @ 2024-02-23 22:24  学Java的`Bei  阅读(25)  评论(0编辑  收藏  举报