Autenticação da API REST com Spring Security e MongoDB

Olá a todos! Partindo para o final de semana, estamos compartilhando com você um artigo que foi traduzido antes do início do curso "Desenvolvedor no Spring Framework" .





Nos artigos anteriores, criamos um serviço da web RESTful, agora falaremos sobre segurança

Introdução


Em uma postagem anterior, vimos como criar uma API REST usando as estruturas Java Spring Boot e MongoDB. A API, no entanto, não requer autenticação, o que significa que provavelmente ainda não está pronta para uso. Portanto, este guia mostrará como usar o ambiente de segurança interno do Spring para adicionar um nível de autenticação a esta API.

Por que nossa API precisa de autenticação?


As APIs fornecem uma interface simples para interagir com dados internos; portanto, faz sentido que você não queira que ninguém tenha acesso a esses dados e os altere. A autenticação garante que apenas usuários confiáveis ​​possam acessar a API.

Como funciona


Usaremos a autenticação HTTP básica , que usa um nome de usuário e senha. O nome de usuário e a senha são separados em uma linha por dois pontos no seguinte formato username:password.

Essa linha é então codificada usando a codificação Base64 , então a linha admin:p@55w0Rdserá codificada na próxima linha YWRtaW46cEA1NXcwUmQ=(embora eu sugira usar uma senha mais forte que "p @ 55w0Rd"). Podemos anexar essa autenticação aos nossos pedidos adicionando um cabeçalho Authentication. Este cabeçalho do exemplo anterior ficaria assim (onde "Básico" significa que a senha usa autenticação HTTP básica):

Authentication: Basic YWRtaW46cEA1NXcwUmQ=

Como o Spring gerencia a segurança


O Spring oferece um suplemento chamado Spring Security , que torna a autenticação altamente personalizável e extremamente simples. Podemos até usar algumas das habilidades que aprendemos em um post anterior ao configurar!

O que nós precisamos


  • Uma nova coleção em nossa instância do MongoDB chamada "users"
  • Um novo documento na coleção de usuários com os seguintes campos (quaisquer outros campos são opcionais, mas são necessários): nome de usuário, senha (hash usando o algoritmo BCrypt, mais sobre isso posteriormente)
  • Fontes da postagem anterior

BCrypt para hash de senha


Hashing é um algoritmo de criptografia unidirecional. De fato, após o hash, é quase impossível descobrir como eram os dados originais. O algoritmo de hash BCrypt primeiro salga um pedaço de texto e, em seguida, o hashe em uma cadeia de 60 caracteres. O codificador Java BCrypt oferece um método matchesque verifica se uma sequência corresponde a um hash. Por exemplo, uma senha p@55w0Rdque é hash com BCrypt pode ter um significado $2b$10$Qrc6rGzIGaHpbgPM5kVXdeNZ9NiyRWC69Wk/17mttHKnDR2lW49KS. Ao chamar o método matchesBCrypt para uma senha não criptografada e com hash, obtemos um valor true. Esses hashes podem ser gerados usando o codificador BCrypt incorporado ao Spring Security.

Por que devemos hash senhas?


Todos nós já ouvimos falar de ataques cibernéticos recentes que resultaram em senhas roubadas de grandes empresas. Então, por que é recomendado apenas alterar nossas senhas após o hacking? Porque essas grandes empresas certificaram-se de que as senhas sejam sempre hash em seus bancos de dados!

Embora sempre valha a pena alterar as senhas após esses hacks de dados, o hash de senha torna extremamente difícil encontrar a senha real do usuário, pois é um algoritmo unidirecional. De fato, pode levar anos para quebrar uma senha complexa com hash corretamente. Isso fornece um nível adicional de proteção contra roubo de senha. E o Spring Security simplifica o hash, então a verdadeira pergunta deve ser: "Por que não?"

Adicionando um usuário ao MongoDB


Vou adicionar um mínimo de campos necessários para minha coleção users(usuários), para que um documento com usuários no meu banco de dados contenha apenas username(nome de usuário) e BCrypt password(senha) com hash . Neste exemplo, meu nome de usuário será admine minha senha será welcome1, mas eu sugeriria o uso de um nome de usuário e senha mais robustos na API no nível de produção.

db.users.insert({
  “username” : “admin”,
  “password” : “$2a$10$AjHGc4x3Nez/p4ZpvFDWeO6FGxee/cVqj5KHHnHfuLnIOzC5ag4fm”
});

Essas são todas as configurações necessárias no MongoDB! O restante da configuração será feito em nosso código Java.

Incluindo um Modelo de Usuário e Repositório


O post anterior descrito em detalhes sobre os modelos e repositórios do Mongo, portanto, não entrarei em detalhes sobre como eles funcionam aqui - se você quiser atualizar seu conhecimento, não hesite em visitar o meu post anterior!

A desvantagem é que o Spring precisa saber como será o documento user(modelo) e como acessar a coleção userno banco de dados (repositórios). Podemos colocar esses arquivos nas mesmas pastas de modelos e repositórios, respectivamente, como fizemos no exercício anterior.

Modelo


O modelo será uma classe base Java com custom _id, usernamee password. O arquivo será nomeado Users.java. e ficará assim:

package com.example.gtommee.rest_tutorial.models;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;

public class Users {
  @Id
  public ObjectId _id;

  public String username;
  public String password;

  public Users() {}

  public Users(ObjectId _id, String username, String password) 
{
    this._id = _id;
    this.username = username;
    this.password = password;
  }

  public void set_id(ObjectId _id) { this._id = _id; }

  public String get_id() { return this._id.toHexString(); }

  public void setPassword(String password) { this.password = password; }

  public String getPassword() { return password; }

  public void setUsername(String username) { this.username = username; }

  public String getUsername() { return username; }
}

Repositório


O repositório será chamado UsersRepository.javae terá a seguinte aparência - lembre-se, precisaremos encontrar usuários por eles username, portanto, precisaremos incluir o método findByUsernamena interface do repositório.

package com.example.gtommee.rest_tutorial.repositories;

import com.example.gtommee.rest_tutorial.models.Users;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UsersRepository extends MongoRepository<Users, String> {
  Users findByUsername(String username);
}

E é isso para o modelo e o repositório!

Adicionando dependências de segurança


Deve haver um arquivo com um nome no diretório raiz do projeto pom.xml. Ainda não tocamos nesse arquivo, mas o arquivo pom contém todas as dependências do nosso projeto, e vamos adicionar algumas, então vamos começar abrindo esse arquivo e rolando para baixo até a tag . A única nova dependência de que precisamos é a segurança inicial . O Spring possui um gerenciador de versão interno, portanto, a dependência que precisamos adicionar à tag é a seguinte:<dependencies>

<dependencies>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

E o Maven fará o download dos arquivos de origem para nós, portanto nossas dependências devem estar prontas!

Criar serviço de autenticação


Precisamos informar ao Spring onde estão os dados de nossos usuários e onde encontrar as informações necessárias para autenticação. Para fazer isso, podemos criar um serviço de autenticação (Serviço de Autenticação). Vamos começar por criar uma nova pasta no src/main/resources/java/[package name]chamado serviços, e nós podemos criar um novo arquivo nesta pasta de configuração com o nome MongoUserDetailsService.java.

MongoUserDetailsService.java


Esta classe tem um componente principal, então eu darei a classe inteira aqui e depois a explicarei abaixo:

package com.example.gtommee.rest_tutorial.services;

import com.example.gtommee.rest_tutorial.models.Users;
import com.example.gtommee.rest_tutorial.repositories.UsersRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class MongoUserDetailsService implements UserDetailsService{
  @Autowired
  private UsersRepository repository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Users user = repository.findByUsername(username);

    if(user == null) {
      throw new UsernameNotFoundException(“User not found”);
    }

    List<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(“user”));

    return new User(user.getUsername(), user.getPassword(), authorities);
  }
}

Este trecho começa com as importações necessárias no arquivo. Além disso, a seção implements UserDetailsServiceindica que essa classe criará um serviço para pesquisar e autenticar usuários. Em seguida, a anotação @Componentindica que essa classe pode ser incorporada em outro arquivo (por exemplo, o arquivo SecurityConfiguration, sobre o qual passaremos por várias seções).

Anotação @Autowiredencerrada private UsersRepository repository; é um exemplo de implementação, essa propriedade nos fornece uma instância nossa UsersRepositorypara o trabalho. A anotação @Overrideindica que este método será usado em vez do método padrão UserDetailsService. Primeiro, esse método obtém o objeto Usersda fonte de dados do MongoDB usando o método findByUsernameque declaramos UsersRepository.

O método verifica se o usuário foi encontrado ou não. Em seguida, o usuário recebe permissões / função (isso pode adicionar níveis de autenticação adicionais aos níveis de acesso, mas uma função será suficiente para esta lição). Finalmente, o método retorna um objeto Spring Usercom username, passworde o roleusuário autenticado.

Criar configuração de segurança


Precisamos redefinir alguns dos protocolos de segurança internos do Spring para usar nosso banco de dados e algoritmo de hash; portanto, precisamos de um arquivo de configuração especial. Para criá-lo, devemos criar uma nova pasta no src/main/resources/java/[package name]com o nome config, e também precisará criar um novo arquivo nesta pasta de configuração com o nome SecurityConfiguration.java. Este arquivo possui várias partes importantes, então vamos começar com a classe base SecurityConfiguration:

SecurityConfiguration.java


package com.example.gtommee.rest_tutorial.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;
}

Já existe o suficiente para lidar, então vamos começar de cima. Uma anotação @Configurationindica que a classe conterá Java beans, descritos em detalhes aqui. Uma anotação @EnableConfigurationPropertiesindica o que a classe conterá como um bean de configuração especial. Em seguida, a instrução extends WebSecurityConfigurerAdaptermapeará a classe pai WebSecurityConfigurerAdapterpara nossa classe de configuração , fornecendo à nossa classe todo o necessário para garantir que suas regras de segurança sejam respeitadas. Por fim, a classe injeta automaticamente uma instância ( @Autowired) MongoUserDetailsService, que podemos usar posteriormente neste arquivo.

Etapa de autenticação


Em seguida, precisamos informar ao Spring Security como queremos lidar com a autenticação do usuário. Por padrão, o Spring Security tem um nome de usuário e senha predefinidos, proteção CSRF e gerenciamento de sessões . No entanto, queremos que nossos usuários usem seu nome de usuário e senha para acessar o banco de dados. Além disso, como nossos usuários serão autenticados novamente a cada solicitação, em vez de efetuar login, não precisamos da proteção CSRF e do gerenciamento de sessões, portanto, podemos adicionar um método com um nome configureque substitua o esquema de autenticação padrão para informar exatamente ao Spring como queremos lidar com a autenticação e teremos a seguinte aparência:

@Override
protected void configure(HttpSecurity http) throws Exception {
 http
   .csrf().disable()
   .authorizeRequests().anyRequest().authenticated()
   .and().httpBasic()
   .and().sessionManagement().disable();
}

Novamente, muitas coisas estão acontecendo aqui, então vamos resolver isso em etapas. A anotação @Overrideinforma ao Spring Boot para usar o método em configure (HttpSecurity http)vez da configuração padrão do Spring. Em seguida, chamamos uma série de métodos para o objeto em httpque a configuração real ocorre. Esses métodos fazem o seguinte:

  • csrf().disable(): Desativa a proteção CSRF porque não é necessário para a API
  • authorizeRequests().anyRequest().authenticated(): Declara que todas as solicitações para qualquer terminal devem ser autorizadas, caso contrário, devem ser rejeitadas.
  • and().httpBasic(): Springpara que ele espere autenticação HTTP básica (discutida acima).
  • .and().sessionManagement().disable(): informa ao Spring para não armazenar informações da sessão para os usuários, pois isso não é necessário para a API

Adicionando um codificador Bcrypt


Agora precisamos dizer ao Spring para usar o codificador BCrypt para fazer hash e comparar senhas - parece uma tarefa difícil, mas na verdade é muito simples. Podemos adicionar esse codificador simplesmente adicionando as seguintes linhas à nossa classe SecurityConfiguration:

@Bean
public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
}

E todo o trabalho! Esse bean simples informa ao Spring que o PasswordEncoder que queremos usar é o Spring Boot BCryptPasswordEncoder()para codificar e comparar os hashes de senha. O Spring Boot também inclui vários outros codificadores de senha - eu recomendo experimentá-los se quiser experimentar!

Especifique o Gerenciador de autenticação


Finalmente, devemos indicar no nosso SecurityConfigurationque queremos usar MongoUserDetailsService(que criamos na seção anterior) para nossa autenticação. Podemos fazer isso usando o seguinte método:

@Override
public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
  builder.userDetailsService(userDetailsService);
}

Esse método simplesmente substitui a configuração padrão AuthenticationManagerBuilder, substituindo nosso próprio serviço de transferência de dados personalizado.

Arquivo final SecurityConfiguration.java


import com.example.gtommee.rest_tutorial.services.MongoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;

 @Override
 protected void configure(HttpSecurity http) throws Exception {
   http
     .csrf().disable()
     .authorizeRequests().anyRequest().authenticated()
     .and().httpBasic()
     .and().sessionManagement().disable();
 }

 @Bean
 public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
 }

 @Override
 public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
   builder.userDetailsService(userDetailsService);
 }
}

Verificação de autenticação


Testarei uma GETsolicitação rápida com autenticação válida e incorreta para garantir que a configuração funcione conforme o planejado.

Nome de usuário / senha incorretos

URL: http://localhost:8080/pets/
Método: GET
Login: Basic YWRtaW46d2VsY29tZQ==

Resposta:

401 Unauthorized

Nome de usuário / senha válidos

URL: http://localhost:8080/pets/
Método: GET
Login: Basic YWRtaW46d2VsY29tZTE=

Responder:

[
 {
   “_id”: “5aeccb0a18365ba07414356c”,
   “name”: “Spot”,
   “species”: “dog”,
   “breed”: “pitbull”
 },
 {
   “_id”: “5aeccb0a18365ba07414356d”,
   “name”: “Daisy”,
   “species”: “cat”,
   “breed”: “calico”
 },
 {
   “_id”: “5aeccb0a18365ba07414356e”,
   “name”: “Bella”,
   “species”: “dog”,
   “breed”: “australian shepard”
 }
]

Conclusão


Funciona como deveria! A autenticação HTTP básica para a API do Spring Boot pode ser complicada, mas espero que este guia ajude a torná-lo mais compreensível. A autenticação é essencial no clima cibernético de hoje; portanto, ferramentas como o Spring Security são essenciais para garantir a integridade e a segurança dos seus dados.

Isso é tudo! Encontre-me no curso .

All Articles