
過去這段時間主要負責了項目中的用戶管理模塊,用戶管理模塊會涉及到加密及認證流程,加密已經在前面的文章中介紹了,可以閱讀用戶管理模塊:
https://juejin.cn/post/6916150628955717646
今天就來講講認證功能的技術選型及實現。技術上沒啥難度當然也沒啥挑戰,但是對一個原先沒寫過認證功能的菜雞甜來說也是一種鍛煉吧
技術選型要實現認證功能,很容易就會想到JWT或者session,但是兩者有啥區別?各自的優缺點?應該Pick誰?奪命三連

基於session和基於JWT的方式的主要區別就是用戶的狀態保存的位置,session是保存在服務端的,而JWT是保存在客戶端的
認證流程基於session的認證流程JWT保存在客戶端,在分布式環境下不需要做額外工作。而session因為保存在服務端,分布式環境下需要實現多機數據共享 session一般需要結合Cookie實現認證,所以需要瀏覽器支持cookie,因此移動端無法使用session認證方案
安全性JWT的payload使用的是base64編碼的,因此在JWT中不能存儲敏感數據。而session的信息是存在服務端的,相對來說更安全

如果在JWT中存儲了敏感信息,可以解碼出來非常的不安全
性能經過編碼之後JWT將非常長,cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage裡面。並且用戶在系統中的每一次http請求都會把JWT攜帶在Header裡面,HTTP請求的Header可能比Body還要大。而sessionId只是很短的一個字符串,因此使用JWT的HTTP請求比使用session的開銷大得多
一次性無狀態是JWT的特點,但也導致了這個問題,JWT是一次性的。想修改裡面的內容,就必須簽發一個新的JWT
無法廢棄一旦簽發一個JWT,在到期之前就會始終有效,無法中途廢棄。若想廢棄,一種常用的處理手段是結合redis
續簽如果使用JWT做會話管理,傳統的cookie續簽方案一般都是框架自帶的,session有效期30分鐘,30分鐘內如果有訪問,有效期被刷新至30分鐘。一樣的道理,要改變JWT的有效時間,就要簽發新的JWT。
最簡單的一種方式是每次請求刷新JWT,即每個HTTP請求都返回一個新的JWT。這個方法不僅暴力不優雅,而且每次請求都要做JWT的加密解密,會帶來性能問題。另一種方法是在redis中單獨為每個JWT設置過期時間,每次訪問時刷新JWT的過期時間
選擇JWT或session我投JWT一票,JWT有很多缺點,但是在分布式環境下不需要像session一樣額外實現多機數據共享,雖然seesion的多機數據共享可以通過粘性session、session共享、session複製、持久化session、terracoa實現seesion複製等多種成熟的方案來解決這個問題。但是JWT不需要額外的工作,使用JWT不香嗎?且JWT一次性的缺點可以結合redis進行彌補。
揚長補短,因此在實際項目中選擇的是使用JWT來進行認證
功能實現JWT所需依賴
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency>JWT工具類
publicclassJWTUtil{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(JWTUtil.class);//私鑰privatestaticfinalStringTOKEN_SECRET="123456";/***生成token,自定義過期時間毫秒**@paramuserTokenDTO*@return*/publicstaticStringgenerateToken(UserTokenDTOuserTokenDTO){try{//私鑰和加密算法Algorithmalgorithm=Algorithm.HMAC256(TOKEN_SECRET);//設置頭部信息Map<String,Object>header=newHashMap<>(2);header.put("Type","Jwt");header.put("alg","HS256");returnJWT.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(algorithm).build();DecodedJWTjwt=verifier.verify(token);StringtokenInfo=jwt.getClaim("token").asString();returnJSON.parseObject(tokenInfo,UserTokenDTO.class);}}說明:
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());Stringtoken=JWTUtil.generateToken(userTokenDTO);//3.存入token至redisredisService.set(userPO.getId(),token);returntoken;}說明:
將對應的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.生成新的tokenUserTokenDTOuserTokenDTO=UserTokenDTO.builder().id(updatePasswordUserVO.getId()).username(user.getUsername()).gmtCreate(System.currentTimeMillis()).build();Stringtoken=JWTUtil.generateToken(userTokenDTO);//3.更新tokenredisService.set(user.getId(),token);returntoken;}說明:
更新用戶密碼時需要重新生成新的token,並將新的token返回給前端,由前端更新保存在local storage中的token,同時更新存儲在redis中的token,這樣實現可以避免用戶重新登陸,用戶體驗感不至於太差
其他說明在實際項目中,用戶分為普通用戶和管理員用戶,只有管理員用戶擁有刪除用戶的權限,這一塊功能也是涉及token操作的,但是我太懶了,demo工程就不寫了
在實際項目中,密碼傳輸是加密過的
攔截器類publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{StringauthToken=request.getHeader("Authorization");Stringtoken=authToken.substring("Bearer".length()+1).trim();UserTokenDTOuserTokenDTO=JWTUtil.parseToken(token);//1.判斷請求是否有效if(redisService.get(userTokenDTO.getId())==null||!redisService.get(userTokenDTO.getId()).equals(token)){returnfalse;}//2.判斷是否需要續期if(redisService.getExpireTime(userTokenDTO.getId())<1*60*30){redisService.set(userTokenDTO.getId(),token);log.error("updatetokeninfo,idis:{},userinfois:{}",userTokenDTO.getId(),token);}returntrue;}說明:攔截器中主要做兩件事,一是對token進行校驗,二是判斷token是否需要進行續期
token校驗:
token自動續期:
為了不頻繁操作redis,只有當離過期時間只有30分鐘時才更新過期時間
攔截器配置類@ConfigurationpublicclassInterceptorConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(authenticateInterceptor()).excludePathPatterns("/logout/**").excludePathPatterns("/login/**").addPathPatterns("/**");}@BeanpublicAuthenticateInterceptorauthenticateInterceptor(){returnnewAuthenticateInterceptor();}}END
關注後端面試那些事,回復【2022面經】
獲取最新大廠Java面經

