¿Cómo puedo actualizar tokens en Spring Security?

Aug 15 2020

Esta línea:

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

Lanza un error como este cuando caduca mi token jwt:

JWT expiró el 2020-05-13T07:50:39Z. Hora actual: 2020-05-16T21:29:41Z.

Más específicamente, es esta función la que lanza la excepción "ExpiredJwtException":

¿Cómo hago para manejar estas excepciones? ¿Debería atraparlos y enviarle al cliente un mensaje de error y obligarlos a volver a iniciar sesión?

¿Cómo puedo implementar una función de tokens de actualización? Estoy usando Spring y mysql en el backend y vuejs en el front-end.

Genero el token inicial así:

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

Y luego en la clase 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();
        }

Para obtener más información, aquí está mi función doFilterInternal que filtra cada solicitud:

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

Respuestas

3 doctore Aug 16 2020 at 17:38

Hay 2 enfoques principales para hacer frente a tales situaciones:


Administrar tokens de acceso y actualización

En este caso, el flujo es el siguiente:

  1. El usuario inicia sesión en la aplicación (incluidos usernamey password)

  2. Su aplicación back-end devuelve cualquier información de credenciales requerida y:

    2.1 Acceda al token JWT con un tiempo de caducidad generalmente "bajo" (15, 30 minutos, etc.).

    2.2 Actualice el token JWT con un tiempo de caducidad superior al de acceso.

  3. A partir de ahora, su aplicación frontend se utilizará access tokenen el Authorizationencabezado de cada solicitud.

Cuando el backend regresa 401, la aplicación frontend intentará usar refresh token(usando un punto final específico) para obtener nuevas credenciales, sin obligar al usuario a iniciar sesión nuevamente.

Flujo de token de actualización (Esto es solo un ejemplo, generalmente solo se envía el token de actualización)

Si no hay problema, entonces el usuario podrá continuar usando la aplicación. Si el backend devuelve un nuevo 401=> frontend debería redirigir a la página de inicio de sesión.


Administre solo un token Jwt

En este caso, el flujo es similar al anterior y puedes crear tu propio punto final para hacer frente a este tipo de situaciones: /auth/token/extend(por ejemplo), incluyendo el Jwt caducado como parámetro de la solicitud.

Ahora te toca a ti administrar:

  • ¿Cuánto tiempo será "válido" un token Jwt vencido para extenderlo?

El nuevo punto final tendrá un comportamiento similar a la actualización de la sección anterior, es decir, devolverá un nuevo token Jwt más o 401menos, desde el punto de vista de la interfaz, el flujo será el mismo.


Una cosa importante , independientemente del enfoque que desee seguir, el "nuevo punto final" debe excluirse de los puntos finales autenticados de Spring requeridos, porque usted mismo administrará la seguridad:

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

Puede llamar a la API para obtener el token de actualización como se muestra a continuación

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

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

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

Tenga en cuenta que, el

  • base64encode es el método para cifrar la autorización del cliente. Puede usar en línea enhttps://www.base64encode.org/
  • el refresh_token es el valor de cadena del grant_type
  • yourRefreshToken es el token de actualización recibido con el token de acceso JWT

El resultado puede verse como

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

Buena suerte.