• Java - JWT的简单介绍和使用


    前言

    目前自己在做一个云直播个人项目,后端架构是微服务,目前准备用JWT来做Token的校验。借此机会来复习和学习一遍JWT的相关知识。

    一. JWT 基础知识

    JWT的全称是JSON Web Tokens主要是服务器认证相关信息之后,生成一种JSON数据,并通过一种算法(例如HMAC)对其进行安全的签名加密。主要用于两个用途:

    • 数据交换:因为JWT使用JSON来存储相关数据,而JSON这一种数据格式可以在各方之间传递。
    • 安全验证:登录之后,后续的请求可以携带JWT,只有携带了JWT(认证Token)的请求才能够正常地访问到相关的数据及资源。

    一般我们在开发过程中,凡是涉及到用户登录的,我们就需要去考虑用什么去存储用户登录的一个状态,本文只说两种:

    • session(基于cookie的实现):只不过session相关的数据存储于服务器。
    • JWT:一旦生成,一般就抛给客户端去保存。客户端只需要每次携带这个Token就可以正常地访问接口。

    1.1 session 案例测试

    我们来写一个简单的案例:pom依赖:

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.3.2.RELEASEversion>
    parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Controller代码:

    @RestController
    public class UserController {
        @GetMapping("/login")
        public String login(HttpServletRequest request) {
            request.getSession().setAttribute("user", "Ljj");
            return "Login Success";
        }
    
    	@GetMapping("/getUser")
        public String getUser(HttpServletRequest request) {
            String user = (String) request.getSession().getAttribute("user");
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    那么我们项目启动之后,访问以下路径:http://localhost:8080/login,就能发现当前客户端会多出一个名为JSESSIONIDCookie
    在这里插入图片描述
    之后我们的每一次请求,都会自动携带上这个cookie去访问服务端:
    在这里插入图片描述
    如果我们采取session来存储用户信息,那么大致流程就是如下:

    1. 用户登录成功,那么我们就request.getSession().setAttribute("user"+userId, "用户信息")
    2. 此时服务端就会写入一个session来保存相关的数据(基于cookie实现)。因此客户端能看到一个名为JSESSIONIDCookie
    3. 后续的相关请求,都会自动携带这个Cookie,后端就可以取到这个用户信息了。

    在这里插入图片描述

    但是,采用session这种方式,是以本地缓存来存储相关的数据的,当有100个用户进行登录的时候,就需要在本地缓存存储100个信息。因此会给服务器带来一定的内存压力。

    相关的数据存储于StandardSession.attributes字段中:

    public class StandardSession implements HttpSession, Session, Serializable {
    	protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
    }
    
    • 1
    • 2
    • 3

    因此现在主流的都是使用JWT来代替传统的session存储方案。

    1.2 JWT 结构

    JWT如上文所说,他是一个JSON串,但是它有着自己的特定结构:

    Header.Payload.Signature
    
    • 1

    HeaderPayload部分都是一个base64编码串,三个结构之间通过 ” . “ 进行连接。

    1.2.1 Header

    Header头部,通常有两个部分组成:

    • alg:表示签名的算法类型,比如HMAC SHA256 或者 RSA
    • typ:代表这个token令牌的类型,比如JWT

    案例如下:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
    • 1
    • 2
    • 3
    • 4

    最后这个JSON串会通过Base64Url进行编码,然后作为JWT的第一部分。

    1.2.2 Payload

    JWT的第二部分就是这个Payload了,它的中文含义叫做:有效载荷。相当于我们HTTP请求的一个请求体了。一般用来存储我们实际要传递的数据。例如:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Payload本身还规定了几个属性,同时它可以分为三种类型:

    标准注册声明:

    • iss (issuer):签发人。
    • exp (expiration time):过期时间。
    • sub (subject):主题。
    • aud (audience):接收方。
    • nbf (not before):生效时间。
    • iat (issued at):签发时间。
    • jti (jwt id):编号。

    公共声明: 一般用于我们自己定义一些业务属性。


    私有声明: 服务器和客户端共同定义的声明

    这一部分的内容可见比较丰富,因为我们可以将自定义的属性塞进去,但是有一点我们需要格外地注意:Payload部分不建议添加敏感信息,例如密码、身份证等信息。因为Payload这一部分最终也是经过Base64Url进行编码,然后作为JWT的第二部分暴露给客户端。因此这个是可以被解码的。

    1.2.3 Signature ☆

    上面的两个JWT组成部分:HeaderPayload,我们知道它是由Base64Url进行编码的,那么自然而然的它就可以被解码。那么JWT的安全性谈何而来?这就得看最后一个部分Signature签名了。他的作用一句话概括就是:对前两部分的内容进行算法签名,防止数据篡改。

    HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
    
    • 1

    这个代码是啥意思呢?

    1. 拿到经过Base64编码的HeaderPayload部分。以及我们自己提供的一个秘钥secret
    2. 使用Header中指定的签名算法:HMAC SHA256 进行签名。

    签名的目的:

    1. 如果有人对头部以负载内容进行解码,然后篡改相关信息,在进行base64编码重新组合一个新的JWT
    2. 那么这个JWT传输给服务器后,服务器能够判断,新的头部和新的负载内容形成的签名和原有的不一致。
    3. 那么此时就能够判断这个JWT是不合法的。因为外部使用者无法得知你进行签名时的秘钥是什么。 因此通过修改JWT算出来签名是不一样的。

    那么最后再来说下JWT的特点:

    • JWT的两个组成部分虽然是经过base64编码的,因此他可以被解码。但是JWT本身是可以被加密的。这样就更加安全。
    • JWT生成之后,不必存储在服务器端,就没有使用session存储那样,有着内存压力。
    • JWT 无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改。
    • 安全:因为Token不是Cookie。我们只需要每次请求的时候携带Token即可。由于没有Cookie被发送,还有助于防止CSRF攻击。
    • 不要再JWT中存储一些敏感信息,一般都是存储userId

    二. JWT 简单使用

    首先准备pom依赖:

    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <scope>testscope>
    dependency>
    <dependency>
        <groupId>com.auth0groupId>
        <artifactId>java-jwtartifactId>
        <version>3.19.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.1 生成JWT

    public class JwtTest {
        // 秘钥,你可以随便取,可以取的难一点
        public static final String SECRET = "ASD!@#F^%A";
    
        @Test
        public void testTokenCreate() {
            HashMap<String, Object> headers = new HashMap<>();
            // 过期时间,60s
            Calendar expires = Calendar.getInstance();
            expires.add(Calendar.SECOND, 600);
    
            String jwtToken = JWT.create()
                    // 第一部分Header
                    .withHeader(headers)
                    // 第二部分Payload
                    .withClaim("userId", 20)
                    .withClaim("userName", "LJJ")
                    .withExpiresAt(expires.getTime())
                    // 第三部分Signature
                    .sign(Algorithm.HMAC256(SECRET));
            System.out.println(jwtToken);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    结果如下:看红圈的两个"."符号,可见JWT的格式和第一章节描述的相吻合。
    在这里插入图片描述

    那么拿到令牌了,我们接下来就是根据令牌去解析数据。

    2.2 解析JWT

    注意:这里你要把2.1节生成的token自己复制一下,贴到代码里面。

    @Test
    public void testReadJWT() {
        // 创建一个验证的对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6IkxKSiIsImV4cCI6MTY2NzcyMTMzMiwidXNlcklkIjoyMH0.AJA88B0F-zKEJUCIGp9kSx2TlbpoyH88GzEn-9xN5XI");
        System.out.println(verify.getClaim("userId").asInt());
        System.out.println(verify.getClaim("userName").asString());
        System.out.println("过期时间:" + verify.getExpiresAt());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    结果如下:
    在这里插入图片描述

    2.3 常见的异常

    这里贴出4个常见的异常,如果JWT校验不通过,就会抛出异常:

    • SignatureVerificationException:签名不一致。
    • TokenExpiredException:令牌过期。
    • AlgorithmMismatchException:算法不匹配异常。
    • InvalidClaimException:失效的Payload异常。一般存在于这种情况:获取token的服务器比使用token的服务器时钟快,请求分发到时间慢的服务器上导致token还没生效。
  • 相关阅读:
    域名解析信息易语言代码
    (2022版)一套教程搞定k8s安装到实战 | ConfiMap
    聚类算法的先验基础知识
    计算机毕业设计Java咖啡销售平台(源码+mysql数据库+系统+lw文档)
    第十九章总结:Java绘图
    Vant Weapp的Slider组件自定义button
    [Springboot]统一响应和异常处理配置
    2024年云服务器ECS价格表出炉——阿里云
    左孩子右兄弟
    把微信变成找券返利机器人,淘宝客查券返利小助手自动赚佣金实现方法分享
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/127714702