DEV Community

guilhermegarcia86
guilhermegarcia86

Posted on • Updated on • Originally published at programadev.com.br

Spring Security e JWT

Implementar segurança em aplicações não é algo simples e requer atenção em muitos detalhes.

Pensando nisso o Spring Security surgiu para descomplicar muitos pontos e juntamente com o Spring Boot deixou o desenvolvimento de aplicações seguras muito mais produtivo e descomplicado, nesse post iremos implementar emuma aplicação uma API REST para autenticação e geração de um token JWT para completar as requisições de forma segura.

Criando o projeto

Usando o site spring initializr criamos um projeto base e já adicionamos as dependências do Spring Security, Spring Web e adicionamos a dependência do Json Web Token; como estamos criando um projeto Maven o pom.xml ficará assim:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.irs.register</groupId>
    <artifactId>register</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>register</name>
    <description>Register Service</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Enter fullscreen mode Exit fullscreen mode

No nosso pom.xml podemos ver além das dependências que foram mencionadas a cima, as dependências do Lombok, Spring Data, Actuator e DevTools que irão ajudar no desenvolvimento.

Habilitando o contexto de segurança

Para começarmos a configurar o Spring Security vamos criar uma classe e adicionaremos as anotações @EnableWebSecurity e @Configuration para que ela seja carregada e esteja configurada no inicio da aplicação. Além disso estendemos a classe WebSecurityConfigurerAdapter que expõe três métodos para fazer as configurações necessárias:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    //Configurations for authentication
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    }

    //Configuration for authorization
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    }

    //Configuration for static resources
    @Override
    public void configure(WebSecurity web) throws Exception {
    }
}

Enter fullscreen mode Exit fullscreen mode

Podemos observar que temos três métodos chamados configure mas que recebem parâmetros diferentes:

  • O primeiro configure que recebe AuthenticationManagerBuilder é o método que será usado para configurar a autenticação.
  • O segundo configure que recebe HttpSecurity é para a autorização das requisições.
  • O terceiro configure que recebe WebSecurity é para arquivos estáticos como CSS, JS, HTML, mas não usaremos nesse exemplo pois estamos criando uma API REST.

Como estamos usando o Actuator já recebemos alguns endpoints para monitorar a saúde da nossa aplicação, entre eles temos o /actuator/health que retorna se a aplicação está no ar ou instável, se iniciarmos a aplicação e tentarmos acessar esse endpoint receberemos a seguinte mensagem:

{
    "timestamp": "2020-12-11T22:29:51.654+00:00",
    "status": 403,
    "error": "Forbidden",
    "message": "Access Denied",
    "path": "/actuator/health"
}
Enter fullscreen mode Exit fullscreen mode

BLOCK!!!

Recebemos o status code 403 Forbidden nos indicando que a url não pode ser acessada pois o acesso foi negado.

Configurando Endpoint de Autenticação

Já temos as urls protegidas, agora vamos configurar a autenticação. Primeiramente vamos criar um Controller que receba uma requisição de login de um usuário:

@RestController
@RequestMapping("/auth")
public class AuthenticationController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenService tokenService;

    @PostMapping
    public ResponseEntity<TokenDTO> auth(@RequestBody @Validated LoginDTO loginDTO){
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUser(), loginDTO.getPass());

        Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

        String token = tokenService.generateToken(authentication);

        return ResponseEntity.ok(TokenDTO.builder().type("Bearer").token(token).build());

    }

}
Enter fullscreen mode Exit fullscreen mode

Esse Controller é um endpoint REST mapeado para /auth que tem uma entrada do tipo POST que recebe um objeto do tipo LoginDTO que possui o email e a senha e realiza a autenticação e devolve o token de acesso, podemos ver que já existem as classes de serviço do token e de autenticação, mais detalhes a diante.

Se tentarmos fazer um requisição do tipo POST com um usuário e senha vamos tomar um erro 403 Forbidden mas por que isso acontece?

Hummm???

Bom antes de mais nada devemos voltar para a nossa classe SecurityConfiguration e precisamos configurar quais rotas serão protegidas e quais rotas podem estar disponíveis, como é o caso da rota de login que deve estar aberta pois sem ela não há como um usuário se autenticar, vamos colocar também a configuração de sessão. Como estamos estamos usando a estratégia de token a configuração de sessão será STATELESS que significa que não será guardado estado como era feito antigamente, onde um usuário logado ficava com seus dados guardados no servidor e era enviado algum tipo de identificador, Cookie, para o client ou front-end para que nas próximas requisições fosse enviado esse Cookie para o servidor e ele saber quem é aquele usuário.

    //Configuration for authorization
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers(HttpMethod.POST, "/auth").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
Enter fullscreen mode Exit fullscreen mode

O objeto HttpSecurity possui uma API fluída o que significa que os métodos podem ser chamados em sequência e essa sequência forma uma frase explícita da intenção.

Lendo o código a cima temos a seguinte intenção: Autorize, os requests com o padrão /auth são permitidos, qualquer outro é necessário estar autenticado, desabilite a política CSRF e no gerenciamento de sessão use a política STATELESS

Obs: CSRF (Cross-site request forgery) ou XSRF é tipo de ataque na web porém para o tipo de autenticação com web token estamos livre desse tipo de problema.

Configuração de autenticação

Feito isso podemos chamar o nosso endpoint porém ainda é necessário fazer mais configurações pois temos que criar as classes de serviços para fazer a autenticação.

Voltando para o Controller temos duas classes sendo injetadas

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenService tokenService;
Enter fullscreen mode Exit fullscreen mode

A primeira é uma classe do próprio Spring Security e só injetaremos ela aqui mas precisamos fazer as configurações pertinentes a ela lá na nossa classe de configuração no método configure que recebe um objeto do tipo AuthenticationManagerBuilder e é o método responsável por lidar com a autenticação:


    @Autowired
    private AuthenticationService authenticationService;

    //Configurations for authentication
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(authenticationService).passwordEncoder(new BCryptPasswordEncoder());
    }
Enter fullscreen mode Exit fullscreen mode

Esse método será chamado durante a autenticação e para validação dos dados de usuário irá chamar a classe AuthenticationService, importante também verificar que estamos usando um encoder para o nosso password pois não queremos ser capazes de armazenar o password em texto simples, mas para entender melhor precisamos entrar nessa classe de serviço e verificar o que é feito.

@Service
public class AuthenticationService implements UserDetailsService{

    @Autowired
    private UserRepositoryPort repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<User> optional = repository.findByEmail(username);

        if(optional.isPresent()) {
            return optional.get();
        }

        throw new UsernameNotFoundException("User not found");
    }   

}
Enter fullscreen mode Exit fullscreen mode

A primeira coisa que percebemos é que essa classe implementa a interface UserDetailsService que possui o método loadUserByUsername e devolve um objeto do tipo UserDetails.

O que basicamente acontece aqui é que ele vai buscar em algum repositório por esse usuário e verificar se ele existe e devolver para o processo de autenticação, mas ainda não temos nada disso criado, então vamos criar o nosso repositório agora.

Criando Repositório para autenticação

O repositório será um banco de dados que armazena os usuários dessa aplicação, então segue o mapeamento da classe User que implementa a classe UserDetails do Spring Security:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "User")
public class User implements UserDetails{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String email;

    private String pass;

    @ManyToMany(fetch = FetchType.EAGER)
    private Set<Perfil> perfis;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.perfis;
    }

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

}
Enter fullscreen mode Exit fullscreen mode

O fato de implementarmos UserDetails faz com que implementemos alguns métodos para fazer a checagem se o usuário existe e se ele pode se autenticar. Também é necessário informar as permissões de acesso para esse usuário através do método getAuthorities e para fazer esse mapeamento foi criado a classe Perfil:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "Perfil")
public class Perfil implements GrantedAuthority{


    private static final long serialVersionUID = 1L;

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @Override
    public String getAuthority() {
        return this.name;
    }

}
Enter fullscreen mode Exit fullscreen mode

Essa classe por sua vez implementa a interface GrantedAuthority que representa as permissões concedidas para um usuário.

Agora criamos a interface que será o serviço de Repository para buscar essas informações:

@Repository
public interface UserRepository extends CrudRepository<User, Integer>{

    Optional<User> findByEmail(String email);

}
Enter fullscreen mode Exit fullscreen mode

Com isso agora a nossa classe AuthenticationService vai funcionar quando for chamada em uma requisição com usuário e senha.

Devolvendo JSON Web Token

Após a autenticação ser efetuada o nosso Controller irá retornar o JWT para que seja usado em próximas requisições, precisamos agora fazer a configuração para retornar esse token para o usuário.

No Controller temos o seguinte trecho de código:

    @PostMapping
    public ResponseEntity<TokenDTO> auth(@RequestBody @Validated LoginDTO loginDTO){
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUser(), loginDTO.getPass());

        Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

        String token = tokenService.generateToken(authentication);

        return ResponseEntity.ok(TokenDTO.builder().type("Bearer").token(token).build());

    }
Enter fullscreen mode Exit fullscreen mode

Aqui usamos a classe TokenService para gerar o token, essa classe recebe um objeto do tipo Authentication que é retornado após realizar todos os passos de autenticação.

@Service
public class TokenService {

    @Value("${jwt.expiration}")
    private String expiration;

    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(Authentication authentication) {

        User usuario = (User) authentication.getPrincipal();

        Date now = new Date();
        Date exp = new Date(now.getTime() + Long.parseLong(expiration));

        return Jwts.builder().setIssuer("MinhaAplicacao")
                             .setSubject(usuario.getId().toString())
                             .setIssuedAt(new Date())
                             .setExpiration(exp)
                             .signWith(SignatureAlgorithm.HS256, secret).compact();
    }

}
Enter fullscreen mode Exit fullscreen mode

Aqui recebemos o objeto Authentication e dentro dele temos o método getPrincipal onde temos o nosso User e vamos devolver o nosso JWT com algumas informações:

  • Issuer: Nome da aplicação que está retornando o JWT.
  • Subject: Aqui iremos retornar o id do usuário.
  • IssuedAt: Data de geração do token.
  • Expiration: Data de expiração do token.
  • SignWith: Assinatura do token com o algoritmo HmacSHA256 juntamente com o secret.
  • Compact: Método que serializa o token.

*As variáveis


 e

 ```secret```

 foram definidas no arquivos *application.yml* *

Com isso já teremos o retorno do **JWT** que pode ser usado nas próxima requests.

## Autorizando JWT

Agora já conseguimos autenticar com usuário e senha e devolver um token para ser usado nas próximas requisições, mas precisamos configurar essa parte na aplicação.

Como estamos usando gerenciamento de sessão **Stateless** cada requisição é "nova" para a aplicação, a aplicação não tem conhecimento que o usuário já realizou anteriormente a autenticação. Por isso usamos o token que contém informações para permitir o acesso do usuário aos recursos da aplicação.

![Passport!!!](https://media4.giphy.com/media/xT5LMAwG85SxRhYube/source.gif)

O que podemos fazer é verificar se uma requisição que está tentando acessar algum recurso possui um token e se esse token é válido.

Podemos fazer isso interceptando uma requisição.



```java
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        filterChain.doFilter(request, response);
    }

}
Enter fullscreen mode Exit fullscreen mode

Criamos a classe TokenAuthenticationFilter que estende de OncePerRequestFilter que já faz a interceptação das requisições e temos o método doFilterInternal onde podemos manipular vários dados.

Fora isso temos que informar para o Spring Security que temos um Filter e que ele deve ser processado em ordem, vamos voltar na classe SecurityConfiguration e adicionar no método configure:

    //Configuration for authorization
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers(HttpMethod.POST, "/auth").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            //Configuração do Filtro
            .and().addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
Enter fullscreen mode Exit fullscreen mode

Aqui adicionamos a configuração de filtro e que ele deve ser antes do filtro do Spring Security UsernamePasswordAuthenticationFilter, agora voltando para a classe de filtro precisamos configurá-la para extrair o token da requisição e validar:

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = request.getHeader("Authorization");
        if(token == null || token.isEmpty() || !token.startsWith("Bearer ")) {
            token = null;
        }

        token = token.substring(7, token.length());

        filterChain.doFilter(request, response);
    }
Enter fullscreen mode Exit fullscreen mode

No código a cima conseguimos extrair o token do cabeçalho Authorization e verificamos se ele existe e se ele é um token do tipo Bearer, agora precisamos verificar se ele é válido. Para isso precisamos da secret que foi usada quando geramos o token então essa lógica vai ficar na classe TokenService no método isValid que recebe a String do token e retorna um boolean.

    public boolean isTokenValid(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Esse método faz uso da biblioteca jsonwebtoken e aqui fazemos uso do método parseClaimsJws, caso não consiga fazer o parse do token com a secret irá lançar uma Exception, caso não dê erro é por que o token é válido.

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String tokenFromHeader = getTokenFromHeader(request);
        boolean tokenValid = tokenService.isTokenValid(tokenFromHeader);
        if(tokenValid) {
            this.authenticate(tokenFromHeader);
        }

        filterChain.doFilter(request, response);
    }

    private void authenticate(String tokenFromHeader) {
        Integer id = tokenService.getTokenId(tokenFromHeader);

        Optional<User> optionalUser = repository.findById(id);

        if(optionalUser.isPresent()) {

            User user = optionalUser.get();

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getPerfis());
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }
    }

    private String getTokenFromHeader(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if(token == null || token.isEmpty() || !token.startsWith("Bearer ")) {
            return null;
        }

        return token.substring(7, token.length());
    }
Enter fullscreen mode Exit fullscreen mode

Aqui acima temos o uso dos dois métodos e de mais um novo que é o authenticate que usa o método getTokenId para extrair o id que é enviado no token, no Subject e após isso faz uma busca por esse id no repositório. Caso encontre instânciamos um objeto UsernamePasswordAuthenticationToken passando o user, null no parâmetro da senha pois não precisamos dela nesse ponto e a lista de perfis. Passamos ele para o objeto SecurityContextHolder que é objeto que lida com o contexto de segurança da aplicação.

Analisando a TokenService no método getTokenId:

    public Integer getTokenId(String token) {
        Claims body = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        return Integer.valueOf(body.getSubject());
    }
Enter fullscreen mode Exit fullscreen mode

Vemos que aqui novamente fazemos uso da biblioteca jsonwebtoken mas agora pegamos o body para recuperar o Subject.

Após iniciarmos a aplicação e autenticarmos com um usuário já cadastrado conseguimos acessar a rota que o Actuator expõe e até o momento estava inacessível.

Aqui fazemos a autenticação e recebemos o token na resposta.

Imagem resposta token

E agora com o JWT no Header de Authorization fazemos o GET para a url protegida.

Imagem status OK

Código fonte

Segue o código completo no GitHub.

Discussion (0)