Comment puis-je actualiser les jetons dans la sécurité Spring

Aug 15 2020

Cette ligne:

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

Génère une erreur comme celle-ci lorsque mon jeton jwt expire :

JWT a expiré le 2020-05-13T07:50:39Z. Heure actuelle : 2020-05-16T21:29:41Z.

Plus précisément, c'est cette fonction qui lève l'exception "ExpiredJwtException" :

Comment gérer ces exceptions ? Dois-je les attraper et renvoyer au client un message d'erreur et les forcer à se reconnecter ?

Comment puis-je implémenter une fonctionnalité de jetons d'actualisation ? J'utilise Spring et mysql dans le backend et vuejs dans le front-end.

Je génère le jeton initial comme ceci :

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

Et puis dans la 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();
        }

Pour plus d'informations, voici ma fonction doFilterInternal qui filtre chaque requête :

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

Réponses

3 doctore Aug 16 2020 at 17:38

Il existe 2 approches principales pour faire face à de telles situations :


Gérer l'accès et actualiser les jetons

Dans ce cas, le flux est le suivant :

  1. L'utilisateur se connecte à l'application (y compris usernameet password)

  2. Votre application backend renvoie toutes les informations d'identification requises et :

    2.1 Accédez au jeton JWT avec un délai expiré généralement "faible" (15, 30 minutes, etc.).

    2.2 Actualiser le jeton JWT avec un temps expiré supérieur à celui d'accès.

  3. À partir de maintenant, votre application frontale utilisera access tokendans l'en- Authorizationtête de chaque requête.

Lorsque le backend renvoie 401, l'application frontale essaiera d'utiliser refresh token(en utilisant un point de terminaison spécifique) pour obtenir de nouvelles informations d'identification, sans forcer l'utilisateur à se reconnecter.

Flux de jeton d'actualisation (Ce n'est qu'un exemple, généralement seul le jeton d'actualisation est envoyé)

S'il n'y a pas de problème, l'utilisateur pourra continuer à utiliser l'application. Si le backend renvoie un nouveau 401=> le frontend doit rediriger vers la page de connexion.


Gérer un seul jeton Jwt

Dans ce cas, le flux est similaire au précédent et vous pouvez créer votre propre point de terminaison pour faire face à de telles situations : /auth/token/extend(par exemple), en incluant le Jwt expiré comme paramètre de la requête.

A vous maintenant de gérer :

  • Combien de temps un jeton Jwt expiré sera-t-il "valide" pour le prolonger ?

Le nouveau point de terminaison aura un comportement similaire à celui de l'actualisation de la section précédente, je veux dire, renverra un nouveau jeton Jwt 401environ, du point de vue de l'interface, le flux sera le même.


Une chose importante , indépendamment de l'approche que vous souhaitez suivre, le "nouveau point de terminaison" doit être exclu des points de terminaison authentifiés Spring requis, car vous gérerez vous-même la sécurité :

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

Vous pouvez appeler l'API pour obtenir le jeton d'actualisation comme ci-dessous

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

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

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

Veuillez noter que le

  • base64encode est la méthode pour chiffrer l'autorisation du client. Vous pouvez utiliser en ligne surhttps://www.base64encode.org/
  • le refresh_token est la valeur de chaîne du grant_type
  • yourRefreshToken est le jeton d'actualisation reçu avec le jeton d'accès JWT

Le résultat peut être vu comme

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

Bonne chance.