Comment concevoir un bon filtre d'authentification JWT

je suis nouveau à JWT. Il n'y a pas beaucoup d'informations disponibles sur le web, depuis que je suis venu ici en dernier recours. J'ai déjà développé une application de démarrage à ressort en utilisant la sécurité à ressort en utilisant la session à ressort. Maintenant, au lieu de la session de printemps, nous allons vers JWT. J'ai trouvé peu de liens et maintenant je peux authentifier un utilisateur et générer un token. Maintenant la partie difficile est, je veux créer un filtre qui sera authentifier chaque requête au serveur,

  1. Comment le filtre valider le jeton? (Valider la signature suffit?)
  2. si quelqu'un d'autre a volé le jeton et fait l'appel de repos, comment vais-je vérifier cela.
  3. Comment puis-je contourner la demande de connexion dans le filtre? Car elle n'a pas l'autorisation d'en-tête.
23
demandé sur arunan 2017-02-01 11:28:04

3 réponses

voici un filtre qui peut faire ce dont vous avez besoin :

public class JWTFilter extends GenericFilterBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);

    private final TokenProvider tokenProvider;

    public JWTFilter(TokenProvider tokenProvider) {

        this.tokenProvider = tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
        ServletException {

        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String jwt = this.resolveToken(httpServletRequest);
            if (StringUtils.hasText(jwt)) {
                if (this.tokenProvider.validateToken(jwt)) {
                    Authentication authentication = this.tokenProvider.getAuthentication(jwt);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
            filterChain.doFilter(servletRequest, servletResponse);

            this.resetAuthenticationAfterRequest();
        } catch (ExpiredJwtException eje) {
            LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
            ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            LOGGER.debug("Exception " + eje.getMessage(), eje);
        }
    }

    private void resetAuthenticationAfterRequest() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    private String resolveToken(HttpServletRequest request) {

        String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            String jwt = bearerToken.substring(7, bearerToken.length());
            return jwt;
        }
        return null;
    }
}

Et l'inclusion du filtre dans le filtre de la chaîne :

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    public final static String AUTHORIZATION_HEADER = "Authorization";

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(this.authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        JWTFilter customFilter = new JWTFilter(this.tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);

        // @formatter:off
        http.authorizeRequests().antMatchers("/css/**").permitAll()
        .antMatchers("/images/**").permitAll()
        .antMatchers("/js/**").permitAll()
        .antMatchers("/authenticate").permitAll()
        .anyRequest().fullyAuthenticated()
        .and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
        .and().logout().permitAll();
        // @formatter:on
        http.csrf().disable();

    }
}

la classe TokenProvider:

public class TokenProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);

    private static final String AUTHORITIES_KEY = "auth";

    @Value("${spring.security.authentication.jwt.validity}")
    private long tokenValidityInMilliSeconds;

    @Value("${spring.security.authentication.jwt.secret}")
    private String secretKey;

    public String createToken(Authentication authentication) {

        String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));

        ZonedDateTime now = ZonedDateTime.now();
        ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);

        Date issueDate = Date.from(now.toInstant());
        Date expirationDate = Date.from(expirationDateTime.toInstant());

        return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
                    .signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
    }

    public Authentication getAuthentication(String token) {

        Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();

        Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
                    .map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    public boolean validateToken(String authToken) {

        try {
            Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException e) {
            LOGGER.info("Invalid JWT signature: " + e.getMessage());
            LOGGER.debug("Exception " + e.getMessage(), e);
            return false;
        }
    }
}

Maintenant, pour répondre à vos questions :

  1. Fait dans ce filtre
  2. Protégez votre requête HTTP, utilisez HTTPS
  3. il suffit de permettre tout sur le /login URI ( /authenticate dans mon code)
17
répondu Matthieu Saleta 2017-02-01 12:57:58

je vais me concentrer dans les conseils généraux sur JWT, sans qui concerne le code de implemementation (voir les autres réponses)

Comment le filtre de valider le jeton? (Valider la signature suffit?)

RFC7519 spécifie comment valider une JWT (voir 7.2. Validation D'une JWT), en gros, un validation syntaxique et vérification de la signature.

Si JWT est utilisé dans un flux d'authentification, nous pouvons regarder la validation proposée par la spécification OpenID connect3.1.3.4 ID de Jeton de Validation. Résumer:

  • iss contient l'émetteur de l'identificateur (et aud contient

  • heure actuelle entre iat et exp

  • valider la signature du jeton en utilisant la clé secrète

  • sub identifie un valide utilisateur

si quelqu'un d'autre a volé le jeton et fait l'appel de repos, comment vais-je vérifier cela.

la possession d'une JWT est la preuve d'authentification. Un attaquant qui vole un jeton peut se faire passer pour l'utilisateur. Ainsi, gardez les jetons en sécurité!--9-->

  • crypter le canal de communication en utilisant TLS

  • un stockage sécurisé vos jetons. Si vous utilisez une interface web envisager d'ajouter des mesures de sécurité supplémentaires pour protéger localStorage / cookies contre les attaques XSS ou CSRF

  • set courte durée d'expiration sur les jetons d'authentification et nécessitent des informations d'identification si le token est expiré

Comment puis-je contourner la demande de connexion dans le filtre? Car elle n'a pas l'autorisation d'en-tête.

le formulaire de connexion ne nécessite pas de token JWT car vous allez valider le justificatif d'utilisateur. Garder la forme hors de la portée du filtre. Lancez le JWT après authentification réussie et appliquez le filtre d'authentification au reste des services

puis le filtre doit intercepter toutes les requêtes sauf le formulaire de connexion, et cochez:

  1. si l'utilisateur authentifié? Si pas jeter de l' 401-Unauthorized

  2. si l'utilisateur est autorisé à la ressource demandée? Si pas jeter 403-Forbidden

  3. Accès autorisé. Mettre les données de l'utilisateur dans le contexte de la requête (par exemple en utilisant un ThreadLocal)

5
répondu pedrofb 2017-02-01 13:47:46

regardez projet, il est très bien mis en œuvre et la documentation.

1. C'est la seule chose dont vous avez besoin pour valider le token et c'est suffisant. Où token est la valeur de l' Bearer dans l'entête de la requête.

try {
    final Claims claims = Jwts.parser().setSigningKey("secretkey")
        .parseClaimsJws(token).getBody();
    request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
    throw new ServletException("Invalid token.");
}

2. Voler le jeton n'est pas si facile, mais dans mon expérience, vous pouvez vous protéger en créant une session de Printemps manuellement pour chaque succès de la connexion. De plus, la mise en correspondance de l'ID unique de la session et de la valeur au porteur(le jeton) dans un Carte (création d'un Bean par exemple avec API scope).

@Component
public class SessionMapBean {
    private Map<String, String> jwtSessionMap;
    private Map<String, Boolean> sessionsForInvalidation;
    public SessionMapBean() {
        this.jwtSessionMap = new HashMap<String, String>();
        this.sessionsForInvalidation = new HashMap<String, Boolean>();
    }
    public Map<String, String> getJwtSessionMap() {
        return jwtSessionMap;
    }
    public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
        this.jwtSessionMap = jwtSessionMap;
    }
    public Map<String, Boolean> getSessionsForInvalidation() {
        return sessionsForInvalidation;
    }
    public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
        this.sessionsForInvalidation = sessionsForInvalidation;
    }
}

SessionMapBean sera disponible pour toutes les sessions. Maintenant, sur chaque requête, vous vérifierez non seulement le token, mais vous vérifierez aussi s'il calcule la session (vérifier l'id de session de la requête correspond à celui stocké dans le SessionMapBean). Bien sûr, l'ID de session peut aussi être volé donc vous devez sécuriser communication. Les moyens les plus communs de voler L'ID de session est Session Sniffing (ou les Hommes dans le milieu) et script de site à site d'attaque. Je n'entrerai pas dans plus de détails à leur sujet, vous pouvez lire comment vous protéger contre ce genre d'attaques.

3. Vous pouvez le voir dans le projet, j'ai lié. Plus simplement le filtre validera tout /api/* et vous vous connecterez dans un /user/login par exemple.

1
répondu Lazar Lazarov 2017-02-01 09:27:03