Sspring Boot整合JWT

1. 为什么要用 JWT ?

在谈起 JWT 之前,我们先了解一下什么是认证。
在登录淘宝、微博等软件或者网站之前,我们需要通过填写账号和密码来校验身份。认证是用来验证用户身份合法性的一种方式。
在这里插入图片描述
那我们登录成功之后,网站如何记录我们的身份信息呢?

前面我们在学习 servlet 的时候,知道了传统的系统主要是通过 session 来存储用户的信息。session 将用户的信息存储在服务端。
但是随着用户数量的增多,服务端就需要存一堆用户的认证信息,这种方式会不断增加服务端的压力。

如果是分布式系统,用 session 存储用户信息就太拘束了。因为分布式系统一般都会做负载均衡,如果这次认证成功了,那么意味着下次请求必须仍要访问这台服务器才能认证成功。

如果是前后端分离的系统就更难受了,因为前端代码和后端代码放在不同的服务器上,除了会增加服务器的压力,还会产生跨域等一系列问题,有点得不偿失。

那有没有一种工具能帮我们解决这些认证问题?

  • 服务端不需要存储用户的认证信息
  • 避免跨域
  • 保证数据的安全性

2. 什么是 JWT?

JWT 简称 JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于各方之间安全地将信息作为JSON对象传输,在数据传输的过程中还可以完成数据加密、签名等相关处理。

用户要想访问系统中的某些页面,在发起的请求中必须携带使用 JWT 生成的令牌。令牌校验通过了,方可访问系统。这里的令牌简称为 token

3. JWT的结构

这里所说的 JWT 的结构,指的是用 JWT 生成令牌的结构,也就是 token 的结构。

token 组成主要有三部分:

  • 标头(Header)
  • 有效载荷(Payload)
  • 签名(Signature)
    令牌最终的样子是由这三部分组成的字符串:
Header.Payload.Signature

例如:

hjYGH1dajUU.dajhjksfiu2h27jjghg2.kjbhjkf982bhh2lk2

3.1 标头

标头是使用 Base64 编码将令牌类型签名算法经过加工后生成的一段字符串。
在这里插入图片描述

  • 令牌的类型:JWT(一般是默认的)
  • 签名算法:例如 SHA256、HMAC等
{
  "alg": "HS256",
  "typ": "JWT"
}

3.2 载荷

载荷主要存储一些自定义信息。它也是使用 Base64 编码加工后生成的一段字符串。
在这里插入图片描述
签名是通过一个秘钥和标头中提供的算法再将标头和载荷进行加工后生成的一段字符串。例如:
在这里插入图片描述

4. JWT的认证流程

在这里插入图片描述

5. JWT 的使用

5.1导入依赖

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

5.2 生成token

    @Test
    void contextLoads() {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND,90);
        String token = JWT.create()
                .withClaim("age",21)
                .withClaim("username", "张三")//设置自定义用户名  payload
                .withExpiresAt(instance.getTime())//设置过期时间
                .sign(Algorithm.HMAC256("token!Q2W#E$RW"));//设置签名 保密 复杂
        System.out.println(token); //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Njg5MTQyMTEsImFnZSI6MjEsInVzZXJuYW1lIjoi5byg5LiJIn0.l3YSKPolnXeelafFDwh92-O-8kGWXagXQD_WGfZ2Qjo
    }

5.3 校验token

    @Test
    public void test(){
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
        DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Njg5MTQzNzAsImFnZSI6MjEsInVzZXJuYW1lIjoi5byg5LiJIn0.AzyCQZxi86vYNFWbTskWDqh2ow59Yd1ih2Oz7rpsiW8");
        System.out.println(decodedJWT.getClaim("age").asInt());// 存的是时候是什么类型,取得时候就是什么类型,否则取不到值。
        System.out.println(decodedJWT.getClaim("username").asString());
        System.out.println(decodedJWT.getExpiresAt());
    }

6. JWT 工具类

因为 JWT 的作用主要是生成 token、校验 token、获取token中存储的自定义信息,所以我们一般会把 JWT 封装成一个工具类。

public class JwtUtils {
    // 秘钥
    private static final String SECRET = "SECRET_PRIVATE!";
    private static final long TIME_UNIT = 1000;

    // 生成包含用户id的token
    public static String createJwtToken(String userId, long expireTime) {
        Date date = new Date(System.currentTimeMillis() + expireTime * TIME_UNIT);
        Algorithm algorithm = Algorithm.HMAC256(SECRET);

        return JWT.create()
                .withClaim("userId", userId)
                .withExpiresAt(date) // 设置过期时间
                .sign(algorithm);     // 设置签名算法
    }

    // 生成包含自定义信息的token
    public static String createJwtToken(Map<String, String> map, long expireTime) {
        JWTCreator.Builder builder = JWT.create();
        if (map!=null) {
            map.forEach((k, v) -> {
                builder.withClaim(k, v);
            });
        }
        Date date = new Date(System.currentTimeMillis() + expireTime * TIME_UNIT);
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        return builder
                .withExpiresAt(date) // 设置过期时间
                .sign(algorithm);     // 设置签名算法
    }

    // 校验token,其实就是比较token
    public static DecodedJWT verifyToken(String token) {
        // 如果校验失败,程序会抛出异常
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }

    // 从token中获取用户id
    public static String getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("userId").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    // 从token中获取定义的荷载信息
    public static String getTokenClaim(String token, String key) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(key).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    // 判断 token 是否过期
    public static boolean isExpire(String token) {
        DecodedJWT jwt = JWT.decode(token);
        // 如果token的过期时间小于当前时间,则表示已过期,为true
        return jwt.getExpiresAt().getTime() < System.currentTimeMillis();
    }
}

7. JWT 案例

这里我们通过一个 springboot 项目来感受一下 JWT 的使用过程。

  • 开发工具:IDEA
  • 技术栈:SpringBoot、MyBatisPlus、JWT
  • 数据库:Mysql

7.1 用户登录

7.1.1 创建 SpringBoot 项目

在这里插入图片描述
在这里插入图片描述

7.1.2 引入依赖

        <!--引入jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!--引入web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--引入druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>
        <!--引入mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
        <!--引入test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

7.1.3 application.yml

server:
    port: 8989
spring:
    application:
        name: jwt
    datasource:
        driver-class-name: com.mysql.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://192.168.44.133:3306/jwt?characterEncoding=UTF-8
        username: root
        password: 1111
mybatis-plus:
    mapperLocations: classpath:mapper/*.xml
    typeAliasesPackage: com.example.jwt.entity
    configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 

7.1.4 创建用户表

CREATE TABLE `t_user` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '姓名',
  `password` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

7.1.5 用户实体类

@Data
public class User {
    private String id;
    private String name;
    private String password;
}

7.1.6 Service 和实现类

public interface UserService extends IService<User> {
    /**
     * 登录
     * @param user
     * @return
     */
    User login(User user);

}
@Service
@Transactional
public class UserServiceImpl  extends ServiceImpl<UserMapper,User> implements UserService{
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public User login(User user) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName,user.getName());
        wrapper.eq(User::getPassword,user.getPassword());
        User userDB = this.getOne(wrapper);
        if(Objects.isNull(user)){
            throw  new RuntimeException("登录失败");
        }
        return userDB;
    }
}

7.1.7 mapper 和 mapper.xml

public interface UserMapper extends BaseMapper<User> {
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.jwt.mapper.UserMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.example.jwt.entity.User">
        <id column="id" property="id" />
        <result column="username" property="name" />
        <result column="password" property="password" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, password
    </sql>
</mapper>

7.1.8 登录接口

    @GetMapping("/user/login")
    public Map<String,Object> login(User user){
        HashMap<String, Object> result = new HashMap<>();
        log.info("用户名:{}",user.getName());
        log.info("密码:{}",user.getPassword());
        try {
            User userDB = userService.login(user);
            HashMap<String, String> map = new HashMap<>(); //payload
            map.put("id",userDB.getId());
            map.put("username",userDB.getName());
            String token = JwtUtils.createJwtToken(map,30);
            result.put("state",true);
            result.put("msg","登录成功");
            result.put("token",token); //成功返回的token信息
        }catch (Exception e) {
            e.printStackTrace();
            result.put("state","false");
            result.put("msg","错误信息");
        }
        return result;
    }

7.1.9 测试

在这里插入图片描述

7.2 登录成功访问其他资源

用户登录成功后,我们把 token 返回给了前端。用户再次访问该网站的其他资源,我们怎么判断当前的用户和上次登录成功后的用户是同一个用户呢?

只需要两步:

  • 前端:请求头中携带 token
  • 后端:配置拦截器,校验 token

7.2.1 创建和配置拦截器

/**
 * 创建拦截器
 */
public class JWTInterceptor implements HandlerInterceptor {

  @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      String token = request.getHeader("token");
      Map<String,Object> map = new HashMap<>();
      try {
        JwtUtils.verifyToken(token);
        return true;
      } catch (TokenExpiredException e) {
        map.put("state", false);
        map.put("msg", "Token已经过期!!!");
      } catch (SignatureVerificationException e){
        map.put("state", false);
        map.put("msg", "签名错误!!!");
      } catch (AlgorithmMismatchException e){
        map.put("state", false);
        map.put("msg", "加密算法不匹配!!!");
      } catch (Exception e) {
        e.printStackTrace();
        map.put("state", false);
        map.put("msg", "无效token~~");
      }
      String json = new ObjectMapper().writeValueAsString(map);
      response.setContentType("application/json;charset=UTF-8");
      response.getWriter().println(json);
      return false;
    }
}
/**
 * 配置拦截器
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor()).
          excludePathPatterns("/user/**")  // 放行
          .addPathPatterns("/**"); 
    }
}
/**
 * 配置拦截器
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
          .addPathPatterns("/**") // 拦截所有请求
          .excludePathPatterns("/user/login");  // 排除路径,比如用户登录、退出等
    }
}

7.2.2 测试

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值