Champs de connexion supplémentaires avec Spring Security
1. introduction
Dans cet article, nous allons mettre en œuvre un scénario d'authentification personnalisé avecSpring Security paradding an extra field to the standard login form.
Nous allons nous concentrer sur2 different approaches, pour montrer la polyvalence du framework et les manières flexibles de l'utiliser.
Our first approach sera une solution simple qui se concentre sur la réutilisation des implémentations de base existantes de Spring Security.
Our second approach sera une solution plus personnalisée qui peut être plus adaptée aux cas d'utilisation avancés.
Nous nous baserons sur les concepts abordés dans nosprevious articles on Spring Security login.
2. Maven Setup
Nous utiliserons les démarreurs Spring Boot pour démarrer notre projet et intégrer toutes les dépendances nécessaires.
La configuration que nous utiliserons nécessite une déclaration parent, un démarreur Web et un démarreur de sécurité. nous inclurons également thymeleaf:
org.springframework.boot
spring-boot-starter-parent
2.0.0.M7
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.thymeleaf.extras
thymeleaf-extras-springsecurity4
La version la plus récente du démarreur de sécurité Spring Boot peut être trouvéeover at Maven Central.
3. Configuration de projet simple
Dans notre première approche, nous nous concentrerons sur la réutilisation des implémentations fournies par Spring Security. En particulier, nous réutiliseronsDaoAuthenticationProvider etUsernamePasswordToken tels qu’ils existent «prêts à l’emploi».
Les composants clés comprendront:
-
SimpleAuthenticationFilter – une extension deUsernamePasswordAuthenticationFilter
-
SimpleUserDetailsService – une implémentation deUserDetailsService
-
User – une extension de la classeUser fournie par Spring Security qui déclare notre champdomain supplémentaire
-
SecurityConfig – notre configuration Spring Security qui insère nosSimpleAuthenticationFilter dans la chaîne de filtres, déclare les règles de sécurité et connecte les dépendances
-
login.html– une page de connexion qui collecte lesusername,password etdomain
3.1. Filtre d'authentification simple
Dans nosSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Nous concaténons ces valeurs et les utilisons pour créer une instance deUsernamePasswordAuthenticationToken.
The token is then passed along to the AuthenticationProvider pour l'authentification:
public class SimpleAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
UsernamePasswordAuthenticationToken authRequest
= getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager()
.authenticate(authRequest);
}
private UsernamePasswordAuthenticationToken getAuthRequest(
HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
String usernameDomain = String.format("%s%s%s", username.trim(),
String.valueOf(Character.LINE_SEPARATOR), domain);
return new UsernamePasswordAuthenticationToken(
usernameDomain, password);
}
// other methods
}
3.2. ServiceUserDetails simple
Le contratUserDetailsService définit une seule méthode appeléeloadUserByUsername.Our implementation extracts the username and domain. Les valeurs sont ensuite passées à nos UserRepository pour obtenir lesUser:
public class SimpleUserDetailsService implements UserDetailsService {
// ...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String[] usernameAndDomain = StringUtils.split(
username, String.valueOf(Character.LINE_SEPARATOR));
if (usernameAndDomain == null || usernameAndDomain.length != 2) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
usernameAndDomain[0], usernameAndDomain[1]));
}
return user;
}
}
3.3. Configuration de sécurité de printemps
Notre configuration est différente d'une configuration standard de Spring Security carwe insert our SimpleAuthenticationFilter into the filter chain before the default avec un appel àaddFilterBefore:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").authenticated()
.and()
.formLogin().loginPage("/login")
.and()
.logout()
.logoutUrl("/logout");
}
Nous pouvons utiliser lesDaoAuthenticationProvider fournis car nous les configurons avec nosSimpleUserDetailsService. Rappelez-vous queour SimpleUserDetailsService knows how to parse out our username and domain fields et renvoyez lesUser appropriés à utiliser lors de l'authentification:
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
Puisque nous utilisons unSimpleAuthenticationFilter, nous configurons nos propresAuthenticationFailureHandler pour nous assurer que les tentatives de connexion infructueuses sont gérées de manière appropriée:
public SimpleAuthenticationFilter authenticationFilter() throws Exception {
SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
3.4. Page de connexion
La page de connexion que nous utilisons recueille notre champdomain supplémentaire qui est extrait par nosSimpleAuthenticationFilter:
Lorsque nous exécutons l'application et accédons au contexte àhttp://localhost:8081, nous voyons un lien pour accéder à une page sécurisée. Cliquez sur le lien pour afficher la page de connexion. Comme prévu,we see the additional domain field:
3.5. Sommaire
Dans notre premier exemple, nous avons pu réutiliserDaoAuthenticationProvider etUsernamePasswordAuthenticationToken en «simulant» le champ du nom d'utilisateur.
En conséquence, nous avons puadd support for an extra login field with a minimal amount of configuration and additional code.
4. Configuration de projet personnalisée
Notre seconde approche sera très similaire à la première mais peut être plus appropriée pour des cas d'utilisation non triviaux.
Les éléments clés de notre deuxième approche comprendront:
-
CustomAuthenticationFilter – une extension deUsernamePasswordAuthenticationFilter
-
CustomUserDetailsService – une interface personnalisée déclarant une méthodeloadUserbyUsernameAndDomain
-
CustomUserDetailsServiceImpl – une implémentation de nosCustomUserDetailsService
-
CustomUserDetailsAuthenticationProvider – une extension deAbstractUserDetailsAuthenticationProvider
-
CustomAuthenticationToken – une extension deUsernamePasswordAuthenticationToken
-
User – une extension de la classeUser fournie par Spring Security qui déclare notre champdomain supplémentaire
-
SecurityConfig – notre configuration Spring Security qui insère nosCustomAuthenticationFilter dans la chaîne de filtres, déclare les règles de sécurité et connecte les dépendances
-
login.html– la page de connexion qui collecte lesusername,password etdomain
4.1. Filtre d'authentification personnalisé
Dans nosCustomAuthenticationFilter, nousextract the username, password, and domain fields from the request. Ces valeurs sont utilisées pour créer une instance de nosAuthenticationToken personnalisés qui est passée auxAuthenticationProvider pour l'authentification:
public class CustomAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
CustomAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
return new CustomAuthenticationToken(username, password, domain);
}
4.2. Service personnaliséUserDetails
Notre contratCustomUserDetailsService définit une seule méthode appeléeloadUserByUsernameAndDomain.
La classeCustomUserDetailsServiceImpl que nous créons implémente simplement le contrat et délègue à nosCustomUserRepository pour obtenir lesUser:
public UserDetails loadUserByUsernameAndDomain(String username, String domain)
throws UsernameNotFoundException {
if (StringUtils.isAnyBlank(username, domain)) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(username, domain);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
username, domain));
}
return user;
}
4.3. PersonnaliséUserDetailsAuthenticationProvider
NotreCustomUserDetailsAuthenticationProvider étendAbstractUserDetailsAuthenticationProvider et les délègue à nosCustomUserDetailService pour récupérer lesUser. The most important feature of this class is the implementation of the retrieveUser method.
Notez que nous devons convertir le jeton d'authentification à nosCustomAuthenticationToken pour accéder à notre champ personnalisé:
@Override
protected UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
UserDetails loadedUser;
try {
loadedUser = this.userDetailsService
.loadUserByUsernameAndDomain(auth.getPrincipal()
.toString(), auth.getDomain());
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials()
.toString();
passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
// ...
return loadedUser;
}
4.4. Sommaire
Notre deuxième approche est presque identique à l'approche simple que nous avons présentée en premier. En implémentant nos propresAuthenticationProvider etCustomAuthenticationToken, nous avons évité d'avoir à adapter notre champ de nom d'utilisateur avec une logique d'analyse personnalisée.
5. Conclusion
Dans cet article, nous avons implémenté un formulaire de connexion dans Spring Security qui utilisait un champ de connexion supplémentaire. Nous l'avons fait de 2 manières différentes:
-
Dans notre approche simple, nous avons minimisé la quantité de code nécessaire à l’écriture. Nous avons pureuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username avec une logique d'analyse personnalisée
-
Dans notre approche plus personnalisée, nous avons fourni un support de terrain personnalisé parextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken
Comme toujours, tout le code source peut être trouvéover on GitHub.