Plataformas, Microsserviços, DevOps e APIs · Hands-on 3

Hands-on 3 — Segurança

Faça login uma vez, receba um JWT assinado e deixe o Gateway verificá-lo a cada requisição

Quem é você, e o que você pode fazer?

As requisições para /accounts agora chegam da internet. Cada serviço deveria verificar a senha do usuário?

  • Autenticação: verificar quem é o usuário → após o login, emitir um JWT assinado, válido por tempo limitado.
  • Autorização: verificar o que o usuário pode acessar → o Gateway confere o JWT antes de rotear.
  • Um ponto único de entrada: os serviços atrás dele nunca veem uma requisição não autenticada.

Três novos módulos

internet → gateway → account / auth / others · auth → account · tudo dentro da camada confiável

Módulo Papel
auth biblioteca de interface: AuthController + DTOs LoginIn, RegisterIn, TokenOut
auth-service implementação: AuthResource, AuthService, JwtService; delega as contas ao account via Feign
gateway-service ponto único de entrada: AuthorizationFilter + RouterValidator

Mesmo padrão de account / account-service: uma biblioteca instalada com mvn clean install, mais um serviço executável.

JSON Web Token

Três partes em Base64Url separadas por pontos: header.payload.signature

HMAC( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
  • Header: tipo do token e algoritmo de assinatura.
  • Payload: as claims — declarações sobre o usuário.
  • Signature: prova que o token foi emitido por quem tem o segredo e não foi alterado.
  • Stateless: sem sessão no servidor, então escala horizontalmente.

Quem tem a chave secreta consegue forjar tokens: guarde-a em uma variável de ambiente, nunca fixa no código.

As claims que esta plataforma usa

Claim Campo no JJWT Conteúdo
jti id o identificador único da conta
sub subject o nome da conta
iss issuer a plataforma emissora (Insper::PMA)
nbf notBefore válido a partir deste instante
exp expiration expira neste instante

Mais uma claim personalizada (privada): email. O payload é só codificado, não criptografado — qualquer um pode lê-lo.

O contrato auth

@FeignClient(name="auth", url="http://auth:8080") · nome do cookie AUTH_COOKIE_TOKEN = "__store_jwt_token"

Método Caminho Faz
POST /auth/register create: RegisterIn(name, email, password) → cria a conta
POST /auth/login LoginIn(email, password) → define o cookie JWT
GET /auth/logout apaga o cookie
GET /auth/me header id-account → AccountOut
POST /auth/solve TokenOut(token) → { "idAccount": … } — interno, só o Gateway
GET /auth/health-check 200 OK

auth-service

Classe Responsabilidade
AuthApplication @EnableFeignClients(basePackages = "store.account")
AuthResource implementa AuthController; grava o cookie no login/logout
AuthService cadastra via account (AuthParser → Register); no login, o POST /accounts/login confere as credenciais e então pede um token
JwtService assina e interpreta JWTs com a biblioteca JJWT (chave HMAC: segredo de 256 bits → HS256)
store:
  jwt:
    secretKey: ${JWT_SECRET_KEY:EBwrdOOcAIDRLON9hbJV0+O3PTdfTjgMCQIxwLH7ws8=}
    httpOnly: ${JWT_HTTP_ONLY:true}
    duration: ${JWT_DURATION:3600000} # 1 hour in milliseconds

JwtService.generate

public String generate(AccountOut account, long duration) {
    Date now = new Date();
    return Jwts.builder()
        .id(account.id())
        .issuer("Insper::PMA")
        .claims(Map.of("email", account.email()))
        .signWith(getKey())  // Keys.hmacShaKeyFor(Base64-decoded secretKey)
        .subject(account.name())
        .notBefore(now)
        .expiration(new Date(now.getTime() + duration))
        .compact();
}

JwtService.getId

public String getId(String jwt) {
    try {
        JwtParser parser = Jwts.parser().verifyWith(getKey()).build();
        // checks the signature, nbf and exp: a JwtException otherwise
        Claims claims = parser.parseSignedClaims(jwt).getPayload();
        return claims.getId();
    } catch (JwtException e) {
        throw new ResponseStatusException(
            HttpStatus.UNAUTHORIZED, "Invalid token");
    }
}

É isso que o /auth/solve executa: um token válido vira um idAccount; um inválido ou expirado, um 401 (não um 500).

  1. POST /auth/login → AuthService.login → POST /accounts/login (confere o hash) → JwtService.generate.
  2. O AuthResource responde com Set-Cookie: __store_jwt_token=<jwt>.
  3. O navegador o guarda e o envia automaticamente em cada requisição seguinte.
Atributo Valor Por quê
HttpOnly true o JavaScript não consegue lê-lo → mitiga XSS (ao contrário do localStorage)
Secure true enviado só por HTTPS
SameSite None enviado em requisições cross-site (frontends SPA)
Path / vale para todos os caminhos

Os navegadores rejeitam SameSite=None sem Secure: em produção, frontend e API precisam usar HTTPS.

Rotas abertas: RouterValidator

Sem rotas abertas, ninguém conseguiria fazer login.

private List<String> openApiEndpoints = List.of(
    "POST /auth/register",
    "GET /auth/logout",
    "POST /auth/login");

public Predicate<ServerHttpRequest> isSecured = request ->
    openApiEndpoints.stream().noneMatch(/* method and path match, /** = prefix */);

Protegido por padrão: tudo o que não está na lista — inclusive todo /accounts/** — exige um cookie válido.

O Gateway decide: AuthorizationFilter

Um GlobalFilter: toda requisição passa por ele antes do roteamento.

  1. isSecured? Não → encaminha como está.
  2. Cookie __store_jwt_token presente? Não → 401 Unauthorized.
  3. POST http://auth:8080/auth/solve com { "token": jwt }.
  4. Token inválido → 401. Válido → adiciona o header e encaminha:
Header Valor
id-account o UUID da conta, tirado do token

Um id-account enviado pelo cliente é descartado antes (RemoveRequestHeader): ninguém se passa por outra conta.

AuthorizationFilter.filter

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    if (!routerValidator.isSecured.test(request)) {
        return chain.filter(exchange);
    }
    if (request.getCookies().containsKey(AUTH_COOKIE_TOKEN)) {
        String token = request.getCookies().getFirst(AUTH_COOKIE_TOKEN).getValue();
        if (null != token && token.length() > 0) {
            return requestAuthTokenSolve(exchange, chain, token.trim());
        }
    }
    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}

O Spring Cloud Gateway é reativo (WebFlux): o filtro devolve Mono<Void> e chama o auth com WebClient, não com RestTemplate.

Ligando tudo: rotas e Compose

default-filters:
  - RemoveRequestHeader=id-account
routes:
  - id: account-service
    uri: http://account:8080
    predicates:
      - Path=/accounts/**
  - id: auth-service
    uri: http://auth:8080
    predicates:
      - Path=/auth/**
  • Serviço novo: auth (depois do gateway, hands-on 2).
  • Só o gateway publica a porta 8080; o auth fica do lado de dentro.
  • O auth roda com os padrões: JWT_SECRET_KEY, JWT_HTTP_ONLY, JWT_DURATION.
  • gateway: o CORS_ALLOWED_ORIGINS também tem padrão.

A chave padrão é pública (está no repositório): fora do desenvolvimento, defina a JWT_SECRET_KEY com uma sua — openssl rand -base64 32.

Teste

docker compose up -d --build

curl -X POST http://localhost:8080/auth/register -H "Content-Type: application/json" \
  -d '{"name": "Antonio do Estudo", "email": "acme@insper.edu.br", "password": "123@321"}'

curl -c cookies.txt -X POST http://localhost:8080/auth/login -H "Content-Type: application/json" \
  -d '{"email": "acme@insper.edu.br", "password": "123@321"}'

curl -b cookies.txt http://localhost:8080/accounts   # 200 — cookie sent
curl http://localhost:8080/accounts                  # 401 — no cookie

Pontos-chave

  1. A autenticação emite um JWT assinado; a autorização o confere a cada requisição.
  2. Um JWT é header.payload.signature; a chave secreta é o que o torna confiável.
  3. O JwtService assina com jti, sub, iss, nbf, exp — e valida a assinatura, nbf e exp.
  4. O token viaja em um cookie HttpOnly e Secure: __store_jwt_token.
  5. RouterValidator: só cadastro, login e logout são abertos — protegido por padrão.
  6. O AuthorizationFilter resolve o token via /auth/solve e repassa o id-account.
  7. Os serviços seguintes nunca interpretam JWTs: o Gateway faz isso uma única vez.

Roteiro: (1) autenticação × autorização; (2) o JWT e suas claims; (3) a interface auth e o auth-service; (4) o cookie; (5) rotas abertas; (6) o AuthorizationFilter no gateway; (7) compose e testes.

Deixe a turma responder primeiro: verificar credenciais em cada serviço multiplica a superfície de ataque e o código.

Enfatize: os serviços seguintes nunca interpretam JWTs; eles só leem o header id-account.