O módulo auth-service é a implementação executável do microsserviço de autenticação. Ele expõe os endpoints definidos em auth, cuida da geração de JWT via JwtService e delega o gerenciamento de contas ao microsserviço account via Feign. O componente RouterValidator — também apresentado aqui — é a peça que diz ao Gateway quais rotas exigem autenticação e quais são abertas a todos.
1. Rotas abertas vs. rotas protegidas
Nem todo endpoint exige autenticação. O cadastro e o login precisam ser públicos — caso contrário, os usuários nunca conseguiriam se autenticar. Todas as outras rotas são protegidas: elas exigem um cookie JWT válido.
O RouterValidator codifica essa política como um predicado simples:
flowchart LR
request[Requisição recebida]
open{É rota aberta?}
secured[Segue para o filtro]
forward[Encaminha a requisição]
request --> open
open -- Sim --> forward
open -- Não --> secured
secured --> forward
As rotas abertas definidas nesta plataforma são:
Método
Caminho
Finalidade
POST
/auth/register
Criar uma nova conta
POST
/auth/login
Autenticar e receber um cookie JWT
GET
/auth/logout
Apagar o cookie JWT
Todo o resto — inclusive todos os endpoints /accounts/** — exige um cookie válido.
2. O módulo auth-service
2.1 Repositório
Crie um novo repositório git para o serviço de autenticação e adicione-o como submódulo:
Ponto de entrada do Spring Boot; habilita os clientes Feign do pacote store.account
AuthResource
Controller REST que implementa AuthController; define o cookie JWT no login/logout
AuthService
Lógica de negócio: delega o cadastro e a conferência das credenciais ao account e chama o JwtService para gerar tokens
JwtService
Encapsula a biblioteca JJWT; assina e interpreta JWTs com uma chave secreta HMAC
AuthParser
Converte o DTO RegisterIn no objeto interno Register
Register
Dados internos de cadastro (nome, e-mail, senha) entregues ao AuthService.create
2.3 Conferindo as credenciais no account
O auth-service nunca confere uma senha por conta própria. Essa conferência pertence ao account: ele é o único serviço que guarda a senha, e guarda apenas o seu hash SHA-256 (campo hashPassword, coluna hash_password, calculado por AccountService.calcHash no create). Por isso, o contrato do account do Hands-on 1 ganha uma operação — POST /accounts/login (AccountController.login); adicione as partes abaixo ao account e ao account-service. O account-service calcula o hash da senha recebida com o mesmocalcHash e procura uma conta com aquele e-mail e aquele hash, pela query derivada do Spring Data findByEmailAndHashPassword. Ele responde 200 com o AccountOut (nunca o hash) ou 401 Unauthorized. A senha só trafega dentro da camada confiável — exatamente como já acontece no POST /accounts durante o cadastro — e o hash nunca sai do account.
Um .orElseThrow() sem argumento lança NoSuchElementException quando nenhuma conta confere, e o Spring responde 500 Internal Server Error — como se o servidor estivesse quebrado. Passe a exceção a lançar: .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, ...)). Do outro lado, o Feign transforma esse 401 em uma FeignException.Unauthorized dentro do auth-service (e o 400 de uma senha vazia em uma FeignException.BadRequest); o AuthService.login (abaixo) as captura e responde 401 — senão o login voltaria a falhar com 500.
Reinstale a biblioteca account
O AccountController fica no módulo de interface account, então rode mvn clean install em api/account de novo antes de compilar o account-service e o auth-service.
Uma observação sobre o hash
Um SHA-256 simples mantém o curso enxuto, mas é rápido e não usa salt, então hashes vazados são fáceis de quebrar por força bruta. Sistemas em produção usam um algoritmo lento e com salt, como bcrypt ou Argon2 (por exemplo, o PasswordEncoder do Spring Security, que confere com matches(raw, hash) em vez de buscar no banco pelo hash).
2.4 O JwtService em detalhe
O JwtService é responsável por duas coisas:
Gerar um token (generate): ele monta um JWT com o id da conta na claim jti, o name da conta como subject e o email como claim personalizada. O token é assinado com uma chave HMAC derivada do segredo codificado em base64 e expira após a duração configurada (store.jwt.duration, em milissegundos — 1 hora por padrão).
Validar um token (getId): o parser do JJWT verifica a assinatura e também recusa um token prematuro (nbf) ou expirado (exp), lançando uma JwtException (SignatureException, PrematureJwtException, ExpiredJwtException, …). O getId transforma qualquer uma delas em 401 Unauthorized — sem o catch, um cookie apenas expirado terminaria em 500. Se o token for válido, devolve o id da conta (jti).
sequenceDiagram
autonumber
actor User as Usuário
User->>+AuthResource: POST /auth/login (LoginIn)
AuthResource->>+AuthService: login(email, password)
AuthService->>+AccountController: POST /accounts/login (AccountIn)
AccountController-->>-AuthService: AccountOut (ou 401)
AuthService->>+JwtService: generate(AccountOut, duration)
JwtService-->>-AuthService: string JWT assinada
AuthService-->>-AuthResource: string JWT assinada
AuthResource->>-User: Set-Cookie: __store_jwt_token=<jwt>
spring:application:name:authstore:jwt:secretKey:${JWT_SECRET_KEY:EBwrdOOcAIDRLON9hbJV0+O3PTdfTjgMCQIxwLH7ws8=}httpOnly:${JWT_HTTP_ONLY:true}duration:${JWT_DURATION:3600000}# 1 hour in milliseconds
packagestore.auth;importjava.util.Map;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.http.HttpStatus;importorg.springframework.stereotype.Service;importorg.springframework.web.server.ResponseStatusException;importfeign.FeignException;importstore.account.AccountController;importstore.account.AccountIn;importstore.account.AccountOut;@ServicepublicclassAuthService{@AutowiredprivateAccountControlleraccountController;@AutowiredprivateJwtServicejwtService;@Value("${store.jwt.duration}")privateLongduration;@Value("${store.jwt.httpOnly}")privateBooleanhttpOnly;publicvoidcreate(Registerregister){accountController.create(AccountIn.builder().name(register.name()).email(register.email()).password(register.password()).build());}publicStringlogin(Stringemail,Stringpassword){finalAccountOutaccount;try{// account hashes the password and compares it with the stored hashaccount=accountController.login(AccountIn.builder().email(email).password(password).build()).getBody();}catch(FeignException.Unauthorized|FeignException.BadRequeste){thrownewResponseStatusException(HttpStatus.UNAUTHORIZED,"Invalid credentials");}returnjwtService.generate(account,getDuration());}publiclonggetDuration(){returnduration;}publicbooleangetHttpOnly(){returnhttpOnly;}publicMap<String,String>solve(finalStringjwt){returnMap.of("idAccount",jwtService.getId(jwt));}publicAccountOutme(StringidAccount){returnaccountController.findById(idAccount).getBody();}}
packagestore.auth;importjava.util.Base64;importjava.util.Date;importjava.util.Map;importjavax.crypto.SecretKey;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.http.HttpStatus;importorg.springframework.stereotype.Service;importorg.springframework.web.server.ResponseStatusException;importio.jsonwebtoken.Claims;importio.jsonwebtoken.JwtException;importio.jsonwebtoken.JwtParser;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.security.Keys;importstore.account.AccountOut;@ServicepublicclassJwtService{@Value("${store.jwt.secretKey}")privateStringsecretKey;publicStringgenerate(AccountOutaccount,longduration){Datenow=newDate();returnJwts.builder().header().and().id(account.id()).issuer("Insper::PMA").claims(Map.of("email",account.email())).signWith(getKey()).subject(account.name()).notBefore(now).expiration(newDate(now.getTime()+duration)).compact();}privateSecretKeygetKey(){returnKeys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));}publicStringgetId(Stringjwt){try{JwtParserparser=Jwts.parser().verifyWith(getKey()).build();// checks the signature, nbf and exp: a JwtException otherwiseClaimsclaims=parser.parseSignedClaims(jwt).getPayload();returnclaims.getId();}catch(JwtExceptione){thrownewResponseStatusException(HttpStatus.UNAUTHORIZED,"Invalid token");}}}
O JWT é entregue ao navegador em um cookie HttpOnly chamado __store_jwt_token. Usar um cookie HttpOnly, em vez de guardar o token no localStorage, impede que o JavaScript o leia, o que mitiga ataques de XSS.
Outros atributos do cookie usados:
Atributo
Valor
Finalidade
HttpOnly
true
O JavaScript não consegue acessar o cookie
Secure
true
O cookie só é enviado por HTTPS
SameSite
None
O cookie é enviado em requisições cross-site (necessário para frontends SPA)
Path
/
O cookie vale para todos os caminhos
SameSite=None exige Secure=true
Os navegadores rejeitam cookies SameSite=None sem o atributo Secure. Isso significa que o frontend e a API precisam se comunicar por HTTPS em produção.
Pronto! O auth-service está pronto. Na próxima seção vamos implementar o filtro do Gateway que intercepta cada requisição, extrai o cookie e pede ao auth-service que resolva o token.