|
June 1, 2017
|

Adicionando segurança a uma aplicação java com JWT

Introdução

Nos últimos posts, aprendemos como criar uma aplicação AngularJS e, também, como utilizá-la para consumir uma API Rest. Neste tutorial, implementaremos uma camada de segurança utilizando JSON WEB Tokens (JWT).

O JWT é um padrão aberto (RFC 7519) para autenticação web baseado em requisições JSON. Ele é um mecanismo de autenticação simples e leve, pois não exige que haja armazenamento de dados no servidor. Para completar, o token é compacto, formado por uma string dividida em três partes: header, payload e signature. 

Para entendermos como ele funciona, vamos verificar a sequência de passos abaixo:

Diagrama JWT

Diagrama JWT

  1. O cliente envia os dados de login para a aplicação;
  2. O servidor valida as credenciais da requisição e, se tudo estiver correto, é gerado um token, enviado como resposta ao cliente;
  3. O cliente recebe o token e armazena da maneira que lhe for conveniente. Por exemplo, Cookie ou LocalStorage;
  4. Sempre que for realizada uma requisição ao servidor, o token tem que ser enviado para que seja validado o acesso a uma determinada API;
  5. Ao receber o token, o servidor irá validá-lo e liberará o acesso;
  6. Então, o servidor devolverá o resultado da requisição solicitada pelo cliente.

Agora que sabemos como o JWT funciona, o aplicaremos ao nosso exemplo de Cadastro de Usuários, desenvolvido nos posts anteriores. Vamos focar na implementação do lado do servidor. Já a parte do cliente (AngularJS) será implementada no próximo post.

Configurando o projeto

Nossa primeira alteração será adicionar as dependências para a nossa implementação ao arquivo pom.xml:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.4</version>
    <scope>provided</scope>
</dependency>

Precisaremos criar um filtro para tratar as requisições que chegarem ao servidor. Para isso, adicionaremos ao arquivo web.xml à configuração do filtro que tratará essas requisições:

<filter>
    <filter-name>jwtFilter</filter-name>
    <filter-class>com.gabrielfeitosa.example.filter.JWTFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>jwtFilter</filter-name>
    <url-pattern>/api/*</url-pattern>
</filter-mapping>

Nesta configuração, definimos que toda requisição que começar com /api passará pelo filtro.

Implementando a classe para manipular o JWT

Vamos implementar uma classe utilitária que será a responsável pela manipulação do token JWT. Para isso, criaremos a classe JWTUtil abaixo. Percebam que temos um atributo key, ele contém a chave secreta que será usada para codificar e decodificar o token JWT.

Já os métodos create e decode, são os responsáveis pela criação e decodificação do token. No método create, passamos como parâmetro o subject, no nosso exemplo será o username. Ele ficará codificado dentro do token e será possível extraí-lo quando necessário. Veremos a utilização dos métodos mais adiante.

public class JWTUtil {

    private static String key = "SECRET_TOKEN;

    public static final String TOKEN_HEADER = "Authentication";

    public static String create(String subject) {
        return Jwts.builder()
                .setSubject(subject)
                .signWith(SignatureAlgorithm.HS512, key)
                .compact();
    }

    public static Jws<Claims> decode(String token){
        return Jwts.parser().setSigningKey(key).parseClaimsJws(token);
    }
}

Implementando a classe de LoginController

Nesta classe, desenvolveremos os serviços de realizar o login e de retornar o usuário logado. O método de login, será mapeado através do path /api/login e a requisição deverá ser do tipo HTTP POST. Este método, receberá as credenciais do usuário como parâmetro e em seguida as validará.

Na sequência, caso as credenciais sejam válidas, o token será criado passando como subject o username do usuário. Em seguida, o objeto UserLogged será montado com o username e o token. Ele será retornado para o cliente em formato JSON. O fluxo de exceção ocorrerá quando as credenciais forem inválidas. Caso isso aconteça, a requisição terá status de não autorizado.

@POST
@Path("/login")
public Response login(Credentials credentials) {
    if(this.USERNAME.equals(credentials.getUsername()) && this.PASSWORD.equals(credentials.getPassword())){
        String token = JWTUtil.create(credentials.getUsername());
        UserLogged me = new UserLogged();
        me.setUsername(credentials.getUsername());
        me.setToken(token);
        return Response.ok().entity(me).build();
    }else{
        return Response.status(Response.Status.UNAUTHORIZED).build();
    }
}

Para a implementação do método de retorno do usuário logado, receberemos como parâmetro o HttpServletRequest. Ele será injetado por meio da anotação @Context. Este método irá mapear as requisições HTTP GET no path /api/me. O processo é bem simples, primeiro o token será extraído do cabeçalho e em seguida decodificado. Por último, retornaremos o UserLogged preenchido com o subject extraído do token.

@GET
@Path("/me")
public UserLogged me(@Context HttpServletRequest httpRequest) {
    String token = httpRequest.getHeader(JWTUtil.TOKEN_HEADER);
    Jws<Claims> jws = JWTUtil.decode(token);
    UserLogged me = new UserLogged();
    me.setUsername(jws.getBody().getSubject());
    return me;
}

Até agora nada muito difícil, concorda? =)

Criando o filtro

Agora que temos os métodos de login e de retorno do usuário logado, precisamos criar o filtro que será o responsável por avaliar se a requisição será autorizada ou não. As regras serão as seguintes:

  • A API para fazer o login deverá ser pública, ou seja, não é necessário o token no cabeçalho da requisição. Afinal, ele ainda não foi criado;
  • Para os demais serviços, o token é obrigatório. Então, caso ele não seja enviado no cabeçalho da requisição, a resposta ao cliente terá o status 401 (UNAUTHORIZED);
  • Caso o token não seja válido, também retornaremos a requisição com status 401;
  • Com tudo validado, a requisição seguirá o seu fluxo normal.

Vamos criar a classe JWTFilter que implementará a interface javax.servlet.Filter. O principal trecho de código do nosso filtro está abaixo:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletRequest;
    HttpServletResponse res = (HttpServletResponse) servletResponse;

    if(req.getRequestURI().startsWith("/api/login")){
        filterChain.doFilter(servletRequest, servletResponse);
        return;
    }

    String token = req.getHeader(JWTUtil.TOKEN_HEADER);

    if(token == null || token.trim().isEmpty()){
        res.setStatus(401);
        return;
    }

    try {
        Jws<Claims> parser = JWTUtil.parser(token);
        System.out.println("User request: "+ parser.getBody().getSubject());
        filterChain.doFilter(servletRequest, servletResponse);
    } catch (SignatureException e) {
        res.setStatus(401);
    }

}

Percebam que caso o token não seja válido, o método JWTUtil.parser lançará uma exceção e a requisição terá o status de retorno de não autorizado.

E aí, concorda que tá bem fácil? Não houve sequer necessidade de modificar classes que já existiam.

Você confia em mim ou é melhor realizar uns testes? Melhor testar, né? 😉

Testando a aplicação

Utilizaremos o curl para realizar as chamadas as nossas URL’s. Vamos ver o que acontece quando enviamos uma requisição sem o token para /api/user?

No terminal, digitaremos o seguinte comando:

$ curl -i -H 'Content-Type: application/json' http://localhost:8080/api/user

E teremos uma resposta semelhante a esta:

HTTP/1.1 401
Content-Length: 0
Date: Wed, 31 May 2017 02:00:41 GMT

Os demais testes vão seguir o seguinte fluxo:

  • Autenticando
    • curl -i -H ‘Content-Type: application/json’ -X POST -d ‘{“username”: “admin”, “password”: “admin”}’ http://localhost:8080/api/login
    • HTTP/1.1 200
      Content-Type: application/json
      Content-Length: 159
      Date: Wed, 31 May 2017 02:04:49 GMT{“token”:”eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiJ9.H_ILSDiAbyAFPKygwcL0w8CJshwUgEE1aVXG85I9tfISgASBxXMf6mkB0O86z5b7USnEVXdCC_2sK4XMcO4Kmg”,”username”:”admin”}
  • Retornando usuário logado
    • curl -i -H ‘Content-Type: application/json’ -H ‘Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiJ9.H_ILSDiAbyAFPKygwcL0w8CJshwUgEE1aVXG85I9tfISgASBxXMf6mkB0O86z5b7USnEVXdCC_2sK4XMcO4Kmg’ http://localhost:8080/api/me
    • HTTP/1.1 200
      Content-Type: application/json
      Content-Length: 20
      Date: Wed, 31 May 2017 02:08:06 GMT{“username”:”admin”}
  • Retornando a lista de usuário cadastrados
    • curl -i -H ‘Content-Type: application/json’ -H ‘Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiJ9.H_ILSDiAbyAFPKygwcL0w8CJshwUgEE1aVXG85I9tfISgASBxXMf6mkB0O86z5b7USnEVXdCC_2sK4XMcO4Kmg’ http://localhost:8080/api/user
    • HTTP/1.1 200
      Content-Type: application/json
      Content-Length: 129
      Date: Wed, 31 May 2017 02:08:49 GMT[{“password”:”123456″,”username”:”User 1″},{“password”:”659849″,”username”:”User 2″},{“password”:”65498987″,”username”:”User 3″}]

 

Pronto, terminamos. E agora, concorda que foi simples?

O código fonte do exemplo está no nosso github!

Se você curtiu este post, compartilhe e marque seus amigos!!!

Abraços e até a próxima semana!

Comments

More articles

Afinal, é possível criar app sem saber programação?

November 16, 2018

Como transformar um website em aplicativo?

November 14, 2018

Aplicativos: O que conhecer antes de criar um

November 13, 2018

É possível instalar aplicativos Android no Windows?

November 9, 2018