Как обновить токены в Spring Security

Aug 15 2020

Эта строка:

Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();

Выдает такую ​​ошибку, когда истекает срок действия моего токена jwt:

Срок действия JWT истек 2020-05-13T07: 50: 39Z. Текущее время: 2020-05-16T21: 29: 41Z.

В частности, именно эта функция вызывает исключение ExpiredJwtException:

Как мне обработать эти исключения? Должен ли я их поймать и отправить обратно клиенту сообщение об ошибке и заставить его повторно войти в систему?

Как я могу реализовать функцию обновления токенов? Я использую Spring и mysql в бэкэнде и vuejs во фронтэнде.

Я генерирую начальный токен следующим образом:

   @Override
        public JSONObject login(AuthenticationRequest authreq) {
            JSONObject json = new JSONObject();
    
            try {
                Authentication authentication = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(authreq.getUsername(), authreq.getPassword()));
    
                UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
                List<String> roles = userDetails.getAuthorities().stream().map(item -> item.getAuthority())
                        .collect(Collectors.toList());
    
                if (userDetails != null) {
    
                    final String jwt = jwtTokenUtil.generateToken(userDetails);
    
    
                    JwtResponse jwtres = new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(),
                            userDetails.getEmail(), roles, jwtTokenUtil.extractExpiration(jwt).toString());
    
                    return json.put("jwtresponse", jwtres);
                }
            } catch (BadCredentialsException ex) {
                json.put("status", "badcredentials");
            } catch (LockedException ex) {
                json.put("status", "LockedException");
            } catch (DisabledException ex) {
                json.put("status", "DisabledException");
            }
    
            return json;
        }

А затем в классе JwtUtil:

   public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return createToken(claims, userDetails.getUsername());
        }
    
   private String createToken(Map<String, Object> claims, String subject) {
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRESIN))
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
        }

Для получения дополнительной информации вот моя функция doFilterInternal, которая фильтрует каждый запрос:

   @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException, ExpiredJwtException, MalformedJwtException {

        try {

            final String authorizationHeader = request.getHeader("Authorization");

            String username = null;
            String jwt = null;

            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                jwt = authorizationHeader.substring(7);
                username = jwtUtil.extractUsername(jwt);
            }

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userService.loadUserByUsername(username);

                boolean correct = jwtUtil.validateToken(jwt, userDetails);

                if (correct) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());

                    usernamePasswordAuthenticationToken
                            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

                }
            }

            chain.doFilter(request, response);
        } catch (ExpiredJwtException ex) {
            resolver.resolveException(request, response, null, ex);
        }
    } 

Ответы

3 doctore Aug 16 2020 at 17:38

Есть 2 основных подхода к таким ситуациям:


Управляйте доступом и обновляйте токены

В этом случае поток следующий:

  1. Пользователь входит в приложение (в том числе usernameи password)

  2. Ваше внутреннее приложение возвращает всю необходимую информацию об учетных данных и:

    2.1 Доступ к токену JWT с истекшим временем, обычно «низким» (15, 30 минут и т. Д.).

    2.2 Обновите токен JWT с истекшим временем, превышающим время доступа.

  3. Теперь ваше веб-приложение будет использоваться access tokenв Authorizationзаголовке для каждого запроса.

Когда серверная часть возвращается 401, клиентское приложение будет пытаться использовать refresh token(используя определенную конечную точку) для получения новых учетных данных, не заставляя пользователя снова входить в систему.

Поток токенов обновления (это только пример, обычно отправляется только токен обновления)

Если проблем нет, то пользователь сможет продолжить использование приложения. Если бэкэнд возвращает новый 401=> интерфейс должен перенаправить на страницу входа.


Управляйте только одним токеном Jwt

В этом случае поток аналогичен предыдущему, и вы можете создать свою собственную конечную точку для работы в таких ситуациях: /auth/token/extend(например), включая просроченный Jwt в качестве параметра запроса.

Теперь вам решать:

  • Сколько времени токен Jwt с истекшим сроком действия будет «действителен» для его продления?

Новая конечная точка будет иметь аналогичное поведение обновления, описанное в предыдущем разделе, я имею в виду, вернет новый токен Jwt или 401около того, с точки зрения внешнего интерфейса поток будет таким же.


Одна важная вещь , независимо от подхода, которому вы хотите следовать, «новую конечную точку» следует исключить из требуемых конечных точек с аутентификацией Spring, потому что вы будете управлять безопасностью самостоятельно:

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
  ..

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.
      ..
      .authorizeRequests()
      // List of services do not require authentication
      .antMatchers(Rest Operator, "MyEndpointToRefreshOrExtendToken").permitAll()
      // Any other request must be authenticated
      .anyRequest().authenticated()
      ..
   }
}
2 QuocTruong Aug 16 2020 at 18:18

Вы можете вызвать API для получения токена обновления, как показано ниже.

POST https://yourdomain.com/oauth/token 

Header
  "Authorization": "Bearer [base64encode(clientId:clientSecret)]" 

Parameters
  "grant_type": "refresh_token"
  "refresh_token": "[yourRefreshToken]"

Обратите внимание, что

  • base64encode - это метод шифрования авторизации клиента. Вы можете использовать онлайн наhttps://www.base64encode.org/
  • refresh_token - это строковое значение grant_type
  • yourRefreshToken - это токен обновления, полученный с токеном доступа JWT

Результат можно увидеть как

{
    "token_type":"bearer",
    "access_token":"eyJ0eXAiOiJK.iLCJpYXQiO.Dww7TC9xu_2s",
    "expires_in":20,
    "refresh_token":"7fd15938c823cf58e78019bea2af142f9449696a"
}

Удачи.