JWT

Definição

  • JWT = JSON Web Token

Autenticação VS Autorização

  • Autenticação

    • Definição: validar a identidade de uma pessoa

    • Métodos de autenticação: formulário de login, HTTP authentication ou HTTP authentication customizada

  • Autorização

    • Definição: permissões concedidas a uma pessoa/grupo de pessoas

    • Métodos de autorização: URLs de controle de acesso, lista de controle de acesso (ACLs)

Estrutura do JWT

  • Header: algoritmo e tipo

  • Payload: contém informações do dono do token (permissões, autorizações, informações do proprietário do token)

  • Signature: informações do header e payload encriptadas

Fluxo de uma requisição com JWT

Implementação com Spring Security

  • Criação da classe AppUser

    • OBS: no Spring Security existe uma classe com nome User e para evitar conflitos, o nome padrão para essa classe na aplicação é AppUser

    • Campo username: é comum utilizar email ou nome do usuário

    • Exemplo

      @Entity
      @NoArgsConstructor
      @AllArgsConstructor
      @Data
      public class AppUser {
       
          @Id
          @GeneratedValue(strategy = IDENTITY)
          private Long id;
       
          private String name;
       
          private String username;
       
          private String password;
       
          @ManyToMany(fetch = EAGER)
          private Collection<Role> roles = new ArrayList<>();
       
      }

      fetch = EAGER: ao fazer carregar o usuário(s), as Roles relacionadas com esse usuário serão carregadas também

  • Criação da classe Role

    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Role {
     
        @Id
        @GeneratedValue(strategy = IDENTITY)
        private Long id;
        
        private String name;
     
    }
  • Criação da camada de acesso ao banco de dados (UserRepository e RoleRepository)

    public interface UserRepository extends JpaRepository<AppUser, Long> {
     
        AppUser findByUsername(String username);
     
    }
    public interface RoleRepository extends JpaRepository<Role, Long> {
     
        Role findByName(String name);
     
    }
  • Criação da camada de serviço (UserService)

    public interface UserService {
     
        AppUser saveUser(AppUser appUser);
     
        Role saveRole(Role role);
     
        void addRoleToUser(String username, String roleName);
     
        AppUser getUser(String username);
     
        List<AppUser> getUsers();
     
    }
    @Service
    @Transactional
    @Slf4j
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class UserServiceImpl implements UserService {
     
        private final UserRepository USER_REPOSITORY;
        private final RoleRepository ROLE_REPOSITORY;
     
        @Override
        public AppUser saveUser(AppUser appUser) {
            appUser.setPassword(PASSWORD_ENCODER.encode(appUser.getPassword()));
     
            return USER_REPOSITORY.save(appUser);
        }
     
        @Override
        public Role saveRole(Role role) {
            return ROLE_REPOSITORY.save(role);
        }
     
        @Override
        public void addRoleToUser(String username, String roleName) {
            AppUser appUser = USER_REPOSITORY.findByUsername(username);
            Role role = ROLE_REPOSITORY.findByName(roleName);
     
            appUser.getRoles().add(role);
     
            // Por esta classe estar anotado com @Transactional, qualquer alteração na entidade é salva automaticamente
        }
     
        @Override
        public AppUser getUser(String username) {
            return USER_REPOSITORY.findByUsername(username);
        }
     
        @Override
        public List<AppUser> getUsers() {
            return USER_REPOSITORY.findAll();
        }
    }
    • Usuário e senha padrão do Spring Security

      • Usuário: user

      • Senha: gerada automaticamente e impressa no console

      • Fazer login: localhost:8080/login

    • Configurações de autenticação e URLs de controle de acesso

      @Configuration
      @EnableWebSecurity
      @RequiredArgsConstructor(onConstructor = @__(@Autowired))
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
       
          private final UserDetailsService USER_DETAILS_SERVICE;
          private final BCryptPasswordEncoder B_CRYPT_PASSWORD_ENCODER;
       
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(USER_DETAILS_SERVICE)
                      .passwordEncoder(B_CRYPT_PASSWORD_ENCODER);
          }
       
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
              customAuthenticationFilter.setFilterProcessesUrl("/api/login");
       
              http.csrf().disable();
              http.sessionManagement().sessionCreationPolicy(STATELESS);
       
              http.authorizeRequests().antMatchers("/api/login/**").permitAll();
              http.authorizeRequests().antMatchers(GET, "/api/users/**").hasAuthority("ROLE_USER");
              http.authorizeRequests().antMatchers(POST, "/api/users/**").hasAuthority("ROLE_ADMIN");
              http.authorizeRequests().anyRequest().authenticated();
       
              http.addFilter(customAuthenticationFilter);
          }
       
          @Bean
          @Override
          public AuthenticationManager authenticationManagerBean() throws Exception {
              return super.authenticationManagerBean();
          }
      }
    • Adicionar filters

      • attemptAuthentication: esse método será chamado toda vez que um usuário tentar logar

      • successfulAuthentication: esse método será chamada quando o usuário logar com sucesso

      @Slf4j
      @RequiredArgsConstructor(onConstructor = @__(@Autowired))
      public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
       
          private final AuthenticationManager AUTHENTICATION_MANAGER;
       
          @Override
          public Authentication attemptAuthentication(
                  HttpServletRequest request,
                  HttpServletResponse response
          ) throws AuthenticationException {
              String username = request.getParameter("username");
              String password = request.getParameter("password");
       
              log.info("Username is: {}", username);
              log.info("Password is: {}", password);
       
              UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       
              return AUTHENTICATION_MANAGER.authenticate(authenticationToken);
          }
       
          @Override
          protected void successfulAuthentication(
                  HttpServletRequest request,
                  HttpServletResponse response,
                  FilterChain chain,
                  Authentication authentication
          ) throws IOException, ServletException {
              User user = (User) authentication.getPrincipal();
       
              Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
       
              String access_token = JWT.create()
                      .withSubject(user.getUsername())
                      .withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1_000))
                      .withIssuer(request.getRequestURI())
                      .withClaim(
                              "roles",
                              user.getAuthorities().stream()
                                      .map(GrantedAuthority::getAuthority)
                                      .collect(Collectors.toList())
                      )
                      .sign(algorithm);
              String refresh_token = JWT.create()
                      .withSubject(user.getUsername())
                      .withExpiresAt(new Date(System.currentTimeMillis() + 30 * 60 * 1_000))
                      .withIssuer(request.getRequestURI())
                      .sign(algorithm);
       
              Map<String, String> tokens = new HashMap<>() {{
                  put("access_token", access_token);
                  put("refresh_token", refresh_token);
              }};
       
              response.setContentType(APPLICATION_JSON_VALUE);
              new ObjectMapper().writeValue(response.getOutputStream(), tokens);
          } 
      }
      • Access Token: geralmente, possuem data de expiração baixa (no máximo algumas horas)

      • Refresh Token: geralmente, possuem data de expiração bem maior em relação ao Access Token (até 6 meses)

      • Verificar o conteúdo de um token JWT: https://jwt.io/

      OBS: “secret”, em ambiente de produção, NÃO DEVE estar hard coded (SOLUÇÃO: encriptar e desencriptar “secret” a partir de classe utilitária)

    • Permitir requisições conforme as permissões do token JWT do usuário

      @Slf4j
      public class CustomAuthorizationFilter extends OncePerRequestFilter {
       
        @Override
        protected void doFilterInternal(
                HttpServletRequest request,
                HttpServletResponse response,
                FilterChain filterChain
        ) throws ServletException, IOException {
            if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh")) {
                filterChain.doFilter(request, response);
                return;
            }
       
            String authorizationHeader = request.getHeader(AUTHORIZATION);
       
            if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
                filterChain.doFilter(request, response);
                return;
            }
       
            try {
                String token = authorizationHeader.substring("Bearer ".length());
       
                Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
                JWTVerifier verifier = JWT.require(algorithm).build();
       
                DecodedJWT decodedJWT = verifier.verify(token);
       
                String username = decodedJWT.getSubject();
                String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
       
                List<SimpleGrantedAuthority> authorities = stream(roles)
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
       
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        username,
                        null,
                        authorities
                );
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
       
                filterChain.doFilter(request, response);
       
            } catch (Exception e) {
                log.error("Error logging in: {}", e.getMessage());
       
                Map<String, String> tokens = new HashMap<>() {{
                    put("error", e.getMessage());
                }};
       
                response.setContentType(APPLICATION_JSON_VALUE);
                response.setStatus(FORBIDDEN.value());
                new ObjectMapper().writeValue(response.getOutputStream(), tokens);
            }
        }
       
      }

      Toda requisição passará pelo método doFilterInternal, que por sua vez fará a verificação das permissões do usuário e permitir o acesso a um determinado recurso da aplicação (autorização)