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); }