AngularJS或带JWT的SPA - 到期和刷新

时间:2015-03-16 06:01:09

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

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

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

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

3 个答案:

答案 0 :(得分:6)

如果您的服务器中没有其他限制,您需要检查1小时不活动状态以将用户注销,则每15分钟更新一次令牌(如果它的寿命为30)。如果你只是想要这个短暂的JWT并继续更新它,那就行了。

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

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

这样你就不必在数据库中保存JTI,你不必拥有服务器会话,并且它的方法不比另一个更差。

您怎么看?

答案 1 :(得分:3)

我认为,对于我的实施,经过一些搜索,我会继续...

使用案例

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

<强>流量:

  1. 用户登录并发出JWT

    1. JWT有15分钟到期,声明&#39; exp&#39;
    2. JWT JTI在db中记录的会话为1小时
  2. JWT到期后(15分钟后):

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

    1. JWT已从客户端
    2. 中删除
    3. 从数据库中删除JTI,因此无法刷新JWT
  4. 话虽如此,每隔15分钟就会有数据库调用来检查JTI是否有效。滑动会话将在跟踪JWT的JTI的DB上进行扩展。如果JTI过期,则删除该条目,从而强制用户重新进入。

    这确实会暴露令牌在15分钟内处于活动状态的漏洞。但是,如果没有跟踪每个API请求的状态,我不确定该怎么做。

答案 2 :(得分:1)

我可以提供一种不同的方法来刷新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');
});

让我解释它的作用。

让我们说我们想要&#34;会话超时&#34; (令牌到期)为1小时。 服务器创建具有1小时到期日期的令牌。 上面的代码创建了一个http inteceptor,它拦截每个请求并设置一个请求标识符。然后我们创建一个将在2种情况下解决的未来承诺:

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

2)如果我们被轰炸&#34;如果请求没有&#34;最后一个请求&#34;,我们检查是否接近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));
}

希望能有所帮助。