Comment s’authentifier avec Spring Security en utilisant Token
Comment s’authentifier à l’aide de Token
On va activer l’authentification basée sur un Token dans Spring MVC en suivant les étapes suivantes :
1-L’utilisateur envoie ses informations d’identification (nom d’utilisateur et mot de passe) au serveur.
2-Le serveur authentifie les informations d’identification et génère un Token.
3-Le serveur stocke le Token généré précédemment dans une zone de stockage avec l’identifiant de l’utilisateur et une date d’expiration.
4-Le serveur envoie le Token généré à l’utilisateur.
5-Le serveur, dans chaque demande, extrait le Token de la demande entrante. Avec ce Token, le serveur recherche les détails de l’utilisateur pour effectuer l’authentification et l’autorisation:
* Si le Token est valide, le serveur accepte la demande.
* Si le Token n’est pas valide, le serveur refuse la demande
Comment générer le Token
Un Token peut être opaque qui ne révèle aucun détail autre que la valeur elle-même (comme une chaîne aléatoire) ou peut être autonome (comme l’algorithme MD5 ou SHA).
src/main/java/com/intellitech /springlabs/util/TokenUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package com.intellitech.springlabs.util; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.codec.Hex; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class TokenUtil { public static final String MAGIC_KEY = "IntelliTech"; public static String createToken(UserDetails userDetails) { long expires = System.currentTimeMillis() + 1000L * 60 * 60; return userDetails.getUsername() + ":" + expires + ":" + computeSignature(userDetails, expires); } public static String computeSignature(UserDetails userDetails, long expires) { StringBuilder signatureBuilder = new StringBuilder(); signatureBuilder.append(userDetails.getUsername()).append(":"); signatureBuilder.append(expires).append(":"); signatureBuilder.append(userDetails.getPassword()).append(":"); signatureBuilder.append(TokenUtil.MAGIC_KEY); MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("No MD5 algorithm available!"); } return new String(Hex.encode(digest.digest(signatureBuilder.toString().getBytes()))); } public static String getUserNameFromToken(String authToken) { if (authToken == null) { return null; } String[] parts = authToken.split(":"); return parts[0]; } public static boolean validateToken(String authToken, UserDetails userDetails) { String[] parts = authToken.split(":"); long expires = Long.parseLong(parts[1]); String signature = parts[2]; String signatureToMatch = computeSignature(userDetails, expires); return expires >= System.currentTimeMillis() && signature.equals(signatureToMatch); } } |
src/main/java/com/intellitech /springlabs/util/AuthTokenFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package com.intellitech.springlabs.util; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; public class AuthTokenFilter extends GenericFilterBean { private UserDetailsService customUserDetailsService; private String authTokenHeaderName = "x-auth-token"; public AuthTokenFilter(UserDetailsService userDetailsService) { this.customUserDetailsService = userDetailsService; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String authToken = httpServletRequest.getHeader(authTokenHeaderName); if (StringUtils.hasText(authToken)) { String username = TokenUtil.getUserNameFromToken(authToken); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); if (TokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(token); } } filterChain.doFilter(servletRequest, servletResponse); } catch (Exception ex) { throw new RuntimeException(ex); } } } |
src/main/java/com/intellitech /springlabs/util/SecurityConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
package com.intellitech.springlabs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.DefaultSecurityFilterChain; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("customUserDetailsService") private UserDetailsService customUserDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { String [] methodSecured={"/users/*","/swagger-ui.html"}; http.csrf().disable() .authorizeRequests().antMatchers("/","/login/authenticate").permitAll() .antMatchers(methodSecured).authenticated() .and().formLogin().loginPage("/login").defaultSuccessUrl("/swagger-ui.html").failureUrl("/login?error=true").permitAll() .and().logout().deleteCookies("JSESSIONID").logoutUrl("/logout").logoutSuccessUrl("/login"); SecurityConfigurer securityConfigurerAdapter = new AuthTokenConfig(customUserDetailsService); http.apply(securityConfigurerAdapter); } @Override protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception { authManagerBuilder.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder()); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } |
Service d’authentification
src/main/java/com/intellitech /springlabs/controller/LoginController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
package com.intellitech.springlabs.controller; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.intellitech.springlabs.model.request.AuthenticationRequest; import com.intellitech.springlabs.model.response.UserTransfer; import com.intellitech.springlabs.util.TokenUtil; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; @RestController @RequestMapping("/login") public class LoginController { @Autowired private AuthenticationManager authenticationManager; @Autowired @Qualifier("customUserDetailsService") private UserDetailsService customUserDetailsService; @RequestMapping(value = "/authenticate", method = { RequestMethod.POST }) @ApiOperation(value = "authenticate") @ApiResponses(value = { @ApiResponse(code = 200, message = "Success", response = UserTransfer.class), @ApiResponse(code = 403, message = Constants.FORBIDDEN), @ApiResponse(code = 422, message = Constants.USER_NOT_FOUND), @ApiResponse(code = 417, message = Constants.EXCEPTION_FAILED) }) public ResponseEntity<UserTransfer> authenticate(@RequestBody AuthenticationRequest authenticationRequest) { try { String username = authenticationRequest.getUsername(); String password = authenticationRequest.getPassword(); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); Authentication authentication = this.authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = this.customUserDetailsService.loadUserByUsername(username); List<String> roles = new ArrayList(); for (GrantedAuthority authority : userDetails.getAuthorities()) { roles.add(authority.toString()); } return new ResponseEntity<UserTransfer>(new UserTransfer(userDetails.getUsername(), roles, TokenUtil.createToken(userDetails), HttpStatus.OK), HttpStatus.OK); } catch (BadCredentialsException bce) { return new ResponseEntity<UserTransfer>(new UserTransfer(), HttpStatus.UNPROCESSABLE_ENTITY); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED); } } } |
src/main/java/com/intellitech /springlabs/util/Constants.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.intellitech.springlabs.util; public class Constants { public final static String SUCCESS = "Success"; public final static String UNIQUE_ID_ALREADY_EXIST = "Unique id already exist"; public final static String USER_NOT_FOUND = "User not found"; public final static String DEVICE_NOT_FOUND = "User not found"; public final static String INTERNAL_SERVER_ERROR = "Internal server error"; public final static String EXCEPTION_FAILED = "Exception failed"; public final static String NOT_FOUND = "Not Found"; public final static String FORBIDDEN = "Forbidden"; public final static String USER_PROFILE_NOT_UPDATED = "User profile not updated"; } |
src/main/java/com/intellitech /springlabs/model/response/UserTransfer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
package com.intellitech.springlabs.model.response; import java.util.Collections; import java.util.List; import org.springframework.http.HttpStatus; public class UserTransfer { private String username; private List<String> roles; private String token; private HttpStatus status; public UserTransfer(String username, List<String> roles, String token, HttpStatus status) { this.roles = roles; this.token = token; this.username = username; this.status = status; } public UserTransfer() { this.token = ""; this.username = ""; this.roles = Collections.emptyList(); this.status = HttpStatus.NOT_FOUND; } public List<String> getRoles() { return this.roles; } public String getToken() { return this.token; } public String getUsername() { return this.username; } public HttpStatus getStatus() { return this.status; } } |
Par défaut, le serveur Web tomcat rejettera toute demande qui ne provient pas de localhost.
CORS est une fonctionnalité de navigateur qui protège contre les scripts intersites en JavaScript et le Web serait un endroit beaucoup plus dangereux sans cela.
Comme toujours, la sécurité a un prix et dans ce cas, les services Web qui interagissent avec une API sur un autre domaine ou IP doivent autoriser l’accès multi-site dans leurs en-têtes de réponse, sinon presque tous les navigateurs modernes ne laisseront pas la réponse.
src/main/java/com/intellitech /springlabs/SimpleCORSFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package com.intellitech.springlabs; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; @Component public class SimpleCORSFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "x-auth-token, Content-Type, Accept, X-Requested-With, remember-me"); chain.doFilter(req, res); } @Override public void destroy() { } @Override public void init(FilterConfig filterConfig) throws ServletException { } } |
Télécharger le code source à partir de