Protegendo sua API REST com Spring Security e autenticando usuários com token JWT em uma aplicação Spring Boot: um tutorial prático

Felipe Acelino
11 min readJun 2, 2023

--

Neste tutorial, você aprenderá como tornar sua aplicação Spring Boot segura usando Spring Security e token JWT. O Spring Security é um framework amplamente usado para segurança em aplicativos Java, que fornece recursos para autenticação e autorização. O JSON Web Token, ou JWT, é um padrão aberto para criar tokens de autenticação seguros. A combinação do Spring Security e JWT fornece uma solução poderosa e flexível para proteger sua aplicação Sprint Boot.

Links úteis:

Tudo que eu desenvolvi neste tutorial está disponível no seguinte repositório do GitHub: https://github.com/lipeacelino/spring-boot-jwt-security.

Também criei uma collection no Postman com todas as requisições necessárias para você poder testar os endpoints que você irá criar. Basta fazer o download e importar no seu postman: https://github.com/lipeacelino/spring-boot-jwt-security/blob/master/spring-boot-jwt-security.postman_collection.json

A aplicação que desenvolveremos é parte de um outro projeto que fiz, mas que não é essencial para este tutorial.
Apesar disso, há muitas coisas legais que foram desenvolvidas nele, e se você quiser vê-lo, pode encontrá-lo aqui: https://github.com/lipeacelino/pizzurg-api

Sumário

  1. Criando o projeto com as dependências necessárias
  2. Criando as entidades do usuário
  3. Implementando as interfaces UserDetails
  4. Configurando a criação e validação do token JWT
  5. Definindo a classe de configuração de segurança do Spring Security
  6. Criando um filtro de segurança personalizado
  7. Definindo as demais configurações de segurança do Spring Security
  8. Criando a estrutura para testarmos as configuração de segurança que criamos
  9. Testando a autenticação e autorização da nossa aplicação

1. Criando o projeto com as dependências necessárias

Acesse o sprint initializr e crie um projeto com as seguintes dependências:

Dependências necessárias para impletarmos o tutorial

Para garantir que tudo funcione corretamente é necessário:

  • Java 17 (no mínimo)
  • Utilizar o Spring Boot 3.x.x ou superior. Ao utilizar o Spring Boot na versão 3.x.x é obrigatório que a sua versão do Java seja no mínimo a 17.

2. Criando as entidades do usuário

Agora nós iremos criar as entidades relacionadas ao usuário:

1. Criaremos o enum RoleName que representará o nome dos papéis que um usuário pode ter:

public enum RoleName {

ROLE_CUSTOMER,
ROLE_ADMINISTRATOR

}

2. Criaremos a entidade Role que representa o papel de um usuário:

@Entity
@Table(name="roles")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Role {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
private RoleName name;

}

3. Criaremos a entidade User que representa um usuário:

@Table(name = "users")
@Entity(name = "User")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String email;

private String password;

@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinTable(name="users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name="role_id"))
private List<Role> roles;

}

4. Também é importante criarmos agora o repositório da nossa classe que representa um usuário, porque na próxima seção do tutorial nós iremos precisar dela. Criaremos então, o UserRepository:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByEmail(String email);

}

3. Implementando as interfaces UserDetails

Para configurar a autenticação baseada em token em sua API REST com Spring Security, é importante definir como os detalhes do usuário serão obtidos e validados. Para isso, o Spring Security fornece duas interfaces importantes: UserDetails e UserDetailsService.

O UserDetails é uma interface que representa os detalhes do usuário, como seu nome de usuário, senha e autorizações. Já o UserDetailsService é uma interface que retorna um UserDetails com base no nome de usuário fornecido. Juntas, essas interfaces fornecem uma maneira de obter e validar os detalhes do usuário durante o processo de autenticação.

  1. Criaremos uma classe chamada UserDetailsImpl que implementa a classe UserDetails:
@Getter
public class UserDetailsImpl implements UserDetails {

private User user; // Classe de usuário que criamos anteriormente

public UserDetailsImpl(User user) {
this.user = user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
/*
Este método converte a lista de papéis (roles) associados ao usuário
em uma coleção de GrantedAuthorities, que é a forma que o Spring Security
usa para representar papéis. Isso é feito mapeando cada papel para um
novo SimpleGrantedAuthority, que é uma implementação simples de
GrantedAuthority
*/
return user.getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
}

@Override
public String getPassword() {
return user.getPassword();
} // Retorna a credencial do usuário que criamos anteriormente

@Override
public String getUsername() {
return user.getEmail();
} // Retorna o nome de usuário do usuário que criamos anteriormente

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

}

2. Criaremos uma classe chamada UserDetailsServiceImpl que implementa a classe UserDetailsService:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username).orElseThrow(() -> new RuntimeException("Usuário não encontrado."));
return new UserDetailsImpl(user);
}

}

O método loadUserByUsername() é um método da interface UserDetailsService, e é usado para carregar os detalhes do usuário com base no nome de usuário fornecido. Esse método é chamado automaticamente pelo Spring durante o processo de autenticação, e é responsável por retornar um UserDetails com base no nome de usuário fornecido.

4. Configurando a criação e validação do token JWT

Para implementar a autenticação baseada em token em sua API REST, você precisa de uma maneira segura de gerar e validar os tokens. Uma das maneiras mais populares de fazer isso é usando a biblioteca Auth0, que fornece uma solução completa e confiável para gerar e verificar tokens JWT.

Para isso, nós teremos que adicionar a dependência do Auth0 ao pom.xml do nosso projeto:

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>

Após adicionarmos a dependência do Auth0, agora nós criaremos uma classe chamada JwtTokenService com os métodos necessários para gerar um token e recuperar um usuário a partir de um token:

@Service
public class JwtTokenService {

private static final String SECRET_KEY = "4Z^XrroxR@dWxqf$mTTKwW$!@#qGr4P"; // Chave secreta utilizada para gerar e verificar o token

private static final String ISSUER = "pizzurg-api"; // Emissor do token

public String generateToken(UserDetailsImpl user) {
try {
// Define o algoritmo HMAC SHA256 para criar a assinatura do token passando a chave secreta definida
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
return JWT.create()
.withIssuer(ISSUER) // Define o emissor do token
.withIssuedAt(creationDate()) // Define a data de emissão do token
.withExpiresAt(expirationDate()) // Define a data de expiração do token
.withSubject(user.getUsername()) // Define o assunto do token (neste caso, o nome de usuário)
.sign(algorithm); // Assina o token usando o algoritmo especificado
} catch (JWTCreationException exception){
throw new JWTCreationException("Erro ao gerar token.", exception);
}
}

public String getSubjectFromToken(String token) {
try {
// Define o algoritmo HMAC SHA256 para verificar a assinatura do token passando a chave secreta definida
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
return JWT.require(algorithm)
.withIssuer(ISSUER) // Define o emissor do token
.build()
.verify(token) // Verifica a validade do token
.getSubject(); // Obtém o assunto (neste caso, o nome de usuário) do token
} catch (JWTVerificationException exception){
throw new JWTVerificationException("Token inválido ou expirado.");
}
}

private Instant creationDate() {
return ZonedDateTime.now(ZoneId.of("America/Recife")).toInstant();
}

private Instant expirationDate() {
return ZonedDateTime.now(ZoneId.of("America/Recife")).plusHours(4).toInstant();
}

}

5. Definindo a classe de configuração de segurança do Spring Security

Antes de irmos para a próxima etapa, é importante definirmos os nossos endpoints que não requerem autenticação, e essa configuração nós fazemos na classe de configuração do Spring Security. Criaremos então a classe SecurityConfiguration, e por hora definiremos apenas os endpoints que não requerem autenticação:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

public static final String [] ENDPOINTS_WITH_AUTHENTICATION_NOT_REQUIRED = {
"/users/login", //url que usaremos para fazer login
"/users" //url que usaremos para criar um usuário
};

}

Entendendo algumas anotações:

@Configuration: Esta é uma anotação do Spring que indica que a classe anotada é uma classe de configuração. As classes de configuração podem conter métodos anotados com @Bean, que são usados para instanciar, configurar e inicializar objetos a serem gerenciados pelo contêiner Spring.

@EnableWebSecurity: Esta anotação é usada para ativar a segurança da web no projeto Spring Boot. Essa anotação sinaliza ao Spring que a classe anotada deve ser usada para a configuração do Spring Security. Isso permite ao desenvolvedor personalizar as regras de segurança, como regras de autenticação e autorização para rotas específicas, bem como outras configurações de segurança.

6. Criando um filtro de segurança personalizado

Agora nós iremos criar um filtro personalizado para verificar se o usuário é um usuário válido e autenticá-lo. O filtro usará o UserRepository e JwtTokenService que implementamos para encontrar e verificar se o usuário é válido e autenticá-lo.

Criaremos a classe UserAuthenticationFilter que extende a classe OncePerRequestFilter e sobrescreve o método doFilterInternal():

@Component
public class UserAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtTokenService jwtTokenService; // Service que definimos anteriormente

@Autowired
private UserRepository userRepository; // Repository que definimos anteriormente

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Verifica se o endpoint requer autenticação antes de processar a requisição
if (checkIfEndpointIsNotPublic(request)) {
String token = recoveryToken(request); // Recupera o token do cabeçalho Authorization da requisição
if (token != null) {
String subject = jwtTokenService.getSubjectFromToken(token); // Obtém o assunto (neste caso, o nome de usuário) do token
User user = userRepository.findByEmail(subject).get(); // Busca o usuário pelo email (que é o assunto do token)
UserDetailsImpl userDetails = new UserDetailsImpl(user); // Cria um UserDetails com o usuário encontrado

// Cria um objeto de autenticação do Spring Security
Authentication authentication =
new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities());

// Define o objeto de autenticação no contexto de segurança do Spring Security
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
throw new RuntimeException("O token está ausente.");
}
}
filterChain.doFilter(request, response); // Continua o processamento da requisição
}

// Recupera o token do cabeçalho Authorization da requisição
private String recoveryToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null) {
return authorizationHeader.replace("Bearer ", "");
}
return null;
}

// Verifica se o endpoint requer autenticação antes de processar a requisição
private boolean checkIfEndpointIsNotPublic(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return !Arrays.asList(SecurityConfiguration.ENDPOINTS_WITH_AUTHENTICATION_NOT_REQUIRED).contains(requestURI);
}

}

7. Definindo as demais configurações de segurança do Spring Security

Agora nós voltaremos a classe SecurityConfiguration que criamos anteriormente, e adicionaremos o restante das configurações que faltavam:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Autowired
private UserAuthenticationFilter userAuthenticationFilter;

public static final String [] ENDPOINTS_WITH_AUTHENTICATION_NOT_REQUIRED = {
"/users/login", // Url que usaremos para fazer login
"/users" // Url que usaremos para criar um usuário
};

// Endpoints que requerem autenticação para serem acessados
public static final String [] ENDPOINTS_WITH_AUTHENTICATION_REQUIRED = {
"/users/test"
};

// Endpoints que só podem ser acessador por usuários com permissão de cliente
public static final String [] ENDPOINTS_CUSTOMER = {
"/users/test/customer"
};

// Endpoints que só podem ser acessador por usuários com permissão de administrador
public static final String [] ENDPOINTS_ADMIN = {
"/users/test/administrator"
};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.csrf().disable() // Desativa a proteção contra CSRF
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Configura a política de criação de sessão como stateless
.and().authorizeHttpRequests() // Habilita a autorização para as requisições HTTP
.requestMatchers(ENDPOINTS_WITH_AUTHENTICATION_NOT_REQUIRED).permitAll()
.requestMatchers(ENDPOINTS_WITH_AUTHENTICATION_REQUIRED).authenticated()
.requestMatchers(ENDPOINTS_ADMIN).hasRole("ADMINISTRATOR") // Repare que não é necessário colocar "ROLE" antes do nome, como fizemos na definição das roles
.requestMatchers(ENDPOINTS_CUSTOMER).hasRole("CUSTOMER")
.anyRequest().denyAll()
// Adiciona o filtro de autenticação de usuário que criamos, antes do filtro de segurança padrão do Spring Security
.and().addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

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

}

Repare que foram adicionados três métodos a essa classe, são eles:

  1. Primeiro, é importante notar que as três classes estão anotadas com @Bean o que significa que esses métodos retornam objetos que são gerenciados pelo contâiner do spring.
  2. securityFilterChain(): Este método cria uma SecurityFilterChain, que é a configuração principal de segurança do Spring Security para a aplicação. Ele define a política de autorização para os endpoints da API REST.
  3. authenticationManager(): Este método retorna uma instância de AuthenticationManager. Essa instância é utilizada pelo Spring Security para realizar a autenticação de um usuário.
  4. passwordEncoder(): Este método retorna uma instância de PasswordEnconder que é utilizada pelo Spring Security para codificar as senhas dos usuários de forma segura, protegendo as informações confidenciais. No tutorial, estamos usando o algoritmo bcrypt para codificar essas senhas.

8. Criando a estrutura para testarmos as configuração de segurança que criamos

Terminamos as configurações de segurança, agora temos que implementar os endpoints e a lógica da nossa aplicação:

  1. Começaremos pelos DTOs que criaremos usando o tipo Record:
public record CreateUserDto(

String email,
String password,
RoleName role

) {
}
public record LoginUserDto(

String email,
String password

) {
}
public record RecoveryJwtTokenDto(

String token

) {
}
public record RecoveryUserDto(

Long id,
String email,
List<Role> roles

) {
}

2. Agora implementaremos os controller:

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@PostMapping("/login")
public ResponseEntity<RecoveryJwtTokenDto> authenticateUser(@RequestBody LoginUserDto loginUserDto) {
RecoveryJwtTokenDto token = userService.authenticateUser(loginUserDto);
return new ResponseEntity<>(token, HttpStatus.OK);
}

@PostMapping
public ResponseEntity<Void> createUser(@RequestBody CreateUserDto createUserDto) {
userService.createUser(createUserDto);
return new ResponseEntity<>(HttpStatus.CREATED);
}

@GetMapping("/test")
public ResponseEntity<String> getAuthenticationTest() {
return new ResponseEntity<>("Autenticado com sucesso", HttpStatus.OK);
}

@GetMapping("/test/customer")
public ResponseEntity<String> getCustomerAuthenticationTest() {
return new ResponseEntity<>("Cliente autenticado com sucesso", HttpStatus.OK);
}

@GetMapping("/test/administrator")
public ResponseEntity<String> getAdminAuthenticationTest() {
return new ResponseEntity<>("Administrador autenticado com sucesso", HttpStatus.OK);
}

}

Nós Criamos três métodos para endpoints diferentes: getAuthenticationTest(), getCustomerAuthenticationTest(), getAdminAuthenticationTest(), que respectivamente, permitem acesso com um usuário autenticado, um usuário autenticado com permissão de cliente, e um usuário autenticado com permissão administrador.

3. E por fim o service:

@Service
public class UserService {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtTokenService jwtTokenService;

@Autowired
private UserRepository userRepository;

@Autowired
private SecurityConfiguration securityConfiguration;

// Método responsável por autenticar um usuário e retornar um token JWT
public RecoveryJwtTokenDto authenticateUser(LoginUserDto loginUserDto) {
// Cria um objeto de autenticação com o email e a senha do usuário
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(loginUserDto.email(), loginUserDto.password());

// Autentica o usuário com as credenciais fornecidas
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

// Obtém o objeto UserDetails do usuário autenticado
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

// Gera um token JWT para o usuário autenticado
return new RecoveryJwtTokenDto(jwtTokenService.generateToken(userDetails));
}

// Método responsável por criar um usuário
public void createUser(CreateUserDto createUserDto) {

// Cria um novo usuário com os dados fornecidos
User newUser = User.builder()
.email(createUserDto.email())
// Codifica a senha do usuário com o algoritmo bcrypt
.password(securityConfiguration.passwordEncoder().encode(createUserDto.password()))
// Atribui ao usuário uma permissão específica
.roles(List.of(Role.builder().name(createUserDto.role()).build()))
.build();

// Salva o novo usuário no banco de dados
userRepository.save(newUser);
}
}

9. Testando a autenticação e autorização da nossa aplicação

Agora nós podemos testar nosso código no Postman e verificar a autenticação e autorização que configuramos na nossa aplicação:

1. Criando um usuário:

Criando um usuário com permissão de cliente

2. Logando e obtendo um token JWT:

Logando um cliente e obtendo um token jwt para usar em futuras requisições

3. Testando endpoint com autorização “comum” (que tanto o cliente quanto o administrador podem acessar), para isso nós copiamos o token gerado na requisição anterior sem as aspas, e colamos no Authorization:

Testando endpoint com autorização “comum”

4. Testando endpoint com autorização apenas para usuários que têm permissão de cliente:

Testando endpoint com permissão de cliente

5. Testando endpoint com autorização apenas para usuários que têm permissão de administrador utilizando o token de um cliente:

Tentando acessar um com permissão de administrador com um token de cliente

Se você chegou até aqui, você concluiu o tutorial com sucesso :)

--

--

Felipe Acelino
Felipe Acelino

Written by Felipe Acelino

Desenvolvedor backend, Bac. em Sistemas de Informação pela UFPB, atualmente trabalhando na Compass.uol. Linkedin: https://www.linkedin.com/in/felipe-acelino/

Responses (4)