APIs são alvos frequentes de atores maliciosos, pois expõem dados e funcionalidades. Segurá-las mantendo a usabilidade é muitas vezes uma das partes mais desafiadoras e demoradas do desenvolvimento de APIs. OAuth 2.0 e JSON Web Tokens (JWT) ajudam a tornar esses processos mais gerenciáveis e confiáveis. Eles permitem que os desenvolvedores representem e verifiquem identidades e gerenciem o acesso transmitindo reivindicações de forma segura e habilitando autorização delegada.
Este artigo discute essas tecnologias e as maneiras mais eficientes que você pode usá-las para proteger suas APIs e backends construídos com Spring Boot. Se você está interessado em uma solução baseada em coroutines, um tutorial acompanhante usando Ktor também está planejado e será publicado em breve.
Introdução a OAuth2 e JWT
OAuth 2.0 e JWT não são tecnologias concorrentes. São partes complementares do quebra-cabeça, com uma lidando com a delegação de autorização e a outra servindo como o formato compacto e verificável que carrega informações seguras.
Autenticação vs Autorização
A autenticação verifica a identidade (quem você é), geralmente através de credenciais como senhas, tokens ou certificados. JWTs podem transportar informações de identificação e atuar como um tipo de documento de identificação uma vez emitidos. Papéis e outras reivindicações dentro de um JWT são então usados para autorização.
A autorização ajuda a controlar o que um usuário tem acesso (o que eles podem fazer). Isso inclui os escopos ou recursos aos quais eles têm acesso e como essas permissões são gerenciadas. Em um sistema que usa OAuth2 e JWT, o crachá de acesso é embutido no seu documento de identificação. O OAuth2 supervisiona e gerencia esse processo.
O Papel do OAuth2
OAuth 2.0 é um quadro para acesso delegado. Em vez de compartilhar senhas diretamente, os usuários concedem aos aplicativos um token que representa suas permissões. Isso significa que seu backend (atuando como um Resource Server) não precisa emitir tokens. Em vez disso, ele confia e valida os tokens provenientes do Authorization Server dentro do quadro OAuth 2.0. Essa separação de funções permite simplificar suas APIs enquanto reduz riscos de segurança e garante que todos os tokens sigam uma política clara, consistente e centralizada.
Você não precisa se preocupar em implementar logins de usuários ou redirecionamentos do navegador dentro da sua API. Quanto à validação e autorização, a responsabilidade do backend ou da API é receber o Token Bearer, autenticar a assinatura, verificar a expiração e aplicar escopos/permissões.
Sua API apenas verifica os crachás; ela não é responsável por emitir esses crachás. Então como os JWTs se encaixam nisso?
O que é um JWT?
Um JWT é uma pequena e amigável para a web peça de texto (string) que transporta informações seguramente entre sistemas. Sua compactação os torna fáceis de passar em cabeçalhos HTTP ou URLs. Cada token usa Base64URL codificação, tornando-os seguros para incluir em strings de consulta ou cabeçalhos.
Os JWTs são assinados (e às vezes criptografados), então os destinatários podem verificar que eles não foram alterados. Eles também são autônomos, carregando detalhes como ID do usuário, papéis ou permissões. Esses elementos (especialmente a autonomia e a assinatura) permitem autenticação sem estado sem armazenamento de sessão. Isso significa que você não precisa de um banco de dados ou cache para rastrear sessões ativas. Também incentiva menos consultas e menor complexidade infraestrutural, o que reduz a sobrecarga do seu sistema.
Os JWTs têm uma estrutura simples e padronizada composta por três partes, separadas por pontos:
- O Header: contém metadados sobre o token, como o tipo (
JWT) e o algoritmo de assinatura (HS256,RS256). - A Payload: apresenta as afirmações, que são declarações sobre o usuário ou sistema (como ID do usuário, papéis ou expiração do token).
- A Assinatura: é uma assinatura criptográfica criada usando o header, payload e uma chave secreta ou privada. Isso garante que o token não foi alterado.
A estrutura básica de um JWT parece com isso:
xxxxx.yyyyy.zzzzz
Um token real Base64URL codificado geralmente se parece com o seguinte:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Quando OAuth2 encontra JWT
Há quatro papéis-chave na implementação do OAuth2:
- Proprietário do Recurso: a entidade (geralmente o usuário) que concede acesso aos recursos protegidos.
- Cliente: o aplicativo que solicita acesso ao recurso em nome do proprietário do recurso.
- Servidor de Autorização: o servidor que autentica o proprietário do recurso e emite tokens de acesso para o cliente.
- Servidor de Recursos: o servidor que hospeda os recursos protegidos, aceita e valida os tokens.
O Proprietário do Recurso concede permissão (por exemplo, você clica em “Permitir” quando um aplicativo solicita acesso), então o Cliente solicita autorização do Servidor de Autorização, que emite um token de acesso (JWT) se o Proprietário do Recurso aprovar. O Cliente usa esse token de acesso para acessar dados do Servidor de Recursos.
Aviso: É importante notar que os JWTs não são o único formato de token com o qual OAuth2 pode trabalhar; eles apenas são os mais populares por causa dos seus benefícios. O OAuth2 também pode funcionar com Tokens Opacos, Tokens SAML ou formatos de token personalizados como os tokens de referência da Microsoft ou os tokens de acesso do Google.
Como Implementar OAuth2 e JWT
Imagine que você está construindo um sistema simples de gerenciamento de documentos com uma base backend em Kotlin e Spring que expõe uma API REST. Essa implementação permite aos clientes fazer upload de documentos, listá-los, visualizar os específicos etc. Alguns dos endpoints potenciais que a API pode exibir incluem:
GET /documents: Lista todos os documentos.GET /documents/{id}: Visualiza um documento específico.POST /documents: Faz upload de um novo documento.
Você quer restringir o acesso para que apenas usuários autenticados possam visualizar ou fazer upload de documentos, mas não quer gerenciar senhas no backend. Também não precisa manter sessões ou lidar com formulários de login.
Pré-requisitos
Se você quiser seguir junto, precisará:
Todos os códigos usados neste tutorial estão disponíveis no repositório GitHub.
Configuração Inicial da Aplicação
Para começar, execute o IntelliJ IDEA e crie um novo projeto (Arquivo > Novo > Projeto):

Selecione Spring Boot sob a seção Generators na barra lateral esquerda. Dê um nome ao seu projeto (como doc-manager), selecione Kotlin como o idioma, Gradle – Kotlin como o tipo, 17 como a versão do Java, Jar como a embalagem e Properties como a configuração. Mantenha todos os outros propriedades em seu estado padrão e então clique em Próximo.

No próximo tela, selecione dependências para o seu projeto. Certifique-se de que você está usando a versão mais recente e estável do Spring Boot (4.0.3 na época da escrita) e então use a barra de pesquisa para encontrar e adicionar as seguintes dependências:
- Spring Security
- OAuth2 Authorization Server
- OAuth2 Resource Server
- Spring Web
Uma vez que isso está feito, clique em Criar.

Depois que o seu projeto terminar de importar e carregar, expanda o seu projeto, desça a página e encontre o arquivo application.properties sob a pasta recursos (src > main > resources). Adicione as seguintes linhas a ele:
spring.application.name=doc-manager-kotlin-demo spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public.pem
Em muitos casos, você especificaria um Servidor de Autorização (issuer-uri) aqui. Mas para manter as coisas simples, você não usará um servidor de autorização real nesta parte da implementação (isso virá mais tarde). Então você precisa fornecer a sua aplicação com uma chave pública para verificar tokens assinados. Você pode gerar o seu próprio publickey.pem usando OpenSSL ou usar os fornecidos nesta pasta de recursos do projeto.. Certifique-se de salvar e armazenar o private.pem. Você precisará dele para a geração do JWT.
Configure seu Servidor de Recursos
Crie um controlador de recursos para seu endpoint:
// Insira o nome do seu pacote aqui + .controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api")
class ResourceController {
@GetMapping("/fetchDocuments")
fun fetchDocumentsEndpoint(): String {
return "Aqui estão seus documentos"
}
}
Por enquanto, a classe ResourceController contém apenas um endpoint.
Agora, crie uma configuração de segurança para o seu Servidor de Recursos:
// Insira o nome do seu pacote aqui + .config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class OAuth2ResourceServerSecurityConfiguration {
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
.httpBasic { it.disable() }
.formLogin { it.disable() } // Desativa o login baseado em formulário padrão do Spring
.csrf { it.disable() }
.authorizeHttpRequests {
it.requestMatchers(HttpMethod.GET, "/api/fetchDocuments").hasAuthority("SCOPE_read:documents") // Verifica se o cliente tem acesso de leitura
it.anyRequest().authenticated()
}
.oauth2ResourceServer { // Habilita a autenticação baseada em JWT para um Servidor de Recursos OAuth2.
it.jwt { }
}
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.build()
}
Se você trabalhou com Spring Security em Java antes, provavelmente notará como o DSL do Kotlin parece limpo em comparação. Referências a OAuth2LoginConfigurer, envolvendo lambdas em Customizer, ou até mesmo anotações como @Throws(Exception::class) não são estritamente necessárias (a menos que você esteja trabalhando com uma mistura de Java e Kotlin). O DSL do Kotlin corta isso fora e permite expressar as regras diretamente.
Agora, gere o JWT usando a chave privada (encontrada em private.pem). Certifique-se de codificá-lo usando o RS256 e que as reivindicações estão configuradas corretamente:

Execute seu aplicativo Spring Boot e, em seguida, inicie uma solicitação autenticada para o endpoint da API /fetchDocuments com seu JWT gerado como token portador:
GET http://localhost:8080/api/fetchDocuments Bearer Token <JWT>

Se funcionar como deve, você verá “Here are your documents” na resposta. Esta implementação permite que você simule um cliente enviando uma solicitação com um token portador (JWT). Ao receber o token, seu Resource Server (backend) verifica a data de expiração e assinatura usando os detalhes no arquivo de propriedades do aplicativo. Ele também procura pelo escopo read:documents antes de conceder acesso ao endpoint fetchDocument.
Gerenciando padrões avançados e validações
No gerenciamento de documentos (e em quase todos os sistemas complexos), verificações simples de escopo não são suficientes. Elas podem conceder permissões grosseiras, mas geralmente falham em capturar a sutileza do controle de acesso real-world. Para resolver isso, o sistema deve separar a validação do token (garantindo que o JWT seja autêntico) da autorização de negócios (decidindo quais ações um usuário pode realizar).
Os escopos por si só não podem impor propriedade ou regras hierárquicas, e eles não capturam papéis organizacionais. É por isso que você precisa de uma combinação de acesso baseado em escopo e papel, onde administradores têm acesso a todas as funcionalidades, enquanto usuários de nível inferior são concedidos apenas algumas delas. Ao camada roles, escopos e verificações de recurso, a API atinge um autorização granular e contextual que equilibra segurança com usabilidade.
Dureza decisões de segurança em tais sistemas deve ser evitada a todo custo. Práticas como incorporar verificação de papel ou lógica de escopo diretamente nos métodos do controlador podem parecer convenientes no início, mas introduzem riscos significativos à medida que seu sistema cresce. Um desenvolvedor pode esquecer de atualizar uma dessas verificações dureza quando as exigências de negócios mudam, deixando certos endpoints expostos ou inconsistentes. Dureza também subverte separações de preocupação. Decisões de segurança devem ser modeladas em uma camada dedicada, não misturadas na lógica de negócio.
Usando extração personalizada de reivindicação e Spring Security’s PreAuthorize
Assim como a maioria dos formatos de token, os JWTs podem transportar reivindicações personalizadas em seus payloads. Um JWT com reivindicações personalizadas para papéis e permissões ficaria mais ou menos assim:
{
"iss": "https://myapp.com/auth",
"sub": "mdu",
"iat": 1773754406,
"exp": 1773840838,
"scope": "read:documents",
"roles": ["admin", "editor"],
"permissions": ["documents:read:all", "documents:write:own"]
}
O Spring lida com mapeamento de autoridade para escopos por padrão e fornece uma função hasRole. No entanto, os papéis não são automaticamente extraídos dos JWTs porque não há um padrão universal para como provedores de identidade os representam. Os escopos estão padronizados em OAuth2 e OpenID Connect, então o Spring pode mapeá-los seguramente em autoridades. Papéis geralmente aparecem sob reivindicações personalizadas e exigem um conversor personalizado para traduzi-los no formato esperado pelo Spring antes que possam ser usados efetivamente.
Suponha que você deseje autenticar e autorizar com base em papéis e escopo. Navegue até sua configuração de segurança e adicione a seguinte função:
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val converter = JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
val authorities = mutableListOf<GrantedAuthority>()
// Mapear escopos
val scopes = (jwt.claims["scope"] as? String)?.split(" ") ?: emptyList()
authorities.addAll(scopes.map { SimpleGrantedAuthority("SCOPE_$it") })
// Mapear papéis
val roles = jwt.claims["roles"] as? Collection<*> ?: emptyList<Any>()
authorities.addAll(roles.map { SimpleGrantedAuthority("ROLE_$it") })
// Mapear permissões
val permissions = jwt.claims["permissions"] as? Collection<*> ?: emptyList<Any>()
authorities.addAll(permissions.map { SimpleGrantedAuthority(it.toString()) })
authorities
}
return converter
}
Isso altera o comportamento do JwtAuthenticationConverter para que ele não dependa apenas da mapeação de escopos padrão do Spring Security. Em vez disso, ele mapeia explicitamente tanto os escopos quanto os papéis do JWT em autoridades do Spring. Se você mapeou apenas papéis, a Spring Security ignoraria completamente a reivindicação scope.
O Kotlin garante a extração segura de reivindicações personalizadas graças à sua segurança contra nulos. Por exemplo, observe a seção de mapeamento de escopos do código. O operador de chamada segura (?.) garante que, se jwt.claims["scope"] for null, a cadeia para graciosamente em vez de lançar uma exceção NullPointerException. O operador de cast seguro (as? String) tenta converter o valor retornado da operação jwt.claims["scope"] em um String a partir de um Any? (pode ser qualquer coisa ou nulo). Se o operador de cast seguro falhar, ele retorna null em vez de lançar uma exceção ClassCastException. Isso permite conversões seguras do tipo que não interromperão nem quebrarão seu código. O operador Elvis (?:) fornece um valor alternativo quando o lado esquerdo é null. Portanto, se a função estiver ausente por qualquer motivo, ela retorna uma lista vazia como valor padrão.
A parte complicada é adicionar validações para todas essas reivindicações. Se você verificasse essas reivindicações individualmente, poderia usar a função hasRole para papéis e hasAuthority para escopos e permissões. Uma maneira de encadear essas validações seria usar a função access. Aqui, você usará [Spring’s Method Security](Anotação Spring @EnableMethodSecurity | Baeldung) (@PreAuthorize) porque oferece uma abordagem mais granular e limpa.
Vá para seu arquivo de configuração de segurança e coloque o @EnableMethodSecurity(prePostEnabled = true) acima da definição da classe:
...
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
class OAuth2ResourceServerSecurityConfiguration {
class SecurityConfig(
...
Você pode manter sua cadeia de filtros de segurança como está por enquanto. Navegue até seu controlador de recursos e adicione a anotação @PreAuthorize a ele:
...
@GetMapping("/fetchDocuments")
@PreAuthorize("hasRole('admin') and hasAuthority('documents:read:all')")
fun fetchDocumentsEndpoint(): String {
return "Aqui estão seus documentos"
}
...
Aviso: Você precisará importar a anotação @PreAuthorize para que isso funcione.
Isto garante que apenas administradores com permissões de leitura total possam acessar o endpoint fetchDocuments. Você pode criar mais endpoints, como getDocument e deleteDocument, para testar a combinação de seus papéis e permissões. A anotação @PreAuthorize ajuda você a evitar o embedding de verificações de papel ou lógica de escopo diretamente dentro dos métodos do controlador (por exemplo, escrevendo if (user.hasRole("admin")) { ... } no corpo de um controlador). Alternativamente, você pode realizar suas verificações de papel na sua cadeia de filtros e suas verificações de escopo e permissões no nível do método.
Fortalecendo a confiança em Tokens: Enforcamento de Emissor e Público-alvo
Em condições normais, você forneceria uma URI de emissor para o Spring Security em seu arquivo de propriedades da aplicação. Então, o Spring faria o trabalho de encontrar a Configuração do Provedor ou Metadados de Servidor de Autorização e usá-las para decodificar seu JWT. Mas, como você aprendeu aqui, esses podem ser contornados quando você está usando chaves geradas manualmente.
Independentemente de ter configurado uma URI de emissor ou não, é importante verificar explicitamente o emissor (iss) no código para garantir que cada token recebido realmente afirma o mesmo emissor e prevenir a reutilização de tokens entre aplicativos. Isso adiciona defesa em profundidade e torna sua postura de segurança clara no código. Da mesma forma, público-alvo (aud) garante que o token é destinado à sua API, não para algum outro aplicativo. Quando tanto o emissor quanto o público-alvo são verificados, isso previne tokens de outros aplicativos ou ambientes de serem aceitos por sua API.
Para validar essas reivindicações, você precisará criar um JwtDecoder personalizado. Mas, como o Spring não tem um validador de token OAuth2 dedicado para seu público-alvo, você precisará criar um. Reabra seu arquivo de configuração de segurança e adicione a seguinte classe (aninhada):
class AudienceValidator(private val audience: String) : OAuth2TokenValidator<Jwt> {
override fun validate(token: Jwt): OAuth2TokenValidatorResult =
if (token.audience.contains(audience)) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(OAuth2Error("invalid_token", "O público-alvo necessário está ausente", null))
}
}
Aviso: Não se esqueça de importar todas as classes e interfaces necessárias.
Em seguida, adicione o seguinte método:
@Bean
fun jwtDecoder(): JwtDecoder {
val issuer = "https://myapp.com/auth" // Substitua pelo seu próprio URI oficial do emissor
val audience = "http://localhost:8080/api/"
val decoder = JwtDecoders.fromIssuerLocation<NimbusJwtDecoder>(issuer)
// Adicione validação de público-alvo
val audienceValidator = AudienceValidator(audience)
val issuerValidator = JwtValidators.createDefaultWithIssuer(issuer)
val validator = DelegatingOAuth2TokenValidator(listOf(issuerValidator, audienceValidator))
(decoder as NimbusJwtDecoder).setJwtValidator(validator)
return decoder
}
Esta função cria um JwtDecoder personalizado que impõe uma validação mais rígida nos JWTs recebidos. Inicia criando um decodificador a partir do emissor configurado, então define dois validadores: um para garantir que a afirmação aud da token corresponda ao público-alvo esperado e outro para garantir que a afirmação iss corresponda ao emissor confiável. Esses validadores são combinados em um DelegatingOAuth2TokenValidator e aplicados ao decodificador, de modo que apenas tokens emitidos pelo provedor de identidade correto e destinados ao seu aplicativo sejam aceitos.
Aviso: Se você precisar de um Servidor de Autorização (emissor) para testar esta sequência, pode usar um servidor local ou mock como mock-oauth2-server. Ele também suporta a geração de JWT personalizados.
Adicione a validação à sua cadeia de filtros de segurança:
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
.httpBasic { it.disable() }
.formLogin { it.disable() }
.csrf { it.disable() }
.authorizeHttpRequests {
it.requestMatchers("/api/fetchDocuments").hasAuthority("SCOPE_read:documents")
it.anyRequest().authenticated()
}
.oauth2ResourceServer {
it.jwt { jwt ->
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
jwt.decoder(jwtDecoder()) // Adicione JwtDecoder personalizado
}
}
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.build()
Isto permite a aplicação rígida das regras pelo backend, nunca deixando isso para lógica do frontend autorizar ou validar informações sensíveis.
O que vem a seguir?
Uma segurança forte requer controle granular e salvaguardas em camadas além da autenticação básica. Use tokens de vida curta com estratégias claras de renovação e revogação para limitar a exposição e evitar que tokens comprometidos persistam. Evite usar JWTs para armazenamento de sessão, pois isso leva ao acúmulo de tokens, complica a revogação e aumenta o risco de expor dados sensíveis. Em vez disso, mantenha os JWTs focados em afirmações de autenticação e autorização, e forneça validação do emissor, público-alvo, assinatura e data de expiração para garantir que os tokens sejam confiáveis e destinados ao seu aplicativo.
No final das contas, a segurança de APIs Spring Boot com OAuth2 e JWT depende de um design cuidadoso, configuração explícita e uma compreensão clara do modo como tokens, escopos e identidades são validados e impostos. O Kotlin complementa isso ao promover segurança contra nulos, imutabilidade e configurações concisas, ajudando a reduzir erros de configuração e casos de uso negligenciados.

