【SpringBoot】SpringBoot + JWT+ Mybatis 整合

现需要完成以下需求:

用户执行登录接口之后,需要生成一个 token 返回给前端。之后,前端在请求头中携带着 token 去请求其它的后台接口。

完成这个需求之前,咱们先来了解下什么是 JWT 吧。

1. 了解 JWT

1.1 JWT 的介绍

JWT 简介:

JWT 全称 Json Web Token。它是 RFC 7519 中定义的,用于安全地将信息作为 Json 对象进行传输的一种形式。JWT 中存储的信息是经过“数字签名”的,因此可以被信任和理解。可以使用 HMAC 算法或使用 RSA/ECDSA 的公用/专用 密钥对 JWT 进行签名。

JWT 作用:

JWT 有以下两个作用:

  1. 认证:一旦用户登录,后面每个请求都会包含 JWT,从而允许用户访问该令牌所允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小。
  2. 信息交换:JWT 是能够安全传输信息的一种方式。通过使用公钥/私钥对 JWT 进行签名认证。此外,由于签名是使用 head 和 payload 计算的,因此你还可以验证内容是否遭到篡改。

1.2 JWT 的结构

JWT 主要由三部分组成,每个部分用 . 进行分割,各个部分分别是:

  • 标头 Header
  • 有效载荷 Payload
  • 签名 Signature

因此,JWT 通常是:xxx.yyyy.zzzzz

例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1.2.1 标头 Header

Header是 JWT 的标头,包括两部分信息:

  • 令牌的类型:JWT
  • 加密算法:HMAC SHA256 或 RSA

如 JWT 的默认标头为:

{
    "alg": "HS256",  // 算法
    "typ": "JWT"     // 类型
}

然后将 Header 进行 base64 编码,就构成了 JWT 第一部分:

eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9

Base64

在 Java 中,就可以实现 Base64 编码。如下:

这是对 JWT 的默认标头进行 Base64 编码、解码:

public class Test {
    public static void main(String[] args) throws Exception{
        Base64.Encoder encoder = Base64.getEncoder();
        Base64.Decoder decoder = Base64.getDecoder();

        String header = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}";
        byte[] headerBytes = header.getBytes();
        // 编码
        String encodeHeader = encoder.encodeToString(headerBytes);
        System.out.println(encodeHeader);
        // 解码
        byte[] decode = decoder.decode(encodeHeader);
        System.out.println(new String(decode, "UTF-8"));
    }
}
1.2.2 有效载荷 Payload

Payload 中包含一个声明。声明是有关实体(通常是用户)和其他数据的声明。同样地,它会使用 base64 编码构成了 JWT 第二部分 。共有三种类型的声明:registered、public 和 private 声明

1、registered 声明

registered 声明:包含一组建议使用的预定义声明,主要包括:

  • iss: jwt 签发者
  • sub: jwt 所面向的用户
  • aud: 接收 jwt 的一方
  • exp: jwt 的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该 jwt 都是不可用的
  • iat: jwt 的签发时间
  • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。

2、public 声明

public 声明:公共的声明,可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。

3、private 声明

private 声明:自定义声明,旨在在同意使用它们的各方之间共享信息,既不是注册声明也不是公共声明。

如:

{
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022
}
  • name:自定义字段
  • sub/iat:标准声明

然后将 Payload进行 base64编码 构成了第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
1.2.3 签名 Signature

Signature表示签证信息,它包含三个部分:

  • header (base64后的)
  • payload (base64后的)
  • secret

比如我们需要 HMAC SHA256 算法进行签名:

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

签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证 JWT 的发送者的真实身份

2. 使用 JWT

引入依赖:

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

这里是使用的 SpringBoot 的测试类。这里可以跟我不一样哈,把这两个方法换成 main 就行。

public class AppTest {

    // 密钥(复杂)
    private static final String SECRET = "1qazXSW2";

    @Test
    public void createToken() {
        Map<String, Object> map = new HashMap<>();
        Calendar expireTime = Calendar.getInstance();
        expireTime.add(Calendar.SECOND, 2000);

        String token = JWT.create()
                // Header(使用默认数据,故map没有值。也可省略此行代码)
                .withHeader(map)
                // Payload
                .withClaim("userId", 666)
                .withClaim("username", "zzc")
                // 过期时间
                .withExpiresAt(expireTime.getTime())
                // Signature
                .sign(Algorithm.HMAC256(SECRET));
        System.out.println(token);
    }

    @Test
    public void verifyToken() {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        String token = "";
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
		// 获取 token 中的信息
        System.out.println(decodedJWT.getClaim("userId").asInt());
        System.out.println(decodedJWT.getClaim("username").asString());
    }
}

说明:

  1. createToken():生成 token 的方法。由于 token 的第二部分 Payload 是可以存放信息的。所以,这里,我把 userIdusername 放入 token 中。后期,可以根据 token 进行获取相应的值。
  2. verifyToken():验证 token 的方法。DecodedJWT decodedJWT = jwtVerifier.verify(token); 如果 token 验证失败,则这行代码会抛出异常;否则,就会执行成功,继续往下执行。然后,就可以获取 token 中的信息 decodedJWT.getClaim("username").asString()

3. SpringBoot 集成 JWT

【开发环境】

  • IDEA-2020.2
  • SpringBoot-2.5.5
  • MAVEN-3.5.3
  • Mybatis
  • Mysql

【项目结构图】
在这里插入图片描述
1、引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jwt-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

2、添加 application.yml 配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/zzc?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    password: root
    username: root
    driver-class-name: com.mysql.jdbc.Driver

mybatis:
  type-aliases-package: com.zzc.entity # 实体类别名
  mapper-locations: classpath:mapper/*.xml # mapper 配置文件(必要)
  configuration:
    map-underscore-to-camel-case: true # 驼峰命名

3、添加 JWT 工具类

将生成 token 方法、验证 token 方法抽象成一个工具类:

public class JwtUtil {

    private static final String SECRET = "1qazXSW2";

    // 生成 Token
    public static String createToken(Map<String, String> paramMap) {
        Map<String, Object> headMap = new HashMap<>();
        Calendar expireTime = Calendar.getInstance();
        // 过期时间默认是 7 天
        expireTime.add(Calendar.DATE, 7);

        JWTCreator.Builder builder = JWT.create();
        // Header(使用默认数据,故map没有值,可以省略)
        builder.withHeader(headMap);
        // Payload
        paramMap.forEach((key, value) -> {
            builder.withClaim(key, value);
        });
        // 过期时间
        String token = builder.withExpiresAt(expireTime.getTime())
                // Signature
                .sign(Algorithm.HMAC256(SECRET));
        return token;
    }

    // 验证token合法性
    public static void verify(String token) {
        JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }

    // 获取token信息
    public static DecodedJWT getTokenInfo(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }

}

4、添加 Controller 类

这个 Controller 类中有两个接口:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public Map<String, Object> login(User user) {
        log.info("用户名:【{}】", user.getName());
        log.info("密码:【{}】", user.getPwd());
        Map<String, Object> map = new HashMap<>();
        try {
            User u = userService.login(user);

            // 生成 token
            Map<String, String> payload = new HashMap<>();
            payload.put("id", u.getId());
            payload.put("name", u.getName());
            String token = JwtUtil.createToken(payload);

            map.put("status", true);
            map.put("msg", "认证成功");
            map.put("token", token);
        } catch (Exception e) {
            map.put("status", false);
            map.put("msg", e.getMessage());
        }
        return map;
    }

    @PostMapping("/testToken")
    public Map<String, Object> testToken(String token) {
        log.info("当前登录token:【{}】", token);
        Map<String, Object> map = new HashMap<>();
        try {
            JwtUtil.verify(token);
            map.put("status", true);
            map.put("msg", "请求成功");
            return map;
        } catch (SignatureVerificationException e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "无效签名");
        } catch (TokenExpiredException e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "token过期");
        } catch (AlgorithmMismatchException e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "token算法不一致");
        } catch (Exception e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "token无效");
        }
        map.put("status", false);
        return map;
    }
}

说明:

  1. login():用户登录成功后,通过 JWT 生成一个 token,然后,token 中存入了用户的 idname,然后,直接将 token 返回给前端
  2. testToken():请求此接口时,需要在请求头中携带 token JwtUtil.verify(token);,否则,就会报错。

5、添加 UserServiceImpl 类

UserService 接口省略

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User login(User user) {
        User u = userMapper.login(user);
        if (null != u) {
            return u;
        }
        throw new RuntimeException("认证失败");
    }
}

6、添加 UserMapper 接口

public interface UserMapper {
    User login(User user);
}

与之对应的 UserMapper.xml 文件:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzc.mapper.UserMapper">

    <select id="login" parameterType="User" resultType="User">
        SELECT
            id,name,pwd
        FROM TAB_USER
        WHERE 1=1
        AND name = #{name}
        AND pwd = #{pwd}
    </select>

</mapper>

通过用户名和密码查询用户信息。

7、运行代码

7-1、调用登录接口

在这里插入图片描述
认证成功后,返回了一个 token。前端可以将这个 token 进行保存,然后携带在请求头中。

7-2、调用其它接口

在这里插入图片描述
请求中没有携带 token,则会请求失败 “token 无效”。

在这里插入图片描述
请求中携带了 token,则会请求成功 。

8、优化代码

通常,一个项目中是有非常多的后台接口的,难道每一个后台接口都需要写那么多冗余的代码来校验 token 吗?

显然,不是这样滴!

这个时候我们想到了拦截器 Interceptor。如下:

8-1、添加一个拦截器

@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String, Object> map = new HashMap<>();
        String token = request.getHeader("token");
        try {
            JwtUtil.verify(token);
            // 放行
            return true;
        } catch (SignatureVerificationException e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "无效签名");
        } catch (TokenExpiredException e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "token过期");
        } catch (AlgorithmMismatchException e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "token算法不一致");
        } catch (Exception e) {
            log.error("【请求失败】:{}", e.getMessage());
            map.put("msg", "token无效");
        }
        // 将错误消息返回给前台
        map.put("status", false);
        // 将Map转化为json字符串
        String errorResult = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(errorResult);
        return false;
    }

}

注意:从请求头中获取 token

若 token 校验失败,则将失败信息返回给前台。

8-2、添加一个拦截器配置类

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/user/testToken2")
                .excludePathPatterns("/user/login");
    }
}

说明:

  • 上述配置:/user/testToken2 的 url 进行拦截;/user/login 不进行拦截。

8-3-1、新增一个接口

public class UserController {

    @Autowired
    private UserService userService;
	
	// ...

    @PostMapping("/testToken2")
    public Map<String, Object> testToken2(HttpServletRequest request) {
        // 处理自己的业务逻辑
        String token = request.getHeader("token");
        DecodedJWT tokenInfo = JwtUtil.getTokenInfo(token);
        log.info("用户ID:【{}】", tokenInfo.getClaim("id").asString());
        log.info("用户名:【{}】", tokenInfo.getClaim("name").asString());
        Map<String, Object> map = new HashMap<>();
        map.put("status", true);
        map.put("msg", "请求成功");
        return map;
    }

}

8-3-2、测试这个接口
在这里插入图片描述
请求头 中不携带 token,则请求失败

在这里插入图片描述
请求头 中携带 token,则请求成功

好了,SpringBoot + JWT 就到这了。


源码地址

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值