Come posso aggiornare i token nella sicurezza di primavera

Aug 15 2020

Questa riga:

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

Genera un errore come questo quando il mio token jwt scade:

JWT è scaduto il 2020-05-13T07:50:39Z. Ora corrente: 2020-05-16T21:29:41Z.

Più specificamente, è questa funzione che genera l'eccezione "ExpiredJwtException":

Come faccio a gestire queste eccezioni? Devo rilevarli e inviare al client un messaggio di errore e costringerli a riaccedere?

Come posso implementare una funzione di token di aggiornamento? Sto usando Spring e mysql nel backend e vuejs nel front-end.

Genero il token iniziale in questo modo:

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

E poi nella classe 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();
        }

Per maggiori informazioni, ecco la mia funzione doFilterInternal che filtra ogni richiesta:

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

Risposte

3 doctore Aug 16 2020 at 17:38

Esistono 2 approcci principali per affrontare tali situazioni:


Gestisci gli accessi e aggiorna i token

In questo caso il flusso è il seguente:

  1. Accesso utente all'applicazione (inclusi usernamee password)

  2. L'applicazione back-end restituisce tutte le informazioni sulle credenziali richieste e:

    2.1 Accedere al token JWT con un tempo scaduto solitamente "basso" (15, 30 minuti, ecc.).

    2.2 Token JWT di aggiornamento con un tempo scaduto superiore a quello di accesso.

  3. D'ora in poi, la tua applicazione frontend utilizzerà access tokennell'intestazione Authorizationper ogni richiesta.

Quando il backend restituisce 401, l'applicazione frontend tenterà di utilizzare refresh token(utilizzando un endpoint specifico) per ottenere nuove credenziali, senza costringere l'utente ad accedere nuovamente.

Flusso token di aggiornamento (questo è solo un esempio, di solito viene inviato solo il token di aggiornamento)

Se non ci sono problemi, l'utente potrà continuare a utilizzare l'applicazione. Se il backend restituisce un nuovo 401=> il frontend dovrebbe reindirizzare alla pagina di accesso.


Gestisci un solo token Jwt

In questo caso il flusso è simile al precedente ed è possibile creare il proprio endpoint per far fronte a tali situazioni: /auth/token/extend(ad esempio), includendo il Jwt scaduto come parametro della richiesta.

Ora tocca a te gestire:

  • Per quanto tempo un token Jwt scaduto sarà "valido" per estenderlo?

Il nuovo endpoint avrà un comportamento simile al refresh di quello della sezione precedente, cioè restituirà un nuovo token Jwt o 401giù di lì, dal punto di vista del frontend il flusso sarà lo stesso.


Una cosa importante , indipendentemente dall'approccio che vuoi seguire, il "nuovo endpoint" dovrebbe essere escluso dagli endpoint autenticati Spring richiesti, perché gestirai la sicurezza da solo:

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

Puoi chiamare l'API per ottenere il token di aggiornamento come di seguito

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

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

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

Si prega di notare che, il

  • base64encode è il metodo per crittografare l'autorizzazione del client. Puoi usare online suhttps://www.base64encode.org/
  • refresh_token è il valore String di grant_type
  • yourRefreshToken è il token di aggiornamento ricevuto con il token di accesso JWT

Il risultato può essere visto come

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

Buona fortuna.