這個模塊分離至項目api權限管理系統與先後端分離實踐,感受那樣太長了找不到重點,分離出來要好點。html
對於登陸的用戶簽發其對應的jwt,咱們在jwt設置他的固定有效期時間,在有效期內用戶攜帶jwt訪問沒問題,當過有效期後jwt失效,用戶須要從新登陸獲取新的jwt。這個體驗不太好,好的體驗應該是:活躍的用戶應該在無感知的狀況下在jwt失效後獲取到新的jwt,攜帶這個新的jwt進行訪問,而長時間不活躍的用戶應該在jwt失效後須要進行從新的登陸認證。 前端
這裏就涉及到了token的超時刷新問題,解決方案看圖: java
在簽發有效期爲 t 時間的jwt後,把jwt用("JWT-SESSION-"+appId,jwt)的key-value形式存儲到redis中,有效期設置爲2倍的 t 。這樣jwt在有效期事後的 t 時間段內能夠申請刷新token。
還有個問題是用戶攜帶過時的jwt對後臺請求,在可刷新時間段內返回了新的jwt,應該在用戶無感知的狀況下返回請求的內容,而不是接收一個刷新的jwt。咱們是否是能夠在每次request請求回調的時候判斷返回的是否是刷新jwt,可是判斷是以後咱們是否放棄以前的用戶請求,若是不放棄,那是否是應該在最開始的用戶request請求前先保存這個請求,在以後的回調中若是是返回刷新jwt,咱們再攜帶這個新的jwt再請求一次保存好的request請求?但對於前端這麼大量的不一樣請求,這樣是否是太麻煩了? git
這困擾了我好久哎,直到我用到了angualr的HttpInterceptor
哈哈哈哈哈哈哈哈哈哈哈哈哈哈。 github
angualr的HttpInterceptor
就是前端的攔截過濾器,發起請求會攔截處理,接收請求也會攔截處理。最大的好處對每次的原始request他都會完整的保存下來,咱們向後臺發生的request是他的clone。next.handle(request.clone)
繼承HttpInterceptor的AuthInterceptor,攔截response判斷是否爲refresh token,是則攜帶新token再次發起保存的request:redis
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService, private router: Router) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authToken = this.authService.getAuthorizationToken(); const uid = this.authService.getUid(); let authReq: any; if (authToken != null && uid != null) { authReq = req.clone({ setHeaders: { 'authorization': authToken, 'appId': uid } }); } else { authReq = req.clone(); } console.log(authReq); return next.handle(authReq).pipe( mergeMap(event => { // 返回response if (event instanceof HttpResponse) { if (event.status === 200) { // 若返回JWT過時但refresh token未過時,返回新的JWT 狀態碼爲1005 if (event.body.meta.code === 1005) { const jwt = event.body.data.jwt; // 更新AuthorizationToken this.authService.updateAuthorizationToken(jwt); // clone request 從新發起請求 // retry(1); authReq = req.clone({ setHeaders: { 'authorization': jwt, 'appId': uid } }); return next.handle(authReq); } } if (event.status === 404) { // go to 404 html this.router.navigateByUrl('/404'); } if (event.status === 500) { // go to 500 html this.router.navigateByUrl('/500'); } } console.log(event); // 返回正常狀況的可觀察對象 return of(event); }), catchError(this.handleError) ); } private handleError(error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. console.error('An error occurred:', error.error.message); } else { console.error( `Backend returned code ${error.status}, ` + `body was: ${error.error}`); } repeat(1); return new ErrorObservable('親請檢查網絡'); } }
後端簽發jwt時所作的:segmentfault
/* * * @Description 這裏已經在 passwordFilter 進行了登陸認證 * @Param [] 登陸簽發 JWT * @Return java.lang.String */ @ApiOperation(value = "用戶登陸",notes = "POST用戶登陸簽發JWT") @PostMapping("/login") public Message accountLogin(HttpServletRequest request, HttpServletResponse response) { Map<String,String> params = RequestResponseUtil.getRequestParameters(request); String appId = params.get("appId"); // 根據appId獲取其對應所擁有的角色(這裏設計爲角色對應資源,沒有權限對應資源) String roles = accountService.loadAccountRole(appId); // 時間以秒計算,token有效刷新時間是token有效過時時間的2倍 long refreshPeriodTime = 36000L; String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId, "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512); // 將簽發的JWT存儲到Redis: {JWT-SESSION-{appID} , jwt} redisTemplate.opsForValue().set("JWT-SESSION-"+appId,jwt,refreshPeriodTime, TimeUnit.SECONDS); AuthUser authUser = userService.getUserByAppId(appId); return new Message().ok(1003,"issue jwt success").addData("jwt",jwt).addData("user",authUser); }
後端refresh token時所作的:後端
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception { Subject subject = getSubject(servletRequest,servletResponse); // 判斷是否爲JWT認證請求 if ((null == subject || !subject.isAuthenticated()) && isJwtSubmission(servletRequest)) { AuthenticationToken token = createJwtToken(servletRequest); try { subject.login(token); // return this.checkRoles(subject,mappedValue) && this.checkPerms(subject,mappedValue); return this.checkRoles(subject,mappedValue); }catch (AuthenticationException e) { LOGGER.info(e.getMessage(),e); // 若是是JWT過時 if (e.getMessage().equals("expiredJwt")) { // 這裏初始方案先拋出令牌過時,以後設計爲在Redis中查詢當前appId對應令牌,其設置的過時時間是JWT的兩倍,此做爲JWT的refresh時間 // 當JWT的有效時間過時後,查詢其refresh時間,refresh時間有效即從新派發新的JWT給客戶端, // refresh也過時則告知客戶端JWT時間過時從新認證 // 當存儲在redis的JWT沒有過時,即refresh time 沒有過時 String appId = WebUtils.toHttp(servletRequest).getHeader("appId"); String jwt = WebUtils.toHttp(servletRequest).getHeader("authorization"); String refreshJwt = redisTemplate.opsForValue().get("JWT-SESSION-"+appId); if (null != refreshJwt && refreshJwt.equals(jwt)) { // 從新申請新的JWT // 根據appId獲取其對應所擁有的角色(這裏設計爲角色對應資源,沒有權限對應資源) String roles = accountService.loadAccountRole(appId); long refreshPeriodTime = 36000L; //seconds爲單位,10 hours String newJwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId, "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512); // 將簽發的JWT存儲到Redis: {JWT-SESSION-{appID} , jwt} redisTemplate.opsForValue().set("JWT-SESSION-"+appId,newJwt,refreshPeriodTime, TimeUnit.SECONDS); Message message = new Message().ok(1005,"new jwt").addData("jwt",newJwt); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; }else { // jwt時間失效過時,jwt refresh time失效 返回jwt過時客戶端從新登陸 Message message = new Message().error(1006,"expired jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } } // 其餘的判斷爲JWT錯誤無效 Message message = new Message().error(1007,"error Jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; }catch (Exception e) { // 其餘錯誤 LOGGER.warn(servletRequest.getRemoteAddr()+"JWT認證"+e.getMessage(),e); // 告知客戶端JWT錯誤1005,需從新登陸申請jwt Message message = new Message().error(1007,"error jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } }else { // 請求未攜帶jwt 判斷爲無效請求 Message message = new Message().error(1111,"error request"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } }
持續更新。。。。。。
分享一波阿里雲代金券快速上雲
轉載請註明 from tomsun28