JavaRush /Blogue Java /Random-PT /Vamos apresentar o login regular via e-mail e OAuth2 ao S...

Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas

Publicado no grupo Random-PT
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. Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 1Nota: é 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
  1. Criando um Projeto
  2. Criando Entidades e Lógica de Aplicação
    1. Entidades
    2. Repositórios
    3. Controladores
    4. Páginas
  3. Configurando Spring Security para login clássico
    1. Configuração básica SecurityConfig
    2. Login de usuário personalizado
    3. Vamos melhorar o controlador
    4. Lançar
  4. Configurando OAuth2 usando o Google como exemplo no Spring Security
    1. Configuração de filtro e application.properties
    2. Destaques do registro de um aplicativo no Google Cloud Platform
    3. CustomUserInfoTokenServices
  5. 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.
Baixe o arquivo resultante e descompacte-o na pasta necessária. Nós o lançamos no IDE. Você pode escolher o banco de dados a seu critério. Eu uso MySQL como banco de dados do projeto, então adiciono a seguinte dependência ao arquivo pom.xml no bloco <dependencies>:
<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 pacote entitiesno qual colocaremos as entidades do banco de dados. O usuário será descrito por uma classe Userque 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 ( googleNamee 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 pacote repos, nele criaremos interfaces UserRepoherdadas NoteRepoda 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.
Apenas um usuário autorizado deverá ter acesso à lista de notas. As restantes páginas são públicas. Vamos criar um pacote controllerscontendo uma classe IndexControllercontendo 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. PasswordEncoderserá 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 confige 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 classe SecurityConfigherdada 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:
  1. Criptografe senhas usando BCryptPasswordEncoder:

    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Bean
    PasswordEncoder passwordEncoder()
    {
      PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
      return passwordEncoder;
    }
  2. Faça login usando um provedor de autenticação especialmente escrito:

    @Autowired
    private AuthProvider authProvider;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
    {
      auth.authenticationProvider(authProvider);
    }
  3. 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-escrito AuthProviderpermitirá 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 UserServicelocalizada 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 classe User? 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.
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 1
Vamos apresentar o login regular via email e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 2
Vemos que um novo usuário apareceu no banco de dados. A senha é criptografada.
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 3
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 4
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 5
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 6
As notas são salvas no banco de dados.
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 7
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!

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
Credenciais:
  • 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
Nota: como o Google não quer trabalhar com o endereço localhost:8080, adicione a linha “127.0.0.1 me.org” ou algo semelhante ao arquivo C:\Windows\System32\drivers\etc\hosts no final. O principal é que o domínio esteja na forma clássica.

CustomUserInfoTokenServices

Você notou a palavra Personalizado na descrição da função de filtro? Aula CustomUserInfoTokenServices. 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 UserInfoTokenServicespadrã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 UserRepoe 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. 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.
@GetMapping("/")
public String index(Principal principal)
{
  if(principal != null)
  {
     return "redirect:/notes";
  }
  return "index";
}

Lançamento final do projeto

Após pequenas alterações cosméticas e adição de um botão de saída, realizamos o lançamento final do projeto.
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 8
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 9
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 10
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 11
Vamos apresentar o login regular via email e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 12
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 13
Vamos apresentar o login regular via e-mail e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 14
Vamos apresentar o login regular via email e OAuth2 ao Spring Security usando o exemplo do serviço de notas - 15
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
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION