AngularJS or SPA with JWT - expiry and refresh

前端 未结 3 1871
粉色の甜心
粉色の甜心 2021-02-03 10:25

I understand the flow of JWT and a single page application in terms of login and JWT issuance. However, if the JWT has a baked in expiry, AND the server isn\'t issuing a new JW

3条回答
  •  半阙折子戏
    2021-02-03 10:48

    I can offer a different approach for refreshing the jwt token. I am using Angular with Satellizer and Spring Boot for the server side.

    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. The server creates the token with 1 hour expiration date. The code above creates a http inteceptor, that intercepts each request and sets a request identifier. Then we create a future promise that will be resolved in 2 cases:

    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.

    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.

    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). We subtract 10 seconds, because of the $timeout with 10 seconds delay.

    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 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.

提交回复
热议问题