JWT 的使用

JWT 的使用

1.什么是 JWT?

JWT (JSON Web Token) 是一种基于 Token 的认证授权机制。JWT 有很强的自解释能力,自身包含了身份验证所需要的所有信息。

JWT(JSON Web Token)由三部分组成,本质上就是一个字符串:

这三部分用 . 分割,每一个部分都是一个 JSON 对象,结构是:{}.{}.{}。JSON 对象经过编码后,表现出来的形式是:xxx.yyy.zzz。

  • Header:头部。头部由两部分组成:令牌的类型和使用的签名算法。比如:算法是"HS256",类型是"JWT"。头部的内容会通过 Base64 编码成字符串,Base64 是一种编码规则,不是加密方式,可以通过 Base64 解码得到头部原本的内容。
json 复制代码
{
	"alg": "HS256",
	"typ": "JWT"
}
  • Payload:负载。负载用来保存身份验证需要的信息,同样是通过 Base64 编码将一个 JSON 对象变成字符串。并且,因为 Base64 可以被解码,所以不建议在负载中存放隐私数据,只用来存放一般性的数据。
json 复制代码
{
    "userId": "1959649015717580800"
}
  • Signature:签名。Header 和 Payload 是通过 Base64 进行编码的,可以被解码获取里面的内容。而 Signature 是通过编码后的 Header 和 Payload 以及指定的密钥,通过 Header 中指定的签名算法进行签名得到的。\(Signature = 签名算法 ( Base64(Header), Base64(Payload), 密钥 )\)。

Signature 可以用来检查 token 是否被篡改。

已知 Signature = 签名算法 ( Base64(Header), Base64(Payload), 密钥 ),相当于 y = f(x1, x2, x3)

  • 如果前端提交过来的 Header 或者 Payload 被修改,也就是 Base64(Header) 和 Base64(Payload) 发生改变,则 y* = f(x1*, x2*, x3),后端计算得到的 Signature* != 前端提交过来的 Signature,请求不会放行。
  • 如果前端提交过来的 Signature 被修改,也就是 Signature*,则 y = f(x1, x2, x3),后端计算得到的 Signature != Signature*,请求不会放行。

2.JWT 基本使用

引入依赖:

xml 复制代码
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.12.6</version>
</dependency>
java 复制代码
    @Test
    void generateJWT() {
        // 密钥
        String key = "qwertyuiopasdfghjklzxcvbnm2345678sdfghjkxdft7823edrfvgh";

        // 负荷
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("userId", 1959649015717580800L);

        // 根据提供的字节数组长度选择适当的算法,并返回SecretKey实例
        SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
        // 两小时内有效
        Date date = new Date(System.currentTimeMillis() + 60 * 60 * 2 * 1000);

        String token = Jwts.builder()
                .claims(claims) // 在payload中存入userId
                .signWith(secretKey) // 签名加密
                .expiration(date) // 有效时间
                .compact();

        // JWT字符串作为token
        // eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOjE5NTk2NDkwMTU3MTc1ODA4MDAsImV4cCI6MTc1NjE0Nzk4Mn0.JNJVZuDGrHT99tkHBkzTS83zDORbKlWnqvq3riLK8W1PgFYZieYUnBUfZSHR0F31
        System.out.println(token);
    }
java 复制代码
    @Test
    void parseJWT() {
        String key = "qwertyuiopasdfghjklzxcvbnm2345678sdfghjkxdft7823edrfvgh";

        String token = "eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOjE5NTk2NDkwMTU3MTc1ODA4MDAsImV4cCI6MTc1NjE0Nzk4Mn0.JNJVZuDGrHT99tkHBkzTS83zDORbKlWnqvq3riLK8W1PgFYZieYUnBUfZSHR0F31";
        // 获取SecretKey实例
        SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));

        JwtParser parser = Jwts
                .parser()
                // 用SecretKey加密,同样用SecretKey解密,解密不通过则
                .verifyWith(secretKey)
                .build();

        // 解析token
        Claims payload = parser
                .parseSignedClaims(token)
                .getPayload();

        Long userId = (Long) payload.get("userId");
        if (userId == null)
            throw new RuntimeException();

        // 1959649015717580800
        System.out.println(userId);
    }

3.SpringBoot 集成 JWT 身份验证

下面是一个简单的 demo,演示如何在 SpringBoot 项目中使用 JWT 进行身份验证:

  • 当用户访问登录、注册等接口时,不需要验证用户身份。
  • 用户登录成功后,返回一个 JWT 作为 token,保存用户的 userId。
  • 用户访问其他接口时,需要携带一个自定义请求头 Authorization,Authorization 保存的值就是 token。
  • 请求到达后端会经过拦截器(或者过滤器),在拦截器中检查是否携带 token,以及 token 是否有效。如果检查不通过,则不放行该请求。

3.1准备工作

创建 JWT 工具类:

java 复制代码
public class JwtUtil {

    /**
     * 生成token
     * @param key 密钥
     * @param expired 过期时间
     * @param map payload保存的数据
     * @return
     */
    public static String generateToken(String key, long expired, Map<String, Object> map) {
        String token = Jwts.builder()
                .claims(map)
                .signWith(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)))
                .expiration(new Date(System.currentTimeMillis() + expired))
                .compact();
        return token;
    }

    /**
     * 解析token
     * @param token
     * @param key
     * @return
     */
    public static Claims parseToken(String token, String key) {
        JwtParser jwtParser = Jwts
                .parser()
                .verifyWith(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)))
                .build();
        Claims claims = jwtParser
                .parseSignedClaims(token)
                .getPayload();
        return claims;
    }
}

将 JWT 需要使用的参数放在 application.yml 中方便管理和配置,同时创建一个 properties 读取 application.yml 中的配置:

yml 复制代码
jwt:
  # 密钥
  secret-key: qwertyuiopasdfghjklzxcvbnm2345678sdfghjkxdft7823edrfvgh
  # 有效时间,毫秒
  expired: 7200000
  # 请求头名称
  authorization: Authorization
java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    private String secretKey;
    private long expired;
    private String authorization;
}

编写简易接口,测试身份验证是否实现:

java 复制代码
@RestController
@RequestMapping("/api")
@Validated
public class DemoController {

    @Autowired
    private DemoService demoService;

    /**
     * 用户登录,不需要身份验证
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/login")
    public Result<String> login(@NotBlank(message = "用户名不能为空") String username, @NotBlank(message = "密码不能为空") String password) {
        String token = demoService.login(username, password);
        return Result.ok(token);
    }

    /**
     * 用户注销账号,需要进行身份验证
     *
     * @return
     */
    @DeleteMapping("/remove")
    public Result remove() {
        // 空实现,仅用来测试身份验证是否生效
        return Result.ok(null);
    }
}
java 复制代码
@Service
public class DemoService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private JwtProperties jwtProperties;

    public String login(String username, String password) {
        // 检查用户是否存在
        User user = userMapper.getByUsername(username);
        if (user == null)
            throw new RuntimeException("用户不存在");

        // 检查密码是否正确
        String originPassword = user.getPassword(); // 原密码

        password = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8)); // md5加密

        if (!originPassword.equals(password))
            throw new RuntimeException("密码不正确");

        // 生成jwt
        HashMap<String, Object> map = new HashMap<>();
        map.put("userId", user.getUserId());

        return JwtUtil.generateToken(jwtProperties.getSecretKey(), jwtProperties.getExpired(), map);
    }
}

登录成功返回的数据如下,前端获取 token 后每次发起请求都会携带 token。

json 复制代码
{
    "code": 200,
    "msg": null,
    # eyJhbGciOiJIUzM4NCJ9: Header
    # eyJ1c2VySWQiOjE5NTk2NDksImV4cCI6MTc1NjIwNjE4Nn0: Payload
    # ckfbYSF7GMT4gYJpla-4-P8dWEgoj6z6BcGTEMO29zzdpkP6dKhtTak5WExwGWMM: Signature
    # 三个部分用 . 分隔
    "data": "eyJhbGciOiJIUzM4NCJ9.eyJ1c2VySWQiOjE5NTk2NDksImV4cCI6MTc1NjIwNjE4Nn0.ckfbYSF7GMT4gYJpla-4-P8dWEgoj6z6BcGTEMO29zzdpkP6dKhtTak5WExwGWMM"
}

3.2拦截器拦截请求

创建拦截器,在请求到达后端时,检查是否携带 token 并校验 token 是否是一个正确的 JWT 字符串。

java 复制代码
@Component
public class JwtInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        try {
            // 获取token,Authorization请求头中保存了token
            String token = request.getHeader(jwtProperties.getAuthorization());
            // 解析token,获取userId
            Claims claims = JwtUtil.parseToken(token, jwtProperties.getSecretKey());
            Integer userId = (Integer) claims.get("userId");

            // 保存userId并放行
            UserContext.setUserId(userId);
            return true;
        } catch (Exception e) {
            // 401:认证失败
            response.setStatus(401);
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除ThreadLocal中的数据
        UserContext.removeUserId();
    }
}

login 请求不会被拦截,可以顺利登录,得到 token;但是 remove 请求会被拦截,可以使用 Debug 观察。

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private JwtInterceptor jwtInterceptor;

    /**
     * 向Spring MVC框架注册自定义拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                // addPathPatterns: 定义拦截的请求路径
                .addPathPatterns("/**")
                // excludePathPatterns: 定义直接放行,不拦截的请求路径
                .excludePathPatterns("/api/login");

                // .addPathPatterns("/api/user/**"): 拦截所有以 /api/user 为前缀的请求
                // .excludePathPatterns("/api/user/logout"): 在拦截 /api/user/** 请求的情况下,如果是发向 /api/user/logout 的请求不会被拦截
    }
}

3.3过滤器拦截请求

过滤器和拦截器在这里的作用都一样,都是拦截请求。但是拦截器需要注册到 Spring MVC 框架中,过滤器只需要成为一个 Bean 即可,但是需要在过滤器中设置白名单,也就是不需要检查的路径。

java 复制代码
@Component
public class JwtFilter implements Filter {

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 登录请求直接放行
        if ("/api/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        try {
            // 获取token
            String token = request.getHeader(jwtProperties.getAuthorization());
            // 解析token
            Claims claims = JwtUtil.parseToken(token, jwtProperties.getSecretKey());
            Integer userId = (Integer) claims.get("userId");

            // 保存userId并放行
            UserContext.setUserId(userId);
            chain.doFilter(request, response);

        } catch (Exception e) {
            // 401:认证失败
            response.setStatus(401);
        }
    }
}
相关推荐
叫我阿柒啊31 分钟前
Java全栈开发面试实战:从基础到微服务架构
java·vue.js·spring boot·redis·git·full stack·interview
小凡敲代码38 分钟前
2025年金九银十Java面试场景题大全:高频考点+深度解析+实战方案
java·程序员·java面试·后端开发·求职面试·java场景题·金九银十
你的人类朋友1 小时前
【操作系统】Unix和Linux是什么关系?
后端·操作系统·unix
拉法豆粉1 小时前
在压力测试中如何确定合适的并发用户数?
java·开发语言
爱上纯净的蓝天2 小时前
迁移面试题
java·网络·c++·pdf·c#
uzong2 小时前
半小时打造七夕传统文化网站:Qoder AI编程实战记录
后端·ai编程
快乐就是哈哈哈2 小时前
从传统遍历到函数式编程:彻底掌握 Java Stream 流
后端
chenglin0162 小时前
Logstash_Input插件
java·开发语言
bemyrunningdog3 小时前
Spring文件上传核心技术解析
java
Fireworkitte3 小时前
Java 系统中实现高性能
java