【问题标题】:AngularJS or SPA with JWT - expiry and refresh带有 JWT 的 AngularJS 或 SPA - 到期和刷新
【发布时间】:2015-05-18 04:35:28
【问题描述】:

我了解 JWT 和单页应用在登录和 JWT 发行方面的流程。但是,如果 JWT 已过期,并且服务器没有针对每个请求发布新的 JWT,那么更新的最佳方式是什么?有一个刷新令牌的概念,但是在网络浏览器中存储这样的东西听起来像是一张金票。

IE 我可以轻松进入浏览器本地存储并窃取刷新令牌。然后我可以去另一台电脑给自己发一个新的令牌。我觉得在 JWT 中引用的数据库中需要有一个服务器会话。因此,服务器可以通过刷新令牌查看会话 ID 是否仍处于活动状态或失效。

在 SPA 中实施 JWT 并在用户处于活动状态时处理新令牌发行的安全方法是什么?

【问题讨论】:

    标签: angularjs security authentication single-page-application jwt


    【解决方案1】:

    如果您的服务器中没有其他限制,您需要检查 1 小时不活动以注销用户,则每 15 分钟更新一次令牌(如果它存在 30 分钟)就可以工作。如果您只想要这个短暂的 JWT 并继续更新它,它会起作用。

    我认为使用 JWT 的一大优势是实际上不需要服务器会话,因此不使用 JTI。这样一来,您根本不需要同步,因此我建议您采用这种方法。

    如果您想在用户处于非活动状态时强制注销该用户,只需将 JWT 设置为一小时后过期即可。有一个 $interval ,它每隔约 50 分钟自动获取一个基于旧 JWT 的新 JWT,如果在过去 50 分钟内至少完成了一项操作(您可以有一个请求拦截器,它只计算请求以检查他是否处于活动状态)就是这样。

    这样您就不必将 JTI 保存在 DB 中,也不必进行服务器会话,这并不比另一种方法差多少。

    你怎么看?

    【讨论】:

    • 我认为我唯一的问题是它就像一个刷新令牌。如果我窃取了你的 JWT,我可以继续更新会话并无限期保持活动状态。
    • 如果他知道委派 API,是的,他可以。但是窃取 JWT 与窃取 cookie 一样不可能。一次在 localStorage 中,另一个在 cookie 上,但窃取意味着转到该人的浏览器并进行复制,这通常不会发生。我不太担心@patbaker82
    • 我同意。似乎有取舍。我认为有人可以获得 JWT 的唯一方法是通过 XSS 漏洞。感谢你的帮助。它非常有用且内容丰富。
    【解决方案2】:

    我认为,经过一番搜索后,我将采用的实现是......

    用例:

    • JWT 仅在 15 分钟内有效
    • 用户会话将在 1 小时不活动后超时

    流程:

    1. 用户登录并获得 JWT

      1. JWT 的有效期为 15 分钟,声明为“exp”
      2. JWT JTI 记录在 db 中,会话为 1 小时
    2. JWT 过期后(15 分钟后):

      1. 当前过期的 JWT 将被使用 @a /refresh URI 来交换一个新的。过期的 JWT 只会在刷新端点上工作。 IE API 调用将不接受过期的 JWT。刷新端点也不会接受未过期的 JWT。
      2. 将检查 JTI 是否已被撤销
      3. 将在 1 小时内检查 JTI 是否仍在
      4. JTI 会话将从 DB 中删除
      5. 将发布新的 JWT,并将新的 JTI 条目添加到数据库中
    3. 如果用户退出:

      1. JWT 已从客户端删除
      2. JTI 已从 db 中删除,因此无法刷新 JWT

    话虽如此,每 15 分钟就会有一次数据库调用来检查 JTI 是否有效。滑动会话将在跟踪 JWT 的 JTI 的数据库上扩展。如果 JTI 过期,则删除该条目,从而强制用户重新验证。

    这确实暴露了令牌活动 15 分钟的漏洞。但是,如果不跟踪每个 API 请求的状态,我不知道该怎么做。

    【讨论】:

      【解决方案3】:

      我可以提供一种不同的方法来刷新 jwt 令牌。 我在服务器端使用 Angular 和 Satellizer 和 Spring Boot。

      这是客户端的代码:

      var app = angular.module('MyApp',[....]);
      
      app.factory('jwtRefreshTokenInterceptor', ['$rootScope', '$q', '$timeout', '$injector', function($rootScope, $q, $timeout, $injector) {
          const REQUEST_BUFFER_TIME = 10 * 1000;  // 10 seconds
          const SESSION_EXPIRY_TIME = 3600 * 1000;    // 60 minutes
          const REFRESH_TOKEN_URL = '/auth/refresh/';
      
          var global_request_identifier = 0;
          var requestInterceptor = {
          request: function(config) {
              var authService = $injector.get('$auth');
              // No need to call the refresh_token api if we don't have a token.
              if(config.url.indexOf(REFRESH_TOKEN_URL) == -1 && authService.isAuthenticated()) {
                  config.global_request_identifier = $rootScope.global_request_identifier = global_request_identifier;    
                  var deferred = $q.defer();
                  if(!$rootScope.lastTokenUpdateTime) {
                      $rootScope.lastTokenUpdateTime = new Date();
                  }
                  if((new Date() - $rootScope.lastTokenUpdateTime) >= SESSION_EXPIRY_TIME - REQUEST_BUFFER_TIME) {                    
                      // We resolve immediately with 0, because the token is close to expiration.
                      // That's why we cannot afford a timer with REQUEST_BUFFER_TIME seconds delay. 
                      deferred.resolve(0);
                  } else {
                      $timeout(function() {
                          // We update the token if we get to the last buffered request.
                          if($rootScope.global_request_identifier == config.global_request_identifier) {
                              deferred.resolve(REQUEST_BUFFER_TIME);
                          } else {
                              deferred.reject('This is not the last request in the queue!');
                          }
                      }, REQUEST_BUFFER_TIME);
                  }
                  var promise = deferred.promise;
                  promise.then(function(result){
                      $rootScope.lastTokenUpdateTime = new Date();
                      // we use $injector, because the $http creates a circular dependency.
                      var httpService = $injector.get('$http');
                      httpService.get(REFRESH_TOKEN_URL + result).success(function(data, status, headers, config) {
                         authService.setToken(data.token);
                      });
                  });
              }
              return config;
          }
         };
         return requestInterceptor;
      }]);
      
      app.config(function($stateProvider, $urlRouterProvider, $httpProvider, $authProvider) {
           .............
           .............
           $httpProvider.interceptors.push('jwtRefreshTokenInterceptor');
      });
      

      让我解释一下它的作用。

      假设我们希望“会话超时”(令牌到期)为 1 小时。 服务器创建具有 1 小时到期日期的令牌。 上面的代码创建了一个 http 拦截器,它拦截每个请求并设置一个请求标识符。然后我们创建一个未来的 Promise,它将在 2 种情况下解决:

      1) 例如,如果我们创建了 3 个请求,并且在 10 秒内没有发出其他请求,那么只有最后一个请求会触发令牌刷新 GET 请求。

      2) 如果我们被请求“轰炸”,因此没有“最后一个请求”,我们检查我们是否接近 SESSION_EXPIRY_TIME,在这种情况下,我们会立即开始令牌刷新。

      最后但并非最不重要的一点是,我们使用一个参数来解决承诺。这是以秒为单位的增量,因此当我们在服务器端创建新令牌时,我们应该使用过期时间(60 分钟 - 10 秒)创建它。我们减去 10 秒,因为 $timeout 有 10 秒的延迟。

      服务器端代码如下所示:

      @RequestMapping(value = "auth/refresh/{delta}", method = RequestMethod.GET)
      @ResponseBody
      public ResponseEntity<?> refreshAuthenticationToken(HttpServletRequest request, @PathVariable("delta") Long delta, Device device) {
          String authToken = request.getHeader(tokenHeader);
          if(authToken != null && authToken.startsWith("Bearer ")) {
              authToken = authToken.substring(7);
          }
          String username = jwtTokenUtil.getUsernameFromToken(authToken);
          boolean isOk = true;
          if(username == null) {
              isOk = false;
          } else {
              final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
              isOk = jwtTokenUtil.validateToken(authToken, userDetails);
          }
          if(!isOk) {
              Map<String, String> errorMap = new HashMap<>();
              errorMap.put("message", "You are not authorized");
              return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorMap);
          }
          // renew the token
          final String token = jwtTokenUtil.generateToken(username, device, delta);
          return ResponseEntity.ok(new JwtAuthenticationResponse(token));
      }
      

      希望对某人有所帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-11-08
        • 2021-04-06
        • 2018-11-02
        • 2015-05-19
        • 2017-11-28
        • 1970-01-01
        • 2014-08-28
        相关资源
        最近更新 更多