Campos extras de login com Spring Security
1. Introdução
Neste artigo, implementaremos um cenário de autenticação personalizado comSpring Security poradding an extra field to the standard login form.
Vamos nos concentrar em2 different approaches, para mostrar a versatilidade da estrutura e as formas flexíveis em que podemos usá-la.
Our first approach será uma solução simples que se concentra na reutilização de implementações centrais existentes do Spring Security.
Our second approach será uma solução mais personalizada que pode ser mais adequada para casos de uso avançados.
Vamos construir sobre os conceitos que são discutidos em nossoprevious articles on Spring Security login.
2. Configuração do Maven
Usaremos iniciadores Spring Boot para inicializar nosso projeto e trazer todas as dependências necessárias.
A configuração que usaremos requer uma declaração pai, web starter e security starter; também incluiremos a folha timbrado:
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
A versão mais atual do Spring Boot Security Starter pode ser encontradaover at Maven Central.
3. Configuração de projeto simples
Em nossa primeira abordagem, vamos nos concentrar em reutilizar as implementações fornecidas pelo Spring Security. Em particular, vamos reutilizarDaoAuthenticationProvider eUsernamePasswordToken, pois eles existem "fora da caixa".
Os principais componentes incluirão:
-
SimpleAuthenticationFilter – uma extensão deUsernamePasswordAuthenticationFilter
-
SimpleUserDetailsService – uma implementação deUserDetailsService
-
User – uma extensão da classeUser fornecida pelo Spring Security que declara nosso campodomain extra
-
SecurityConfig – nossa configuração Spring Security que insere nossoSimpleAuthenticationFilter na cadeia de filtros, declara regras de segurança e conecta dependências
-
login.html– uma página de login que coletausername,password edomain
3.1. Filtro de autenticação simples
Em nossoSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Concatenamos esses valores e os usamos para criar uma instância deUsernamePasswordAuthenticationToken.
The token is then passed along to the AuthenticationProvider para autenticação:
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. ServiçoUserDetails simples
O contratoUserDetailsService define um único método chamadoloadUserByUsername.Our implementation extracts the username and domain. Os valores são então passados para o nosso UserRepository para obter oUser:
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. Configuração de segurança da primavera
Nossa configuração é diferente de uma configuração padrão do Spring Security porquewe insert our SimpleAuthenticationFilter into the filter chain before the default com uma chamada paraaddFilterBefore:
@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");
}
Podemos usar oDaoAuthenticationProvider fornecido porque o configuramos com nossoSimpleUserDetailsService. Lembre-se de queour SimpleUserDetailsService knows how to parse out our username and domain fieldse retorne oUser apropriado para usar na autenticação:
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
Como estamos usando umSimpleAuthenticationFilter, configuramos nosso próprioAuthenticationFailureHandler para garantir que as tentativas de login malsucedidas sejam tratadas de maneira adequada:
public SimpleAuthenticationFilter authenticationFilter() throws Exception {
SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
3.4. Página de login
A página de login que usamos coleta nosso campodomain adicional que é extraído por nossoSimpleAuthenticationFilter:
Quando executamos o aplicativo e acessamos o contexto emhttp://localhost:8081, vemos um link para acessar uma página segura. Clicar no link fará com que a página de login seja exibida. Como esperado,we see the additional domain field:
3.5. Sumário
Em nosso primeiro exemplo, fomos capazes de reutilizarDaoAuthenticationProvidereUsernamePasswordAuthenticationToken “falsificando” o campo de nome de usuário.
Como resultado, conseguimosadd support for an extra login field with a minimal amount of configuration and additional code.
4. Configuração de projeto personalizado
Nossa segunda abordagem será muito semelhante à primeira, mas pode ser mais apropriada para casos de uso não triviais.
Os principais componentes de nossa segunda abordagem incluirão:
-
CustomAuthenticationFilter – uma extensão deUsernamePasswordAuthenticationFilter
-
CustomUserDetailsService – uma interface personalizada que declara um métodoloadUserbyUsernameAndDomain
-
CustomUserDetailsServiceImpl – uma implementação de nossoCustomUserDetailsService
-
CustomUserDetailsAuthenticationProvider – uma extensão deAbstractUserDetailsAuthenticationProvider
-
CustomAuthenticationToken – uma extensão deUsernamePasswordAuthenticationToken
-
User – uma extensão da classeUser fornecida pelo Spring Security que declara nosso campodomain extra
-
SecurityConfig – nossa configuração Spring Security que insere nossoCustomAuthenticationFilter na cadeia de filtros, declara regras de segurança e conecta dependências
-
login.html– a página de login que coletausername,password edomain
4.1. Filtro de autenticação personalizado
Em nossoCustomAuthenticationFilter, nósextract the username, password, and domain fields from the request. Esses valores são usados para criar uma instância de nossoAuthenticationToken personalizado que é passado paraAuthenticationProvider para autenticação:
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. ServiçoUserDetails personalizado
Nosso contratoCustomUserDetailsService define um único método chamadoloadUserByUsernameAndDomain.
A classeCustomUserDetailsServiceImpl que criamos simplesmente implementa o contrato e delega ao nossoCustomUserRepository para obter oUser:
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. UserDetailsAuthenticationProvider personalizado
NossoCustomUserDetailsAuthenticationProvider estendeAbstractUserDetailsAuthenticationProvider e delega ao nossoCustomUserDetailService para recuperar oUser. The most important feature of this class is the implementation of the retrieveUser method.
Observe que devemos lançar o token de autenticação em nossoCustomAuthenticationToken para acessar nosso campo personalizado:
@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. Sumário
Nossa segunda abordagem é quase idêntica à abordagem simples que apresentamos primeiro. Ao implementar nossos própriosAuthenticationProvidereCustomAuthenticationToken, evitamos a necessidade de adaptar nosso campo de nome de usuário com lógica de análise personalizada.
5. Conclusão
Neste artigo, implementamos um formulário de login no Spring Security que faz uso de um campo de login extra. Fizemos isso de 2 maneiras diferentes:
-
Em nossa abordagem simples, minimizamos a quantidade de código que precisávamos escrever. Conseguimosreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username com lógica de análise personalizada
-
Em nossa abordagem mais personalizada, fornecemos suporte de campo personalizado porextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken
Como sempre, todo o código-fonte pode ser encontradoover on GitHub.
