当前位置: 首页 > 后端技术 > Java

JWT实现登录认证+Token自动续费方案

时间:2023-04-01 20:13:27 Java

JWT实现登录认证+Token自动续费方案,这才是正确的使用姿势!项目中基本都有用户管理模块,用户管理模块会涉及到加密和认证过程。今天就来谈谈鉴权功能的技术选型和实现。没有技术难度,当然也没有挑战,但是对于一个没有写过认证函数的人来说,也是一种锻炼。技术选择要实现认证功能,很容易想到JWT或者session,但是两者是有区别的。有什么不同?各自的优缺点?谁应该被选中?基于会话和基于JWT的方法之间的主要区别在于保存用户状态的位置。session保存在服务器端,而JWT保存在客户端。认证过程基于会话认证过程。用户在浏览器中输入用户名和密码,服务器端通过密码验证后生成一个session并保存到数据库中。服务器为用户生成一个sessionId,并在用户的浏览器中放置一个带有sessionId的cookie。后续请求会携带这个cookie信息访问服务器获取cookie,通过获取cookie中的sessionId查询数据库判断当前请求是否有效。基于JWT认证流程,用户在浏览器中输入用户名和密码,服务器通过密码验证密码。验证通过后生成token存入数据库。前端获取令牌并将其存储在cookie或本地存储中。后续请求会携带这个token信息访问服务器获取token值。检查数据库以确定当前令牌是否有效。缺点JWT存储在客户端,在分布式环境下不需要额外的工作。由于session保存在服务器端,需要在分布式环境下实现多机数据共享。session一般需要结合cookies来实现认证,所以浏览器需要支持cookies,所以移动端不能使用session认证方案。安全JWT负载使用base64编码,因此敏感数据不能存储在JWT中。session信息存在于server端,相对安全。如果敏感信息存储在JWT中,它可以被解码为非常不安全的。编码后的JWT会很长。cookie限制大小一般为4k,cookie很可能放不下,所以JWT一般放在本地存储。而用户在系统中发出的每一个http请求都会在Header中携带JWT,而HTTP请求的Header可能比Body大。sessionId只是一个很短的字符串,所以使用JWT的HTTP请求比使用session的开销要大得多。一次性无状态是JWT的特点,但也导致了这个问题。JWT是一次性的。如果要修改里面的内容,必须发出新的JWT,不能丢弃。JWT一旦发出,将一直有效,直到过期,不能中途丢弃。如果要丢弃,常见的处理方式是结合redis。续订如果使用JWT进行session管理,传统的cookie续订方案一般内置在框架中。会话有效期为30分钟。30分钟内有访问,则有效期刷新为30分钟。同理,要改变JWT的有效时间,必须发行新的JWT。最简单的方法是每次请求都刷新JWT,即每次HTTP请求都返回一个新的JWT。这种方式不仅暴力不优雅,而且每次请求都需要JWT加解密,会造成性能问题。另一种方法是在redis中为每个JWT分别设置过期时间,每次访问时刷新JWT的过期时间。选择JWT或会话。我投票给智威汤逊。JWT有很多缺点,但是在分布式环境下没有必要。Session也实现了多机数据共享,虽然seesion的多机数据共享可以通过stickysession、sessionsharing、sessionreplication、persistentsession、terracoa实现seesionreplication等各种成熟方案解决这个问题。但是JWT不需要额外的工作,用JWT不是很好吗?而JWT一次性的缺点可以结合redis来弥补。扬长避短,所以在实际项目中选择JWT做认证。JWT功能需要依赖com.auth0java-jwt3.10.3JWT工具类publicclassJWTUtil{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(JWTUtil.class);//私钥privatestaticfinalStringTOKEN_SECRET="123456";/***生成token,自定义过期时间毫秒**@paramuserTokenDTO*@return*/publicstaticStringgenerateToken(UserTokenDTOuserTokenDTO){try{//私钥和加密算法Algorithmalgorithm=Algorithm.HMAC256(TOKEN_SECRET);//设置头部信息Mapheader=newHashMap<>(2);header.put("类型","Jwt");header.put("alg","HS256");返回JWT.create().withHeader(header).withClaim("token",JSONObject.toJSONString(userTokenDTO))//.withExpiresAt(日期).sign(算法);}catch(Exceptione){logger.error("生成令牌发生错误,错误为:{}",e);返回空值;}}/***检查token是否正确**@paramtoken*@return*/publicstaticUserTokenDTOparseToken(Stringtoken){Algorithmalgorithm=Algorithm.HMAC256(TOKEN_SECRET);JWTVerifier验证器=JWT.require(algorithm).build();DecodedJWTjwt=verifier.verify(token);StringtokenInfo=jwt.getClaim("token").asString();返回JSON.parseObject(tokenInfo,UserTokenDTO.class);}}说明:生成的token不包含Expiration时间,token的过期时间由redis管理UserTokenDTO不包含敏感信息,比如密码字段不会出现在token中Redis工具类publicfinalclassRedisServiceImplimplementsRedisService{/***过期时间*/privatefinalLongDURATION=1*24*60*60*1000L;@资源优先vateRedisTemplateredisTemplate;私有ValueOperationsvalueOperations;@PostConstructpublicvoidinit(){RedisSerializerredisSerializer=newStringRedisSerializer();redisTemplate.setKeySerializer(redisSerializer);redisTemplate.setValueSerializer(redisSerializer);redisTemplate.setHashKeySerializer(redisSerializer);redisTemplate.setHashValueSerializer(redisSerializer);valueOperations=redisTemplate.opsForValue();}@Overridepublicvoidset(Stringkey,Stringvalue){valueOperations.set(key,value,DURATION,TimeUnit.MILLISECONDS);log.info("key={},valueis:{}intorediscache",key,value);}@OverridepublicStringget(Stringkey){StringredisValue=valueOperations.get(key);log.info("从redis获取,值为:{}",redisValue);返回redis值;}@Overridepublicbooleanddelete(Stringkey){布尔结果=redisTemplate.delete(key);log.info("从redis中删除,key是:{}",key);返回结果;}@OverridepublicLonggetExpireTime(Stringkey){returnvalueOperations.getOperations().getExpire(key);}}RedisTemplate简单封装了实现登录功能的业务publicStringlogin(LoginUserVOloginUserVO){//1.判断用户名和密码是否正确UserPOuserPO=userMapper.getByUsername(loginUserVO.getUsername());if(userPO==null){thrownewUserException(ErrorCodeEnum.TNP1001001);}if(!loginUserVO.getPassword().equals(userPO.getPassword())){thrownewUserException(ErrorCodeEnum.TNP1001002);}//2。用户名和密码正确生成tokenUserTokenDTOuserTokenDTO=newUserTokenDTO();PropertiesUtil.copyProperties(userTokenDTO,loginUserVO);userTokenDTO.setId(userPO.getId());userTokenDTO.setGmtCreate(System.currentTimeMillis());字符串令牌=JWTUtil.generateToken(userTokenDTO);//3。保存token到redisredisService.set(userPO.getId(),token);returntoken;}描述:判断用户名和密码是否正确。如果用户名和密码正确,将生成令牌。将生成的token保存到redislogoutfunctionpublicbooleanloginOut(Stringid){booleanresult=redisService.delete(id);如果(!redisService.delete(id)){抛出新的UserException(ErrorCodeEnum.TNP1001003);}returnresult;}删除对应的key即可更新密码函数publicStringupdatePassword(UpdatePasswordUserVOupdatePasswordUserVO){//1.修改密码UserPOuserPO=UserPO.builder().password(updatePasswordUserVO.getPassword()).id(updatePasswordUserVO.getId()).build();UserPOuser=userMapper.getById(updatePasswordUserVO.getId());if(user==null){thrownewUserException(ErrorCodeEnum.TNP1001001);}if(userMapper.updatePassword(userPO)!=1){thrownewUserException(ErrorCodeEnum.TNP1001005);}//2。生成新令牌UserTokenDTOuserTokenDTO=UserTokenDTO.builder().id(updatePasswordUserVO.getId()).username(user.getUsername()).gmtCreate(System.currentTimeMillis()).build();字符串令牌=JWTUtil.generateToken(userTokenDTO);//3。更新令牌redisService.set(user.getId(),token);returntoken;}注意:更新用户密码时,需要重新生成新的token,并将新的token返回给前端,前端更新本地存储的token,更新存储的token同时在redis中,这样用户就可以避免重新登录,用户体验还算不错其他说明在实际项目中,用户分为普通用户和管理员用户,只有管理员用户才有删除用户的权限。这个功能也涉及token操作,不过我懒得写了,demo工程就不写实践了。项目中,密码传输是一个加密的拦截器类Stringtoken=authToken.substring("Bearer".length()+1).trim();UserTokenDTOuserTokenDTO=JWTUtil.parseToken(token);//1。判断请求是否有效}//2。判断是否需要续费if(redisService.getExpireTime(userTokenDTO.getId())<1*60*30){redisService.设置(userTokenDTO.getId(),令牌);log.error("更新token信息,id为:{},用户信息为:{}",userTokenDTO.getId(),token);}returntrue;}说明:拦截器主要做两件事,一是验证token,二是判断token是否需要更新。token校验:判断对应的IDtoken是否不存在,不存在则token过期。如果token存在,比较token是否一致,保证只有一个用户同时操作token。token的自动更新:为了不频繁操作redis,只在距离过期时间只有30分钟时才更新过期时间registry.addInterceptor(authenticateInterceptor()).excludePathPatterns("/logout/**").excludePathPatterns/**("/log).addPathPatterns("/**");}@BeanpublicAuthenticateInterceptorauthenticateInterceptor(){返回新的AuthenticateInterceptor();}}源码附件已经打包上传到百度云,大家可以自行下载~链接:https://pan.baidu.com/s/14G-b...提取码:yu27百度云链接不稳定,随时可能失效,大家赶紧保存,如果百度云链接失效,请留言告诉我,我看到会及时更新~开源地址码云地址:https://gitee.com/ZhongBangKe...Github地址:https://gitee.com/ZhongBangKe...