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,
- Comment le filtre valider le jeton? (Valider la signature suffit?)
- si quelqu'un d'autre a volé le jeton et fait l'appel de repos, comment vais-je vérifier cela.
- Comment puis-je contourner la demande de connexion dans le filtre? Car elle n'a pas l'autorisation d'en-tête.
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 :
- Fait dans ce filtre
- Protégez votre requête HTTP, utilisez HTTPS
- il suffit de permettre tout sur le
/login
URI (/authenticate
dans mon code)
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 (etaud
contientheure actuelle entre
iat
etexp
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:
si l'utilisateur authentifié? Si pas jeter de l'
401-Unauthorized
si l'utilisateur est autorisé à la ressource demandée? Si pas jeter
403-Forbidden
Accès autorisé. Mettre les données de l'utilisateur dans le contexte de la requête (par exemple en utilisant un ThreadLocal)
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.