Java

Implementando autenticação baseada em JWT em uma API RESTful JAX-RS

Segurança é um item essencial e uma forma de limitar o acesso da aplicação é através de autenticação, que uma API JAX-RS pode implementar através do JWT.

há 3 anos 10 meses

Formação Desenvolvedor Java
Conheça a formação em detalhes

Segurança deve ser um ponto vital para qualquer aplicação web. Não importando o tamanho ela sempre conterá dados que necessitam de alguma proteção. APIs também se enquadram neste quesito, mas as formas tradicionais de autenticação, baseadas em telas de login e sessão, não podem ser aplicadas neste tipo de aplicação.

Por serem stateless por definição, APIs RESTful procuram implementar autenticações baseadas em alguma informação nas solicitações do usuário. Sendo que as opções mais comuns são: HTTP Basic Authentication, onde o usuário e senha codificado em base64 é enviado no header Authorization da solicitação; e Token Based Authentication, onde é enviado no header Authorization da solicitação um token assinado, garantindo que ele não foi adulterado.

Devido a facilidade de implementação e a maior segurança, dessas duas opções, a opção mais utilizada é a Token Based Authentication, que utiliza o padrão aberto JSON Web Token (JWT). Baseado em JSON este padrão nos permite fornecer várias informações sobre o usuário de forma compacta e auto-contida.

Neste artigo veremos como implementar este tipo de autenticação em uma API RESTful JAX-RS.

Java - Fundamentos de JAX-WS e JAX-RS
Curso Java - Fundamentos de JAX-WS e JAX-RS
Conhecer o curso

Gerando o Token de acesso da API

Para este artigo não criarei uma aplicação do zero, utilizarei como base a API RESTful com a JAX-RS API que demostrei no meu artigo passado.

Para gerar o token, utilizaremos a biblioteca jjwt, assim, a primeira coisa a ser feita na aplicação é a adição das dependências dela:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.1</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.1</version>
    <scope>runtime</scope>
</dependency>

Com as dependências adicionadas, criaremos uma nova entidade chamada Usuario:

public class Usuario {
    private int idUsuario;
    private String usuario;
    private String senha;

    public int getIdUsuario() {
        return idUsuario;
    }

    public String getSenha() {
        return senha;
    }

    public void setSenha(String senha) {
        this.senha = senha;
    }

    public String getUsuario() {
        return usuario;
    }

    public void setUsuario(String usuario) {
        this.usuario = usuario;
    }

    public void setIdUsuario(int idUsuario) {
        this.idUsuario = idUsuario;
    }
}

E o recurso abaixo:

@Path("/login")
public class LoginResource {
    private final SecretKey CHAVE = Keys.hmacShaKeyFor(
		"7f-j&CKk=coNzZc0y7_4obMP?#TfcYq%fcD0mDpenW2nc!lfGoZ|d?f&RNbDHUX6"
		.getBytes(StandardCharsets.UTF_8));

    @POST
    @Produces(MediaType.TEXT_PLAIN)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response post(Usuario usuario)
    {
        try{
            if(
				usuario.getUsuario().equals("teste@treinaweb.com.br") 
					&& 
				usuario.getSenha().equals("1234")
			)
            {
                String jwtToken = Jwts.builder()
                    .setSubject(usuario.getUsuario())
                    .setIssuer("localhost:8080")
                    .setIssuedAt(new Date())
                    .setExpiration(
						Date.from(
							LocalDateTime.now().plusMinutes(15L)
								.atZone(ZoneId.systemDefault())
							.toInstant()))
                    .signWith(CHAVE, SignatureAlgorithm.RS512)
                    .compact();
                
                return Response.status(Response.Status.OK).entity(jwtToken).build();
            }
            else
                return Response.status(Response.Status.UNAUTHORIZED)
						.entity("Usuário e/ou senha inválidos").build();
        }
        catch(Exception ex)
        {
            return Response.status(
						Response.Status.INTERNAL_SERVER_ERROR
					).entity(ex.getMessage())
					.build();
        } 
    }
}

Note que nele há apenas um método, que realizará o login do usuário. Por esta ser uma aplicação simples, o usuário e senha válidos já estão definidos no código. Ao autenticar, será gerado um token:

String jwtToken = Jwts.builder()
    .setSubject(usuario.getUsuario())
    .setIssuer("localhost:8080")
    .setIssuedAt(new Date())
    .setExpiration(
		Date.from(
			LocalDateTime.now().plusMinutes(15L)
				.atZone(ZoneId.systemDefault())
			.toInstant()))
    .signWith(CHAVE, SignatureAlgorithm.RS512)
    .compact();

Onde, é informado:

  • Subject: o login do usuário;
  • Issuer: quem está gerando o token;
  • IssuedAt: data que o token foi gerado;
  • Expiration: tempo de vida do token;
  • sign: como o token será assinado;

Das informações, a mais importante é a assinatura do token. Neste exemplo ela utiliza uma chave textual visível:

private final SecretKey CHAVE = Keys.hmacShaKeyFor(
	"7f-j&CKk=coNzZc0y7_4obMP?#TfcYq%fcD0mDpenW2nc!lfGoZ|d?f&RNbDHUX6"
	.getBytes(StandardCharsets.UTF_8));

Entretanto em uma aplicação real, esta chave precisa ser bem protegida, pois é através dela que os tokens serão validados pela aplicação. Assim, se uma pessoa mal-intencionada obter esta informação, ela poderá gerar tokens válidos e terá acesso aos recursos protegidos da API.

Ao testar o endpoint, se for informado o usuário e senha corretos, um token será gerado:

Requisição de login correta, gerando um token

Caso contrário será retornado 401 Unauthorized:

Requisição com a senha inválida, gerando uma resposta 401 Unauthorized

Com a geração do token pronta, temos que proteger nossos endpoints, o que veremos a seguir.

Autenticando as solicitações

Como vimos acima, o token será enviado no header Authorization da solicitação. Assim, para autenticá-las é necessário analisar este header e verificar se o token informado é valido. Apenas nessas situações as solicitações devem ser permitidas.

Para fazer isso, podemos utilizar o @NameBinding, que é uma meta annotation que nos permite definir filtros e interceptadores no pipeline da solicitação. Estes filtros são implementados via anotação, assim, inicialmente uma anotação deve ser definida:

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Authorize { }

Onde utilizamos o @NameBinding e indicamos que a anotação será aplicada em métodos.

É na implementação desta anotação que verificamos a validade do token:

@Provider
@Authorize
@Priority(Priorities.AUTHENTICATION)
public class AuthorizeFilter implements ContainerRequestFilter {
    private final SecretKey CHAVE = 
            Keys.hmacShaKeyFor("7f-j&CKk=coNzZc0y7_4obMP?#TfcYq%fcD0mDpenW2nc!lfGoZ|d?f&RNbDHUX6"
                                .getBytes(StandardCharsets.UTF_8));

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String authorizationHeader = requestContext
										.getHeaderString(HttpHeaders.AUTHORIZATION);
        try {
            String token = authorizationHeader.substring("Bearer".length()).trim();

            Jwts.parserBuilder()
					.setSigningKey(CHAVE)
					.build()
					.parseClaimsJws(token);
        } catch (Exception e) {
            requestContext
				.abortWith(Response.status(Response.Status.UNAUTHORIZED)
				.build());
        }

    }

}

Inicialmente é indicado que a classe irá prover (@Provider) a implementação a nossa anotação (@Authorize) e que a prioridade dela no pipeline do JAX-RS é de autenticação (AUTHENTICATION). Ou seja, ela será executada antes dos endpoints.

Na classe, é obtido o token do header da solicitação:

String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
String token = authorizationHeader.substring("Bearer".length()).trim();

Em seguida é validado:

Jwts.parserBuilder()
	.setSigningKey(CHAVE)
	.build()
	.parseClaimsJws(token);

O método parseClaimsJws obtém os dados presentes no token como: Subject, Issuer, etc; caso haja algo errado, é gerada uma exceção. Assim, caso ocorra qualquer erro é retornado que o usuário não tem permissão de acesso:

requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());

Com a anotação definida, ela pode ser aplicada aos endpoints:

@Path("/pessoa")
public class PessoaResource {

    private PessoaRepository _repositorio = new PessoaRepository();
    
    @Authorize
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Pessoa> get() {
        return _repositorio.GetAll();
    }

    @Authorize
    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Pessoa getById(@PathParam("id") int id) {
        return _repositorio.Get(id);
    }

    @Authorize
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response post(Pessoa pessoa)
    {
        try{
            _repositorio.Add(pessoa);
            return Response.status(Response.Status.CREATED).entity(pessoa).build();
        }
        catch(Exception ex)
        {
            return Response.status(
						Response.Status.INTERNAL_SERVER_ERROR
					).entity(ex.getMessage())
					.build();
        } 
    }

    @Authorize
    @PUT
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response put(@PathParam("id") int id, Pessoa pessoa)
    {
        Pessoa p = _repositorio.Get(id);
        if(p == null)
            return Response.status(Response.Status.NOT_FOUND).build();
        
        try{
            pessoa.setId(id);
            _repositorio.Edit(pessoa);
            return Response.status(Response.Status.OK).entity(pessoa).build();
        }
        catch(Exception ex)
        {
            return Response.status(
						Response.Status.INTERNAL_SERVER_ERROR
					).entity(ex.getMessage())
					.build();
        } 
    }

    @Authorize
    @DELETE
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response delete(@PathParam("id") int id)
    {
        Pessoa p = _repositorio.Get(id);
        if(p == null)
            return Response.status(Response.Status.NOT_FOUND).build();
        
        try{
            _repositorio.Delete(id);
            return Response.status(Response.Status.OK).build();
        }
        catch(Exception ex)
        {
            return Response.status(
						Response.Status.INTERNAL_SERVER_ERROR
					).entity(ex.getMessage())
					.build();
        } 
    }
}

Para testá-la vamos utilizar o Postman.

Testando a autenticação

Inicialmente gere um token:

Requisição de login gerando o token

Com o token gerado, ele deve ser informado na aba Auth do Postman:

Aba Auth do Postman, informando o token

Não se esqueça de definir o tipo como Bearer Token. Com isso, ao enviar uma solicitação, ela será aceita:

Requisição POST para pessoa, com token válido

Se o token não for informado, ou caso seja inválido, será retornado 401 Unauthorized:

Requisição POST para pessoa, com token inválido

Java - Fundamentos de JAX-WS e JAX-RS
Curso Java - Fundamentos de JAX-WS e JAX-RS
Conhecer o curso

Conclusão

Neste artigo vimos uma implementação simples de autenticação utilizando o padrão JWT. Um padrão bem difundido e muito utilizado, então caso necessite implementar este tipo de autenticação na sua aplicação não deixe de dar uma olhada nele.

Mas independente do padrão utilizado, procure não deixar suas aplicações expostas, sempre adote um sistema de autenticação e conexão segura, HTTPS.

Por fim, você pode ver a aplicação demonstrada aqui no meu Github.

Autor(a) do artigo

Wladimilson M. Nascimento
Wladimilson M. Nascimento

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

Todos os artigos

Artigos relacionados Ver todos