close
關注我,回復關鍵字「spring」,
免費領取Spring學習資料。

來源:juejin.cn/post/6932702419344162823

過去這段時間主要負責了項目中的用戶管理模塊,用戶管理模塊會涉及到加密及認證流程,加密已經在前面的文章中介紹了,可以閱讀用戶管理模塊:

https://juejin.cn/post/6916150628955717646

今天就來講講認證功能的技術選型及實現。技術上沒啥難度當然也沒啥挑戰,但是對一個原先沒寫過認證功能的菜雞甜來說也是一種鍛煉吧

技術選型

要實現認證功能,很容易就會想到JWT或者session,但是兩者有啥區別?各自的優缺點?應該Pick誰?奪命三連

區別

基於session和基於JWT的方式的主要區別就是用戶的狀態保存的位置,session是保存在服務端的,而JWT是保存在客戶端的

認證流程基於session的認證流程
用戶在瀏覽器中輸入用戶名和密碼,服務器通過密碼校驗後生成一個session並保存到數據庫
服務器為用戶生成一個sessionId,並將具有sesssionId的cookie放置在用戶瀏覽器中,在後續的請求中都將帶有這個cookie信息進行訪問
服務器獲取cookie,通過獲取cookie中的sessionId查找數據庫判斷當前請求是否有效
基於JWT的認證流程
用戶在瀏覽器中輸入用戶名和密碼,服務器通過密碼校驗後生成一個token並保存到數據庫
前端獲取到token,存儲到cookie或者local storage中,在後續的請求中都將帶有這個token信息進行訪問
服務器獲取token值,通過查找數據庫判斷當前token是否有效
優缺點

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);}}

說明:

生成的token中不帶有過期時間,token的過期時間由redis進行管理
UserTokenDTO中不帶有敏感信息,如password字段不會出現在token中
Redis工具類publicfinalclassRedisServiceImplimplementsRedisService{/***過期時長*/privatefinalLongDURATION=1*24*60*60*1000L;@ResourceprivateRedisTemplateredisTemplate;privateValueOperations<String,String>valueOperations;@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("getfromredis,valueis:{}",redisValue);returnredisValue;}@Overridepublicbooleandelete(Stringkey){booleanresult=redisTemplate.delete(key);log.info("deletefromredis,keyis:{}",key);returnresult;}@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());Stringtoken=JWTUtil.generateToken(userTokenDTO);//3.存入token至redisredisService.set(userPO.getId(),token);returntoken;}

說明:

判斷用戶名密碼是否正確
用戶名密碼正確則生成token
將生成的token保存至redis
登出功能publicbooleanloginOut(Stringid){booleanresult=redisService.delete(id);if(!redisService.delete(id)){thrownewUserException(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.生成新的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校驗:

判斷id對應的token是否不存在,不存在則token過期
若token存在則比較token是否一致,保證同一時間只有一個用戶操作

token自動續期:

為了不頻繁操作redis,只有當離過期時間只有30分鐘時才更新過期時間

攔截器配置類@ConfigurationpublicclassInterceptorConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(authenticateInterceptor()).excludePathPatterns("/logout/**").excludePathPatterns("/login/**").addPathPatterns("/**");}@BeanpublicAuthenticateInterceptorauthenticateInterceptor(){returnnewAuthenticateInterceptor();}}

END



Spring Boot 實現 Office 各種格式在線預覽(詳細教程,包教包會)
CompletableFuture:讓你的代碼免受阻塞之苦
@RequestBody 接收數組、List 參數、@Deprecated 標記廢棄方法
改造BeanUtils,優雅實現List數據拷貝
6 款 Java 8 自帶工具,輕鬆分析定位 JVM 問題!

關注後端面試那些事,回復【2022面經】

獲取最新大廠Java面經


最後重要提示:高質量的技術交流群,限時免費開放,今年抱團最重要。想進群的,關注SpringForAll社區,回復關鍵詞:加群,拉你進群。




點擊「閱讀原文」領取2022大廠面經
↓↓↓
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()