Artigo original: https://www.freecodecamp.org/news/how-to-setup-jwt-authorization-and-authentication-in-spring/

Há pouco tempo, tive a chance de implementar a autorização em JWT para um projeto à parte. Trabalhei anterior com JWT em Ruby on Rails, mas essa foi a primeira vez com o Spring.

Neste artigo, tentarei explicar o que aprendi e apliquei no meu projeto para compartilhar minha experiência e, espero, ajudar algumas pessoas.

Começaremos examinando rapidamente a teoria por trás do JWT e como ele funciona. Em seguida, veremos como implementá-lo em uma aplicação com Spring Boot.

Fundamentos de JWT

JWT, ou JSON Web Tokens (RFC 7519 - texto em inglês), é um padrão que, em grande parte, é usado para a segurança em APIs REST. Embora seja uma tecnologia relativamente nova, ela vem ganhando popularidade rapidamente.

No processo de autorização do JWT, o front-end (o client) primeiramente envia algumas credenciais para se autenticar (nome de usuário e senha, em nosso caso, já que estamos trabalhando com uma autenticação para a web).

O servidor (a aplicação com Spring, em nosso caso), em seguida, verifica essas credenciais. Se elas forem válidas, o servidor gera um JWT e o retorna.

Depois desse passo, o client precisa fornecer esse token no cabeçalho Authorization da solicitação, no formulário “Bearer TOKEN” (Token do portador). O back-end verificará a validade desse token e autorizará ou rejeitará as solicitações. O token também pode armazenar funções de usuário e autorizar as solicitações com base na autoridade fornecida.

1

Implementação

Vamos ver agora como podemos implementar o login em JWT e o mecanismo de salvamento em uma aplicação em Spring real.

Dependências

Você pode ver abaixo uma lista das dependências do Maven que nosso código de exemplo usa. Observe que as dependências principais, como o Spring Boot e o Hibernate, ainda não estão incluídas nessa captura de tela.

2-1

Salvamento de usuários

Começaremos criando os controllers para salvar os usuários com segurança e autenticá-los com base no nome e na senha.

Temos uma entidade modelo chamada User (Usuário). É uma classe de entidade simples que mapeiam para a tabela USER. Você pode usar as propriedades que quiser e necessitar, dependendo de sua aplicação.

3-1

Também temos uma classe UserRepository (Repositório de usuários) simples para salvar os usuários. Precisamos sobrescrever o método findByUsername (encontrar por nome de usuário), já que vamos usá-lo na autenticação.

public interface UserRepository extends JpaRepository<User, String>{ 
    User findByUsername(String username); 
}

Jamais devemos armazenar senhas em texto simples no banco de dados, pois muitos usuários costumam usar a mesma senha para vários sites.

Existem muitos algoritmos de hashing diferentes. O mais comumente utilizado é o BCrypt, sendo recomendado para um hashing seguro. Confira este artigo (em inglês) para obter mais informações sobre o tópico.

Para fazer o hashing de uma senha, definiremos um bean do BCrypt em @SpringBootApplication e faremos a anotação da classe principal, como vemos abaixo:

@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder(); 
}

Chamaremos os métodos nesse bean quando precisarmos fazer o hashing de uma senha.

Também precisaremos de um UserController para salvar os usuários. Criamos o controller, fazemos a anotação com @RestController e definimos o mapeamento correspondente.

Em nossa aplicação, salvamos o usuário com base em um objeto DTO passado para o front-end. Também é possível passar um objeto User em @RequestBody.

Depois de passarmos o objeto DTO, criptografamos o campo da senha usando o bean do BCrypt que criamos anteriormente. Também é possível fazer isso no controller, mas é uma prática recomendada colocar essa lógica na classe do serviço.

@Transactional(rollbackFor = Exception.class) 
public String saveDto(UserDto userDto) { 
    userDto.setPassword(bCryptPasswordEncoder
           .encode(userDto.getPassword())); 
    return save(new User(userDto)).getId(); 
}

Filtro de autenticação

Precisamos da autenticação para garantir que o usuário seja realmente quem ele diz que é. Usaremos o par clássico de nome de usuário/senha para realizar isso.

Estes são os passos para implementar a autenticação:

  1. Criar nosso Authentication Filter (filtro de autenticação), que é uma extensão de UsernamePasswordAuthenticationFilter
  2. Criar uma classe de configuração de segurança, que é uma extensão de WebSecurityConfigurerAdapter e aplicar o filtro

Este é o código para o nosso Authentication Filter – como você já deve saber, os filtros são a base do Spring Security.

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/api/services/controller/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException {
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(Algorithm.HMAC512(SECRET.getBytes()));
String body = ((User) auth.getPrincipal()).getUsername() + " " + token;
res.getWriter().write(body);
res.getWriter().flush();
}
}

Ver o JWTAuthenticationFilter.java, hospedado com ❤ pelo GitHub

Vamos analisar esse código passo a passo.

Essa classe é a extensão de UsernamePasswordAuthenticationFilter, que é a classe padrão para a autenticação de senha no Spring Security. Estendemos essa classe para definir nossa lógica de autenticação personalizada.

Fazemos uma chamada para o método setFilterProcessesUrl em nosso construtor. Esse método define o URL de login padrão para o parâmetro fornecido.

Se removermos essa linha, o Spring Security cria o endpoint "/login" por padrão. Ele define o endpoint de login por nós, motivo pelo qual não definimos um endpoint de login em nosso controller explicitamente.

Depois dessa linha, nosso endpoint de login será /api/services/controller/user/login. Você pode usar essa função para se manter consistente com seus endpoints.

Sobrescrevemos os métodos attemptAuthentication esuccessfulAuthentication da classe UsernameAuthenticationFilter.

A função attemptAuthentication é executada quando o usuário tentar fazer o login em nossa aplicação. Ela lê as credenciais, cria um POJO de usuário para eles e verifica as credenciais para a autenticação.

Passamos o nome de usuário, a senha e uma lista vazia. A lista vazia representa as autoridades (funções) e a deixamos assim, já que ainda não temos funções em nossa aplicação.

Se a autenticação tiver sucesso, o método successfulAuthentication é executado. Os parâmetros desse método são passados pelo Spring Security internamente.

O método attemptAuthentication retorna um objeto Authentication que contém as autoridades que passamos ao tentar.

Queremos retornar um token para o usuário após o sucesso da autenticação, por isso criamos o  token usando o nome do usuário, o segredo (secret, em inglês) e a data de validade (expiration date, em inglês). Precisamos definir SECRET e EXPIRATION_DATE agora.

public class SecurityConstants {
public static final String SECRET = "SECRET_KEY";
public static final long EXPIRATION_TIME = 900_000; // 15 mins
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
public static final String SIGN_UP_URL = "/api/services/controller/user";
}

Ver o SecurityConstants.java, hospedado com ❤ pelo GitHub

Criamos uma classe para que ela seja um contêiner para as nossas constantes. Você pode definir o segredo de modo que ele seja o que você quiser, mas a prática recomendada é tornar a chave do segredo tão grande quanto o seu hash. Usamos o algoritmo HS256 neste exemplo. Por isso, nosso chave de segredo é de 256 bits/32 caracteres.

O tempo de validade é definido como sendo de 15 minutos, pois é a prática recomendada contra ataques de força bruta contra a chave do segredo. O tempo é em milissegundos.

Preparamos nosso filtro de autenticação, mas ele ainda não está ativo. Também precisamos de um filtro de autorização. Em seguida, aplicaremos os dois por meio de uma classe de configuração.

Este filtro verificará a existência e a validade do token de acesso no cabeçalho Authorization. Especificaremos quais endpoints estarão sujeitos a esse filtro em nossa classe de configuração.

Filtro de autorização

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
// Reads the JWT from the Authorization header, and then uses JWT to validate the token
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = JWT.require(Algorithm.HMAC512(SECRET.getBytes()))
.build()
.verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
if (user != null) {
// new arraylist means authorities
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}

Ver o JWTAuthorizationFilter.java, hospedado com ❤ pelo GitHub

O método doFilterInternal intercepta as solicitações e verifica o cabeçalho Authorization. Se o cabeçalho não estiver presente ou não iniciar com "BEARER" (portador), ele segue com a cadeia de filtros.

Se o cabeçalhos estiver presente, o método getAuthentication é invocado. getAuthentication verifica o JWT e, se o token for válido, ele retorna um token de acesso que o Spring usará internamente.

Este novo token, então, é salve em SecurityContext (contexto de segurança). Você também pode passar Authorities por esse token se precisar de autorização baseada em funções.

Nossos filtros estão prontos. Agora, precisamos colocá-los em ação com a ajuda de uma classe de configuração.

Configuração

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private UserDetailsServiceImpl userDetailsService;
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserDetailsServiceImpl userService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userDetailsService = userService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}

Ver o WebSecurity.java, hospedado com ❤ pelo GitHub

Anotamos essa classe com @EnableWebSecurity e estendemos WebSecurityConfigureAdapter para que implemente nossa lógica de segurança personalizada.

Ativamos automaticamente (auto-wire) o bean do BCrypt que definimos anteriormente. Também ativamos automaticamente UserDetailsService para que encontre a conta do usuário.

O método mais importante é aquele que aceita um objeto HttpSecurity. Aqui, especificamos os endpoints e filtros seguros que queremos aplicar. Configuramos o CORS e, em seguida, permitimos todas as solicitações de post ao nosso URL de entrada, que definimos na classe das constantes.

É possível adicionar outros ant matchers para filtrar com base em padrões de URL e funções. Você pode conferir esta pergunta do StackOverflow (em inglês) para ver exemplos relacionados a isso. O outro método configura o AuthenticationManager (gerenciador de autenticação) para que use nosso objeto de codificação (encoder) como seu codificador de senhas enquanto verifica as credenciais.

Testes

Vamos enviar algumas solicitações para testar se está funcionando corretamente.

4

Aqui, enviamos uma solicitação GET para acessar um recurso protegido. Nosso servidor responde com um código 403. Esse é o comportamento esperado, pois não fornecemos um token no cabeçalho. Vamos, agora, criar um usuário:

5

Para criar um usuário, enviamos uma solicitação de post com nossos dados do User DTO. Com esse usuário, faremos o login e obteremos um token de acesso.

6

Ótimo! Temos o token. Depois disso, usaremos esse token para acessar os recursos protegidos.

7

Fornecemos o token no cabeçalho Authorization e agora temos permissão de acesso ao nosso endpoint protegido.

Conclusão

Neste tutorial, examinamos passo a passo a implementação da autenticação de senha e da autorização com o JWT no Spring. Também aprendemos como salvar um usuário com segurança.

Obrigado pela leitura – espero que tenha sido útil para você. Se estiver interessado em ler mais conteúdo a respeito do assunto, fique à vontade para se inscrever no blog do autor: https://erinc.io (em inglês). 🙂