Ao escrever minha inscrição, me deparei com a falta de artigos claros sobre como fazer com que o usuário se registrasse tanto por e-mail quanto por redes sociais. Houve bons tutoriais sobre como configurar o formulário de login clássico. Houve bons tutoriais sobre OAuth2 . Criminosamente, havia pouca informação sobre como combinar os dois métodos. Durante o processo de pesquisa, conseguimos encontrar uma solução viável. Não pretende ser a verdade última, mas cumpre a sua função. Neste artigo vou mostrar como implementar do zero um serviço de armazenamento de notas com uma configuração semelhante do Spring Security. Nota: é bom que o leitor tenha passado por pelo menos alguns tutoriais sobre Spring, pois a atenção estará focada apenas no Spring Security, sem explicações detalhadas sobre repositórios, controladores, etc. ser gigantesco. Contente
Vemos que um novo usuário apareceu no banco de dados. A senha é criptografada.
As notas são salvas no banco de dados.
Vemos que o projeto foi lançado e executado com sucesso. Para a felicidade completa, só precisamos fazer login nas redes sociais. Bem, vamos começar!
Agora, tanto o usuário quanto o OAuth2Authentication podem atuar como principal. Como levamos em consideração no UserService antecipadamente o carregamento do usuário através dos dados do Google, o aplicativo funcionará para ambos os tipos de usuários. Modificamos o controlador da página principal do projeto para que ele redirecione os usuários logados usando OAuth2 para a página de notas.
O usuário faz login com êxito por meio do formulário normal e por meio de uma conta do Google. Isso é o que queríamos! Espero que este artigo tenha esclarecido alguns pontos sobre a criação de um aplicativo web, protegendo-o com Spring Security e combinando diferentes métodos de login. Com o código completo do projeto você pode
- Criando um Projeto
- Criando Entidades e Lógica de Aplicação
- Configurando Spring Security para login clássico
- Configuração básica SecurityConfig
- Login de usuário personalizado
- Vamos melhorar o controlador
- Lançar
- Configurando OAuth2 usando o Google como exemplo no Spring Security
- Configuração de filtro e application.properties
- Destaques do registro de um aplicativo no Google Cloud Platform
- CustomUserInfoTokenServices
- Lançamento final do projeto
Criando um Projeto
Vamos para start.spring.io e formamos a base do projeto:- Web - lançamento de um aplicativo no Tomcat integrado, mapeamentos de URL e similares;
- JPA - conexão com banco de dados;
- Bigode é um mecanismo de modelo usado para gerar páginas da web;
- Segurança - proteção de aplicativos. É para isso que este artigo foi criado.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
A configuração do application.properties é atualmente a seguinte:
spring.datasource.url=jdbc:mysql://localhost:3306/springsectut?createDatabaseIfNotExist=true&useSSL=false&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=yourUsername
spring.datasource.password=yourPassword
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.connection.characterEncoding=utf-8
spring.jpa.properties.connection.CharSet=utf-8
spring.jpa.properties.connection.useUnicode=true
spring.mustache.expose-request-attributes=true
Criando Entidades e Lógica de Aplicação
Entidades
Vamos criar um pacoteentities
no qual colocaremos as entidades do banco de dados. O usuário será descrito por uma classe User
que implementa a interface UserDetails
, que será necessária para a configuração do Spring Security. O usuário terá um ID, nome de usuário (este é e-mail), senha, nome, função, sinalizador de atividade, nome da conta do Google e e-mail ( googleName
e googleUsername
).
@Entity
@Table(name = "user")
public class User implements UserDetails
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String name;
private boolean active;
private String googleName;
private String googleUsername;
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set<Role> roles;
//Геттеры, сеттеры, toString(), equals(), hashcode(), имплементация UserDetails
}
As funções de usuário são usadas para regular o acesso no Spring Security. Nosso aplicativo usará apenas uma função:
public enum Role implements GrantedAuthority
{
USER;
@Override
public String getAuthority()
{
return name();
}
}
Vamos criar uma classe de nota com id, título da nota, corpo da nota e id do usuário ao qual ela pertence:
@Entity
@Table(name = "note")
public class Note
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String note;
private Long userId;
//Геттеры, сеттеры, toString(), equals(), hashcode()
}
Repositórios
Para salvar entidades no banco de dados, precisamos de repositórios que farão todo o trabalho sujo para nós. Vamos criar um pacoterepos
, nele criaremos interfaces UserRepo
herdadas NoteRepo
da interface JpaRepository<Entity, Id>
.
@Service
@Repository
public interface UserRepo extends JpaRepository<User, Long>
{}
@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
List<Note> findByUserId(Long userId);
}
Controladores
Nosso serviço de notas terá as seguintes páginas:- Lar;
- Cadastro;
- Entrada;
- Lista de notas do usuário.
controllers
contendo uma classe IndexController
contendo o mapeamento normal da página principal. A classe RegistrationController
é responsável por cadastrar o usuário. O pós-mapeamento retira dados do formulário, salva o usuário no banco de dados e redireciona para a página de login. PasswordEncoder
será descrito mais tarde. É usado para criptografar senhas.
@Controller
public class RegistrationController
{
@Autowired
private UserRepo userRepo;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/registration")
public String registration()
{
return "registration";
}
@PostMapping("/registration")
public String addUser(String name, String username, String password)
{
User user = new User();
user.setName(name);
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
userRepo.save(user);
return "redirect:/login";
}
O controlador responsável pela página da lista de notas contém atualmente funcionalidades simplificadas, que se tornarão mais complexas após a implementação do Spring Security.
@Controller
public class NoteController
{
@Autowired
private NoteRepo noteRepo;
@GetMapping("/notes")
public String notes(Model model)
{
List<Note> notes = noteRepo.findAll();
model.addAttribute("notes", notes);
return "notes";
}
@PostMapping("/addnote")
public String addNote(String title, String note)
{
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
noteRepo.save(newNote);
return "redirect:/notes";
}
}
Não escreveremos um controlador para a página de login porque ela é usada no Spring Security. Em vez disso, precisaremos de uma configuração especial. Como sempre, vamos criar outro pacote, chamá-lo config
e colocar a classe lá MvcConfig
. Quando escrevermos a configuração do Spring Security, ele saberá a qual página estamos nos referindo quando usarmos "/login".
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
Páginas
Eu uso o mecanismo de modelo Moustache para criar páginas . Você pode implementar outro, não importa. Um arquivo meta.mustache foi criado para as metainformações usadas em todas as páginas. Também inclui Bootstrap para deixar as páginas do nosso projeto mais bonitas. As páginas são criadas no diretório "src/main/resources/templates". Os arquivos possuem a extensão bigode. Colocar o código HTML diretamente no artigo o tornará muito grande, então aqui está um link para a pasta de modelos no repositório GitHub do projeto .Configurando Spring Security para login clássico
Spring Security nos ajuda a proteger o aplicativo e seus recursos contra acesso não autorizado. Criaremos uma configuração de trabalho concisa em uma classeSecurityConfig
herdada de WebSecurityConfigurerAdapter
, que colocaremos no pacote config
. Vamos marcá-la com a anotação @EnableWebSecurity, que habilitará o suporte ao Spring Security, e a anotação @Configuration, que indica que esta classe contém alguma configuração. Nota: o pom.xml configurado automaticamente continha a versão do componente pai Spring Boot 2.1.4.RELEASE, o que impedia que a Segurança fosse implementada da forma estabelecida. Para evitar conflitos no projeto, recomenda-se alterar a versão para 2.0.1.RELEASE.
Configuração básica SecurityConfig
Nossa configuração será capaz de:-
Criptografe senhas usando
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
Faça login usando um provedor de autenticação especialmente escrito:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
Permitir que usuários anônimos acessem a página inicial, as páginas de registro e de login. Todas as outras solicitações devem ser realizadas por usuários logados. Vamos atribuir o “/login” descrito anteriormente como página de login. Se o login for bem-sucedido, o usuário será direcionado para uma página com uma lista de notas; se houver algum erro, o usuário permanecerá na página de login. Após a saída bem-sucedida, o usuário será direcionado para a página principal.
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/resources/**", "/", "/login**", "/registration").permitAll() .anyRequest().authenticated() .and().formLogin().loginPage("/login") .defaultSuccessUrl("/notes").failureUrl("/login?error").permitAll() .and().logout().logoutSuccessUrl("/").permitAll(); }
Login de usuário personalizado
Um auto-escritoAuthProvider
permitirá que o usuário faça login não apenas por e-mail, mas também por nome de usuário.
@Component
public class AuthProvider implements AuthenticationProvider
{
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
String username = authentication.getName();
String password = (String) authentication.getCredentials();
User user = (User) userService.loadUserByUsername(username);
if(user != null && (user.getUsername().equals(username) || user.getName().equals(username)))
{
if(!passwordEncoder.matches(password, user.getPassword()))
{
throw new BadCredentialsException("Wrong password");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
else
throw new BadCredentialsException("Username not found");
}
public boolean supports(Class<?> arg)
{
return true;
}
}
Como você deve ter notado, a classe UserService
localizada no pacote é responsável por carregar o usuário services
. No nosso caso, ele procura um usuário não apenas pelo campo username
, como na implementação integrada, mas também por nome de usuário, nome da conta Google e e-mail da conta Google. Os dois últimos métodos serão úteis para nós ao implementar o login via OAuth2. Aqui a aula é ministrada em uma versão abreviada.
@Service
public class UserService implements UserDetailsService
{
@Autowired
private UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User userFindByUsername = userRepo.findByUsername(username);
//Остальные поиски
if(userFindByUsername != null)
{
return userFindByUsername;
}
//Остальные проверки
return null;
}
}
Nota: não esqueça de escrever os métodos necessários em UserRepo
!
Vamos melhorar o controlador
Configuramos o Spring Security. Agora é a hora de aproveitar isso no seu controlador de notas. Agora cada mapeamento aceitará um parâmetro Principal adicional, pelo qual tentará encontrar o usuário. Por que não posso injetar diretamente a classeUser
? Então haverá um conflito devido a uma incompatibilidade de tipos de usuários quando escrevermos um login nas redes sociais. Fornecemos a flexibilidade necessária com antecedência. Nosso código do controlador de notas agora se parece com isto:
@GetMapping("/notes")
public String notes(Principal principal, Model model)
{
User user = (User) userService.loadUserByUsername(principal.getName());
List<Note> notes = noteRepo.findByUserId(user.getId());
model.addAttribute("notes", notes);
model.addAttribute("user", user);
return "notes";
}
@PostMapping("/addnote")
public String addNote(Principal principal, String title, String note)
{
User user = (User) userService.loadUserByUsername(principal.getName());
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
newNote.setUserId(user.getId());
noteRepo.save(newNote);
return "redirect:/notes";
}
Nota: o projeto tem proteção CSRF habilitada por padrão , então desative-a (http.csrf().disable()) ou não se esqueça, como autor do artigo, de adicionar um campo oculto com um token csrf para todas as solicitações de postagem.
Lançar
Estamos tentando lançar o projeto.Configurando OAuth2 usando o Google como exemplo no Spring Security
Ao implementar o OAuth2, contei com este tutorial oficial do Spring . Para oferecer suporte ao OAuth2, adicione a seguinte biblioteca ao pom.xml:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
Vamos modificar nossa configuração do Spring Security no arquivo SecurityConfig
. Primeiro, vamos adicionar a anotação @EnableOAuth2Client. Ele exibirá automaticamente o que você precisa para fazer login nas redes sociais.
Configuração de filtro e application.properties
Vamos injetar OAuth2ClientContext para usar em nossa configuração de segurança.@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext é usado ao criar um filtro que valida a solicitação de login social do usuário. O filtro está disponível graças à anotação @EnableOAuth2Client. Tudo o que precisamos fazer é chamá-lo na ordem correta, antes do filtro principal do Spring Security. Só então poderemos capturar redirecionamentos durante o processo de login com OAuth2. Para fazer isso, usamos FilterRegistrationBean
, no qual definimos a prioridade do nosso filtro para -100.
@Bean
public FilterRegistrationBean oAuth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter)
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(oAuth2ClientContextFilter);
registration.setOrder(-100);
return registration;
}
private Filter ssoFilter()
{
OAuth2ClientAuthenticationProcessingFilter googleFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
OAuth2RestTemplate googleTemplate = new OAuth2RestTemplate(google(), oAuth2ClientContext);
googleFilter.setRestTemplate(googleTemplate);
CustomUserInfoTokenServices tokenServices = new CustomUserInfoTokenServices(googleResource().getUserInfoUri(), google().getClientId());
tokenServices.setRestTemplate(googleTemplate);
googleFilter.setTokenServices(tokenServices);
tokenServices.setUserRepo(userRepo);
tokenServices.setPasswordEncoder(passwordEncoder);
return googleFilter;
}
Você também precisa adicionar um novo filtro à função configure(HttpSecurity http):
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
O filtro também precisa saber que o cliente se cadastrou via Google. A anotação @ConfigurationProperties especifica quais propriedades de configuração procurar em application.properties.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
Para concluir a autenticação, você precisa especificar o endpoint de informações do usuário do Google:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
Após registrar nosso aplicativo no Google Cloud Platform , adicionaremos propriedades com os prefixos apropriados a application.properties:
google.client.clientId=yourClientId
google.client.clientSecret=yourClientSecret
google.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token
google.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth
google.client.clientAuthenticationScheme=form
google.client.scope=openid,email,profile
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo
google.resource.preferTokenInfo=true
Destaques do registro de um aplicativo no Google Cloud Platform
Caminho: APIs e serviços -> Janela de solicitação de acesso OAuth de credenciais:- Nome do aplicativo: formulário de login Spring e tutorial OAuth2
- Endereço de e-mail de suporte: seu e-mail
- Escopo da API do Google: email, perfil, openid
- Domínios autorizados: me.org
- Link para a página principal do aplicativo: http://me.org:8080
- Link para a política de privacidade do aplicativo: http://me.org:8080
- Link para os termos de uso do aplicativo: http://me.org:8080
- Tipo: Aplicativo Web
- Título: Formulário de login Spring e tutorial OAuth2
- Fontes JavaScript permitidas: http://me.org, http://me.org:8080
- URIs de redirecionamento permitidos: http://me.org:8080/login, http://me.org:8080/login/google
CustomUserInfoTokenServices
Você notou a palavra Personalizado na descrição da função de filtro? AulaCustomUserInfoTokenServices
. Sim, criaremos nossa própria aula com blackjack e possibilidade de salvar o usuário no banco de dados! Usando o atalho de teclado Ctrl-N no IntelliJ IDEA, você pode encontrar e ver como o UserInfoTokenServices
padrão é implementado. Vamos copiar seu código para a classe recém-criada CustomUserInfoTokenServices
. A maior parte pode permanecer inalterada. Antes de mudar a lógica das funções, vamos adicionar UserRepo
e como campos privados da classe PasswordEncoder
. Vamos criar setters para eles. Vamos adicionar @Autowired UserRepo userRepo à classe SecurityConfig. Observamos como o ponteiro para o erro no método de criação do filtro desaparece e nos alegramos. Por que o @Autowired não pôde ser aplicado diretamente ao CustomUserInfoTokenServices? Porque esta classe não irá captar a dependência, já que ela mesma não está marcada com nenhuma anotação Spring, e seu construtor é criado explicitamente quando o filtro é declarado. Conseqüentemente, o mecanismo DI do Spring não sabe disso. Se anotarmos @Autowired em qualquer coisa nesta classe, obteremos um NullPointerException quando usado. Mas através de setters explícitos tudo funciona muito bem. Após a implementação dos componentes necessários, o principal objeto de interesse passa a ser a função loadAuthentication, na qual é recuperado o Map<String, Object> com informações sobre o usuário. Foi neste projeto que implementei o salvamento de um usuário logado através de uma rede social no banco de dados. Como estamos usando uma conta do Google como provedor OAuth2, verificamos se o mapa contém o campo “sub” típico do Google. Se estiver presente, significa que as informações do usuário foram recebidas corretamente. Criamos um novo usuário e o salvamos no banco de dados.
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException
{
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if(map.containsKey("sub"))
{
String googleName = (String) map.get("name");
String googleUsername = (String) map.get("email");
User user = userRepo.findByGoogleUsername(googleUsername);
if(user == null)
{
user = new User();
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
}
user.setName(googleName);
user.setUsername(googleUsername);
user.setGoogleName(googleName);
user.setGoogleUsername(googleUsername);
user.setPassword(passwordEncoder.encode("oauth2user"));
userRepo.save(user);
}
if (map.containsKey("error"))
{
this.logger.debug("userinfo returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
Ao usar vários provedores, você pode especificar diferentes opções em um CustomUserInfoTokenServices e registrar diferentes classes de serviços semelhantes no método de declaração de filtro.
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION