JWT令牌介绍

为什么要用token?

  • token完全由应用管理,所以它可以避开同源策略
  • token可以避免CSRF攻击
  • Token存储在客户端,是无状态的,可以在多个服务间共享

JWT介绍

Json Web Token:定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
官网:https://jwt.io/
组成:
在这里插入图片描述

  • 第一部分:Header(头) ,记录令牌类型、 签名算法等。例如: {“lg”.“HS256” “type” :“JWT”}
  • 第二部分:Payload(有效载荷) ,携带一些自定义信息、 默认信息等。例如: {“id”:“1”,“username”:“Tom”}
  • 第三部分 Signature(签名),防止Token被篡改、确保安全性。将header.payload,并加入指定秘钥,通过指定签名算法计算而来。
    注意:JWT的Payload使用的是Base64编码,因此在JWT中不能存储敏感数据。

场景:登录认证。
①登录成功后, 生成令牌
②后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
在这里插入图片描述

登录后下发令牌
思路

  • 令牌生成: 登录成功后,生成JWT令牌,并返回给前端。
  • 令牌校验:在请求到达服务端后,对令牌进行统一拦截、校验。
    注意(把生成的jwt令牌封装到json里,前端会解析)
    用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为token,值为登录时下发的JWT令牌。

如何使用JWT?

引入坐标

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

生成token和解析token示例

@Test
public void testGen(){
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 1);
    claims.put("username", "张三");
    String token = JWT.create()
            .withClaim("user", claims.toString()) //添加载荷
            .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) //指定过期时间,12h后过期
            .sign(Algorithm.HMAC256("itheima"));//指定算法,配置密钥
    System.out.println(token);
}

@Test
public void testParse(){
    //定义字符串,模拟用户传递过来的token
    String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
            +".eyJleHAiOjE3MTI2MDQxMDYsInVzZXIiOiJ7aWQ9MSwgdXNlcm5hbWU95byg5LiJfSJ9"
             +".7jp3Sw7dX7x6ezQPUxb3oSHaknGXYzpp6C3UJNu6luk";
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();

    DecodedJWT decodeJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
    Map<String, Claim> claims = decodeJWT.getClaims();
    //如果篡改头部载荷 或者 密钥 或者token过期 都会验证失败
    System.out.println(claims.get("user"));
}

注意:

  • JWT校验时生成的签名密钥,必须和生成JWT令牌时使用的密钥是配套的,此处案例使用的是"itheima"
  • 如果JWT令牌解析校验时出错,说明JWT令牌被篡改或失效了

@JsonIgnore注解:让springmvc把当前对象转为json字符串的时候,忽略掉password,最终的json字符串中就没有password这个属性了

@JsonIgnore
private String password;//密码
```bash

令牌主动失效机制

问题:用户修改密码后,令牌应当失效

  • 登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中
  • LoginInterceptor截器中,需要验证浏览器携带的令牌, 并同时需要获取到redis中存储的与之相同的令牌
  • 当用户修改密码成功后,删除redis中存储的旧令牌
    问题:token刷新机制
    用户长时间没有操作会使Token过期,每次用户点击可以刷新Token过期时间

封装成通用的方法

JWT工具方法

@Component
public class JwtTool {
    private final JWTSigner jwtSigner;

    public JwtTool(KeyPair keyPair) {
        this.jwtSigner = JWTSignerUtil.createSigner("rs256", keyPair);
    }

    /**
     * 创建 access-token
     *
     * @param userDTO 用户信息
     * @return access-token
     */
    public String createToken(Long userId, Duration ttl) {
        // 1.生成jws
        return JWT.create()
                .setPayload("user", userId)
                .setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))
                .setSigner(jwtSigner)
                .sign();
    }

    /**
     * 解析token
     *
     * @param token token
     * @return 解析刷新token得到的用户信息
     */
    public Long parseToken(String token) {
        // 1.校验token是否为空
        if (token == null) {
            throw new UnauthorizedException("未登录");
        }
        // 2.校验并解析jwt
        JWT jwt;
        try {
            jwt = JWT.of(token).setSigner(jwtSigner);
        } catch (Exception e) {
            throw new UnauthorizedException("无效的token", e);
        }
        // 2.校验jwt是否有效
        if (!jwt.verify()) {
            // 验证失败
            throw new UnauthorizedException("无效的token");
        }
        // 3.校验是否过期
        try {
            JWTValidator.of(jwt).validateDate();
        } catch (ValidateException e) {
            throw new UnauthorizedException("token已经过期");
        }
        // 4.数据格式校验
        Object userPayload = jwt.getPayload("user");
        if (userPayload == null) {
            // 数据为空
            throw new UnauthorizedException("无效的token");
        }

        // 5.数据解析
        try {
           return Long.valueOf(userPayload.toString());
        } catch (RuntimeException e) {
            // 数据格式有误
            throw new UnauthorizedException("无效的token");
        }
    }
}

生成token返回给前端

@Override
    public UserLoginVO login(LoginFormDTO loginDTO) {
        // 1.数据校验
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        // 2.根据用户名或手机号查询
        User user = lambdaQuery().eq(User::getUsername, username).one();
        Assert.notNull(user, "用户名错误");
        // 3.校验是否禁用
        if (user.getStatus() == UserStatus.FROZEN) {
            throw new ForbiddenException("用户被冻结");
        }
        // 4.校验密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadRequestException("用户名或密码错误");
        }
        // 5.生成TOKEN
        String token = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());
        // 6.封装VO返回
        UserLoginVO vo = new UserLoginVO();
        vo.setUserId(user.getId());
        vo.setUsername(user.getUsername());
        vo.setBalance(user.getBalance());
        vo.setToken(token);
        return vo;
    }

定义登陆拦截器

  • preHandle:进入controller方法之前进去
  • afterCompletion:执行完controller方法之后执行
  • ThreadLocal对应的是一个线程的数据,每次http请求,tomcat都会创建一个新的线程,也就是说,当前的ThreadLocal只在当前的线程中
@RequiredArgsConstructor
public class LoginInterceptor implements HandlerInterceptor {

    private final JwtTool jwtTool;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的 token
        String token = request.getHeader("authorization");
        // 2.校验token
        Long userId = jwtTool.parseToken(token);
        // 3.存入上下文
        UserContext.setUser(userId);
        // 4.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理用户
        UserContext.removeUser();
    }
}

关于每次请求把token存起来的原因:

  • 提高性能,减少重复验证。尤其是在同一请求的多个组件中,而且存储在内存中比在外部存储(如数据库)中检索数据要快得多。
  • 上下文共享:可以在整个请求中(服务层、控制层)轻松访问token,而不需要在每个方法中进行传递。
  • 存储用户信息:除了token,可能还需要存储用户其他信息(如用户ID、权限等),可以在同一上下文中进行管理

刷新token拦截器

public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取请求头中的token
        String token = request.getHeader("authorization");
        System.out.println("token : " + token);
        if (StrUtil.isBlank(token)){
            return true;
        }
        String key = RedisConstants.LOGIN_USER_KEY + token;
        //2、基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3、判断用户是否存在
        if (userMap.isEmpty()){
            return true;
        }
        //4、将查询到的hash对象转化为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5、存在,保存用户信息到ThreadLocal中
        UserHolder.saveUser(userDTO);
        //6、刷新token有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //7、放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }

}

使用token的优势

  • 避免同源策略
  • 可以用来避免CSRF攻击
  • 无状态可共享
  • 有效期设置及对应有过期策略
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值