简体   繁体   English

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

[英]AngularJS or SPA with JWT - expiry and refresh

I understand the flow of JWT and a single page application in terms of login and JWT issuance. 我了解JWT和单页应用程序在登录和JWT发布方面的流程。 However, if the JWT has a baked in expiry, AND the server isn't issuing a new JWT on each request, what is the best way for renewing? 但是,如果JWT在到期时已烘焙,并且服务器未在每个请求上发出新的JWT,那么更新的最佳方式是什么? There is a concept of refresh tokens, but storing such a thing in a web browser sounds like a golden ticket. 有一个刷新令牌的概念,但在Web浏览器中存储这样的东西听起来像一张金票。

IE I could easily go into a browsers local storage and steal a refresh token. IE我可以很容易地进入浏览器本地存储并窃取刷新令牌。 Then I could go to another computer and issue myself a new token. 然后我可以去另一台计算机并给自己发一个新令牌。 I feel like there would need to be a server session in a db that's referenced in the JWT. 我觉得在JWT中引用的数据库中需要有一个服务器会话。 Therefore the server could see if the session ID is still active or invalidated by a refresh token. 因此,服务器可以查看会话ID是否仍然处于活动状态或是否由刷新令牌无效。

What are the secure ways to implement JWT in a SPA and handling new token issuance whilst the user is active? 在用户处于活动状态时,在SPA中实施JWT以及处理新令牌发布的安全方法是什么?

Renewing the token every 15 minutes (if it lives for 30) works if you don't have another restriction in your server in which you need to check for 1 hour inactivity to log the user out. 如果您的服务器中没有其他限制,您需要检查1小时不活动状态以将用户注销,则每15分钟更新一次令牌(如果它的寿命为30)。 If you just want this short lived JWT and keep on updating it, it'd work. 如果你只是想要这个短暂的JWT并继续更新它,那就行了。

I think one of the big advantages of using JWT is to actually NOT need a server session and therefore not use the JTI. 我认为使用JWT的一大优势是实际上不需要服务器会话,因此不使用JTI。 That way, you don't need syncing at all so that'd be the approach I'd recommend you following. 这样,您根本不需要同步,因此这是我建议您遵循的方法。

If you want to forcibly logout the user if he's inactive, just set a JWT with an expiration in one hour. 如果您想要强制注销用户,如果他处于非活动状态,只需在一小时内设置一个过期的JWT。 Have a $interval which every ~50 minutes it automatically gets a new JWT based on the old one IF there was at least one operation done in the last 50 minutes (You could have a request interceptor that just counts requests to check if he's active) and that's it. 有一个$ interval,每隔约50分钟自动获得一个基于旧JWT的新JWT如果在最后50分钟内至少完成了一次操作(你可以有一个请求拦截器只计算请求以检查他是否有效)就是这样。

That way you don't have to save JTI in DB, you don't have to have a server session and it's not a much worse approach than the other one. 这样您就不必在数据库中保存JTI,您不必拥有服务器会话,并且它不是一个比另一个更差的方法。

What do you think? 你怎么看?

I think for my implementation I'm going to go with, after a bit of search, is... 我认为,对于我的实现,我会在经过一些搜索之后继续...

Use case: 使用案例:

  • JWT is only valid for 15 minutes JWT仅有效15分钟
  • User session will timeout after 1 hour of inactivity 用户会话将在1小时不活动后超时

Flow: 流:

  1. User logs in and is issued a JWT 用户登录并发布JWT

    1. JWT has a 15 minute expiration with claim 'exp' JWT有15分钟到期,索赔'exp'
    2. JWT JTI is recorded in db has a session of 1 hour JWT JTI在db中记录的会话为1小时
  2. After a JWT expires (after 15 min): JWT到期后(15分钟后):

    1. Current expired JWT will be used @ a /refresh URI to exchange for a new one. 当前过期的JWT将使用@ a / refresh URI来交换新的。 The expired JWT will only work at the refresh endpoint. 过期的JWT只能在刷新端点上运行。 IE API calls will not accept an expired JWT. IE API调用不接受过期的JWT。 Also the refresh endpoint will not accept unexpired JWT's. 刷新端点也不会接受未过期的JWT。
    2. JTI will be checked to see if its been revoked 将检查JTI以查看其是否已被撤销
    3. JTI will be checked to see if its still within 1 hour 将检查JTI是否仍在1小时内
    4. JTI session will be deleted from DB JTI会话将从DB中删除
    5. New JWT will be issued and new JTI entry will be added to the db 将发布新的JWT,并将新的JTI条目添加到数据库
  3. If a user logs out: 如果用户注销:

    1. JWT is deleted from client JWT已从客户端删除
    2. JTI is deleted from db so JWT cannot be refreshed JTI从db中删除,因此JWT无法刷新

With that said, there will be database calls every 15 minutes to check a JTI is valid. 话虽如此,每隔15分钟就会有数据库调用来检查JTI是否有效。 The sliding session will be extended on the DB that tracks the JWT's JTI. 滑动会话将在跟踪JWT的JTI的DB上进行扩展。 If the JTI is expired then the entry is removed thus forcing the user to reauth. 如果JTI已过期,则删除该条目,从而强制用户重新执行。

This does expose a vulnerability that a token is active for 15 minutes. 这确实暴露了令牌处于活动状态15分钟的漏洞。 However, without tracking state every API request I'm not sure how else to do it. 但是,如果没有跟踪每个API请求的状态,我不确定如何做到这一点。

I can offer a different approach for refreshing the jwt token. 我可以提供一种不同的方法来刷新jwt令牌。 I am using Angular with Satellizer and Spring Boot for the server side. 我在服务器端使用Angular和Satellizer以及Spring Boot。

This is the code for the client side: 这是客户端的代码:

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

Let me explain what it does. 让我解释它的作用。

Let's say we want the "session timeout" (token expiry) to be 1 hour. 假设我们希望“会话超时”(令牌到期)为1小时。 The server creates the token with 1 hour expiration date. 服务器创建具有1小时到期日期的令牌。 The code above creates a http inteceptor, that intercepts each request and sets a request identifier. 上面的代码创建了一个http inteceptor,它拦截每个请求并设置一个请求标识符。 Then we create a future promise that will be resolved in 2 cases: 然后我们创建一个将在2种情况下解决的未来承诺:

1) If we create for example a 3 requests and in 10 seconds no other request are made, only the last request will trigger an token refresh GET request. 1)如果我们创建例如3个请求并且在10秒内没有做出其他请求,则只有最后一个请求将触发令牌刷新GET请求。

2) If we are "bombarded" with request so that there is no "last request", we check if we are close to the SESSION_EXPIRY_TIME in which case we start an immediate token refresh. 2)如果我们被请求“轰炸”以便没有“最后请求”,我们检查是否接近SESSION_EXPIRY_TIME,在这种情况下我们立即开始令牌刷新。

Last but not least, we resolve the promise with a parameter. 最后但同样重要的是,我们使用参数解决了这个问题。 This is the delta in seconds, so that when we create a new token in the server side, we should create it with the expiration time (60 minutes - 10 seconds). 这是以秒为单位的增量,因此当我们在服务器端创建新令牌时,我们应该使用到期时间(60分钟 - 10秒)创建它。 We subtract 10 seconds, because of the $timeout with 10 seconds delay. 我们减去10秒,因为$ timeout超时延迟10秒。

The server side code looks something like this: 服务器端代码看起来像这样:

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

Hope that helps someone. 希望能帮助别人。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM