Je suis nouveau à JWT. Il n’ya pas beaucoup d’informations disponibles sur le Web, puisque j’y suis arrivé en dernier recours. J'ai déjà développé une application de démarrage à ressort utilisant la sécurité de printemps utilisant la session de printemps. Maintenant, au lieu de la session de printemps, nous passons à JWT. J'ai trouvé peu de liens et je peux maintenant authentifier un utilisateur et générer un jeton. Maintenant la partie difficile est, je veux créer un filtre qui authentifiera chaque demande au serveur,
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 la chaîne de filtres:
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:
/login
URI (/authenticate
dans mon code)Je vais me concentrer sur les astuces générales sur JWT, sans tenir compte de l’implémentation de code (voir les autres réponses)
Comment le filtre va-t-il valider le jeton? (Il suffit de valider la signature?)
La RFC7519 spécifie comment valider un JWT (voir 7.2. Valider un JWT ), essentiellement une validation syntaxique et une vérification de la signature .
Si JWT est utilisé dans un flux d'authentification, nous pouvons examiner la validation proposée par la spécification de connexion OpenID .1.3.4 ID Token Validation . Résumant:
iss
contient l'identifiant de l'émetteur (et aud
contient client_id
_ si vous utilisez oauth)
heure actuelle entre iat
et exp
Valider la signature du jeton à l'aide de la clé secrète
sub
identifie un utilisateur valide
Si quelqu'un d'autre a volé le jeton et a fait une pause, comment vais-je le vérifier?.
La possession d'un JWT est la preuve de l'authentification. Un attaquant qui vole un jeton peut emprunter l'identité de l'utilisateur. Alors gardez les jetons en sécurité
Chiffrer le canal de communication à l'aide de TLS
Utilisez un stockage sécurisé pour vos jetons. Si vous utilisez une interface Web, envisagez d'ajouter des mesures de sécurité supplémentaires pour protéger les stockages/cookies locaux contre les attaques XSS ou CSRF.
définir un délai d'expiration court sur les jetons d'authentification et exiger des informations d'identification si le jeton a expiré
Comment vais-je contourner la demande de connexion dans le filtre? Comme il n'a pas d'en-tête d'autorisation.
Le formulaire de connexion ne nécessite pas de jeton JWT car vous allez valider les informations d'identification de l'utilisateur. Gardez la forme hors de la portée du filtre. Émettez le JWT après une authentification réussie et appliquez le filtre d'authentification au reste des services.
Ensuite, le filtre devrait intercepter toutes les demandes sauf le formulaire de connexion et vérifier:
si utilisateur authentifié? Si non jeter 401-Unauthorized
si utilisateur autorisé à demander la ressource? Si non jeter 403-Forbidden
Accès autorisé. Placez les données utilisateur dans le contexte de la demande (par exemple, en utilisant un ThreadLocal)
Jetez un oeil à this le projet est très bien implémenté et dispose de la documentation nécessaire.
1 . Si le projet ci-dessus est la seule chose dont vous avez besoin pour valider le jeton, cela suffit. Où token
est la valeur de Bearer
dans l'en-tê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 d'après mon expérience, vous pouvez vous protéger en créant une session Spring manuellement pour chaque connexion réussie. Mappez également l'ID unique de la session et la valeur du porteur (le jeton) dans un Carte (création d’un bean par exemple avec une portée d’API).
@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;
}
}
Ce SessionMapBean
sera disponible pour toutes les sessions. Désormais, à chaque demande, vous vérifierez non seulement le jeton, mais également s'il vérifiera si la session est en cours (vérifier que l'identifiant de session de la demande correspond à celui stocké dans le SessionMapBean
). Bien sûr, l'ID de session peut également être volé, vous devez donc sécuriser la communication. Les moyens les plus courants de voler l'ID de session sont Session Sniffing (ou les hommes au milieu) et entre sites attaque de script . Je n'entrerai pas dans les détails, vous pouvez lire comment vous protéger de ce genre d'attaques.
3. Vous pouvez le voir dans le projet que j'ai lié. Le plus simplement, le filtre validera tous les /api/*
et vous vous connecterez à un /user/login
par exemple.