Spring Boot Security – JWT
A proposta deste material é dar continuidade a implementação do processo de implementação de um sistema de autenticação de usuário utilizando o Spring Boot Secutiry, mas agora extendendo para a API utilizando o mecanismo de JWT. A fonte desse passo a passo pode ser encontrado no link: https://www.youtube.com/watch?v=X80nJ5T7YpE
Importante destacar que este tutorial é a segunda parte de um material que apresenta a utilização do Spring Boot Security, segue o link para a primeira parte.
Os JSON Web Tokens (JWT) são um padrão para criação de um token que representa uma reivindicação entre duas partes de forma segura. Fonte: https://jwt.io/introduction/

Funcionamento, o cliente deseja acessar a API
- 1) o Cliente envia suas credenciais para um Servidor de autenticação
- 2) o Servidor de autenticação valida as credenciais e gera um TOKEN que identifica o usuário e a sua permissão de acesso. Esse TOKEN é retornado para o cliente
- 3) o Cliente utiliza o TOKEN para fazer a requisição para a API, e a API é capaz de confirmar a validade do TOKEN para dai responder a requisição.
O primeiro passo é alterar o arquivo pom.xml para incluir as dependências do JJWT e o JAXB-API.

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
Em seguida vamos criar uma nova classe chamada JWTUtil dentro do pacote Security que será anotada como um Serviço. Essa classe será responsável por gerar e validar os tokens JWT.

package br.univille.dacs2020.security;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Service
public class JWTUtil {
private String SECRET_KEY= "SECRET";
public String extractUserName(String token){
return extractClaim(token, Claims::getSubject);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token){
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public String generateToken(UserDetails userDetails){
Map<String,Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}
public Boolean validateToken (String token, UserDetails userDetails){
final String userName = extractUserName(token);
return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
O próximo passo é criar uma nova classe na nossa aplicação no pacote de APIs para permitir que o Cliente passe as credenciais e receba o retorno do JWT.

package br.univille.dacs2020.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import br.univille.dacs2020.model.Usuario;
import br.univille.dacs2020.security.JWTUtil;
import br.univille.dacs2020.service.impl.MyUserDetailsService;
@RestController
@RequestMapping("/api/v1/auth")
public class AuthenticationControllerAPI {
@Autowired
private MyUserDetailsService serviceMyUserDetail;
@Autowired
private JWTUtil serviceJWT;
@PostMapping("/signin")
public ResponseEntity signin(@RequestBody Usuario usuario){
Usuario usuarioValido = serviceMyUserDetail.buscaUsuarioSenha(usuario.getUsuario(), usuario.getSenha());
UserDetails userDetails = serviceMyUserDetail.loadUserByUsername(usuarioValido.getUsuario());
String token = serviceJWT.generateToken(userDetails);
return ResponseEntity.ok(token);
}
}
Agora devemos alterar o código da classe SecurityConfigurer para incluir dois metodos que sobreescreverão os metodos de configuração do HTTPSecurity e o authenticationManager()

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/api/v1/auth/signin").permitAll()
.anyRequest().authenticated();
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
// TODO Auto-generated method stub
return super.authenticationManager();
}
Inicie a aplicação e utilizando o POSTMAN ou outro cliente de API, faça uma requisição POST para o endereço http://localhost:<PORTADASUAPALICACAO>/api/v1/auth/signin e no corpo da requisição passe um objeto JSON contendo os atributos usuario e senha.

{
"usuario": "admin",
"senha": "admin"
}
Se copiarmos o JWT e utilizarmos o site https://jwt.io/#debugger para decodificar o token, podemos verificar que as informações do payload podem ser visualizadas. E o mais importante com a SECRET_KEY a chave pode ter seu conteúdo validado através da verificação da assinatura.

Agora que temos como criar Tokens JWT, precisamos fazer com que todas as requisições solicitem esses tokens e façam a validação para isso vamos implementar um Filtro que será chamado toda vez q uma requisição acontecer, e ele será responsável por validar o token permitindo ou não a execução da requisição.

package br.univille.dacs2020.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import br.univille.dacs2020.service.impl.MyUserDetailsService;
@Component
public class JWTRequestFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService serviceMyUserDetail;
@Autowired
private JWTUtil serviceJWT;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
String username = null;
String token = null;
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
token = authorizationHeader.substring(7);
username = serviceJWT.extractUserName(token);
}
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = serviceMyUserDetail.loadUserByUsername(username);
if(serviceJWT.validateToken(token, userDetails)){
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
Não basta apenas criar o filtro, precisamos vincular ele ao mecanismo de segurança para que ele seja executado. Para isso vamos alterar a classe SecurityConfigurer para além de carregar o filtro e aplicá-lo modificar a forma de autenticação para STATELESS.

package br.univille.dacs2020.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import br.univille.dacs2020.service.impl.MyUserDetailsService;
@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private JWTRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/api/v1/auth/signin").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
// TODO Auto-generated method stub
return super.authenticationManager();
}
}
Por fim podemos acessar novamente o POSTMAN, copiar o token JWT que recebemos da requisição de autenticação, e fazer uma chamada para qualquer outro end point da nossa aplicação incluindo no Headers uma chave com o nome Authorization e o valor Bearer <JWT>
Bearer = Portador

Se o JWT não for enviado podemos observar que o retorno será um HTTP Status Code 403 de acesso negado.

Opcional: para que possamos voltar a acessar a interface HTML da nossa aplicação, devemos alterar a regra de autenticação do método configure HTTPSecurity

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/api/v1/auth/signin").permitAll()
.antMatchers("/api/**").authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}