簽發的用戶認證token超時刷新策略

簽發的用戶認證token超時刷新策略

這個模塊分離至項目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請求?但對於前端這麼大量的不一樣請求,這樣是否是太麻煩了?redis

這困擾了我好久哎,直到我用到了angualr的HttpInterceptor哈哈哈哈哈哈哈哈哈哈哈哈哈哈。segmentfault

angualr的HttpInterceptor就是前端的攔截過濾器,發起請求會攔截處理,接收請求也會攔截處理。最大的好處對每次的原始request他都會完整的保存下來,咱們向後臺發生的request是他的clone。next.handle(request.clone) 
繼承HttpInterceptor的AuthInterceptor,攔截response判斷是否爲refresh token,是則攜帶新token再次發起保存的request:後端

@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時所作的:api

/* *
     * @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;
        }
    }
相關文章
相關標籤/搜索