作者:何甜甜呢?链接:https://juejin.cn/post/693270...以前主要负责项目中的用户管理模块。用户管理模块会涉及到加密和认证过程和加密在之前的文章中已经介绍过,可以阅读用户管理模块:如何保证用户数据安全。今天就来谈谈鉴权功能的技术选型和实现。没有技术上的难度,当然也没有什么挑战,但是对于从来没有写过认证功能的蔡吉田来说,也是一种锻炼。技术选型要实现认证功能,很容易想到JWT或者session,但是这两者有什么区别呢?各自的优缺点?应该选谁?基于会话和基于JWT的方法之间的主要区别在于保存用户状态的位置。session保存在服务器端,而JWT保存在客户端。基于会话的认证过程是用户在浏览器中的输入。用户名和密码,服务器端通过密码验证后生成session并保存到数据库中。服务器为用户生成一个sessionId,并在用户的浏览器中放置一个带有sessionId的cookie。后续的请求都会带着这个cookie信息访问服务器获取cookie,通过获取cookie中的sessionId查找数据库判断当前请求是否有效。基于JWT认证流程,用户在浏览器中输入用户名和密码,服务器端通过密码验证后生成一个token保存在数据库中。前端获取token,保存在cookie或本地存储中,后续访问服务器获取token值的请求中会包含token信息,通过查找数据库判断当前token是否有效。需要额外的工作。由于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进行会话管理,传统的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);//设置头部Part信息Mapheader=newHashMap<>(2);header.put("类型","Jwt");header.put("alg","HS256");返回JWT.create().withHeader(header).withClaim("token",JSONObject.toJSONString(userTokenDTO))//.withExpiresAt(date).sign(algorithm);}catch(Exceptione){logger.error("generatetokenoccurerror,erroris:{}",e);returnnull;}}/***检查token是否正确**@paramtoken*@return*/publicstaticUserTokenDTOparseToken(Stringtoken){Algorithmalgorithm=Algorithm.HMAC256(TOKEN_SECRET);JWTVerifierverifier=JWT.require(算法).build();DecodedJWTjwt=verifier.verify(token);StringtokenInfo=jwt.getClaim("token").asString();returnJSON.parseObject(tokenInfo,UserTokenDTO.class);}}描述:generatedtoken中没有过期时间,token的过期时间由redis管理UserTokenDTO不包含敏感信息,比如密码字段不会出现在token中Redis工具类publicfinalclassRedisServiceImplimplementsRedisService{/***过期时间*/privatefinalLongDURATION=1*24*60*60*1000L;@Resource私有RedisTemplateredisTemplate;私有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值;}@Override浦blicbooleandelete(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(userToken数据传输协议);//3。保存token到redisredisService.set(userPO.getId(),token);returntoken;}描述:判断用户名和密码是否正确,然后生成token并将生成的token保存到redislogoutfunctionpublicbooleanloginOut(Stringid){booleanresult=redisService.delete(id);如果(!redisService.delete(id)){抛出新的UserException(ErrorCodeEnum.TNP1001003);}returnresult;}会对应Delete键更新密码函数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,同时更新存储在redis中的token中的token,这个实现可以防止用户重新登录,用户体验不会太差。其他说明在实际项目中,用户分为普通用户和管理员用户。只有管??理员用户才有权删除用户。这个功能也是涉及token操作,但是我懒,就不写demo工程了。在实际项目中,密码传输是一个加密的拦截器类Stringtoken=authToken.substring("Bearer".length()+1).trim();UserTokenDTOuserTokenDTO=JWTUtil.parseToken(token);//1。判断请求是否有效if(redisService.get(userTokenDTO.getId())==null||!redisService.get(userTokenDTO.getId()).equals(token)){returnf错误的;}//2。判断是否需要续费if(redisService.getExpireTime(userTokenDTO.getId())<1*60*30){redisService.set(userTokenDTO.getId(),token);log.error("更新token信息,id为:{},用户信息为:{}",userTokenDTO.getId(),token);}returntrue;}说明:拦截器主要做两件事,一是验证token,二是判断token是否需要更新token验证:判断id对应的token是否不存在,如果存在不存在,token会过期,如果token存在,比较token是否一致,保证只有一个用户同时操作token自动续期:不要频繁操作redis,更新过期时间拦截器仅在距离过期时间只有30分钟时配置类@ConfigurationpublicclassInterceptorConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(authenticateInterceptor()).excludePathPatterns("/logout/**").excludePathPatterns("/登录/**").addPathPatterns("/**");}@BeanpublicAuthenticateInterceptorauthenticateInterceptor(){返回新的AuthenticateInterceptor();}}写在最后如有不足请指出近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.最好的!Java协程来了。.3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!