十分钟学会JWT


一、简介

1、概述

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑和自包含的方式,并且是数据结构是 JSON 格式的,所以可以在各种语言中使用。并且 JWT 是可信任和验证的,因为它里面包含了通过加密之后的签名,可以保证在传输的过程中不会被篡改。
JWT 由 Hander、PayLoad、Siganture 三部分组成,其中Siganture 是通过加密Hander 和 PayLoad 形成的签名,签名可以验证数据的完整性,防止被篡改。但需要注意的是,签名仅能保证传输的信息不被篡改,而不能保证信息传输的安全。
官网地址:https://jwt.io/introduction

2、作用

  1. 授权:用户登录后可以根据用户的信息构建 token,从而实现访问一些服务和资源
  2. 信息传输:因为 JWT 包含签名,所以可以保证传输的信息不被篡改

3、结构

JWT 的结构是以 . 进行分割的三部分组成:

  • Hander
  • PayLoad
  • Siganture

类似于:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc4Nzc1MTQ3MX0.KcH3xxUSrbf5g36ZlJqQa2CzNRtZoNAtLLgO3LjPf5twE-K25SQCCMbeMU_4f7VMEkY-raJPcdmtdG4kztiDyA

并且我们可以通过官网进行解析:
QQ截图20231130214106.png
我们可以看到上图的 jwt 标了 3 种颜色,红色、紫色和蓝色,也就是Header,PayLoad 和Siganture。其中 Header 和 PayLoad 都是由 Base 64 加密而来的,可以进行解密,只有Siganture 需要通过密钥进行加密。

  • Header 部分主要是标明加密的方式,当然我们也可以自定义其中的内容
  • PayLoad 部分主要是存放信息,例如用户的 id 和用户名等等,需要注意的是,由于仅通过 Base 64 加密,所以是可以解密的,因此不能在这里放用户的一些例如手机号、身份证号或者密码等敏感信息
  • Siganture 部分就是签名了,上图中我们可以把密钥填进去验证数据有没有被篡改

我们在使用中一般是用于用户登录生成 token 的,我们也可以看到很多网站在请求接口时都会在请求头中看到:

Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTc4Nzc1MTQ3MX0.KcH3xxUSrbf5g36ZlJqQa2CzNRtZoNAtLLgO3LjPf5twE-K25SQCCMbeMU_4f7VMEkY-raJPcdmtdG4kztiDyA

这种类型,其格式为Authorization: Bearer ,注意 Bearer 和 token 中间有一个空格

4、为什么使用 JWT

  • 在传统的 session 中,每次用户登录后,服务端都需要将 session 保存在内存中,这样一旦用户增多之后,就会导致内存开销过大,或者服务重启后,会导致用户登录失效。
  • 用户登录之后,也只能在这台服务器上进行资源请求,因为 session 仅保存在这台服务器中。
  • 因为 session 是通过 cookie 传输的,因此如果 cookie 被截获,则很有可能会遭受 csrf。

如果使用 JWT,则不会有上面的这些问题,因为 JWT 的鉴权机制也类似于 HTTP 的无状态,不需要服务端存储认证信息,仅需验证签名即可保证信息的安全性。

5、JWT 的优点

  • JWT 的长度很短,进行传输时,数据量小传输快
  • PayLoad 中包含了用户的信息,因此也不需要频繁的去查询数据库
  • JSON 解析器在绝大部分语言上都支持,可以很容易的解析
  • 不需要在服务端存数据,特别适用于分布式微服务

二、工作流程

工作流程如图所示:未命名文件 (1).png

  1. 用户输入账号密码或者短信登录,服务器验证通过后根据用户的基本信息生成 JWT,然后将 JWT 通过响应返回给前端
  2. 前端拿到 token 后,通过 LocalStorage 等方式将 token 保存下来
  3. 当前端请求后端 API 时,在请求头中通过Authorization: Bearer 带上 token
  4. 服务端接收到请求后,拦截请求并查找 token 和验证 token,验证通过后将数据返回给前端;若不通过,则返回响应码 401,前端接收到响应码后跳转登录页面

三、使用

1、基本使用

  1. 导入 jjwt 依赖,jjwt 是 jwt 对 Java 的支持框架
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>
  1. 测试代码
public class JwtTest {
    public static void main(String[] args) {
        // Header
        Map<String, Object> header = new HashMap<>();
        header.put("alg", "HS512");

        // Payload
        Map<String, Object> payload = new HashMap<>();
        payload.put("sub", "admin");

        // 声明Token失效时间,1小时候失效
        Date expire = new Date(System.currentTimeMillis() + 3600 * 1000);

        // 生成Token
        String token = Jwts.builder()
        // 设置Header
        .setHeader(header)
        // 设置Payload
        .setClaims(payload)
        // 设置生效时间
        .setExpiration(expire)
        // 签名,这里的秘钥最好填复杂一点,千万不要泄露
        .signWith(SignatureAlgorithm.HS512, "symx.club")
        .compact();
        System.out.println(token);
    }
}
  1. 运行结果
    QQ截图20231130223431.png
    4.官网解码
    QQ截图20231130214106.png
    5.Java 代码解码
public class JwtTest {
    public static void main(String[] args) {
        // 生成的token
        String token = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcwMTM1ODQxNn0.xHCSMJDNGc8jYYBva-N-s3xTMEzkul5znK9eSMbWdM0m8AleiqWqNSHQCsdyv-l4EQtvyE7PPHC3SHfYuJZPGg";
        // 解析Header
        JwsHeader header = Jwts
                .parser()
                .setSigningKey("symx.club")
                .parseClaimsJws(token)
                .getHeader();
        System.out.println(header);

        // 解析PayLoad
        Claims claims = Jwts
                .parser()
                .setSigningKey("symx.club")
                .parseClaimsJws(token)
                .getBody();
        System.out.println(claims);

        // 解析Signature
        String signature = Jwts
                .parser()
                .setSigningKey("symx.club")
                .parseClaimsJws(token)
                .getSignature();
        System.out.println(signature);
    }
}

6.解析结果
QQ截图20231130224002.png

2、JWT 工具类

一般工作中都会将一些常用的方法整合成一个工具类方便使用
1.首先在配置文件中加入配置

jwt:
  #JWT存储的请求头
  requestHeader: Authorization
  #JWT加解密使用的密钥
  secret: symx.club
  #JWT的有效时间(60*60*24*7)
  expiration: 604800
  #JWT负载中的开头
  tokenStartWith: 'Bearer '

2.工具类

import club.gggd.demo.domain.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;

/**
 * @Description
 * @Author srx
 * @date 2023/11/30 22:42
 */
@Component
public class JwtUtil {

    /**
     * JWT存储的请求头
     */
    @Value("${jwt.requestHeader}")
    private String requestHeader;

    /**
     * 秘钥
     */
    @Value("${jwt.secret}")
    private String secret;

    /**
     * 有效时间
     */
    @Value("${jwt.expire}")
    private Integer expire;

    /**
     * jwt开头
     */
    @Value("${jwt.tokenStartWith}")
    private String tokenStartWith;

    /**
     * redis工具类
     */
    @Autowired
    private RedisUtils redisUtils;

    /**
     * 获取用户名
     * @param token
     * @return 
     */
    public String getUserNameByToken(String token) {
        Claims claims = getClaimsByToken(token);
        return claims != null ? claims.getSubject() : null;
    }

    /**
     * 获取过期时间
     * @param token
     * @return Date
     */
    public Date getExpiredByToken(String token) {
        Claims claims = getClaimsByToken(token);
        return claims != null ? claims.getExpiration() : null;
    }

    /**
     * 获取Claims
     * @param token
     * @return 
     */
    private Claims getClaimsByToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            // 签名不一致异常
            if (e instanceof SignatureException) {
                throw new BusinessException(ResultCode.TOKEN_INVALID);
            } 
            // token过期异常
            if (e instanceof ExpiredJwtException) {
                throw new BusinessException(ResultCode.TOKEN_TIMEOUT);
            }
            // 如果都不是上面的则弹出token无效异常
            throw new BusinessException(ResultCode.TOKEN_INVALID);
        }
        return claims;
    }

    /**
     * 计算过期时间
     * @return 
     */
    private Date generateExpired() {
        return new Date(System.currentTimeMillis() + expire * 1000);
    }

    /**
     * 判断 Token 是否过期
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        Date expirationDate = getExpiredByToken(token);
        return expirationDate.before(new Date());
    }

    /**
     * 生成 Token
     * @param user 用户信息
     * @return
     */
    public String generateToken(User user) {
        String token = Jwts.builder()
                .setSubject(user.getUserName())
                .setExpiration(generateExpired())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        String key = "login:" + user.getUserName() + ":" + token;
        redisUtils.set(key, token, expire / 1000);
        return token;
    }

    /**
     * 验证 Token
     * @param token
     * @return
     */
    public Boolean validateToken(String token) {
        final String username = getUserNameByToken(token);
        String key = "login:" + username+ ":" + token;
        Object data = redisUtils.get(key);
        String redisToken = data == null ? null : data.toString();
        return StringUtils.isNotEmpty(token) && !isTokenExpired(token) && token.equals(redisToken);
    }

    /**
     * 移除 Token
     * @param token
     */
    public void removeToken(String token) {
        final String username = getUserNameByToken(token);
        String key = "login:" + username+ ":" + token;
        redisUtils.del(key);
    }

}

以上是 jwt 中一些比较常用的方法,大家可以根据自己的需求进行修改和添加

3、Spring Boot 整合 JWT

1.导入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2.配置文件

jwt:
  #JWT存储的请求头
  requestHeader: Authorization
  #JWT加解密使用的密钥
  secret: symx.club
  #JWT的有效时间(60*60*24*7)
  expire: 604800
  #JWT负载中的开头
  tokenStartWith: 'Bearer '

3.编写接口

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public WebResult login(HttpServletRequest request) {
        // 这里直接模拟用户
        User user = new User();
        user.setUserName("admin");
        String token = jwtUtil.generateToken(user);
        return WebResult.ok(token);
    }

    @GetMapping("/test")
    public WebResult test() {
        return WebResult.ok("校验成功");
    }
}

4.创建拦截器

@Component
public class JwtHandlerInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Value("${jwt.requestHeader}")
    private String requestHeader;

    @Value("${jwt.tokenStartWith}")
    private String tokenStartWith;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader(requestHeader);
        // 判断是否有token
        if (StrUtil.isBlank(token)) {
            throw new BusinessException(ResultCode.TOKEN_NOT_FOUND);
        }
        // 去掉token前缀
        token = token.replace(tokenStartWith, "");
        // 验证token是否存在redis,是否过期
        Boolean valid = jwtUtil.validateToken(token);
        if (!valid) {
            throw new BusinessException(ResultCode.TOKEN_INVALID);
        }
        return true;
    }
}

5.注册拦截器

@Configuration
public class AuthWebMvcConfigurer implements WebMvcConfigurer {

    @Autowired
    private JwtHandlerInterceptor jwtHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 加入拦截器
        registry.addInterceptor(jwtHandlerInterceptor)
                // 所有请求都拦截
                .addPathPatterns("/**")
                // 除了登录接口
                .excludePathPatterns("/auth/login");
    }
}

6.验证

首先进行登录:
QQ截图20231201233530.png
可以看到,能正常拿到 token,拿到 token 之后我们对需要 token 的接口在不放 token 的情况下请求一遍:
微信截图_20231201233705.png
可以看到提示 token 为空,此时我们把 token 放进去:
QQ截图20231201233830.png
可以正常访问,接下来我们在配置文件中把过期时间改小一点,比如 30 秒,等 30 秒之后再请求:
QQ截图20231201234054.png
可以看到,提示 token 过期,最后我们自己生成一个 token 尝试呢?
QQ截图20231201234321.png
这里我自己通过密钥生成了一个 token,并拿去请求
QQ截图20231201234426.png
提示 token 无效

完结撒花 ❀❀❀

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值