【Java】基于JWT+Token实现完整登入功能(实操图解)

Java系列文章目录

补充内容 Windows通过SSH连接Linux
第一章 Linux基本命令的学习与Linux历史


一、前言

  • 学习JWT+Token的传输方式
  • 流程图原作者 流程图来源 JWT的讲解也有可看这篇文章
  • 本文以实操为主,部分以图片展示代码完整可自己敲加快掌握
  • 本文自定义工具类仅仅是示例

二、学习内容:

  • Token+Redis 方法

一种结合JWT与Redis的解决方案

  • 服务器生成令牌并将器存储在Redis中,同时只有前端持有此令牌本身

流程图来源地址

实操如下流程:
在这里插入图片描述

实操项目结构与数据流程:
在这里插入图片描述


三、问题描述

  • 保证传输的安全性
  • 想了解JWT原理可看这篇文章 流程图来源

四、解决方案:

4.1 认识相关依赖

4.1.1 工具包依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.16</version>
</dependency>
  • 加密用的就是工具包的加密工具

4.1.2 非空注解依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • DTO使用的非空注解要引依赖

4.1.3 Token相关依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
  • 自定义JWT工具类需要的依赖

4.1.4 依赖文件参考

  • pom.xml文件
    <dependencies>
<!--        提供加密工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
        <!--非空注解-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!--        token依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

        <!--redis依赖-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--MyBatis Plus 代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>


        <!--mysql的连接-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

4.2 使用JWT

4.2.1 JwtConfig配置

JwtConfig通过properties文件配置JWT,properties文件自己配好数据库与Redis

  • properties文件里面加上这个
jwt.key=12345678901234567890123456789012
jwt.ttl=3600000
  • 通过前缀引入

在这里插入图片描述

4.2.2 JWT的工具类

这个只是示例

这个注释掉的在后面的方法比较中会讲.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl()))

代码如下:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import org.example.learnjwt.config.JwtConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.security.Key;

/**
 * JWT工具类,提供JWT的创建、解析和验证功能
 */
@Component
public class JwtUtil {

    // 注入JWT配置
    @Autowired
    private JwtConfig jwtConfig;

    // 用于签名的密钥
    private final Key key ;

    /**
     * 构造函数,初始化JWT配置和密钥
     * 
     * @param jwtConfig JWT配置
     */
    public JwtUtil(JwtConfig jwtConfig){
        this.jwtConfig=jwtConfig;
        key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes());
    }

    /**
     * 创建JWT令牌
     * 
     * @param id 要包含在JWT中的标识符
     * @return 生成的JWT字符串
     */
    public String createJwt(String id) {

        // 创建并设置声明
        Claims claims = Jwts.claims();
        claims.put("adminId",id);

        // 构建并返回JWT
        return Jwts.builder()
                .setClaims(claims)
                //.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl()))
                .signWith(key)
                .compact();
    }

    /**
     * 解析JWT令牌,获取其中的声明
     * 
     * @param token 待解析的JWT令牌
     * @return JWT中的声明
     */
    public Claims parseJwt(String token) {
        // 使用密钥解析JWT,并返回其主体部分
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 验证JWT令牌的有效性
     * 
     * @param token 待验证的JWT令牌
     * @return 如果令牌有效则返回true,否则返回false
     */
    public boolean validateToken(String token) {
        try {
            // 尝试解析JWT,如果成功则令牌有效
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);

            return true;
        } catch (Exception e) {
            // 如果解析异常,则令牌无效
            return false;
        }
    }
}

  • 补充前后端传输使用的DTO

非空注解记得引入相关依赖

@Data
public class AdminLoginDTO {
    @NotNull(message = "手机号不能为空")
    private String phone;
    @NotNull(message = "密码不能为空")
    private String password;
}

4.3 登入实现

4.3.1 登入步骤

密码比较不可以用equals,明码比较的话安全性低

  1. 前端输入账号密码
  2. 根据账号获得数据库加密的密码
  3. 前端密码跟数据库已加密的密码比较
  4. 比对成功后签Token返回

🌟 前端要获取Token
🌟 JWT不是很安全,用户的密码一定不能保存到JWT中

登入逻辑如下:
在这里插入图片描述

4.3.2 代码实现

后端控制层与逻辑层:
在这里插入图片描述

4.3.3 测试结果

  • 测试登入

ApiPost模拟前端

登入后返回前端的数据
在这里插入图片描述

  • 后台显示结果

在这里插入图片描述

4.4 配置拦截器

  • 拦截器排除登入以外的路径

拦截器拦截判断请求头与Token

🌟 前端已经收到Token了接下来交互验证都通过这个Token
🌟 拦截后验证Token是否过期

拦截器逻辑如下:
在这里插入图片描述

拦截器代码如下:
在这里插入图片描述

4.5 通过Token获取数据

4.5.1 拦截器内部判断以及实现业务逻辑

  • 判断有了Token才能继续进入否则直接被拦截

主要判断头部以及Token是否过期并顺便进行延期,登入后访问其他页面不需要再重新登入,因为已经持有Token且没有过期

🌟 通过拦截器后执行业务逻辑

获取数据逻辑:
在这里插入图片描述

  • 这是在TokenInterceptor里面定义的🌟判断请求是否通过

经过拦截器成功后经过AdminController层最后到AdminServicelmpl的获取数据方法

主要代码:
在这里插入图片描述

存储存储和获取当前线程的上下文信息代码:

/**
 * BaseContext类是用于存储和获取当前线程的上下文信息
 * 主要用于在多线程环境下,为每个线程提供独立的存储空间
 */
public class BaseContext {

    // 使用ThreadLocal为每个线程提供独立的存储空间,避免数据共享带来的问题
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    /**
     * 设置当前线程的上下文信息
     * 通常用于标识当前操作的用户ID,以便在日志记录或数据权限控制中使用
     *
     * @param adminId 管理员ID,用于标识当前操作的用户
     */
    public static void set(String adminId)
    {
        threadLocal.set(adminId);
    }

    /**
     * 获取当前线程的上下文信息
     *
     * @return 当前线程存储的管理员ID,如果未设置则返回null
     */
    public static String get()
    {
        return threadLocal.get();
    }

}
  • 如果Token存储时间不够会进行延期处理

如果长时间没使用就需要重新登入

在这里插入图片描述

4.5.2 测试结果

  • 测试成功

ApiPost模拟前端

在这里插入图片描述

  • 测试失败

此时Token在Redis里面已经过期

在这里插入图片描述

4.6 JWT令牌的过期时间方法比较

4.6.1 通过Redis方法

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String token = request.getHeader("Authorization");
    log.info("token还剩下{}秒", redisTemplate.getExpire("adminLogin:token", TimeUnit.SECONDS));
    
    // 检查Redis
    if (!redisTemplate.hasKey("adminLogin:token")) {
        throw new RuntimeException("redis里面token不存在");
    }

    Long expire = redisTemplate.getExpire("adminLogin:token", TimeUnit.SECONDS);
    if (expire < 30) {
        redisTemplate.expire("adminLogin:token", 40, TimeUnit.SECONDS);
    }

    // 获取JWT Claims
    Claims claims = jwtUtil.parseJwt(token);
    String id = claims.get("adminId", String.class);
    log.info("id:{}", id);
    BaseContext.set(id);
    return true;
}

这段代码主要做了以下几件事:

  • 检查JWT令牌的存在:从请求头中获取JWT令牌。
  • 检查Redis中的过期时间:获取Redis中存储的令牌的过期时间。
  • 自动续期:如果过期时间小于30秒,则将其延长到40秒。
  • 解析JWT令牌:解析JWT令牌中的claims,并从中提取用户ID。

4.6.2 设置JWT过期时间方法

.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTtl()))
  • 工具类里面创建令牌时候的配置

4.6.3 比较

区别如下

  1. 存储位置:

    • JWT:令牌存储在客户端,每次请求时都携带。

    • Redis:令牌或相关状态存储在服务器端的Redis中。

  2. 过期时间管理:

    • JWT:令牌本身的过期时间是在生成时就固定的,一旦过期就无法再使用。
    • Redis:令牌或相关状态的过期时间可以动态调整。例如,你的代码中在过期时间接近时会自动续期。
  3. 复杂度:

    • JWT:相对简单,无需服务器端额外的逻辑来管理过期时间。
    • Redis:需要额外的逻辑来检查和续期过期时间,增加了服务器端的复杂度。
  4. 效果

    • JWT过期时间:一旦过期,令牌就无效,客户端需要重新获取新的令牌。
    • Redis过期时间:可以动态续期,使得令牌在一定条件下始终有效。
  5. 使用场景

    • JWT:适用于需要简单、快速的身份验证场景,减少服务器负担。
    • Redis:适用于需要更灵活的过期时间管理场景,如刷新令牌机制。
  6. 总结

    • JWT:简单、无状态,适合轻量级的身份验证。
    • Redis:灵活、状态化,适合需要动态管理过期时间的场景。

选择哪种方式取决于具体的应用需求和场景。

  • 如果你需要更灵活的过期时间管理,可以选择Redis;
  • 如果需要简单的无状态验证,可以选择JWT。

JWT与Session与Token与Cookie原理(后续补充)

本文暂以实操为主


五、总结:

JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。它是一个轻量级的跨平台解决方案,可以在不同的系统之间安全地传输和验证信息。

JWT由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。

  • 头部包含了加密算法、令牌类型等信息,一般使用Base64编码进行编码。
  • 负载是JWT的主要部分,包含了一些声明(claims),例如用户ID、角色等信息。负载也可以包含自定义的声明。负载也使用Base64编码进行编码。
  • 签名用于保证令牌的完整性和真实性。签名由头部、负载、预先定义的密钥和指定的加密算法组成。一般使用HMAC或RSA算法进行签名。

使用JWT进行身份验证和授权的流程如下:

  • 用户登录,服务器验证用户信息。
  • 服务器生成JWT,将用户信息和其他必要的信息编码到负载中。
  • 服务器使用密钥对JWT进行签名,生成签名。
  • 服务器将JWT和签名返回给客户端。
  • 客户端在后续请求中将JWT放入请求头、Cookie或其他合适的位置进行传输。
  • 服务器验证JWT的签名,并根据负载中的信息进行权限控制和身份验证。

JWT的优点是简单、轻量级、跨平台、可扩展性强,并且不需要在服务器端保存用户的登录状态。
缺点是一旦签发的JWT被盗用,无法立即使其失效,除非附加一些额外的逻辑来实现JWT的撤销。

在使用JWT时需要注意以下几点:

  • JWT中不应包含敏感信息,因为负载是经过Base64编码的,可能会被解码。
  • 密钥的安全非常重要,因为密钥用于生成和验证签名。应该使用强密码来保护密钥,并定期更换密钥。
  • JWT的过期时间应该适当设置,以免被盗用后长时间有效。
  • 如果需要撤销JWT,可以使用黑名单或者额外的逻辑来实现。

(后续有遇到问题再添加)


声明:如本内容中存在错误或不准确之处,欢迎指正。转载时请注明原作者信息(麻辣香蝈蝈)。

在这里插入图片描述

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值