202230616学习记录

1. 202230616学习记录

目标

  • 了解jwt
  • jwt的使用
  • jwt的应用

1.1. jwt

以前我们使用session解决http的无状态特性,用来作会话跟踪,单机部署的情况下能够很好解决问题;

分布式部署的情况下,服务器只保存自己的session,而请求会被分发到不同服务器上;

因此需要进行session同步,session的同步会随着session数量的增多,部署服务器的增加,同步的网络消耗和服务器的内存存储消耗都会增加;

或者session集中存储,服务器向session服务器存取数据,但是服务器需要额外的网络读写,还有session服务器出问题影响所有服务器。

1.1.1. 简介

JWT:JSON Web Token,定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。并且使用数字签名保证这些信息是可靠的。

image-20230616101014329

JWT由三部分组成:

  • 第一部分:Header(头),记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

第一部分第二部分的json串,会使用Base64进行编码,第三部分是使用指定的加密算法和指定的密钥将第一第二部分的Base64码加密得到的

数字签名的存在,让收到信息的一方能够确认传输的信息是否有效,是否被篡改

1.1.2. 使用

1.1.2.1. 引入依赖
<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
1.1.2.2. 使用提供的工具类:Jwts
  1. 生成jwt
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "Tom");

String myjwt = Jwts.builder()
    .setClaims(claims) // 添加载荷  还有claim(String var1, Object var2)的方法
    .signWith(SignatureAlgorithm.HS512, "myjwt")
    .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60))
    .compact();
  1. 解析jwt
//解析
Object o = Jwts.parser()
    .setSigningKey("myjwt")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
    .parseClaimsJws(myjwt)//解析token
    .getBody()//获取解析后的载荷部分
    .get("username");//获取键对应的值
System.out.println(o);

1.1.3. 应用

登录认证

登录成功返回jwt给客户端,下次访问从缓存中查询是否有对应token,来判断是否是已登录用户

登录验证时使用上布隆过滤器和缓存

1.1.3.1. 缓存预热

做个定时任务

将用户名和密码放入缓存(密码有所改进,数据库存储的是加密后的密码只存),采用hash数据类型。

@Slf4j
public class UserPreHotJob extends QuartzJobBean {
    @Autowired
    private IUserDao userDao;
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        log.debug("缓存预热开始");
        //查询数据库,将用户名密码全部存入redis key="user:username" 的hash类型
        List<User> allUsers = userDao.selectList(null);
        allUsers.forEach(u -> {
            //缓存存储
            redisTemplate.opsForHash().put("user:"+u.getUsername(),"user:username",u.getUsername());
            redisTemplate.opsForHash().put("user:"+u.getUsername(),"user:password",u.getPassword());
            //存入布隆过滤器组成的白名单
//            WhiteListUtil.add(u.getUsername());
        });
        log.debug("缓存预热结束");
    }
}
1.1.3.2. server层

查询缓存中是否有username对应的password(key是用username拼接的,只要看password能不能取出来,就能验证username存不存在缓存里)

@Override
public User login(String username, String password) {
    //查询缓存
    String rPassword = (String) redisTemplate.opsForHash().get("user:" + username, "user:password");
    if(StringUtils.hasLength(rPassword)&&rPassword.equals(password)){
        //查询到了密码且和输入密码相同,返回用户对象
        User user = new User();
        user.setUsername(username);
        user.setPassword(rPassword);
        return user;
    }else {
        //查询不到
        throw new BusinessException(ResultCode.USER_LOGIN_ERROR,"用户名或密码错误");
    }
}
1.1.3.3. controller层

登录成功,创建一个jwt并放在响应头信息里

@PostMapping("/login")
public RespData login(String username, String password, HttpServletResponse response){
    try {
        //没有异常则登录成功,生成jwt
        User user = userService.login(username, password);
        //载荷内容
        Map<String, Object> map = new HashMap<>();
        map.put("username",user.getUsername());
        //生成jwt
        String jwt = MyJwtUtil.buildJwt(map);
        //添加响应头
        response.addHeader("token",jwt);
        return RespData.success(ResultCode.USER_LOGIN_SUCCESS,user);
    } catch (BusinessException e) {
        //有异常,用户名密码有问题
        return RespData.fail(e.getCode(),e.getMessage(),null);
    }
}

封装的工具类

@Slf4j
public class MyJwtUtil {
    private static String signKey = "f67a4e806a5a7097"; //密钥
    private static long expira = 1000L*60; //过期时间 毫秒

    //创建jwt
    public static String buildJwt(Map<String, Object> map) {
        String jwt = Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis() + expira))
                .signWith(SignatureAlgorithm.HS512, signKey)
                .compact();
        return jwt;
    }

    //验证jwt
    public static Claims parseJwt(String jwt){
        Claims body = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return body;
    }

    //用于获取载荷的方法
    public static Object getPayload(String jwt,String name){
        return JWTUtil.parseToken(jwt).getPayload("username");
    }
}
1.1.3.4. 拦截器中验证

验证其余访问请求是否携带token,未携带token、token失效或有异常,不放行

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //获取token头信息
    String jwt = request.getHeader("token");
    if(!StringUtils.hasLength(jwt)){
        //不存在
        //创建返回对象的json串
        String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "无token", null));
        //设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
        response.setContentType("application/json;charset=utf-8");
        //响应
        response.getWriter().write(jsonStr);
        //拦截器不放行
        return false;
    }else {
        //存在
        //解析token
        try {
            Claims claims = MyJwtUtil.parseJwt(jwt);
            //未出异常,放行
            return true;
        }catch (ExpiredJwtException e){
            //token过期
            String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "token已过期", null));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(jsonStr);
            return false;
        }catch (Exception e) {
            //解析token有异常
            String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "token异常,禁止登录", null));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(jsonStr);
            return false;
        }
    }
}
1.1.3.5. token续期

某些场景,用户在页面操作时间太久,导致token已过期,此时用户在页面的某个操作向后端发起了请求,此时判断tokne过期,跳转登录页面,用户的体验不好,对于这类场景,后端要给token续期。

token一旦生成,无法修改其的失效时间,只能新产生一个token,然而也不能检验token一过期就续期,一过期就续期,还是需要一个上限来约束,假如失效好几天,还给人续期,那就不安全且失去了续期的意义,直接token不过期也就不要续期了。

上限的约束方案:使用redis,登录创建token是,往redis存一个key为token值,value也为token值的数据,并设置过期时间为token过期时间的几倍(由自己选择),在拦截器中,携带了token的情况下,在redis中查询key=token的数据,如果不存在,token以及到达过期上限或者token非法,不放行;如果存在取出value,校验value中的token是否过期,如果过期了就先新建token,覆盖value,并且该key延长过期时间再放行;没过期直接放行;

该方案,客户端持有的token可以一直不改变,每次拿取value中token判断是否过期,是否过期达上限,可以减少该token在redis中延长过期时间的次数(用新的token缓冲了一段时间),比客户端持有token一过期,就延长redis的过期时间次数少。

//获取token头信息
String jwt = request.getHeader("token");
if(!StringUtils.hasLength(jwt)){
    //不存在
    //创建返回对象的json串
    String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "无token", null));
    //设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
    response.setContentType("application/json;charset=utf-8");
    //响应
    response.getWriter().write(jsonStr);
    //拦截器不放行
    return false;
}else {
    //token头信息存在
    //从redis中读取token是否存在
    if(redisTemplate.hasKey("token:"+jwt)){
        //存在,说明redis的过期还没到
        //取出旧token验证是否过期
        String token = redisTemplate.opsForValue().get("token:" + jwt);
        try {
            MyJwtUtil.parseJwt(token);
            log.debug("token验证通过");
        } catch (ExpiredJwtException e) {
            //存储的token已过期,获取载荷信息用于新创建token
            log.debug("token续期");
            String username = (String) MyJwtUtil.getPayload(jwt, "username");
            Map<String,Object> map = new HashMap<>();
            map.put("username",username);
            String newJwt = MyJwtUtil.buildJwt(map);
            //用客户携带的token作存入redis的key,重新设置过期时间
            redisTemplate.opsForValue().set("token:" + jwt,newJwt,2, TimeUnit.MINUTES);
        }
        return true;
    }else {
        //不存在,可能token异常,可能reids中也已经过期
        log.debug("token异常");
        String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "token异常", null));
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(jsonStr);
        return false;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值