JWT 使用教程及 springboot 的使用 (认证授权, 简洁易上手)

介绍

jwt(json web token), 一般用于认证, 双方之间传递安全信息的声明规范, 是一个开放的标志, 定义了一种简洁, 自包含的方法用于通信双方之间以 json 对象的形式安全的传递信息
可以通过 url, post 参数或者 http header 发送
负载中包含了所有用户需要的信息, 避免了多次查询数据库

作用

常用于授权, 即用户登录后每个请求将包含 jwt, 从而允许用户访问该令牌允许的路由, 服务和资源, 其中单点登录就是 jwt 的一项功能; 同时也可作为信息交换, 可以对 jwt 进行签名(公钥/秘钥)

优缺点

由于使用 json, 所以可以进行跨语言
palyload 自身可以存储一些其他业务逻辑所必要的非敏感信息
构成简单, 占用小,便于传输
不需要再服务端保存会话信息, 方便扩展
对于在 jwt 中存储很多信息的人, 劫持问题不好解决
对于大多数 web 身份认证应用中, jwt 会被存储在 cookie 中, 而这样需要两倍 cpu 来验证

JWT 的使用

结构组成

由三段信息构成, 使用 . 链接一起构成 jwt 字符串, 第一段为 头部(header), 第二段为 载荷(payload), 第三段为 签证(signature)

header

头部包含两个信息:
声明类型
声明加密算法, 常用 HMAC SHA256

{
  "typ":"jwt",
  "alg":"HS256"
}

payload

载荷用于存放有效信息, 包含三个部分: 标准中注册的声明, 公共的声明, 私有的声明

iss:  jwt 签发者
sub: jwt 面向的用户
aud: jwt 受众 
exp:  jwt 的有效时间, 必须大于签发时间
iat: jwt 签发时间
nbf: 定义jwt 在什么时间之前不可用
jti: jwt 唯一身份标识
可以添加任何的信息, 一般添加用户相关信息或其他业务需要的必要信息, 不建议加敏感信息
是提供者和消费者共同的声明, 不建议放敏感信息, 因为使用 base64 加密, 属于对称加密, 属于明文信息

payload 示例

{
  "iss": "jack",
  "iat": 1708699474,
  "exp": 1708799474,
  "aud": "xxxx",
  "sub": "xxx@xx.com",
  "from_user": "shanghai",
  "target_user": "hangzhou",
  "name": "john",
  "xxx": "xxx",
}

signature

签证信息, 签证信息由三部分组成: header(base64 加密), payload(base64 加密), secret
由 base64 加密后的 header 和 payload 通过 . 链接成的字符串, 然后通过声明的加密方式进行加盐 secret 组合加密最终构成 jwt 的第三段

注: secret 保存在服务器端, jwt 的签发也是在服务器端, secret 用来进行 jwt 的签发和验证

基本应用

一般在请求头中会加入 Authorization 并加上 Bearer 标注
大概验证流程:
客户端在请求服务端资源时, 服务端会根据用户信息签发一个 token 返回给客户端, 再由客户端携带这个 token 再次请求, 服务端进行校验, 通过后响应客户端

依赖

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

令牌获取

    /**
     * 生成 token
     *
     * @return
     */
    public static String createToken() {
        Calendar instance = Calendar.getInstance();
        // 默认30 s
        instance.add(Calendar.SECOND, 30);

        // 头部map
        Map<String, Object> headerMap = new HashMap<>();
        headerMap.put("typ", "jwt");
        headerMap.put("alg", "sha256");

        return JWT.create()
                .withHeader(headerMap)      // 头部, 默认使用 sha256 算法
                .withIssuer("xiao yang")    // 签发者
                .withIssuedAt(Instant.now())  // 签发时间
                .withExpiresAt(instance.getTime())        // 有效时间
                .withSubject("daji_yang@163.com")   // 面向用户
                .withAudience("www.baidu.com","www.google.com")  // 接收方
                .withNotBefore(Instant.now())           // 定义某个时间前都不可用
                .withJWTId("dddddddd")      // 唯一身份标识
                .withClaim("userId", 11)     // palyload
                .withClaim("username", "guest") // palyload
                .sign(Algorithm.HMAC256("xiaobai"));
    }

image.png

令牌解析

    /**
     * 验证 token
     *
     * @param token
     */
    public static void verification(String token) {
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256("xiaobai")).build();
        DecodedJWT decodedJWT = verifier.verify(token);

        System.out.println(decodedJWT.getClaim("userId"));
        System.out.println(decodedJWT.getClaim("username"));
    }

image.png

常见的异常:
TokenExpiredException: 令牌过期
SignatureVerificationException: 签名不一致
AlgorithmMismatchException: 算法不匹配
IncorrectClaimException: 定义的有效期内不可用

完整代码


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.time.Instant;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author yangdaji
 * @Date 2024/2/22
 * @PackageName ${PACKAGE_NAME}
 * @ClassName ${NAME}
 */
public class JWTTest {
    public static void main(String[] args) {

        String token = createToken();
        System.out.println(token);

        verification(token);
    }

    /**
     * 生成 token
     *
     * @return
     */
    public static String createToken() {
        Calendar instance = Calendar.getInstance();
        // 默认30 s
        instance.add(Calendar.SECOND, 30);

        // 头部map
        Map<String, Object> headerMap = new HashMap<>();
        headerMap.put("typ", "jwt");
        headerMap.put("alg", "sha256");

        return JWT.create()
                .withHeader(headerMap)      // 头部, 默认使用 sha256 算法
                .withIssuer("xiao yang")    // 签发者
                .withIssuedAt(Instant.now())  // 签发时间
                .withExpiresAt(instance.getTime())        // 有效时间
                .withSubject("daji_yang@163.com")   // 面向用户
                .withAudience("www.baidu.com","www.google.com")  // 接收方
                .withNotBefore(Instant.now())           // 定义某个时间前都不可用
                .withJWTId("dddddddd")      // 唯一身份标识
                .withClaim("userId", 11)     // palyload
                .withClaim("username", "guest") // palyload
                .sign(Algorithm.HMAC256("xiaobai"));
    }

    /**
     * 验证 token
     *
     * @param token
     */
    public static void verification(String token) {
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256("xiaobai")).build();
        DecodedJWT decodedJWT = verifier.verify(token);

        System.out.println(decodedJWT.getClaim("userId"));
        System.out.println(decodedJWT.getClaim("username"));
    }
}

springboot 使用 jwt

通过 springboot 使用 jwt 并实现用户登录认证, 同时加入拦截器进行验证 demo

依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.xiaobai</groupId>
    <artifactId>spring-jwt-login-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>


    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mybatis-flex</groupId>
            <artifactId>mybatis-flex-spring-boot-starter</artifactId>
            <version>1.7.3</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.20</version>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置连接数据库

这里使用 mybatis-flex, 所以关闭 springboot 自带的, 如需可进行修改


# 数据源配置, 这里使用 mybatis的配置
#spring:
#  datasource:
#    type: com.alibaba.druid.pool.DruidDataSource
#    url: jdbc:mysql://127.0.0.1:3306/springboot_jwt?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
#    username: root
#    password: xiaobai
#    driver-class-name: com.mysql.cj.jdbc.Driver



# mybatis 配置, 这里使用的是 mybatis-flex 所以不需要怎么配置
mybatis-flex:
  datasource:
    master:
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://127.0.0.1:3306/springboot_jwt?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: xiaobai
      driver-class-name: com.mysql.cj.jdbc.Driver

jwt 工具类

为方便处理, 这里将 JWT 的创建和校验封装成工具

package cn.xiaobai.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

/**
 * JWT 工具
 *
 * @Author yangdaji
 * @Date 2024/2/24
 * @PackageName cn.xiaobai.utils
 * @ClassName JWTUtils
 */
public class JWTUtils {

    // 这里可以使用生成的秘钥
    private static final String SECRET = "XIAOYANG@email.com";


    /**
     * 生成 token
     *
     * @param map payload 数据
     * @return
     */
    public static String createToken(Map<String, String> map) {
        // 设置有效期, 24 小时
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.HOUR_OF_DAY, 24);

        // 创建 jwt 构造器
        JWTCreator.Builder builder = JWT.create();

        // 设置 payload
        map.forEach(builder::withClaim);

        // 设置过期时间, 生成 token
        return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SECRET));
    }

    /**
     * 验证并返回token信息
     *
     * @param token 待校验的token
     * @return
     */
    public static DecodedJWT verifyToken(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET))
                .build().verify(token);
    }
}

创建测试用户

建表, 初始化测试数据 sql

--  创建 user 表
CREATE TABLE IF NOT exists `user`
(
  `id`       int                                        NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_bs_0900_ai_ci NOT NULL COMMENT '用户名',
  `password` varchar(255) COLLATE utf8mb4_bs_0900_ai_ci NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bs_0900_ai_ci



-- 初始化测试数据
INSERT INTO `springboot_jwt`.`user` (`id`, `username`, `password`) VALUES (1, 'admin', '123456');

image.png

主要代码

  • user 实体
package cn.xiaobai.domain;

import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;

/**
 * @Author yangdaji
 * @Date 2024/2/23
 * @PackageName cn.xiaobai.domain
 * @ClassName User
 */
@Table("user")
@Data
public class User {

    @Id(keyType = KeyType.Auto)
    private Integer id;
    private String username;
    private String password;
}

  • mapper
    • 简单测试继承自 框架接口即可
package cn.xiaobai.mapper;

import cn.xiaobai.domain.User;
import com.mybatisflex.core.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @Author yangdaji
 * @Date 2024/2/24
 * @PackageName cn.xiaobai.mapper
 * @ClassName UserMapper
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

  • server
    • 这里还是使用框架提供的接口, 同时简单的测试也可以不需要显示的调用 mapper 来查询, Iservice 进行了封装可以直接使用, 更方便
package cn.xiaobai.service;

import cn.xiaobai.domain.User;
import cn.xiaobai.mapper.UserMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.service.IService;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author yangdaji
 * @Date 2024/2/24
 * @PackageName cn.xiaobai.service
 * @ClassName UserServer
 */
@Service
public class UserServer extends ServiceImpl<UserMapper, User> implements IService<User> {

    @Autowired
    private UserMapper userMapper;


    /**
     * 用户登录
     *
     * @param user 用户信息
     * @return
     * @throws NotFoundException 用户不存在时抛出
     */
    public User login(User user) throws NotFoundException {
        // 查询用户是否存在
        QueryWrapper queryWrapper = QueryWrapper.create()
                .select()
                .eq("username", user.getUsername())
                .eq("password", user.getPassword());
        User byQuery = userMapper.selectOneByQuery(queryWrapper);
        if (byQuery == null) {
            throw new NotFoundException("用户不存在!");
        }
        return byQuery;

    }
}

  • controller
    • 创建两个接口, 一个 login 用于用户认证生成 token, data 用于验证 token 获取数据
package cn.xiaobai.controller;

import cn.xiaobai.domain.User;
import cn.xiaobai.service.UserServer;
import cn.xiaobai.utils.JWTUtils;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.IncorrectClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author yangdaji
 * @Date 2024/2/23
 * @PackageName cn.xiaobai.controller
 * @ClassName LoginController
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    private UserServer userServer;

    /**
     * 用户登录
     *
     * @param user 用户认证信息
     * @return
     */
    @GetMapping("/login")
    public ResponseEntity<Map> login(User user) {
        // 打印用户登录信息, 实际中一般不会打印密码,并且会对密码进行加密
        log.info("用户名: {}", user.getUsername());
        log.info("密码: {}", user.getPassword());

        // 这里将返回信息使用 map 返回, 不必设置统一返回类
        Map<String, Object> response = new HashMap<>();
        try {
            Map<String, String> payload = new HashMap<>();
            // 处理登录逻辑
            User logined = userServer.login(user);

            payload.put("username", logined.getUsername());
            // 生成 token
            String token = JWTUtils.createToken(payload);

            response.put("state", true);
            response.put("msg", "登录成功!");
            response.put("token", token);
        } catch (NotFoundException e) {
            response.put("state", false);
            response.put("msg", "登录失败: " + e.getMessage());
        }
        // 获取用户信息, 设置为 map 并生成 token
        return ResponseEntity.ofNullable(response);
    }


    /**
     * 通过 token 获取资源数据
     *
     * @param token 认证token
     * @return
     */
    @PostMapping("/data")
    public ResponseEntity<Map> queryData(String token) {

        Map<String, Object> response = new HashMap<>();
        try {
            // token 验证
            JWTUtils.verifyToken(token);
            response.put("state", true);
            response.put("msg", "请求成功");
            response.put("data", "获取的数据");
            return ResponseEntity.ofNullable(response);
        } catch (SignatureVerificationException e) {
            log.error("签名无线: {}", e.getMessage());
            response.put("msg", "签名无效");
        } catch (TokenExpiredException e2) {
            log.error("token 过期: {}", e2.getMessage());
            response.put("msg", "token 过期");
            throw new RuntimeException(e2);
        } catch (AlgorithmMismatchException | IncorrectClaimException e3) {
            log.error("token 无效: {}", e3.getMessage());
            response.put("msg", "token 无效");
        } catch (Exception e4) {
            log.error("签名无线: {}", e4.getMessage());
            response.put("msg", "签名无效");
        }
        response.put("state", false);
        return ResponseEntity.ofNullable(response);
    }
}

准备完毕后启动服务

认证生成 token

通过浏览器访问 http://localhost:8080/user/login?username=admin&password=123456 (初始化用户名密码 admin/123456)
可以获取服务端返回的信息, 包含生成的token

image.png

验证token 获取数据

通过生成的token ,我们可以通过工具进行 post 请求并携带 token
请求路径: http://localhost:8080/user/data form-data 方式
携带生成 token

image.png

拦截器验证 token

通过添加 web 拦截器对指定接口进行身份验证

注册拦截器

package cn.xiaobai.interceptor;

import cn.xiaobai.utils.JWTUtils;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.IncorrectClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 *  JWT 拦截器
 * @Author yangdaji
 * @Date 2024/2/24
 * @PackageName cn.xiaobai.interceptor
 * @ClassName JWTInterceptor
 */
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 从请求头中获取携带的 token 进行验证, 这里自定义
        String token = request.getHeader("token");
        Map<String, Object> responseMap = new HashMap<>();
        try {
            // token 验证
            JWTUtils.verifyToken(token);
            responseMap.put("state", true);
            responseMap.put("msg", "请求成功");
            responseMap.put("data", "获取的数据");
            // 验证通过
            return true;
        } catch (SignatureVerificationException e) {
            log.error("签名无线: {}", e.getMessage());
            responseMap.put("msg", "签名无效");
        } catch (TokenExpiredException e2) {
            log.error("token 过期: {}", e2.getMessage());
            responseMap.put("msg", "token 过期");
            throw new RuntimeException(e2);
        } catch (AlgorithmMismatchException | IncorrectClaimException e3) {
            log.error("token 无效: {}", e3.getMessage());
            responseMap.put("msg", "token 无效");
        } catch (Exception e4) {
            log.error("签名无线: {}", e4.getMessage());
            responseMap.put("msg", "签名无效");
        }
        responseMap.put("state", false);

        // 将结果写入响应体返回
        String json = new ObjectMapper().writeValueAsString(responseMap);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);  // application/json
        PrintWriter writer = response.getWriter();
        writer.println(json);
        return false;
    }
}

注册到 webmvc 中

package cn.xiaobai.config;

import cn.xiaobai.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器配置
 *
 * @Author yangdaji
 * @Date 2024/2/24
 * @PackageName cn.xiaobai.config
 * @ClassName InterceptorConfig
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册声明的拦截器
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/user/data")      // 对数据获取接口进行拦截
                .excludePathPatterns("/user/login"); // 放行 登录接口
    }
}

修改接口代码

对于统一验证的逻辑, 提取自拦截器中, 所以获取数据接口只需要关心具体的业务逻辑

    /**
     * 获取资源数据
     * @return
     */
    @PostMapping("/data")
    public ResponseEntity<Map> queryData(HttpServletRequest request) {

        // 实际只需要关心用户请求的参数, 验证的逻辑交给拦截器处理
        Map<String, Object> response = new HashMap<>();
        String token = request.getHeader("token");
        // token 验证
        JWTUtils.verifyToken(token);
        response.put("state", true);
        response.put("msg", "请求成功");
        response.put("data", "获取的数据");
        return ResponseEntity.ofNullable(response);
    }

对于真实的业务场景, 可能会存在多个需要验证的接口, 我们还可以结合 aop + annotation 的方式来处理

请求示例

image.png

对于以上全部代码已上去自gitee: https://gitee.com/yangdaji/examples-demo.git
jwt-demo, spring-jwt-login-demo 模块

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小羊Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值