Spring boot JWT

Fala pessoal! Finalmente vamos implementar autenticação JWT. Como de costume vamos usar o projeto criado nesse post onde já implementamos algumas regras com Spring Secutiry e um endpoint para cadastrar um novo usuário. Podemos controlar via arquivo de configuração se esse endpoint vai ficar público ou não.

Vamos lá! Primeiramente vamos renomear a classe config.ConfigProperties para config.AuthConfigProperties. Isso por que vamos adicionar outras propriedades com o mesmo pré-fixo auth e caso seja necessário criaremos outra classe com outro pré-fixo

     package com.br.davifelipe.springjwt.config;
    import org.springframework.boot.context.properties.ConfigurationProperties;
     import org.springframework.context.annotation.Configuration;
    import lombok.Data;
    @Configuration
     @ConfigurationProperties(prefix = "auth")
     @Data
     public class AuthConfigProperties {
         
         private boolean publicSingUpUrlEnable;
         private String jwtSecret;
         private Long jwtExpirationMiliseg;
     }

Adicionamos jwtSecret que vai ser a palavra secreta utilizada para gerar o token JWT e o tempo de expiração do token em milisegundos.

Em seguida adicionamos os valores no arquivo application-dev.properties

     spring.h2.console.enabled=true
     spring.h2.console.path=/h2-console
     spring.datasource.url=jdbc:h2:file:~/test
     spring.datasource.username=sa
     spring.datasource.password=
     spring.datasource.driver-class-name=org.h2.Driver
     spring.jpa.show-sql=true
     spring.jpa.properties.hibernate.format_sql=true
     auth.public-sing-up-url-enable=true
     auth.jwt-secret=MySecretJwtWord
     auth.jwt-expiration-miliseg=86400000

Agora vamos criar uma classe JWTUtil para gerar o token a partir do e-mail do usuário

     package com.br.davifelipe.springjwt.config;
    import java.util.Date;
    import org.springframework.beans.factory.annotation.Value;
     import org.springframework.stereotype.Component;
    import io.jsonwebtoken.Jwts;
     import io.jsonwebtoken.SignatureAlgorithm;
    @Component
     public class JWTUtil {
        @Value("${auth.jwt-secret}")
         private String jwtSecret;
         
         @Value("${auth.jwt-expiration-miliseg}")
         private Long jwtExpirationMisiseg;
         
         public String genereteToken(String email) {
             return Jwts.builder()
                         .setSubject(email)
                         .setExpiration(new Date(System.currentTimeMillis() + this.jwtExpirationMisiseg))
                         .signWith(SignatureAlgorithm.HS512, this.jwtSecret.getBytes())
                         .compact();
         }
     }
     

Antes de imlementar a autententicação, temos que criar um model para definir o perfil(role) do usuário. Para isso, vamos criar Model, Reposiroty, e service para Role e um relacionamente many to may entre Role e User. Esses perfis servem para controlar os níveis de permissão de cada usuário.

     package com.br.davifelipe.springjwt.model;
    import java.util.ArrayList;
     import java.util.Collection;
     import java.util.List;
     import java.util.stream.Collectors;
    import javax.persistence.Entity;
     import javax.persistence.GeneratedValue;
     import javax.persistence.GenerationType;
     import javax.persistence.Id;
     import javax.persistence.JoinColumn;
     import javax.persistence.JoinTable;
     import javax.persistence.ManyToMany;
    import org.hibernate.annotations.LazyCollection;
     import org.hibernate.annotations.LazyCollectionOption;
     import org.springframework.security.core.GrantedAuthority;
     import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import lombok.Data;
     import lombok.EqualsAndHashCode;
     import lombok.NoArgsConstructor;
     import lombok.ToString;
    @Entity
     @Data
     @NoArgsConstructor
     @ToString(exclude="id")
     @EqualsAndHashCode(exclude={"name","email","password"})
     public class User {
         
         @Id
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         private Integer id;
         private String name;
         private String email;
         private String password;
         
         @ManyToMany()
         @LazyCollection(LazyCollectionOption.FALSE)
         @JoinTable( 
             name = "USERS_ROLES", 
             joinColumns = @JoinColumn(
               name = "user_id", referencedColumnName = "id"), 
             inverseJoinColumns = @JoinColumn(
               name = "role_id", referencedColumnName = "id")) 
         private List<Role> roles = new ArrayList<Role>();
         
         public Collection<? extends GrantedAuthority> getAutorities() {
             
             Collection<? extends GrantedAuthority> authorities = this.getRoles()
                      .stream()
                      .map(r -> new SimpleGrantedAuthority(r.getName()))
                      .collect(Collectors.toList());
             return authorities;
         }
         
         public void addRole(Role role) {
             this.roles.add(role);
         }
     }
     

Model 

     package com.br.davifelipe.springjwt.model;
    import java.util.ArrayList;
     import java.util.Collection;
    import javax.persistence.Entity;
     import javax.persistence.GeneratedValue;
     import javax.persistence.GenerationType;
     import javax.persistence.Id;
     import javax.persistence.ManyToMany;
    import lombok.Data;
    @Entity
     @Data
     public class Role {
      
         @Id
         @GeneratedValue(strategy = GenerationType.AUTO)
         private Integer id;
      
         private String name;
         @ManyToMany(mappedBy = "roles")
         private Collection<User> users = new ArrayList<User>();
         
         public void addUser(User user) {
             this.users.add(user);
         }
       
     }

Repository

     package com.br.davifelipe.springjwt.repositories;
    import java.util.Optional;
    import org.springframework.data.jpa.repository.JpaRepository;
     import org.springframework.data.repository.query.Param;
     import org.springframework.stereotype.Repository;
     import org.springframework.transaction.annotation.Transactional;
    import com.br.davifelipe.springjwt.model.Role;
    @Repository
     public interface RoleRepository extends JpaRepository<Role, Integer>{
         @Transactional(readOnly = true)
         Optional<Role> findByName(@Param("name") String name);
     }
     

Service

     package com.br.davifelipe.springjwt.services;
    import java.util.Optional;
    import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.stereotype.Service;
    import com.br.davifelipe.springjwt.model.Role;
     import com.br.davifelipe.springjwt.repositories.RoleRepository;
    @Service
     public class RoleService {
         
         @Autowired
         private RoleRepository repo;
         
         /**
          * Find Role by id
          * @param  id of the object
          * @return object found or null if the object were not found
          * */
         public Role findById(Integer id) {
             Optional<Role> obj = this.repo.findById(id);
             return obj.orElse(null);
         }
         
         /**
          * Find Role by name
          * @param  name of the object
          * @return object found or null if the object were not found
          * */
         public Role findByName(String name) {
             Optional<Role> obj = this.repo.findByName(name);
             return obj.orElse(null);
         }
         
         /**
          * Find or insert a new role if not exists
          * @param name of role object
          * @return object found or created
          * */
         public Role findOrInsertByName(String name) {
             Role newRole = null;
             newRole = this.findByName(name);
             if(newRole == null) {
                 newRole = new Role();
                 newRole.setId(null);
                 newRole.setName(name);
                 this.repo.save(newRole);
             }
             return newRole;
         }
     }
     

 Agora vamos criar um DTO somente com e-mail e senha para fazer o login

     package com.br.davifelipe.springjwt.dto;
    import javax.validation.constraints.Email;
     import javax.validation.constraints.NotNull;
     import javax.validation.constraints.Size;
    import lombok.Data;
    @Data
     public class SingInDTO{
         
         @NotNull
         @Email
         private String email;
         
         @NotNull
         @Size(min=2)
         private String password;
     }
     

Teoricamente, iríamos criar um resource recebendo um objeto SingInDTO, chamar o JWT Util, validar o login e senha e devolver o token. Mas nesse caso vamos fazer uma implementação da interface org.springframework.security.core.userdetails.UserDetails do Spring security, uma implementação dessa outra interface org.springframework.security.core.userdetails.UserDetailsService um filtro extendendo de org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter e depois adicionar esse filtro em Security config.

A implementação de UserDatails decidi chamá-la de MyUserDatails

    package com.br.davifelipe.springjwt.security;
   import java.util.Collection;
   import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
   import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
   @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class MyUserDatails implements UserDetails{
        
        private static final long serialVersionUID = 1L;
        
        private Integer id;
        private String email;
        private String pass;
        private Collection<? extends GrantedAuthority> authorities;
        
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
       @Override
        public String getPassword() {
            return this.pass;
        }
       @Override
        public String getUsername() {
            return this.email;
        }
       @Override
        public boolean isAccountNonExpired() {
            return true;
        }
       @Override
        public boolean isAccountNonLocked() {
            return true;
        }
       @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
       @Override
        public boolean isEnabled() {
            return true;
        }
   }
    

A implementação de UserDatailsService chamamos de UserDatailsServiceImpl

    package com.br.davifelipe.springjwt.services;
   import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
   import com.br.davifelipe.springjwt.model.User;
    import com.br.davifelipe.springjwt.repositories.UserRepository;
    import com.br.davifelipe.springjwt.security.MyUserDatails;
   @Service
    public class UserDatailsServiceImpl implements UserDetailsService {
        
        @Autowired
        private UserRepository repositoryUser;
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = repositoryUser.findByEmail(username);
            if(user == null) {
                throw new UsernameNotFoundException("User not found: "+username);
            }
            
            return new MyUserDatails(user.getId(), user.getEmail(), user.getPassword(), user.getAutorities());
        }
    }
    

O JWTAuthenticationFilter vai no pacote security

    package com.br.davifelipe.springjwt.security;
   import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Date;
   import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
   import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
   import com.br.davifelipe.springjwt.config.JWTUtil;
    import com.br.davifelipe.springjwt.dto.SingInDTO;
    import com.fasterxml.jackson.databind.ObjectMapper;
   public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
       private AuthenticationManager authenticationManager;
        
        private JWTUtil jwtUtil;
       public JWTAuthenticationFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {
            setAuthenticationFailureHandler(new JWTAuthenticationFailureHandler());
            this.authenticationManager = authenticationManager;
            this.jwtUtil = jwtUtil;
        }
        
        @Override
        public Authentication attemptAuthentication(HttpServletRequest req,
                                                    HttpServletResponse res) throws AuthenticationException {
           try {
                SingInDTO creds = new ObjectMapper()
                        .readValue(req.getInputStream(), SingInDTO.class);
        
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(creds.getEmail(), creds.getPassword(), new ArrayList<>());
                
                Authentication auth = authenticationManager.authenticate(authToken);
                return auth;
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        
        @Override
        protected void successfulAuthentication(HttpServletRequest req,
                                                HttpServletResponse res,
                                                FilterChain chain,
                                                Authentication auth) throws IOException, ServletException {
        
            String username = ((MyUserDatails)auth.getPrincipal()).getEmail();
            String token = jwtUtil.genereteToken(username);
            res.addHeader("Authorization", "Bearer " + token);
            res.addHeader("access-control-expose-headers", "Authorization");
        }
        
        private class JWTAuthenticationFailureHandler implements AuthenticationFailureHandler {
             
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
                    throws IOException, ServletException {
                response.setStatus(401);
                response.setContentType("application/json"); 
                response.getWriter().append(json());
            }
            
            private String json() {
                  long date = new Date().getTime();
                  return "{\"timestamp\": " + date + ", "
                  + "\"status\": 401, "
                  + "\"error\": \"Not authorized\", "
                  + "\"message\": \"Invalid email or password\", "
                  + "\"path\": \"/login\"}";
            }
        }
    }
    

E no Security config adicionamos o filtro http.addFilter(new JWTAuthenticationFilter..

    package com.br.davifelipe.springjwt.config;
   import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
   import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;
    import org.springframework.http.HttpMethod;
    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.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
   import com.br.davifelipe.springjwt.security.JWTAuthenticationFilter;
   @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        public static final List<String> PUBLIC_MATCHERS = new ArrayList<String>();
        public static final List<String> PUBLIC_MATCHERS_GET = new ArrayList<String>();
        public static final List<String> PUBLIC_MATCHERS_POST = new ArrayList<String>();
        
        @Autowired
        JWTUtil jwtUtil;
        
        @Autowired
        UserDetailsService userDetailsService;
        
        @Autowired
        private Environment env;
        
        @Value("${auth.public-sing-up-url-enable}")
        private String publicSingUpUrlEnable;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            PUBLIC_MATCHERS.add("/h2-console/**");
            PUBLIC_MATCHERS_GET.add("/products/**");
            
            String[] activeProfiles = env.getActiveProfiles();
            
            if (Arrays.asList(activeProfiles).contains("dev")) {
                //disable it only for h2-console on dev envioment
                http.headers().frameOptions().disable();
            }
            
            if("true".equals(this.publicSingUpUrlEnable)) {
                PUBLIC_MATCHERS_POST.add("/auth/sing-up/**");
            }
            
            http.cors().and().csrf().disable();
            http.authorizeRequests()
                .antMatchers(PUBLIC_MATCHERS.toArray(new String[0])).permitAll()
                .antMatchers(HttpMethod.POST, PUBLIC_MATCHERS_POST.toArray(new String[0])).permitAll()
                .antMatchers(HttpMethod.GET, PUBLIC_MATCHERS_GET.toArray(new String[0])).permitAll()
                .anyRequest()
                .authenticated();
            http.addFilter(new JWTAuthenticationFilter(authenticationManager(), jwtUtil));
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
        
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(bcCryptPasswordEncoder());
        }
        
        @Bean
        CorsConfigurationSource configurationSource() {
            CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
            configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "DELETE", "OPTIONS"));
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
        
        @Bean
        public BCryptPasswordEncoder bcCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    

Antes de testar, vamos fazer uma alteração no endpoint sing-up para que ao inserir um novo usuário ele já tenha o perfil ROLE_USER. O pré-fixo padrão para nomes de perfis deve ser "ROLE_" Depois do pré-fixo, pode escolher o nome que preferir. É possível também alterar esse pré-fixo padrão. Mas por enquanto vamos manter dessa forma.

    package com.br.davifelipe.springjwt.resources;
   import java.net.URI;
   import javax.validation.Valid;
   import org.modelmapper.ModelMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    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 org.springframework.web.servlet.support.ServletUriComponentsBuilder;
   import com.br.davifelipe.springjwt.dto.SingUpDTO;
    import com.br.davifelipe.springjwt.model.Role;
    import com.br.davifelipe.springjwt.model.User;
    import com.br.davifelipe.springjwt.services.RoleService;
    import com.br.davifelipe.springjwt.services.UserService;
   @RestController
    @RequestMapping("/auth")
    public class AuthResource {
        
        @Autowired
        private UserService serviceUser;
        
        @Autowired
        private RoleService serviceRole;
        
        @PostMapping("/sing-up")
        public ResponseEntity<Void> singUp(@Valid @RequestBody SingUpDTO dto){
            
            ModelMapper modelMapper = new ModelMapper();
            User user = modelMapper.map(dto,User.class);
            
            Role roleUser = serviceRole.findOrInsertByName("ROLE_USER");
            
            user.addRole(roleUser);
            user = this.serviceUser.insert(user);
            
            URI uri = ServletUriComponentsBuilder
                      .fromCurrentContextPath().path("/{id}")
                      .buildAndExpand(user.getId())
                      .toUri();
            return ResponseEntity.created(uri).build();
        }
        
    }
    

Agora já podemos testar criando um novo usuário com endpoint sing-up

Imagem

E em seguida verificar se conseguimos fazer o login no endpoint /login

Imagem

O token é retornado no cabeçalho em Authentication. O endpoint /login é padrão do Spring. Por isso não precisamos implementar o Resource para esse endpoint. O Spring vai usar a implementação que fizemos de UserDetails e UserDetailsService para validar o usuário e senha e devolver o token

https://github.com/davifelipems/spring-backend-template/tree/spring_security

Comentários

 

Quem Sou

Graduado em ADS (Análise e desenvolvimento de sistemas).

Não sou "devoto" de nenhuma linguagem de programação. Procuro aproveitar o melhor de cada uma de acordo com a necessidade do projeto. Prezo por uma arquitetura bem feita, código limpo, puro e simples! 

anuncio atendente