Implementação de validação JWT genérica com anotações personalizadas no Spring Boot

Nem todos os endpoints de uma aplicação precisam de validação JWT prévia. Para isso, criamos uma anotação personalizada @JwtToken que marca os métodos que exigem verificação do token. Exemplo:

@GetMapping("/fazerAlgo")
// Adiciona a anotação personalizada @JwtToken
// Antes de executar o método do Controller, é feita a validação JWT
// Se a validação passar, o método é executado
// Caso contrário, retorna erro de autorização
@JwtToken
public ResponseObject fazerAlgo(){
    return new ResponseObject("...");
}

Pasos de desenvolvimento

1. Adicionar dependências JJWT no pom.xml

<!--JJWT-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- ou jjwt-gson -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

2. Configurar chave secreta no application.yml

# Chave secreta para JWT, deve coincidir com o serviço de autenticação
app:
  secretKey: 1234567890-1234567890-1234567890

3. Criar anotação personalizada @JwtToken

Esta anotação só pode ser aplciada a métodos.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtToken {
    // required = true  significa que o token é obrigatório; se ausente, ocorre erro
    // required = false indica que o token não é obrigatório; a requisição continua para a lógica de negócio
    boolean required() default true;
}

4. Adiiconar endpoint de teste no ArticleController

@JwtToken // aplica validação JWT em dosth
@GetMapping("/dosth")
public ResponseObject dosth() {
    return new ResponseObject("Processamento concluído com sucesso");
}

5. Criar TokenInterceptor para interceptar e validar JWT

/**
 * Antes de executar o método alvo da URI, verifica o JWT no cabeçalho da requisição.
 * Se válido, executa o método; caso contrário, retorna erro.
 */
public class TokenInteceptor implements HandlerInterceptor {
    @Value("${app.secretKey}")
    private String chaveApp = null;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("TokenInterceptor.preHandle()");
        // Se não for uma chamada de método, permite a passagem
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        response.setContentType("text/json;charset=utf-8");
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        // Verifica se o método possui a anotação @JwtToken
        if (method.isAnnotationPresent(JwtToken.class)) {
            String token = request.getHeader("token");
            JwtToken jwtToken = method.getAnnotation(JwtToken.class);

            // Token ausente
            if (token == null) {
                if (jwtToken.required()) {
                    response.setStatus(401);
                    ResponseObject<Object> responseObject = new ResponseObject<>("SecurityException", "Token ausente, verifique o cabeçalho da requisição");
                    response.getWriter().println(objectMapper.writeValueAsString(responseObject));
                    return false;
                }
            } else {
                // Token presente, faz a validação JWT
                String base64Key = new BASE64Encoder().encode(chaveApp.getBytes());
                SecretKey key = Keys.hmacShaKeyFor(base64Key.getBytes());
                try {
                    Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
                    String userJson = claimsJws.getBody().getSubject();
                    User user = objectMapper.readValue(userJson, User.class);
                    request.setAttribute("$usuario", user); // salva o usuário logado no atributo da requisição
                    return true;
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                    response.setStatus(500);
                    ResponseObject<Object> responseObject = new ResponseObject<>(e.getClass().getSimpleName(), e.getMessage());
                    response.getWriter().println(objectMapper.writeValueAsString(responseObject));
                    return false;
                } catch (JwtException e) {
                    e.printStackTrace();
                    response.setStatus(401);
                    ResponseObject<Object> responseObject = new ResponseObject<>(e.getClass().getSimpleName(), e.getMessage());
                    response.getWriter().println(objectMapper.writeValueAsString(responseObject));
                    return false;
                }
            }
        }
        return true;
    }
}

6. Iniciar a aplicação e testar

Acesse http://localhost:8100/dosth.

7. Vincular dados do usuário extraídos do token à lógica de negócio

Quando o token é validado, o TokenInterceptor coloca o objeto User no atributo $usuario da requisição. No método list, podemos recuperá-lo com @RequestAttribute.

/**
 * Usuários VIP podem ver todos os artigos (normais e selecionados)
 * Usuários normais veem apenas artigos comuns
 */
@GetMapping("/list")
@JwtToken(required = false)
public ResponseObject list(@RequestAttribute(value = "$usuario", required = false) User usuario) {
    System.out.println(usuario);
    return new ResponseObject("0", "success", articleService.list(usuario));
}

No ArticleService, a lógica de negócio filtra de acordo com o nível do usuário:

public List<Article> list(User usuario){
    int nivel = 0;
    if(usuario == null || usuario.getGrade().equals("normal")){
        nivel = 1;
    }else if(usuario.getGrade().equals("vip")){
        nivel = 2;
    }
    List<Article> list = articleMapper.list(nivel);
    for(Article article : list){
        ResponseObject<Video> videoResponseObject = videoFeignClient.findByArticleId(article.getArticleId());
        article.setVideo(videoResponseObject.getData());
    }
    return list;
}

O ArticleMapper é modificado para aceitar o parâmetro de nível:

@Mapper
public interface ArticleMapper {
    @Select("select * from article where article_type <= #{valor} order by create_time desc")
    public List<Article> list(int valor);
}

Com isso, a lógica de negócio está finalizada:

  • Usuários não logados ou comuns veem apenas artigos de ArticleType = 1.
  • Usuários VIP podem ver todos os artigos.

Tags: Spring Boot jwt anotação personalizada Interceptor validação de token

Publicado em 6-28 10:01