Primeiramente, vamos extrair o módulo eats-administrativo
do monólito para um serviço eats-administrativo-service
.
Pelo navegador, abra https://start.spring.io/
.
Em Project, mantenha Maven Project.
Em Language, mantenha Java.
Em Spring Boot, mantenha a versão padrão.
No trecho de Project Metadata, defina:
br.com.caelum
em Groupeats-administrativo-service
em Artifact
Mantenha os valores em More options.
Mantenha o Packaging como Jar
.
Mantenha a Java Version em 8
.
Em Package Name, mude para br.com.caelum.eats.administrativo
.
Em Dependencies, adicione:
- Spring Boot DevTools
- Spring Boot Actuator
- Spring Data JPA
- Spring Web Starter
- Config Client
- Eureka Discovery Client
- Zipkin Client
Clique em Generate Project.
Extraia o eats-administrativo-service.zip
e copie a pasta para seu Desktop.
Então, devemos mover as seguintes classes do módulo Administrativo do monólito para o novo serviço:
- FormaDePagamento
- FormaDePagamentoController
- FormaDePagamentoRepository
- TipoDeCozinha
- TipoDeCozinhaController
- TipoDeCozinhaRepository
O serviço Administrativo deve apontar para o Config Server, definindo um bootstrap.properties
com o application name administrativo
.
####### fj33-eats-administrativo-service/src/main/resources/bootstrap.properties
spring.application.name=administrativo
spring.cloud.config.uri=http://localhost:8888
No arquivo administrativo.properties
do config-repo
, definiremos as configurações de data source.
####### fj33-config-repo/administrativo.properties
#DATASOURCE CONFIGS
spring.datasource.url=jdbc:mysql://localhost:3307/eats?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=caelum123
Inicialmente, o serviço administrativo pode apontar para o mesmo BD do monólito. Aos poucos, deve ser feita a migração das tabelas forma_de_pagamento
e tipo_de_cozinha
para um BD próprio.
No application.properties
, deve ser definida 8084
na porta a ser utilizada, além de outras configurações.
####### fj33-eats-administrativo-service/src/main/resources/application.properties
server.port=8084
#JPA CONFIGS
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jackson.serialization.fail-on-empty-beans=false
Então, o módulo eats-administrativo
do monólito pode ser removido, assim como suas autorizações no módulo eats-seguranca
.
Remova a dependência a eats-administrativo
do pom.xml
do módulo eats-application
do monólito:
####### fj33-eats-monolito-modular/eats/eats-application/pom.xml
<̶d̶e̶p̶e̶n̶d̶e̶n̶c̶y̶>̶
̶ ̶ ̶<̶g̶r̶o̶u̶p̶I̶d̶>̶b̶r̶.̶c̶o̶m̶.̶c̶a̶e̶l̶u̶m̶<̶/̶g̶r̶o̶u̶p̶I̶d̶>̶
̶ ̶<̶a̶r̶t̶i̶f̶a̶c̶t̶I̶d̶>̶e̶a̶t̶s̶-̶a̶d̶m̶i̶n̶i̶s̶t̶r̶a̶t̶i̶v̶o̶<̶/̶a̶r̶t̶i̶f̶a̶c̶t̶I̶d̶>̶
̶ ̶ ̶<̶v̶e̶r̶s̶i̶o̶n̶>̶$̶{̶p̶r̶o̶j̶e̶c̶t̶.̶v̶e̶r̶s̶i̶o̶n̶}̶<̶/̶v̶e̶r̶s̶i̶o̶n̶>̶
̶<̶/̶d̶e̶p̶e̶n̶d̶e̶n̶c̶y̶>̶
Faço o mesmo nos arquivos pom.xml
dos módulos eats-restaurante
e eats-pedido
do monólito.
No projeto pai dos módulos, o projeto eats
, remova o módulo eats-administrativo
do pom.xml
:
####### fj33-eats-monolito-modular/eats/pom.xml
<modules>
<̶m̶o̶d̶u̶l̶e̶>̶e̶a̶t̶s̶-̶a̶d̶m̶i̶n̶i̶s̶t̶r̶a̶t̶i̶v̶o̶<̶/̶m̶o̶d̶u̶l̶e̶>̶
<module>eats-restaurante</module>
<module>eats-pedido</module>
<module>eats-seguranca</module>
<module>eats-application</module>
</modules>
Apague o módulo eats-administrativo
do monólito. Pelo Eclipse, tecle Delete em cima do módulo, selecione a opção Delete project contents on disk (cannot be undone) e clique em OK.
Remova, da classe SecurityConfig
do módulo eats-seguranca
do monólito, as configurações de autorização dos endpoints que foram movidos:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/SecurityConfig.java
class SecurityConfig extends WebSecurityConfigurerAdapter {
// código omitido ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/restaurantes/**", "/pedidos/**",̶ ̶"̶/̶t̶i̶p̶o̶s̶-̶d̶e̶-̶c̶o̶z̶i̶n̶h̶a̶/̶*̶*̶"̶,̶ ̶"̶/̶f̶o̶r̶m̶a̶s̶-̶d̶e̶-̶p̶a̶g̶a̶m̶e̶n̶t̶o̶/̶*̶*̶"̶).permitAll()
.antMatchers("/actuator/**").permitAll()
.̶a̶n̶t̶M̶a̶t̶c̶h̶e̶r̶s̶(̶"̶/̶a̶d̶m̶i̶n̶/̶*̶*̶"̶)̶.̶h̶a̶s̶R̶o̶l̶e̶(̶R̶o̶l̶e̶.̶R̶O̶L̶E̶S̶.̶A̶D̶M̶I̶N̶.̶n̶a̶m̶e̶(̶)̶)̶
// código omitido ...
}
}
Também é necessário alterar as referências às classes TipoDeCozinha
e FormaDePagamento
no DistanciaRestClientWiremockTest
do módulo eats-application
.
Já no módulo de restaurantes do monólito, é preciso alterar referências às classes migradas para o serviço Administrativo para apenas utilizarem os ids dos agregados TipoDeCozinha
e FormaDePagamento
. Isso afeta diversas classes do módulo eats-restaurante
:
- Restaurante
- RestauranteController
- RestauranteDto
- RestauranteFormaDePagamento
- RestauranteFormaDePagamentoController
- RestauranteFormaDePagamentoRepository
- RestauranteParaServicoDeDistancia
- RestauranteRepository
- RestauranteService
A UI também será afetada. Uma das mudanças é que chamadas relativas a tipos de cozinha e formas de pagamento devem ser direcionadas para o serviço Administrativo. Esse serviço irá registrar-se no Eureka Server com o nome administrativo
, o seu application name. O API Gateway faz o roteamento dinâmico baseado nas instâncias disponíveis no Service Registry. Por isso, podemos trocar chamadas como a seguinte para utilizarem o prefixo administrativo
:
####### fj33-eats-ui/src/app/services/tipo-de-cozinha.service.ts
export class TipoDeCozinhaService {
p̶r̶i̶v̶a̶t̶e̶ ̶A̶P̶I̶ ̶=̶ ̶e̶n̶v̶i̶r̶o̶n̶m̶e̶n̶t̶.̶b̶a̶s̶e̶U̶r̶l̶;̶
private API = environment.baseUrl + '/administrativo';
// código omitido ...
}
O mesmo deve ser feito para a classe FormaDePagamentoService
.
As diversas mudanças no módulo de restaurantes do monólito também afetam a UI.
- Crie um arquivo
administrativo.properties
noconfig-repo
, definindo um data source que aponta para o mesmo BD do monólito:
####### config-repo/administrativo.properties
spring.datasource.url=jdbc:mysql://localhost:3307/eats?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=caelum123
- Clone o projeto
fj33-eats-administrativo-service
para o seu Desktop:
cd ~/Desktop
git clone https://gitlab.com/aovs/projetos-cursos/fj33-eats-administrativo-service.git
No Eclipse, no workspace de microservices, importe o projeto fj33-eats-administrativo-service
, usando o menu File > Import > Existing Maven Projects.
Com o Service Registry e o Config Server no ar, suba o serviço Administrativo executando a classe EatsAdministrativoServiceApplication
.
- Faça checkout da branch
cap14-extrai-administrativo-service
do monólito modular e da UI:
cd ~/Desktop/fj33-eats-monolito-modular
git checkout -f cap14-extrai-administrativo-service
cd ~/Desktop/fj33-eats-ui
git checkout -f cap14-extrai-administrativo-service
Faça refresh do monólito modular no Eclipse.
Suba os serviços e o front-end. Teste o Caelum Eats. Deve funcionar!
Grande parte das aplicações tem diferentes perfis de usuário, que têm permissão de acesso a diferentes funcionalidades. Isso é o que chamamos de Autorização.
No caso do Caelum Eats, qualquer usuário pode acessar fazer pedidos e acompanhá-los. Porém, a administração do sistema, que permite cadastrar tipos de cozinha, formas de pagamento e aprovar restaurantes, só é acessível pelo perfil de administrador. Já os dados de um restaurante e o gerenciamento dos pedidos pendentes só são acessíveis pelo dono de cada restaurante.
Um usuário precisa identificar-se, ou seja, dizer quem está acessando a aplicação. Isso é o que chamamos de Autenticação.
Uma vez que o usuário está autenticado e sua identidade é conhecida, é possível que a aplicação reforce as permissões de acesso.
Existem algumas maneiras mais comuns de um sistema confirmar a identidade de um usuário:
- algo que o usuário sabe, um segredo, como uma senha
- algo que o usuário tem, como um token físico ou por uma app mobile
- algo que o usuário é, como biometria das digitais, íris ou reconhecimento facial
Two-factor authentication (2FA), ou autenticação de dois fatores, é a associação de duas formas de autenticação para minimizar as chances de alguém mal intencionado identificar-se como outro usuário, no caso de apoderar-se de um dos fatores de autenticação.
Após a autenticação, uma aplicação Web tradicional guarda a identidade do usuário em uma sessão, que comumente é armazenada em memória, mas pode ser armazenada em disco ou em um BD.
O cliente da aplicação, em geral um navegador, deve armazenar um id da sessão. Em toda requisição, o cliente passa esse id para identificar o usuário.
O que acontece quando há um aumento drástico no número de usuários em momento de pico de uso, como na Black Friday?
Se a aplicação suportar esse aumento na carga, podemos dizer que possui a característica arquitetural da Escalabilidade. Quando a escalabilidade é atingida aumentando o número de máquinas, dizemos que é a escalabilidade horizontal.
Mas, se escalarmos horizontalmente a aplicação, onde fica armazenada a sessão se temos mais de uma máquina como servidor Web? Uma estratégia são as sticky sessions, em que cada usuário tem sua sessão em uma máquina específica.
Mas quando alguma máquina falhar, o usuário seria deslogado e não teria mais acesso às funcionalidades. Para que a experiência do usuário seja transparente, de maneira que ele não perceba a falha em uma máquina, há a técnica da replicação de sessão, em que cada servidor compartilha, pela rede, suas sessões com outros servidores. Isso traz uma sobrecarga de processamento, armazenamento e tráfego na rede.
Em sua tese de doutorado Architectural Styles and the Design of Network-based Software Architectures (FIELDING, 2000), Roy Fielding descreve o estilo arquitetural da Web e o chama de Representational State Transfer (REST). Uma das características do REST é que a comunicação deve ser Stateless: toda informação deve estar contida na requisição do cliente ao servidor, sem a necessidade de nenhum contexto armazenado no servidor.
Manter sessões nos servidores é manter estado. Portanto, podemos dizem que utilizar sessões não é RESTful porque não segue a característica do REST de ser stateless.
Mas então como fazer um mecanismo de autenticação que seja stateless e, por consequência, mais próximo do REST?
Como Raphael Lacerda revela no post Morte à sessão! Entenda esse tal de stateless session com tokens (LACERDA, 2017): usando tokens! Há tokens opacos, que são apenas um texto randômico e que não carregam nenhuma informação. Porém, há os self-contained tokens, que contém informações sobre o usuário e/ou sobre o sistema cliente. Cada requisição teria um self-contained token em seu cabeçalho, com todas as informações necessárias para a aplicação. Assim, tiramos a necessidade de armazenamento da sessão no lado do servidor.
A grande questão é como ter um token que contém informações e, ao mesmo tempo, garantir sua integridade, confirmando que os dados do token não foram manipulados?
JWT (JSON Web Token) é um formato de token compacto e self-contained que serve propagar informações de identidade, permissões de um usuário em uma aplicação de maneira segura. Foi definido na RFC 7519 (JONES, 2015) da Internet Engineering Task Force (IETF).
O Working Group da IETF chamado Javascript Object Signing and Encryption (JOSE), definiu duas outras RFCs relacionadas:
- JSON Web Signature (JWS), definido na RFC 7515 (JONES, 2015b), que representa em JSON conteúdo assinado digitalmente
- JSON Web Encryption (JWE), definido na RFC 7516 (JONES, 2015c), que representa em JSON conteúdo criptografado
Para garantir a integridade dos dados de um token, é suficiente usarmos o JWS.
Um JWS consiste de três partes, separadas por .
:
BASE64URL(UTF8(Cabeçalho)) || '.' ||
BASE64URL(Payload) || '.' ||
BASE64URL(Assinatura JWS)
Todas as partes do JWS são codificadas em Base64 URL encoded. Base64 é uma representação em texto de dados binários. URL encoded significa que caracteres especiais são codificados com %
, da mesma maneira como são passados via parâmetros de URLs.
Um exemplo de um JWS usado no Caelum Eats seria o seguinte:
eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjIiLCJyb2xlcyI6WyJQQVJDRUlSTyJdLCJ1c2VybmFtZSI6ImxvbmdmdSIsImlhdCI6MTU2NjQ5ODA5MSwiZXhwIjoxNTY3MTAyODkxfQ.
GOwiEeJMP9t0tV2lQpNiDU211WKL6h5Z6OkNcA-f4EY
Os trechos anteriores podem ser descodificados de Base64 para texto normal usando um site como: http://www.base64url.com/
O primeiro trecho, eyJhbGciOiJIUzI1NiJ9
, é o cabeçalho do JWS. Quando descodificado, é:
{"alg":"HS256"}
O valor de alg
indica que foi utilizado o algoritmo HMAC (hash-based message authentication code) com SHA-256 como função de hash. Nesse algoritmo, há uma chave secreta (um texto) simétrica, que deve ser conhecida tanto pela parte que cria o token como pela parte que o validará. Se essa chave secreta for descoberta por um agente mal intencionado, pode ser usada para gerar tokens válidos deliberadamente.
O segundo trecho, eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjIiLCJyb2xlcyI6WyJQQVJDRUlSTyJdLCJ1c2VybmFtZSI6ImxvbmdmdSIsImlhdCI6MTU2NjQ5ODA5MSwiZXhwIjoxNTY3MTAyODkxfQ
, contém os dados (payload) do JWS:
{
"iss": "Caelum Eats",
"sub": "2",
"roles": [
"PARCEIRO"
],
"username": "longfu",
"iat": 1566498091,
"exp": 1567102891
}
O valor de iss
é o issuer, a aplicação que gerou o token.
O valor de sub
é o subject, que contém informações do usuário.
Os valores de iat
e exp
, são as datas de geração e expiração do token, respectivamente.
Os demais valores são claims customizadas, que declaram informações adicionais do usuário.
O terceiro trecho, GOwiEeJMP9t0tV2lQpNiDU211WKL6h5Z6OkNcA-f4EY
, é a assinatura do JWS e não pode ser decodificada para um texto. O que importam são os bytes.
No site https://jwt.io/ conseguimos obter o algoritmo utilizado, os dados do payload e até validar um JWT.
Se soubermos a chave secreta, podemos verificar se a assinatura bate com o payload do JWS. Se bater, o token é válido. Dessa maneira, conseguimos garantir que não houve manipulação dos dados e, portanto, sua integridade.
Um detalhe importante é que um JWS não garante a confidencialidade dos dados. Se houver algum software bisbilhotando os dados trafegados na rede, o payload do JWS pode ser lido, já que é apenas codificado em Base64 URL encoded. A confidencialidade pode ser reforçada por meio de TLS no canal de comunicação ou por meio de JWE.
Uma grande desvantagem de um JWT é que o token é irrevogável antes de sua expiração. Isso implica que, enquanto o token não estiver espirado será válido. Por isso, implementar um mecanismo de logout pelo usuário passa a ser complicado. Poderíamos trabalhar com intervalos pequenos de expiração, mas isso afetaria a experiência do usuário, já que frequentemente a expiração levaria o usuário a efetuar novo login. Uma maneira comum de implementar logout é ter um cache com JWT invalidados. Porém, isso nos leva novamente a uma solução stateful.
Até o momento, um login de um dono de restaurante ou do administrador do sistema dispara a execução do AuthenticationController
do módulo eats-seguranca
do monólito. No método authenticate
, é gerado e retornado um token JWS.
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/AuthenticationController.java
@RestController
@RequestMapping("/auth")
@AllArgsConstructor
class AuthenticationController {
private AuthenticationManager authManager;
private JwtTokenManager jwtTokenManager;
private UserService userService;
@PostMapping
public ResponseEntity<AuthenticationDto> authenticate(@RequestBody UserInfoDto login) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
login.getUsername(), login.getPassword());
try {
Authentication authentication = authManager.authenticate(authenticationToken);
User user = (User) authentication.getPrincipal();
String jwt = jwtTokenManager.generateToken(user);
AuthenticationDto tokenResponse = new AuthenticationDto(user, jwt);
return ResponseEntity.ok(tokenResponse);
} catch (AuthenticationException e) {
return ResponseEntity.badRequest().build();
}
}
// código omitido...
}
A geração, validação e recuperação dos dados do token é feita por meio da classe JwtTokenManager
, que utiliza a biblioteca jjwt
:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/JwtTokenManager.java
@Component
class JwtTokenManager {
private String secret;
private long expirationInMillis;
public JwtTokenManager(@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration}") long expirationInMillis) {
this.secret = secret;
this.expirationInMillis = expirationInMillis;
}
public String generateToken(User user) {
final Date now = new Date();
final Date expiration = new Date(now.getTime() + this.expirationInMillis);
return Jwts.builder()
.setIssuer("Caelum Eats")
.setSubject(Long.toString(user.getId()))
.claim("username", user.getName())
.claim("roles", user.getRoles())
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, this.secret)
.compact();
}
public boolean isValid(String jwt) {
try {
Jwts
.parser()
.setSigningKey(this.secret)
.parseClaimsJws(jwt);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public Long getUserIdFromToken(String jwt) {
Claims claims = Jwts.parser().setSigningKey(this.secret).parseClaimsJws(jwt).getBody();
return Long.parseLong(claims.getSubject());
}
}
O token JWS é armazenado em um localStorage
no front-end. Há um interceptor do Angular que, antes de cada requisição AJAX, adiciona o cabeçalho Authorization: Bearer
com o valor do token armazenado.
####### fj33-eats-ui/src/app/interceptors/jwt-interceptor.ts
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const currentUser = this.authenticationService.currentUserValue;
if (currentUser && currentUser.token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${currentUser.token}`
}
});
}
return next.handle(request);
}
}
No back-end, a classe JwtAuthenticationFilter
é executada a cada requisição e o token JWS é extraído dos cabeçalhos HTTP e validado. Caso seja válido, é recuperado o sub
(Subject) e obtido o usuário do BD com seus ROLEs (ADMIN
ou PARCEIRO
), setando um Authentication
no contexto de segurança:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/JwtAuthenticationFilter.java
@Component
@AllArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JwtTokenManager tokenManager;
private UserService usersService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String jwt = getTokenFromRequest(request);
if (tokenManager.isValid(jwt)) {
Long userId = tokenManager.getUserIdFromToken(jwt);
UserDetails userDetails = usersService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
// código omitido ...
}
}
As configurações de autorização estão definidas na classe SecurityConfig
do módulo eats-seguranca
do monólito.
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/SecurityConfig.java
@Configuration
@EnableWebSecurity
@AllArgsConstructor
class SecurityConfig extends WebSecurityConfigurerAdapter {
// atributos omitidos ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/restaurantes/**", "/pedidos/**", "/pagamentos/**").permitAll()
.antMatchers("/socket/**").permitAll()
.antMatchers("/auth/**").permitAll()
.antMatchers("/actuator/**").permitAll()
.antMatchers(HttpMethod.POST, "/parceiros/restaurantes").permitAll()
.antMatchers("/parceiros/restaurantes/do-usuario/{username}")
.access("@restauranteAuthorizationService.checaUsername(authentication,#username)")
.antMatchers("/parceiros/restaurantes/{restauranteId}/**")
.access("@restauranteAuthorizationService.checaId(authentication,#restauranteId)")
.antMatchers("/parceiros/**").hasRole(Role.ROLES.PARCEIRO.name())
.anyRequest().authenticated()
.and().cors()
.and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
}
// restante do código ...
}
Antes da extração do serviço administrativo, para as URLs que começavam com /admin
, o ROLE do usuário deveria ser ADMIN
e teria acesso a tudo relativo à administração da aplicação. Esse tipo de autorização, em que um determinado ROLE tem acesso a qualquer endpoint relacionado é o que chamamos de Role-based Authorization.
Porém, ao extrairmos o serviço administrativo, perdemos a autorização feita no módulo de segurança do monólito. Ainda não implementamos autorização no novo serviço.
No caso da URL começar com /parceiros/restaurantes/do-usuario/{username}
ou /parceiros/restaurantes/{restauranteId}
, é necessária uma autorização mais elaborada, que verifica se o usuário tem permissão a um restaurante específico, por meio da classe RestauranteAuthorizationService
. Esse tipo de autorização, em que um usuário ter permissão em apenas alguns objetos de negócio é o que chamamos de ACL-based authorization. A sigla ACL significa Access Control List.
Poderíamos implementar a autenticação numa Arquitetura de Microservices de duas maneiras:
- o usuário precisa autenticar novamente ao acessar cada serviço
- a autenticação é feita apenas uma vez e as informações de identidade do usuário são repassadas para os serviços
Autenticar várias vezes, a cada serviço, é algo que deixaria a experiência do usuário terrível. Além disso, todos os serviços teriam que ser edge services, expostos à rede externa.
Autenticar apenas uma vez e repassar as dados do usuário autenticado permite que os serviços não fiquem expostos, diminuindo a superfície de ataque. Além disso, a experiência para o usuário é transparente, como se todas as funcionalidades fossem parte da mesma aplicação. É esse tipo de solução que chamamos de Single Sign On (SSO).
No livro Microservices Patterns (RICHARDSON, 2018a), Chris Richardson descreve uma maneira comum de lidar com autenticação em uma arquitetura de Microservices: implementá-la API Gateway, o único edge service que fica exposto para o mundo externo. Dessa maneira, as chamadas a URLs protegidas já seriam barradas antes de passar para a rede interna, no caso do usuário não estar autenticado.
E a autorização? Poderíamos fazê-la também no API Gateway. É algo razoável para role-based authorization, em que é preciso saber apenas o ROLE do usuário. Porém, implementar ACL-based authorization no API Gateway levaria a um alto acoplamento com os serviços, já que precisamos saber se um dado usuário tem permissão para um objeto de negócio específico. Então, provavelmente uma atualização em um serviço iria querer uma atualização sincronizada no API Gateway, diminuindo a independência de cada serviço. Portanto, uma ideia melhor é fazer a autorização, role-based ou ACL-based, em cada serviço.
Com a autenticação sendo feito no API Gateway e a autorização em cada downstream service, surge um problema: como passar a identidade de um usuário do API Gateway para cada serviço?
Há duas alternativas:
- um token opaco: simplesmente uma string ou UUID que precisaria ser validada por cada serviço no emissor do token através de uma chamada remota.
- um self-contained token: um token que contém as informações do usuário e que tem sua integridade protegida através de uma assinatura. Assim, o próprio recipiente do token pode validar as informações checando a assinatura. Tanto o emissor como o recipiente devem compartilhar chaves para que a emissão e a checagem do token possam ser realizadas.
Pattern: Acess Token
O API Gateway passa um token contendo informações sobre o usuário, como sua identidade e seus roles, para os demais serviços.
Chris Richardson, no livro Microservices Patterns (RICHARDSON, 2018a)
Implementamos stateless sessions no monólito com um JWS, um tipo de JWT que é um token self-contained e assinado. Podemos usar o mesmo mecanismo, fazendo com que o API Gateway repasse o JWT para cada serviço. Cada serviço checaria a assinatura e extrairia, do payload do JWT, o subject, que contém o id do usuário, e os respectivos roles, usando essas informações para checar a permissão do usuário ao recurso solicitado.
A solução de stateless sessions com JWT do Caelum Eats, foi pensada e implementada visando uma aplicação monolítica.
E o resto dos serviços?
Temos serviços de infraestrutura como:
- API Gateway
- Service Registry
- Config Server
- Hystrix Dashboard
- Turbine
- Admin Server
Como estamos tratando de autorização relacionada a um determinado usuário, deixaremos para um outro momento a discussão da autenticação e autorização desses serviços de infraestrutura.
Temos serviços alinhados a contextos delimitados (bounded contexts) da Caelum Eats, como:
- Distância
- Pagamento
- Nota Fiscal
- Administrativo, um novo serviço que acabamos de extrair
Há ainda módulos do monólito relacionados a contextos delimitados:
- Pedido
- Restaurante
O único módulo cujos endpoints tem seu acesso protegido é o módulo de Restaurante do monólito. O módulo Administrativo foi extraído para um serviço próprio e não implementamos a autorização.
O monólito possui também um módulo de Segurança, que trata desse requisito transversal e contém o código de configuração do Spring Security.
O módulo Administrativo do monólito era protegido por meio de role-based authorization, bastando o usuário estar no role ADMIN para acessar os endpoints de administração de tipos de cozinha e formas de pagamento. Esse tipo de autorização não está sendo feito no eats-administrativo-service
.
Já o módulo de Restaurante efetua ACL-based authorization, limitando o acesso do usuário com role PARCEIRO a um restaurante específico.
Vamos modificar esse cenário, passando a responsabilidade de geração de tokens JWT/JWS para o API Gateway, que também será responsável pelo cadastro de novos usuários. A validação do token e autorização dos recursos ficará a cargo do módulo Restaurante do monólito e do serviço Administrativo.
Poderíamos ter um BD específico para conter dados de usuários nas tabelas user
, role
e user_authorities
. Porém, para simplificar, vamos manter os dados de usuários no BD do próprio monólito.
No config-repo
, adicione um arquivo apigateway.properties
com o dados de conexão do BD do monólito, além das configurações da chave e expiração do JWT, que são usadas na geração do token:
####### config-repo/apigateway.properties
#DATASOURCE CONFIGS
spring.datasource.url=jdbc:mysql://localhost:3307/eats?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=caelum123
#JWT CONFIGS
jwt.secret = um-segredo-bem-secreto
jwt.expiration = 604800000
Adicione, ao API Gateway, dependências ao starter do Spring Data JPA e ao driver do MySQL. Adicione também o jjwt, biblioteca que gera e valida tokens JWT, e ao starter do Spring Security.
####### fj33-api-gateway/pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Copie as classes a seguir do módulo eats-seguranca
do monólito para o pacote br.com.caelum.apigateway.seguranca
do API Gateway:
- AuthenticationController
- AuthenticationDto
- Role
- User
- UserInfoDto
- UserRepository
- UserService
Copie a seguinte classe do módulo de segurança do monólito para o pacote br.com.caelum.apigateway
do API Gateway:
- PasswordEncoderConfig
Não esqueça de ajustar o pacote das classes copiadas.
Essas classes fazem a autenticação de usuários, assim como o cadastro de novos donos de restaurante.
Defina uma classe SecurityConfig
no pacote br.com.caelum.apigateway
para que permita toda e qualquer requisição, desabilitando a autorização, que será feita pelos serviços. A autenticação será stateless.
####### fj33-api-gateway/src/main/java/br/com/caelum/apigateway/SecurityConfig.java
@Configuration
@EnableWebSecurity
@AllArgsConstructor
class SecurityConfig extends WebSecurityConfigurerAdapter {
private UserService userService;
private PasswordEncoder passwordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll()
.and().cors()
.and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Override
@Bean(BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Não deixe de fazer os imports corretos:
####### fj33-api-gateway/src/main/java/br/com/caelum/apigateway/SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import br.com.caelum.apigateway.seguranca.UserService;
import lombok.AllArgsConstructor;
Defina, no pacote br.com.caelum.apigateway.seguranca
do API Gateway, uma classe JwtTokenManager
, responsável pela geração dos tokens. A validação e extração de informações de um token serão responsabilidade de cada serviço.
É importante adicionar o username e os roles do usuário aos claims do JWT.
####### fj33-api-gateway/src/main/java/br/com/caelum/apigateway/seguranca/JwtTokenManager.java
@Component
class JwtTokenManager {
private String secret;
private long expirationInMillis;
public JwtTokenManager(@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration}") long expirationInMillis) {
this.secret = secret;
this.expirationInMillis = expirationInMillis;
}
public String generateToken(User user) {
final Date now = new Date();
final Date expiration = new Date(now.getTime() + this.expirationInMillis);
return Jwts.builder()
.setIssuer("Caelum Eats")
.setSubject(Long.toString(user.getId()))
.claim("roles", user.getRoles())
.claim("username", user.getUsername())
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, this.secret)
.compact();
}
}
Cheque os imports:
####### fj33-api-gateway/src/main/java/br/com/caelum/apigateway/seguranca/JwtTokenManager.java
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
Ainda no API Gateway, adicione um forward para a URL do AuthenticationController
, de maneira que o Zuul não tente fazer o proxy dessa chamada:
####### fj33-api-gateway/src/main/resources/application.properties
zuul.routes.auth.path=/auth/**
zuul.routes.auth.url=forward:/auth
Observação: essa configuração deve ficar antes da rota "coringa", que direciona todas as requisições para o monólito.
Podemos fazer uma chamada como a seguinte, que autentica o dono do restaurante Long Fu:
curl -i -X POST -H 'Content-type: application/json' -d '{"username":"longfu", "password":"123456"}' http://localhost:9999/auth
O retorno obtido será algo como:
{"userId":2,"username":"longfu","roles":["PARCEIRO"],"token":"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjIiLCJyb2xlcyI6WyJQQVJDRUlSTyJdLCJ1c2VybmFtZSI6ImxvbmdmdSIsImlhdCI6MTU2OTU1MDYyOCwiZXhwIjoxNTcwMTU1NDI4fQ.S1V7aBNN206NpxPEEaIibtluJD9Bd-gHPK-MaUHcgxs"}
São retornados, no corpo da resposta, informações sobre o usuário, seus roles e um token. O token, no formato JWS, contém as mesmas informações do corpo da resposta, mas em base 64, e uma assinatura.
Remova as seguintes classes do módulo eats-seguranca
do monólito, cujas responsabilidades foram passadas para o API Gateway. Elas estão no pacote br.com.caelum.eats.seguranca
:
- AuthenticationController
- AuthenticationDto
- UserInfoDto
- UserRepository
- UserService
Remova também a classe a seguir, do pacote br.com.caelum.eats
:
- PasswordEncoderConfig
A classe SecurityConfig
deve apresentar um erro de compilação.
Altere a classe SecurityConfig
do módulo de segurança do monólito, removendo o código associado a autenticação e cadastro de novos usuários:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/SecurityConfig.java
@Configuration
@EnableWebSecurity
@AllArgsConstructor
class SecurityConfig extends WebSecurityConfigurerAdapter {
p̶r̶i̶v̶a̶t̶e̶ ̶U̶s̶e̶r̶S̶e̶r̶v̶i̶c̶e̶ ̶u̶s̶e̶r̶S̶e̶r̶v̶i̶c̶e̶;̶
private JwtAuthenticationFilter jwtAuthenticationFilter;
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
p̶r̶i̶v̶a̶t̶e̶ ̶P̶a̶s̶s̶w̶o̶r̶d̶E̶n̶c̶o̶d̶e̶r̶ ̶p̶a̶s̶s̶w̶o̶r̶d̶E̶n̶c̶o̶d̶e̶r̶;̶
// código omitido...
@̶O̶v̶e̶r̶r̶i̶d̶e̶
p̶r̶o̶t̶e̶c̶t̶e̶d̶ ̶v̶o̶i̶d̶ ̶c̶o̶n̶f̶i̶g̶u̶r̶e̶(̶f̶i̶n̶a̶l̶ ̶A̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶M̶a̶n̶a̶g̶e̶r̶B̶u̶i̶l̶d̶e̶r̶ ̶a̶u̶t̶h̶)̶ ̶t̶h̶r̶o̶w̶s̶ ̶E̶x̶c̶e̶p̶t̶i̶o̶n̶ ̶{̶
a̶u̶t̶h̶.̶u̶s̶e̶r̶D̶e̶t̶a̶i̶l̶s̶S̶e̶r̶v̶i̶c̶e̶(̶u̶s̶e̶r̶S̶e̶r̶v̶i̶c̶e̶)̶.̶p̶a̶s̶s̶w̶o̶r̶d̶E̶n̶c̶o̶d̶e̶r̶(̶t̶h̶i̶s̶.̶p̶a̶s̶s̶w̶o̶r̶d̶E̶n̶c̶o̶d̶e̶r̶)̶;̶
}̶
@̶O̶v̶e̶r̶r̶i̶d̶e̶
@̶B̶e̶a̶n̶(̶B̶e̶a̶n̶I̶d̶s̶.̶A̶U̶T̶H̶E̶N̶T̶I̶C̶A̶T̶I̶O̶N̶_̶M̶A̶N̶A̶G̶E̶R̶)̶
p̶u̶b̶l̶i̶c̶ ̶A̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶M̶a̶n̶a̶g̶e̶r̶ ̶a̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶M̶a̶n̶a̶g̶e̶r̶B̶e̶a̶n̶(̶)̶ ̶t̶h̶r̶o̶w̶s̶ ̶E̶x̶c̶e̶p̶t̶i̶o̶n̶ ̶{̶
r̶e̶t̶u̶r̶n̶ ̶s̶u̶p̶e̶r̶.̶a̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶M̶a̶n̶a̶g̶e̶r̶B̶e̶a̶n̶(̶)̶;̶
}̶
}
Remove os seguintes imports:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/SecurityConfig.java
i̶m̶p̶o̶r̶t̶ ̶o̶r̶g̶.̶s̶p̶r̶i̶n̶g̶f̶r̶a̶m̶e̶w̶o̶r̶k̶.̶c̶o̶n̶t̶e̶x̶t̶.̶a̶n̶n̶o̶t̶a̶t̶i̶o̶n̶.̶B̶e̶a̶n̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶o̶r̶g̶.̶s̶p̶r̶i̶n̶g̶f̶r̶a̶m̶e̶w̶o̶r̶k̶.̶s̶e̶c̶u̶r̶i̶t̶y̶.̶a̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶.̶A̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶M̶a̶n̶a̶g̶e̶r̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶o̶r̶g̶.̶s̶p̶r̶i̶n̶g̶f̶r̶a̶m̶e̶w̶o̶r̶k̶.̶s̶e̶c̶u̶r̶i̶t̶y̶.̶c̶o̶n̶f̶i̶g̶.̶B̶e̶a̶n̶I̶d̶s̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶o̶r̶g̶.̶s̶p̶r̶i̶n̶g̶f̶r̶a̶m̶e̶w̶o̶r̶k̶.̶s̶e̶c̶u̶r̶i̶t̶y̶.̶c̶o̶n̶f̶i̶g̶.̶a̶n̶n̶o̶t̶a̶t̶i̶o̶n̶.̶a̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶.̶b̶u̶i̶l̶d̶e̶r̶s̶.̶A̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶M̶a̶n̶a̶g̶e̶r̶B̶u̶i̶l̶d̶e̶r̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶o̶r̶g̶.̶s̶p̶r̶i̶n̶g̶f̶r̶a̶m̶e̶w̶o̶r̶k̶.̶s̶e̶c̶u̶r̶i̶t̶y̶.̶c̶r̶y̶p̶t̶o̶.̶p̶a̶s̶s̶w̶o̶r̶d̶.̶P̶a̶s̶s̶w̶o̶r̶d̶E̶n̶c̶o̶d̶e̶r̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶b̶r̶.̶c̶o̶m̶.̶c̶a̶e̶l̶u̶m̶.̶e̶a̶t̶s̶.̶s̶e̶g̶u̶r̶a̶n̶c̶a̶.̶U̶s̶e̶r̶S̶e̶r̶v̶i̶c̶e̶;̶
Modifique o JwtTokenManager
do módulo de segurança do monólito, removendo o código de geração de token e o atributo expirationInMillis
, deixando apenas a validação e extração de dados do token.
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/JwtTokenManager.java
@Component
class JwtTokenManager {
private String secret;
p̶r̶i̶v̶a̶t̶e̶ ̶l̶o̶n̶g̶ ̶e̶x̶p̶i̶r̶a̶t̶i̶o̶n̶I̶n̶M̶i̶l̶l̶i̶s̶;̶
public JwtTokenManager(@Value("${jwt.secret}") String secret,̶
@̶V̶a̶l̶u̶e̶(̶"̶$̶{̶j̶w̶t̶.̶e̶x̶p̶i̶r̶a̶t̶i̶o̶n̶}̶"̶)̶ ̶l̶o̶n̶g̶ ̶e̶x̶p̶i̶r̶a̶t̶i̶o̶n̶I̶n̶M̶i̶l̶l̶i̶s̶) {
this.secret = secret;
t̶h̶i̶s̶.̶e̶x̶p̶i̶r̶a̶t̶i̶o̶n̶I̶n̶M̶i̶l̶l̶i̶s̶ ̶=̶ ̶e̶x̶p̶i̶r̶a̶t̶i̶o̶n̶I̶n̶M̶i̶l̶l̶i̶s̶;̶
}
p̶u̶b̶l̶i̶c̶ ̶S̶t̶r̶i̶n̶g̶ ̶g̶e̶n̶e̶r̶a̶t̶e̶T̶o̶k̶e̶n̶(̶U̶s̶e̶r̶ ̶u̶s̶e̶r̶)̶ ̶{̶
̶ ̶ ̶}̶
public boolean isValid(String jwt) {
// não modificado ...
}
@SuppressWarnings("unchecked")
public User getUserFromToken(String jwt) {
// não modificado ...
}
}
Remova os imports desnecessários:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/JwtTokenManager.java
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶.̶u̶t̶i̶l̶.̶D̶a̶t̶e̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶i̶o̶.̶j̶s̶o̶n̶w̶e̶b̶t̶o̶k̶e̶n̶.̶S̶i̶g̶n̶a̶t̶u̶r̶e̶A̶l̶g̶o̶r̶i̶t̶h̶m̶;̶
Remova as anotações do JPA e Beans Validator das classes User
e Role
do módulo de segurança do monólito. O cadastro de usuários será feito pelo API Gateway.
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/User.java
@̶E̶n̶t̶i̶t̶y̶
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@̶I̶d̶
@̶G̶e̶n̶e̶r̶a̶t̶e̶d̶V̶a̶l̶u̶e̶(̶s̶t̶r̶a̶t̶e̶g̶y̶ ̶=̶ ̶G̶e̶n̶e̶r̶a̶t̶i̶o̶n̶T̶y̶p̶e̶.̶I̶D̶E̶N̶T̶I̶T̶Y̶)̶
private Long id;
@̶N̶o̶t̶B̶l̶a̶n̶k̶ ̶@̶J̶s̶o̶n̶I̶g̶n̶o̶r̶e̶
private String name;
@̶N̶o̶t̶B̶l̶a̶n̶k̶ ̶@̶J̶s̶o̶n̶I̶g̶n̶o̶r̶e̶
private String password;
@̶M̶a̶n̶y̶T̶o̶M̶a̶n̶y̶(̶f̶e̶t̶c̶h̶ ̶=̶ ̶F̶e̶t̶c̶h̶T̶y̶p̶e̶.̶E̶A̶G̶E̶R̶)̶ ̶@̶J̶s̶o̶n̶I̶g̶n̶o̶r̶e̶
private List<Role> authorities = new ArrayList<>();
// restante do código ...
Limpe os imports da classe User
:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/User.java
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶E̶n̶t̶i̶t̶y̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶F̶e̶t̶c̶h̶T̶y̶p̶e̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶G̶e̶n̶e̶r̶a̶t̶e̶d̶V̶a̶l̶u̶e̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶G̶e̶n̶e̶r̶a̶t̶i̶o̶n̶T̶y̶p̶e̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶I̶d̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶M̶a̶n̶y̶T̶o̶M̶a̶n̶y̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶v̶a̶l̶i̶d̶a̶t̶i̶o̶n̶.̶c̶o̶n̶s̶t̶r̶a̶i̶n̶t̶s̶.̶N̶o̶t̶B̶l̶a̶n̶k̶;̶
i̶m̶p̶o̶r̶t̶ ̶c̶o̶m̶.̶f̶a̶s̶t̶e̶r̶x̶m̶l̶.̶j̶a̶c̶k̶s̶o̶n̶.̶a̶n̶n̶o̶t̶a̶t̶i̶o̶n̶.̶J̶s̶o̶n̶I̶g̶n̶o̶r̶e̶;̶
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/Role.java
@̶E̶n̶t̶i̶t̶y̶
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Role implements GrantedAuthority {
// código omitido...
@̶I̶d̶
private String authority;
// restante do código...
Limpe também os imports da classe Role
:
####### fj33-eats-monolito-modular/eats/eats-seguranca/src/main/java/br/com/caelum/eats/seguranca/Role.java
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶E̶n̶t̶i̶t̶y̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶I̶d̶;̶
Como a classe User
não é mais uma entidade, devemos modificar seu relacionamento na classe Restaurante
do módulo eats-restaurante
do monólito:
####### fj33-eats-monolito-modular/eats/eats-restaurante/src/main/java/br/com/caelum/eats/restaurante/Restaurante.java
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Restaurante {
// código omitido...
@̶O̶n̶e̶T̶o̶O̶n̶e̶
p̶r̶i̶v̶a̶t̶e̶ ̶U̶s̶e̶r̶ ̶u̶s̶e̶r̶;̶
private Long userId; // modificado
}
Os imports devem ser ajustados:
####### fj33-eats-monolito-modular/eats/eats-restaurante/src/main/java/br/com/caelum/eats/restaurante/Restaurante.java
i̶m̶p̶o̶r̶t̶ ̶j̶a̶v̶a̶x̶.̶p̶e̶r̶s̶i̶s̶t̶e̶n̶c̶e̶.̶O̶n̶e̶T̶o̶O̶n̶e̶;̶
̶i̶m̶p̶o̶r̶t̶ ̶b̶r̶.̶c̶o̶m̶.̶c̶a̶e̶l̶u̶m̶.̶e̶a̶t̶s̶.̶s̶e̶g̶u̶r̶a̶n̶c̶a̶.̶U̶s̶e̶r̶;̶
Ajuste a interface RestauranteRepository
:
####### fj33-eats-monolito-modular/eats/eats-restaurante/src/main/java/br/com/caelum/eats/restaurante/RestauranteRepository.java
interface RestauranteRepository extends JpaRepository<Restaurante, Long> {
// código omitido...
R̶e̶s̶t̶a̶u̶r̶a̶n̶t̶e̶ ̶f̶i̶n̶d̶B̶y̶U̶s̶e̶r̶(̶U̶s̶e̶r̶ ̶u̶s̶e̶r̶)̶;̶
Restaurante findByUserId(Long userId); // modificado
// código omitido...
}
Limpe o import:
####### fj33-eats-monolito-modular/eats/eats-restaurante/src/main/java/br/com/caelum/eats/restaurante/RestauranteRepository.java
i̶m̶p̶o̶r̶t̶ ̶b̶r̶.̶c̶o̶m̶.̶c̶a̶e̶l̶u̶m̶.̶e̶a̶t̶s̶.̶s̶e̶g̶u̶r̶a̶n̶c̶a̶.̶U̶s̶e̶r̶;̶
Faça com que a classe RestauranteAuthorizationService
use o novo método do repository:
####### fj33-eats-monolito-modular/eats/eats-restaurante/src/main/java/br/com/caelum/eats/restaurante/RestauranteAuthorizationService.java
@Service
@AllArgsConstructor
class RestauranteAuthorizationService {
private RestauranteRepository restauranteRepo;
public boolean checaId(Authentication authentication, long id) {
User user = (User) authentication.getPrincipal();
if (user.isInRole(Role.ROLES.PARCEIRO)) {
R̶e̶s̶t̶a̶u̶r̶a̶n̶t̶e̶ ̶r̶e̶s̶t̶a̶u̶r̶a̶n̶t̶e̶ ̶=̶ ̶r̶e̶s̶t̶a̶u̶r̶a̶n̶t̶e̶R̶e̶p̶o̶.̶f̶i̶n̶d̶B̶y̶U̶s̶e̶r̶(̶u̶s̶e̶r̶)̶;̶
Restaurante restaurante = restauranteRepo.findByUserId(user.getId());
if (restaurante != null) {
return id == restaurante.getId();
}
}
return false;
}
// código omitido...
}
Mude o monolito.properties
do config-repo
, removendo a configuração de expiração do token JWT. Essa configuração será usada apenas pelo gerador de tokens, o API Gateway. A chave privada, presente na propriedade jwt.secret
ainda deve ser mantida, pois é usada na validação do token HS256.
####### config-repo/monolito.properties
j̶w̶t̶.̶e̶x̶p̶i̶r̶a̶t̶i̶o̶n̶ ̶=̶ ̶6̶0̶4̶8̶0̶0̶0̶0̶0̶
As URLS que não tem acesso protegido continuam funcionando sem a necessidade de um token. Por exemplo:
http://localhost:9999/restaurantes/1
ou
http://localhost:8080/restaurantes/1
Porém, URLs protegidas precisarão de um access token válido e que foi emitido para um usuário que tenha permissão para fazer operações no recurso solicitado.
Se tentarmos acessar uma URL como a seguir, teremos o acesso negado:
http://localhost:8080/parceiros/restaurantes/1
A resposta será um erro HTTP 401 (Unauthorized).
Devemos usar um token obtido na autenticação como o API Gateway, colocando-o no cabeçalho HTTP Authorization
, com Bearer
como prefixo. O comando cURL em um Terminal, parece com:
curl -i -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjIiLCJyb2xlcyI6WyJQQVJDRUlSTyJdLCJ1c2VybmFtZSI6ImxvbmdmdSIsImlhdCI6MTU2OTU1MDYyOCwiZXhwIjoxNTcwMTU1NDI4fQ.S1V7aBNN206NpxPEEaIibtluJD9Bd-gHPK-MaUHcgxs' http://localhost:8080/parceiros/restaurantes/1
Deverá ser obtida uma resposta bem sucedida!
HTTP/1.1 200
Date: Fri, 23 Aug 2019 00:56:00 GMT
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
{"id":1,"cnpj":"98444252000104","nome":"Long Fu","descricao":"O melhor da China aqui do seu lado.","cep":"70238500","endereco":"ShC/SUL COMERCIO LOCAL QD 404-BL D LJ 17-ASA SUL","taxaDeEntregaEmReais":6.00,"tempoDeEntregaMinimoEmMinutos":40,"tempoDeEntregaMaximoEmMinutos":25,"aprovado":true,"tipoDeCozinhaId":1}
Isso indica que o módulo de segurança do monólito reconheceu o token como válido e extraiu a informação dos roles do usuário, reconhecendo-o no role PARCEIRO.
- Faça checkout da branch
cap14-autenticacao-no-api-gateway-e-autorizacao-nos-servicos
nos projeto do monólito modular, API Gateway e UI:
cd ~/Desktop/fj33-eats-monolito-modular
git checkout -f cap14-autenticacao-no-api-gateway-e-autorizacao-nos-servicos
cd ~/Desktop/fj33-api-gateway
git checkout -f cap14-autenticacao-no-api-gateway-e-autorizacao-nos-servicos
cd ~/Desktop/fj33-eats-ui
git checkout -f cap14-autenticacao-no-api-gateway-e-autorizacao-nos-servicos
Faça refresh no Eclipse nos projetos do monólito modular e do API Gateway.
- Poderíamos ter um BD específico para conter dados de usuários nas tabelas
user
,role
euser_authorities
. Porém, para simplificar, vamos manter os dados de usuários no BD do próprio monólito.
No config-repo
, adicione um arquivo apigateway.properties
com o dados de conexão do BD do monólito, além das configurações da chave e expiração do JWT, que são usadas na geração do token:
####### config-repo/apigateway.properties
#DATASOURCE CONFIGS
spring.datasource.url=jdbc:mysql://localhost:3307/eats?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=caelum123
#JWT CONFIGS
jwt.secret = um-segredo-bem-secreto
jwt.expiration = 604800000
- Execute
ApiGatewayApplication
, certificando-se que o Service Registry e o Config Server estão no ar.
Então, abra o terminal e simule a autenticação do dono do restaurante Long Fu:
curl -i -X POST -H 'Content-type: application/json' -d '{"username":"longfu", "password":"123456"}' http://localhost:9999/auth
Use o seguinte snippet, para evitar digitação: https://gitlab.com/snippets/1888245
Você deve obter um retorno parecido com:
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 23 Aug 2019 00:05:22 GMT
{"userId":2,"username":"longfu","roles":["PARCEIRO"],"token":"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjIiLCJyb2xlcyI6WyJQQVJDRUlSTyJdLCJ1c2VybmFtZSI6ImxvbmdmdSIsImlhdCI6MTU2OTU1MDYyOCwiZXhwIjoxNTcwMTU1NDI4fQ.S1V7aBNN206NpxPEEaIibtluJD9Bd-gHPK-MaUHcgxs"}
São retornados, no corpo da resposta, informações sobre o usuário, seus roles e um token. Abra um editor de texto e guarde esse token: o usaremos em breve!
- Execute o
EatsApplication
do móduloeats-application
do monólito. Certifique-se que o Service Registry, Config Server e API Gateway estejam sendo executados.
As URLS que não tem acesso protegido continuam funcionando. Por exemplo, acesse, pelo navegador, a URL a seguir para obter todas as formas de pagamento:
http://localhost:9999/restaurantes/1
ou
http://localhost:8080/restaurantes/1
Deve funcionar e retornar algo como:
{"id":1,"cnpj":"98444252000104","nome":"Long Fu","descricao":"O melhor da China aqui do seu lado.","cep":"70238500","endereco":"ShC/SUL COMERCIO LOCAL QD 404-BL D LJ 17-ASA SUL","taxaDeEntregaEmReais":6.00,"tempoDeEntregaMinimoEmMinutos":40,"tempoDeEntregaMaximoEmMinutos":25,"aprovado":true,"tipoDeCozinhaId":1}
Porém, URLs protegidas precisarão de um access token válido e que foi emitido para um usuário que tenha permissão para fazer operações no recurso solicitado.
Tente acessar uma variação da URL anterior que só é acessível para usuários com o role PARCEIRO:
http://localhost:8080/parceiros/restaurantes/1
A resposta será um erro HTTP 401 (Unauthorized), com uma mensagem de acesso negado.
Use o token obtido no exercício anterior, de autenticação no API Gateway, colocando-o no cabeçalho HTTP Authorization
, depois do valor Bearer
. Faça o seguinte comando cURL em um Terminal:
curl -i -H 'Authorization: Bearer <TOKEN-DO-PASSO-ANTERIOR>' http://localhost:8080/parceiros/restaurantes/1
Troque <TOKEN-DO-PASSO-ANTERIOR>
pelo token obtido no passo anterior.
Você pode encontrar o comando anterior em: https://gitlab.com/snippets/1888252
Deverá ser obtida uma resposta bem sucedida, com os dados da forma de pagamento alterados!
HTTP/1.1 200
Date: Fri, 23 Aug 2019 00:56:00 GMT
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
{"id":1,"cnpj":"98444252000104","nome":"Long Fu","descricao":"O melhor da China aqui do seu lado.","cep":"70238500","endereco":"ShC/SUL COMERCIO LOCAL QD 404-BL D LJ 17-ASA SUL","taxaDeEntregaEmReais":6.00,"tempoDeEntregaMinimoEmMinutos":40,"tempoDeEntregaMaximoEmMinutos":25,"aprovado":true,"tipoDeCozinhaId":1}
Isso indica que o módulo de segurança do monólito reconheceu o token como válido e extraiu a informação dos roles do usuário, reconhecendo-o no role PARCEIRO.
- Altere o payload do JWT, definindo um valor diferente para o
sub
, o Subject, que indica o id do usuário assim como para o ROLE.
Para isso, vá até um site como o http://www.base64url.com/ e defina no campo Base 64 URL Encoding o payload do token JWT recebido do API Gateway.
Altere o sub
para 1
, simulando um usuário malicioso tentando forjar um token para roubar a identidade de outro usuário, de id diferente. Mude também o role para ADMIN
e o nome do usuário para admin
.
O texto codificado em Base 64 URL Encoding será algo como:
eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjEiLCJyb2xlcyI6WyJBRE1JTiJdLCJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTY2NTE4NzIyLCJleHAiOjE1NjcxMjM1MjJ9
Através de um Terminal, use o cURL para tentar alterar uma forma de pagamento utilizando o payload modificado do JWT:
curl -i -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJDYWVsdW0gRWF0cyIsInN1YiI6IjEiLCJyb2xlcyI6WyJBRE1JTiJdLCJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTY2NTE4NzIyLCJleHAiOjE1NjcxMjM1MjJ9.S1V7aBNN206NpxPEEaIibtluJD9Bd-gHPK-MaUHcgxs' http://localhost:8080/parceiros/restaurantes/1
Obtenha o comando anterior na seguinte URL: https://gitlab.com/snippets/1888416
Como a assinatura do JWT não bate com o payload, o acesso deverá ser negado:
HTTP/1.1 401
Date: Fri, 23 Aug 2019 12:47:07 GMT
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
{"timestamp":"2019-08-23T12:47:07.785+0000","status":401,"error":"Unauthorized","message":"Você não está autorizado a acessar esse recurso.","path":"/parceiros/restaurantes/1"}
Teste também com o cURL o acesso direto ao monólito, usando a porta 8080
. O acesso deve ser negado, da mesma maneira.
- (desafio) Faça com que o novo serviço Administrativo também tenha autorização, fazendo com que apenas usuários no role ADMIN tenham acesso a URLS que começam com
/admin
.
Da maneira como implementamos a autenticação anteriormente, acabamos definindo mais uma responsabilidade para o API Gateway: além de proxy e API Composer, passou a servir como autenticador e gerador de tokens. E, para isso, o API Gateway precisou conhecer tabelas dos usuários e seus respectivos roles. E mais: implementamos a geração e verificação de tokens manualmente.
Autenticação, autorização, tokens, usuário e roles são necessidades comuns e poderiam ser implementadas de maneira genérica. Melhor ainda se houvesse um padrão aberto, que permitisse implementação por diferentes fornecedores. Assim, os desenvolvedores poderiam focar mais em código de negócio e menos em código de segurança.
Há um framework de autorização baseado em tokens que permite que não nos preocupemos com detalhes de implementação de autenticação e autorização: o padrão OAuth 2.0. Foi definido na RFC 6749 (HARDT, 2012) da Internet Engineering Task Force (IETF).
Há extensões do OAuth 2.0 como o OpenID Connect (OIDC), que fornece uma camada de autenticação baseada em tokens JWT em cima do OAuth 2.0.
O foco original do OAuth 2.0, na verdade, é permitir que aplicações de terceiros usem informações de usuários em serviços como Google, Facebook e GitHub. Quando efetuamos login em uma aplicação com uma conta do Facebook ou quando permitimos que um serviço de Integração Contínua como o Travis CI acesse nosso repositório no GitHub, estamos usando OAuth 2.0.
Um padrão como o OAuth 2.0 nos permite instalar softwares como Keycloak, WSO2 Identity Server, OpenAM ou Gluu e até usar soluções prontas de identity as a service (IDaaS) como Auth0 ou Okta.
E, claro, podemos usar as soluções do Spring: Spring Security OAuth, que estende o Spring Security fornecendo implementações para OAuth 1 e OAuth 2.0. Há ainda o Spring Cloud Security, que traz soluções compatíveis com outros projetos do Spring Cloud.
O OAuth 2.0 define quatro componentes, chamados de roles na especificação:
- Resource Owner: em geral, o usuário que tem algum recurso protegido como sua conta no Facebook, suas fotos no Flickr, seus repositórios no GitHub ou seu restaurante no Caelum Eats.
- Resource Server: provê o recurso protegido e permite o acesso mediante o uso de access tokens válidos.
- Client: a aplicação, Web, Single Page Application (SPA), Desktop ou Mobile, que deseja acessar os recursos do Resource Owner. Um Client precisa estar registrado no Authorization Server, sendo identificado por um client id e um client secret.
- Authorization Server: provê uma API para autenticar usuário e gerar access tokens. Pode estar na mesma aplicação do Resource Server.
O padrão OAuth 2.0 não especifica um formato para o access token. Se for usado um access token opaco, como uma String randômica ou UUID, a validação feita pelo Resource Server deve invocar o Authorization Server. Já no caso de um self-contained access token como um JWT/JWS, o próprio token contém informações para sua validação.
O padrão OAuth 2.0 é bastante flexível e especifica diferentes maneiras de um Client obter um access token, chamadas de grant types:
- Password: usada quando há uma forte relação de confiança entre o Client e o Authorization Server, como quando ambos são da mesma organização. O usuário informa suas credenciais (username e senha) diretamente para o Client, que repassa essas credenciais do usuário para o Authorization Server, junto com seu client id e client secret.
- Client credentials: usada quando não há um usuário envolvido, apenas um sistema chamando um recurso protegido de outro sistema. Apenas as credenciais do Client são informadas para o Authorization Server.
- Authorization Code: usada quando aplicações de terceiros desejam acessar informações de um recurso protegido sem que o Client conheça explicitamente as credenciais do usuário. Por exemplo, quando um usuário (Resource Owner) permite que o Travis CI (Client) acesse os seus repositórios do GitHub (Authorization Server e Resource Server). No momento em que o usuário cadastra seu GitHub no Travis CI, é redirecionado para uma tela de login do GitHub. Depois de efetuar o login no GitHub e escolher as permissões (ou scopes nos termos do OAuth), é redirecionado para um servidor do Travis CI com um authorization code como parâmetro da URL. Então, o Travis CI invoca o GitHub passando esse authorization code para obter um access token. As aplicações de terceiro que utilizam um authorization code são, em geral, aplicações Web clássicas com renderização das páginas no serve-side.
- Implicit: o usuário é direcionado a uma página de login do Authorization Server, mas o redirect é feito diretamente para o user-agent (o navegador, no caso da Web) já enviando o access token. Dessa forma, o Client SPA ou Mobile conhece diretamente o access token. Isso traz uma maior eficiência porém traz vulnerabilidades.
A RFC 8252 (OAuth 2.0 for Native Apps), de Outubro de 2017, traz indicações de como fazer autenticação e autorização com OAuth 2.0 para aplicações mobile nativas.
No OAuth 2.0, um access token deve ter um tempo de expiração. Um token expirado levaria à necessidade de nova autenticação pelo usuário. Um Authorization Server pode emitir um refresh token, de expiração mais longa, que seria utilizado para obter um novo access token, sem a necessidade de nova autenticação. De acordo com a especificação, o grant type Implicit não deve permitir um refresh token, já que o token é conhecido e armazenado no próprio user-agent.
Podemos dizer que o API Gateway, que conhece os dados de usuário e seus roles, gera tokens e faz autenticação, é análogo a um Authorization Server do OAuth. O monólito, com a implementação de autorização para os módulos de Restaurante e Admin, serve como um Resource Server do OAuth. O front-end em Angular seria o Client do OAuth.
A autenticação no API Gateway é feita usando o nome do usuário e a respectiva senha que são informadas na própria aplicação do Angular. Ou seja, o Client conhece as credenciais do usuário e as repassa para o Authorization Server para autenticá-lo. Isso é análogo a um Password grant type do OAuth.
Poderíamos reimplementar a autenticação e autorização com OAuth usando código já pronto das bibliotecas Spring Security OAuth 2 e Spring Cloud Security, diminuindo o código que precisamos manter e cujas vulnerabilidades temos que sanar. Para isso, podemos definir um Authorization Server separado do API Gateway, responsável apenas pela autenticação e gerenciamento de tokens.
Para implementarmos um Authorization Server compatível com OAuth 2.0, devemos criar um novo projeto Spring Boot.
Abra https://start.spring.io/
no navegador.
Em Project, mantenha Maven Project.
Em Language, mantenha Java.
Em Spring Boot, mantenha a versão padrão.
No trecho de Project Metadata, defina:
br.com.caelum
em Groupauthorization-server
em Artifact
Clique em More options. Mantenha o valor em Name. Apague a Description, deixando-a em branco.
Mantenha o Packaging como Jar
.
Mantenha a Java Version em 8
.
Em Dependencies, adicione:
- Cloud OAuth2
- Cloud Security
- Web
- JPA
- MySQL
- Config Client
- DevTools
- Lombok
Clique em Generate Project.
Descompacte o authorization-server.zip
para seu Desktop.
Devemos anotar a classe AuthorizationServerApplication
com @EnableAuthorizationServer
:
####### fj33-authorization-server/src/main/java/br/com/caelum/authorizationserver/AuthorizationServerApplication.java
@EnableAuthorizationServer // adicionado
@SpringBootApplication
public class AuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorizationServerApplication.class, args);
}
}
O import correto é:
####### fj33-authorization-server/src/main/java/br/com/caelum/authorizationserver/AuthorizationServerApplication.java
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
No application.properties
, devemos modificar a porta para 8085
.
####### fj33-authorization-server/src/main/resources/application.properties
server.port=8085
Vamos definir authorizationserver
como application name e a URL do Configuration Server no arquivo bootstrap.properties
:
####### fj33-authorization-server/src/main/resources/bootstrap.properties
spring.application.name=authorizationserver
spring.cloud.config.uri=http://localhost:8888
Se quisermos usar o Password grant type, devemos fornecer uma implementação da interface UserDetailsService
, usada pelo Spring Security para obter os detalhes dos usuários. Essa implementação é exatamente igual ao que implementamos no API Gateway. Por isso devemos mover para o Authorization Server as classes:
User
Role
UserService
UserRepository
SecurityConfig
PasswordEncoderConfig
Como o Authorization Server usa os dados de usuários, vamos adicionar o Data Source do BD do Monólito. Uma tarefa posterior seria migrar as tabelas user
e role
para um BD específico.
Também vamos definir um client id e seu respectivo client secret.
Para isso, vamos definir um arquivo authorizationserver.properties
no repositório de configurações:
####### config-repo/authorizationserver.properties
#DATASOURCE CONFIGS
spring.datasource.url=jdbc:mysql://localhost:3307/eats?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=caelum123
security.oauth2.client.client-id=eats
security.oauth2.client.client-secret=eats123
A configuração anterior define apenas um Client. Se tivermos registro de diferentes clients, podemos fornecer uma implementação da interface
ClientDetailsService
, que define o métodoloadClientByClientId
. Nesse método, recebemos uma String com o client id e devemos retornar um objeto que implementa a interfaceClientDetails
.
Com essas configurações mínimas, teremos um Authorization Server que dá suporte a todos os grant types do OAuth 2.0 mencionados anteriormente.
Ao executar a classe AuthorizationServerApplication
, podemos gerar um token enviando uma requisição POST ao endpoint /oauth/token
.
As credenciais do Client devem ser autenticadas com HTTP Basic. Devem ser definidos como parâmetros o grant type e o scope. Como não definimos nenhum scope, devemos usar any
. No caso do Password grant type, devemos informar também as credenciais do usuário.
curl -i -X POST --basic -u eats:eats123 -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=password&username=admin&password=123456&scope=any' http://localhost:8085/oauth/token
Como resposta, obteremos um access token e um refresh token, ambos opacos.
HTTP/1.1 200
Pragma: no-cache
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 28 Aug 2019 13:54:22 GMT
{
"access_token":"bdb22855-5705-4533-b925-f1091d576db7",
"token_type":"bearer",
"refresh_token":"0780c97f-f1d1-4a6f-82cb-c17ba5624caa",
"expires_in":43199,
"scope":"any"}
Podemos checar o access token opaco por meio de uma requisição GET ao endpoint /oauth/check_token
, passando o access token obtido no parâmetro token
:
curl -i localhost:8085/oauth/check_token/?token=bdb22855-5705-4533-b925-f1091d576db7
O corpo da resposta deve conter o username e os roles do usuário, entre outras informações:
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 28 Aug 2019 14:56:32 GMT
{"active":true,"exp":1567046599,"user_name":"admin","authorities":["ROLE_ADMIN"],"client_id":"eats","scope":["any"]}
A dependência spring-cloud-starter-oauth2
já tem como dependência transitiva a biblioteca spring-security-jwt
, que provê suporte a JWT no Spring Security.
Precisamos fazer algumas configurações para que o token gerado seja um JWT. Para isso, devemos definir uma implementação para a interface AuthorizationServerConfigurer
. Podemos usar a classe AuthorizationServerConfigurerAdapter
como auxílio.
As configurações são as seguintes:
- um objeto da classe
JwtTokenStore
, que implementa a interfaceTokenStore
- um objeto da classe
JwtAccessTokenConverter
, que implementa a interfaceAccessTokenConverter
. A classeJwtAccessTokenConverter
gera, por padrão, um chave privada de assinatura (signingKey
) randômica. É interessante definir uma propriedadejwt.secret
, como havíamos feito anteriormente. - uma implementação de
ClientDetailsService
para que as propriedadessecurity.oauth2.client.client-id
esecurity.oauth2.client.client-secret
funcionem e definam o id e a senha do Client com sucesso. Podemos usar a classeClientDetailsServiceConfigurer
. Os valores das propriedades de Client id e secret podem ser obtidas usandoOAuth2ClientProperties
. - devemos definir o
AuthenticationManager
configurado na classeSecurityConfig
por meio da classeAuthorizationServerEndpointsConfigurer
Podemos fazer todas essas configurações em uma classe OAuthServerConfig
, semelhante à seguinte:
####### fj33-authorization-server/src/main/java/br/com/caelum/authorizationserver/OAuthServerConfig.java
@Configuration
public class OAuthServerConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final OAuth2ClientProperties clientProperties;
private final String jwtSecret;
public OAuthServerConfig(AuthenticationManager authenticationManager,
OAuth2ClientProperties clientProperties,
@Value("${jwt.secret}") String jwtSecret) {
this.authenticationManager = authenticationManager;
this.clientProperties = clientProperties;
this.jwtSecret = jwtSecret;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientProperties.getClientId())
.secret(clientProperties.getClientSecret());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(this.jwtSecret);
return converter;
}
}
Os imports corretos são os seguintes:
####### fj33-authorization-server/src/main/java/br/com/caelum/authorizationserver/OAuthServerConfig.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2ClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
Devemos definir a propriedade jwt.secret
no arquivo authorizationserver.properties
no repositório de configurações:
####### config-repo/authorizationserver.properties
jwt.secret = um-segredo-bem-secreto
A configuração padrão habilitada pela anotação @EnableAuthorizationServer
usa um NoOpsPasswordEncoder
, que faz com que as senhas sejam lidas em texto puro. Porém, como definimos um delegating password encoder com a classe PasswordEncoderConfig
, devemos definir o id noop
. Para isso, devemos modificar a propriedade security.oauth2.client.client-secret
no arquivo application.properties
:
security.oauth2.client.client-secret={noop}eats123
Ao executar novamente o Authorization Server, os tokens serão gerados no formato JWT/JWS.
Podemos testar novamente com o cURL:
curl -i -X POST --basic -u eats:eats123 -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=password&username=admin&password=123456&scope=any' http://localhost:8085/oauth/token
Teremos uma resposta bem sucedida, com um access token no formato JWT:
HTTP/1.1 200
Pragma: no-cache
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 28 Aug 2019 18:11:25 GMT
{
"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjcwNTkwODUsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2ODlkMGE0ZS0xZjRmLTQ5OGMtOGMzMS05YjVlYjMyZWYxYjgiLCJjbGllbnRfaWQiOiJlYXRzIiwic2NvcGUiOlsiYW55Il19.ZtYpX3GJPYU8UNhHRtmEtQ7SLiiZdZOrdCRJt64ovF4",
"token_type":"bearer",
"expires_in":43199,
"scope":"any",
"jti":"689d0a4e-1f4f-498c-8c31-9b5eb32ef1b8"
}
O access token anterior contém, como todo JWS, 3 partes.
O cabeçalho:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Que pode ser decodificado, usando um Base 64 URL Decoder, para:
{"alg":"HS256","typ":"JWT"}
Já a segunda parte é o payload, que contém os claims do JWT:
eyJleHAiOjE1NjcwNTkwODUsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2ODlkMGE0ZS0xZjRmLTQ5OGMtOGMzMS05YjVlYjMyZWYxYjgiLCJjbGllbnRfaWQiOiJlYXRzIiwic2NvcGUiOlsiYW55Il19
Após a decodificação Base64, teremos:
{
"exp":1567059085,
"user_name":"admin",
"authorities":["ROLE_ADMIN"],
"jti":"689d0a4e-1f4f-498c-8c31-9b5eb32ef1b8",
"client_id":"eats",
"scope":["any"]}
Perceba que temos o user_name
e os respectivos roles em authorities
.
Há também uma propriedade jti
(JWT ID), uma String randômica (UUID) que serve como um nonce: um valor é diferente a cada request e previne o sistema contra replay attacks.
A terceira parte é a assinatura:
ZtYpX3GJPYU8UNhHRtmEtQ7SLiiZdZOrdCRJt64ovF4
Como usamos o algoritmo HS256
, um algoritmo de chaves simétricas, a chave privada setada em signingKey
precisa ser conhecida para validar a assinatura.
O código de autenticação do API Gateway pode ser removido.
Para isso, delete as seguintes todas as classes do pacote br.com.caelum.apigateway.seguranca
do API Gateway:
- A̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶C̶o̶n̶t̶r̶o̶l̶l̶e̶r̶
- A̶u̶t̶h̶e̶n̶t̶i̶c̶a̶t̶i̶o̶n̶D̶t̶o̶
- J̶w̶t̶T̶o̶k̶e̶n̶M̶a̶n̶a̶g̶e̶r̶
- R̶o̶l̶e̶
- U̶s̶e̶r̶
- U̶s̶e̶r̶I̶n̶f̶o̶D̶t̶o̶
- U̶s̶e̶r̶R̶e̶p̶o̶s̶i̶t̶o̶r̶y̶
- U̶s̶e̶r̶S̶e̶r̶v̶i̶c̶e̶
Apague também as seguintes classes do pacote br.com.caelum.apigateway
:
- P̶a̶s̶s̶w̶o̶r̶d̶E̶n̶c̶o̶d̶e̶r̶C̶o̶n̶f̶i̶g̶
- S̶e̶c̶u̶r̶i̶t̶y̶C̶o̶n̶f̶i̶g̶
Remova as seguintes dependências do pom.xml
do API Gateway:
- j̶j̶w̶t̶
- m̶y̶s̶q̶l̶-̶c̶o̶n̶n̶e̶c̶t̶o̶r̶-̶j̶a̶v̶a̶
- s̶p̶r̶i̶n̶g̶-̶b̶o̶o̶t̶-̶s̶t̶a̶r̶t̶e̶r̶-̶d̶a̶t̶a̶-̶j̶p̶a̶
- s̶p̶r̶i̶n̶g̶-̶b̶o̶o̶t̶-̶s̶t̶a̶r̶t̶e̶r̶-̶s̶e̶c̶u̶r̶i̶t̶y̶
Apague a seguinte rota do application.properties
do API Gateway:
z̶u̶u̶l̶.̶r̶o̶u̶t̶e̶s̶.̶a̶u̶t̶h̶.̶p̶a̶t̶h̶=̶/̶a̶u̶t̶h̶/̶*̶*̶
z̶u̶u̶l̶.̶r̶o̶u̶t̶e̶s̶.̶a̶u̶t̶h̶.̶u̶r̶l̶=̶f̶o̶r̶w̶a̶r̶d̶:̶/̶a̶u̶t̶h̶
- Abra um Terminal e baixe o projeto
fj33-authorization-server
para o seu Desktop usando o Git:
cd ~/Desktop
git clone https://gitlab.com/aovs/projetos-cursos/fj33-authorization-server.git
- No workspace de microservices do Eclipse, acesse File > Import > Existing Maven Projects e clique em Next. Em Root Directory, aponte para o diretório clonado anteriormente.
Veja o código das classes AuthorizationServerApplication
e OAuthServerConfig
, além dos arquivos bootstrap.properties
e application.properties
.
Note que o spring.application.name
é authorizationserver
. A porta definida para o Authorization Server é 8085
.
- Crie o arquivo
authorizationserver.properties
noconfig-repo
, com o seguinte conteúdo:
#DATASOURCE CONFIGS
spring.datasource.url=jdbc:mysql://localhost:3307/eats?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=caelum123
jwt.secret = um-segredo-bem-secreto
security.oauth2.client.client-id=eats
security.oauth2.client.client-secret={noop}eats123
O código anterior pode ser encontrado em: https://gitlab.com/snippets/1890756
Note que copiamos o jwt.secret
e os dados do BD do monólito. Isso indica que o BD será mantido de maneira monolítica. Eventualmente, seria possível fazer a migração de dados de usuário para um BD específico.
Além disso, definimos as propriedades de Client id e secret do Spring Security OAuth 2.
- Execute a classe
AuthorizationServerApplication
.
Então, abra um terminal e execute o seguinte comando:
curl -i -X POST --basic -u eats:eats123 -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=password&username=admin&password=123456&scope=any' http://localhost:8085/oauth/token
Se não quiser digitar, é possível encontrar o comando anterior no seguinte link: https://gitlab.com/snippets/1890014
Como resposta, deverá ser exibido algo como:
HTTP/1.1 200
...
{
"access_token":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJleHAiOjE1NjcwNTkwODUsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2ODlkMGE0ZS0xZjRmLTQ5OGMtOGMzMS05YjVlYjMyZWYxYjgiLCJjbGllbnRfaWQiOiJlYXRzIiwic2NvcGUiOlsiYW55Il19.
ZtYpX3GJPYU8UNhHRtmEtQ7SLiiZdZOrdCRJt64ovF4",
"token_type": "bearer",
"expires_in": 43199,
"scope": "any",
"jti": "689d0a4e-1f4f-498c-8c31-9b5eb32ef1b8"
}
Pegue o conteúdo da propriedade access_token
e guarde em um arquivo. Usaremos esse token em um exercício posterior.
Analise o cabeçalho e o payload em: https://jwt.io
O payload deverá conter algo semelhante a:
{
"exp": 1567059085,
"user_name": "admin",
"authorities": [
"ROLE_ADMIN"
],
"jti": "689d0a4e-1f4f-498c-8c31-9b5eb32ef1b8",
"client_id": "eats",
"scope": [
"any"
]
}
- Faça checkout da branch
cap14-authorization-server
do API Gateway para remover a autenticação:
cd ~/Desktop/fj33-api-gateway
git checkout -f cap14-authorization-server
Delete o arquivo apigateway.properties
do config-repo
.
- (desafio - trabalhoso) Aplique uma estratégia de migração de dados de usuário do monólito para um BD específico para o Authorization Server.
Para definir um Resource Server com o Spring Security OAuth 2, que consiga validar e decodificar os tokens (opacos ou JWT) emitidos pelo Authorization Server, basta anotar a aplicação ou alguma @Configuration
com @EnableResourceServer
.
Podemos definir, na configuração security.oauth2.resource.token-info-uri
, a URI de validação de tokens opacos.
No caso de token self-contained JWT, devemos definir a propriedade security.oauth2.resource.jwt.key-value
. Pode ser a chave simétrica, no caso de algoritmos como o HS256, ou a chave pública, como no RS256. A chave pública em um algoritmo assimétrico pode ser baixada do servidor quando definida a propriedade security.oauth2.resource.jwt.key-uri
.
Por padrão, todos os endereços requerem autenticação. Porém, é possível customizar esse e outros detalhes fornecendo uma implementação da interface ResourceServerConfigurer
. É possível herdar da classe ResourceServerConfigurerAdapter
para facilitar as configurações.
Adicione os starters do Spring Security OAuth 2 e Spring Cloud Security ao pom.xml
do eats-administrativo-service
:
####### fj33-eats-administrativo-service/pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
Anote a classe EatsAdministrativoServiceApplication
com @EnableResourceServer
:
####### fj33-eats-administrativo-service/src/main/java/br/com/caelum/eats/administrativo/EatsAdministrativoServiceApplication.java
@EnableResourceServer // adicionado
@EnableDiscoveryClient
@SpringBootApplication
public class EatsAdministrativoServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EatsAdministrativoServiceApplication.class, args);
}
}
O import correto é o seguinte:
####### fj33-eats-administrativo-service/src/main/java/br/com/caelum/eats/administrativo/EatsAdministrativoServiceApplication.java
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
Adicione ao administrativo.properties
do config-repo
, a mesma chave usada no Authorization Server, porém na propriedade security.oauth2.resource.jwt.key-value
:
####### config-repo/administrativo.properties
security.oauth2.resource.jwt.key-value = um-segredo-bem-secreto
Crie uma classe OAuthResourceServerConfig
. Herde da classe ResourceServerConfigurerAdapter
e permita que todos acessem a listagem de tipos de cozinha e formas de pagamento, assim como os endpoints do Spring Boot Actuator. As URLs que começam com /admin
devem ser restritas a usuário que tem o role ADMIN
.
####### fj33-eats-administrativo-service/src/main/java/br/com/caelum/eats/administrativo/OAuthResourceServerConfig.java
@Configuration
class OAuthResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/tipos-de-cozinha/**", "/formas-de-pagamento/**").permitAll()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and().cors()
.and().csrf().disable()
.formLogin().disable()
.httpBasic().disable();
}
}
Certifique-se que os imports estão corretos:
####### fj33-eats-administrativo-service/src/main/java/br/com/caelum/eats/administrativo/OAuthResourceServerConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
URLs abertas, como a que lista todas as formas de pagamento, terão sucesso sem nenhum token ser passado:
http://localhost:8084/formas-de-pagamento
Já URLs como a que permite a alteração dos dados de uma forma de pagamento estarão protegidas:
curl -i -X PUT -H 'Content-type: application/json' -d '{"id": 3, "tipo": "CARTAO_CREDITO", "nome": "American Express"}' http://localhost:8084/admin/formas-de-pagamento/3
Como resposta, teríamos um erro 401 (Unauthorized)
:
HTTP/1.1 401
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
Será necessário passar um token obtido do Authorization Server que contém o role ADMIN
para que a chamada anterior seja bem sucedida.
- Adicione a propriedade
security.oauth2.resource.jwt.key-value
ao arquivoadministrativo.properties
do repositório de configurações com a mesma chave usada no Authorization Server:
####### config-repo/administrativo.properties
security.oauth2.resource.jwt.key-value = um-segredo-bem-secreto
- Faça checkout da branch
cap14-resource-server-com-spring-security-oauth-2
do serviço Administrativo:
cd ~/Desktop/fj33-eats-administrativo-service
git checkout -f cap14-resource-server-com-spring-security-oauth-2
Faça refresh do projeto no Eclipse e o reinicie.
- Abra um terminal e tente listas todas as formas de pagamento sem passar nenhum token:
curl http://localhost:8084/formas-de-pagamento
A resposta deve ser bem sucedida, contendo algo como:
[
{"id":4,"tipo":"VALE_REFEICAO","nome":"Alelo"},
{"id":3,"tipo":"CARTAO_CREDITO","nome":"Amex Express"},
{"id":2,"tipo":"CARTAO_CREDITO","nome":"MasterCard"},
{"id":6,"tipo":"CARTAO_DEBITO","nome":"MasterCard Maestro"},
{"id":5,"tipo":"VALE_REFEICAO","nome":"Ticket Restaurante"},
{"id":1,"tipo":"CARTAO_CREDITO","nome":"Visa"},
{"id":7,"tipo":"CARTAO_DEBITO","nome":"Visa Débito"}
]
Vamos tentar editar uma forma de pagamento, chamando um endpoint que começa com /admin
, sem um token:
curl -i -X PUT -H 'Content-type: application/json' -d '{"id": 3, "tipo": "CARTAO_CREDITO", "nome": "American Express"}' http://localhost:8084/admin/formas-de-pagamento/3
O comando anterior pode ser encontrado em: https://gitlab.com/snippets/1888251
Deve ser retornado um erro 401 (Unauthorized)
, com a descrição Full authentication is required to access this resource, indicando que o acesso ao recurso depende de autenticação:
HTTP/1.1 401
Pragma: no-cache
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 29 Aug 2019 20:12:57 GMT
{
"error":"unauthorized",
"error_description":"Full authentication is required to access this resource"
}
Devemos incluir, no cabeçalho Authorization
, o token JWT obtido anteriormente:
curl -i -X PUT -H 'Content-type: application/json' -H 'Authorization: Bearer TOKEN-JWT-AQUI' -d '{"id": 3, "tipo": "CARTAO_CREDITO", "nome": "Amex Express"}' http://localhost:8084/admin/formas-de-pagamento/3
O comando anterior pode ser encontrado em: https://gitlab.com/snippets/1890417
Observação: troque TOKEN-JWT-AQUI
pelo token obtido do Authorization Server em exercícios anteriores.
A resposta será um sucesso!
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 29 Aug 2019 20:13:02 GMT
{"id":3,"tipo":"CARTAO_CREDITO","nome":"Amex Express"}
- (desafio - trabalhoso) Faça com que a UI use o Authorization Server para autenticação e extrai as informações do usuário dos claims do token.
Temos serviços de infraestrutura como:
- API Gateway
- Service Registry
- Config Server
- Hystrix Dashboard
- Turbine
- Admin Server
O API Gateway é edge service do Caelum Eats, que fica na fronteira da infra-estrutura. Serve como um proxy que repassa requisições e as respectivas respostas. Podemos fazer um rate limiting, cortando requisições de um mesmo cliente a partir de uma certa taxa de requisições por segundo, afim de evitar um ataque de DDoS (Distributed Denial of Service), que visa deixar um sistema fora do ar. O API Gateway também serve como um API Composer, que dispara requisições a vários serviços, agregando as respostas. Nesse caso, é preciso avaliar se a composição requer algum tipo de autorização. No caso implementado, a composição que agrega dados de um restaurante com sua distância a um determinado CEP, é feita por meio de dados públicas. Portanto, não há a necessidade de autorização. Nesse cenário de composição, a avaliação da necessidade de autorização, deve ser feita caso a caso. Uma ideia simples é repassar erros de autorização dos serviços invocados.
Uma vulnerabilidade da nossa aplicação é que uma vez que o endereço do Service Registry é conhecido, é possível descobrir nomes, hosts e portas de todos os serviços. A partir dos nomes dos serviços, podemos consultar o Config Server e observar detalhes de configuração de cada serviço.
Podemos, de maneira bem fácil, proteger o Config Server, o Service Registry e demais serviços de infraestrutura que criamos.
Basta adicionarmos, às dependências do Maven, o Spring Security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Para que o Spring Security não use uma senha randômica, devemos definir usuário e senha como propriedades no application.properties
. Por exemplo, para o Config Server:
security.user.name=configUser
security.user.password=configPassword
Nos demais serviços, devemos adicionar ao bootstrap.properties
:
spring.cloud.config.uri=http://localhost:8888
spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
No caso do Service Registry, faríamos o mesmo processo.
Definiríamos o endereço nos clientes do Eureka da seguinte maneira:
eureka.client.serviceUrl.defaultZone=http://eurekaUser:eurekaPassword@localhost:8761/eureka/
O protocolo HTTP é baseado em texto e, sem uma estratégia de confidencialidade, as informações serão trafegadas como texto puro da UI para o seu sistema e nas chamadas entre os serviços. Dados como usuário, senha, cartões de crédito estariam totalmente expostos.
HTTPS é uma extensão ao HTTP que usa TLS (Transport Layer Security) para prover confidencialidade aos dados por meio de criptografia. O protocolo SSL (Security Sockets Layer) é o predecessor do TLS e está deprecated.
Além disso, o HTTPS provê a integridade dos dados, evitando que sejam manipulados no meio do caminho, bem como a autenticidade do servidor, garantindo que o servidor é exatamente o que o cliente espera.
A confidencialidade, integridade e autenticidade do servidor no HTTPS é atingida por meio de criptografia assimétrica (public-key cryptography). O servidor tem um par de chaves (key pair): uma pública e uma privada. Algo criptografado com a chave pública só pode ser descriptografado com a chave privada, garantindo confidencialidade. Algo criptografado com a chave privada pode ser verificado com a chave pública, validando a autenticidade.
A chave pública faz parte de um certificado digital, que é emitido por uma Autoridade Certificadora (Certificate Authority) como Comodo, Symantec, Verizon ou Let's Encrypt. Toda a infraestrutura dos certificados digitais é baseada na confiança de ambas as partes, cliente e servidor, nessas Autoridades Certificadoras.
Mas o HTTPS não é um mar de rosas: os certificados tem validade e precisam ser gerenciados. A automação do gerenciamento de certificados ainda deixa a desejar, mas tem melhorado progressivamente. Let's Encrypt sendo uma referência nessa automação.
Certificados gerados sem uma autoridade certificadora (self-signed certificates) não são confiáveis e apresentam erros em navegadores e outros sistemas.
Com o comando keytool
, que vem com a JDK, podemos gerar um self-signed certificate:
keytool -genkey -alias eats -storetype JKS -keyalg RSA -keysize 2048 -keystore eats-keystore.jks -validity 3650
Será solicitada uma senha e uma série de outras informações e gerado o arquivo eats-keystore.jks
.
Podemos configurar o application.properties
de uma aplicação Spring Boot da seguinte maneira:
server.port=8443
server.ssl.key-store=eats-keystore.jks
server.ssl.key-store-password=a-senha-escolhida
server.ssl.keyAlias=eats
Um outro detalhe do HTTPS é que não há garantias da autenticidade do cliente, apenas do servidor.
Para garantir a autenticidade do cliente e do servidor, podemos fazer com que ambos tenham certificados digitais. Quando o cliente é um navegador, isso não é possível porque é inviável exigir a cada um dos usuários a instalação de um certificado. Por isso, o uso mútuo de certificados é comumente usado na comunicação servidor-servidor.
Cada serviço deve ter dois stores com chaves criptográficas, que possuem a extensão .jks
na plataforma Java:
- uma key store, que contém a chave privada de um determinado serviço, além de um certificado com a respectiva chave pública
- uma trust store, que contém os certificados com chaves públicas dos clientes e servidores ou de Autoridades Certificadoras considerados confiáveis
O application.properties
deve ter configurações tanto do key store como do trust store, além da propriedade server.ssl.client-auth
que indica o uso de autenticação mútua e pode ter os valores none
, want
(não obrigatório) e need
(obrigatório).
server.ssl.key-store=eats-keystore.jks
server.ssl.key-store-password=a-senha-escolhida
server.ssl.keyAlias=eats
server.ssl.trust-store=eats-truststore.jks
server.ssl.trust-store-password=senha-do-trust-store
server.ssl.client-auth=need
Mesmo investindo esforço em proteger a rede, a comunicação entre os serviços (data at transit) e os serviços em si, é preciso preparar nosso ambiente para uma possível invasão.
Uma vulnerabilidade está nos dados armazenados (data at rest) em BDs, arquivos de configuração e backups. Em especial, devemos proteger dados sensíveis como cartões de crédito, senhas e chaves criptográficas. Muitos ataques importantes exploraram a falta de criptografia de dados armazenados ou falhas nos algoritmos criptográficos utilizados.
Em seu livro Building Microservices (NEWMAN, 2015), Sam Newman indica algumas medidas que devem ser tomadas para proteger os dados armazenados:
- use implementações padrão de algoritmos criptográficos conhecidos, ficando atento a possíveis vulnerabilidades e aplicando patches regularmente. Não tente criar o seu algoritmo. Para senhas, use Strings randômicas (salts) que minimizam ataques baseados em tabelas de hashes.
- limite a encriptação a tabelas dos BDs e a arquivos que realmente são sensíveis para evitar impactos negativos na performance da aplicação
- criptografe os dados sensíveis logo que entrarem no sistema, descriptografe sob demanda e assegure que os dados não são armazenados em outros lugares
- assegure que os backups estejam criptografados
- armazene as chaves criptográficas em um software ou appliance (hardware) específico para gerenciamento de chaves.
Conforme relatado em artigos como Murder in the Amazon cloud (VENEZIA, 2014), em Junho de 2014, a Code Spaces, uma concorrente do GitHub que fornecia Git e SVN na nuvem, sofreu um ataque em que o invasor, após chantagem, apagou quase todos os dados, configurações de máquinas e backups da empresa. O ataque levou a empresa à falência! Isso aconteceu porque o invasor teve acesso ao painel de controle do AWS e conseguiu apagar quase todos os artefatos, incluindo os backups.
Não se sabe ao certo como o invasor conseguiu o acesso indevido ao painel de controle do AWS, mas há a hipótese de que obteve as credenciais de acesso de um antigo funcionário da empresa.
É imprescindível que as credenciais tenham acesso limitado, minimizando o potencial de destruição de um possível invasor.
No curso Building Secure Microservices Architectures (NEWMAN, 2018), Sam Newman recomenda que as senhas dos usuários, chaves criptográficas, API keys e outras credenciais sejam modificadas de tempos em tempos. Assim, ataques feitos com a ajuda funcionários desonestos terão efeito limitado. Se possível, essa rotação de credenciais deve ser feita de maneira automatizada.
Há alguns softwares que automatizam o gerenciamento de credenciais:
- Vault, da HashiCorp
- AWS Secrets Manager
- KeyWiz, da Square
- CredHub, da Cloud Foundry
Um outro aspecto do caso da Code Spaces é que os backups eram feitos no próprio AWS. É importante que tenhamos offsite backups, em caso de comprometimento de um provedor de cloud computing.
Vault é uma solução de gerenciamento de credenciais da HashiCorp, a mesma empresa que mantém o Vagrant, Consul, Terraform, entre outros.
O Vault armazena de maneira segura e controla o acesso de tokens, senhas, API Keys, chaves criptográficas, e certificados digitais. Provê uma CLI, uma API HTTP e uma UI Web para gerenciamento. É possível criar, revogar e rotacionar credenciais de maneira automatizada.
Para que a senha, por exemplo, de um BD seja alterada pelo Vault, é necessário que seja configurado um usuário do BD que possa criar e remover outros usuários.
Na documentação do Vault, há um exemplo dos comandos da CLI do Vault para criação de credenciais com duração de 1 hora no MySQL:
vault secrets enable mysql
vault write mysql/config/connection connection_url="root:root-password@tcp(192.168.33.10:3306)/"
vault write mysql/config/lease lease=1h lease_max=24h
vault write mysql/roles/readonly sql="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';"
As credenciais dos backends precisam ser conhecidas pelo Vault. No caso do MySQL, o usuário root
e a respectiva senha precisam ser conhecidos. Essas configurações são armazenadas de maneira criptografada na representação interna do Vault. O Vault pode usar para armazenamento Consul, Etcd, o sistema de arquivos, entre diversos outros mecanismos.
Os dados do Vault são criptografados com uma chave simétrica. Essa chave simétrica é criptografada com uma master key. E a master key é criptografada usando o algoritmo Shamir's secret sharing, em que mais de uma chave é necessária para descriptografar os dados. Por padrão, o Vault usa 5 chaves ao todo, sendo 3 delas necessárias para a descriptografia.
O Spring Cloud Config Server permite o uso do Vault como repositório de configurações: https://cloud.spring.io/spring-cloud-config/reference/html/#vault-backend
Há ainda o Spring Cloud Vault, que provê um cliente Vault para aplicações Spring Boot: https://cloud.spring.io/spring-cloud-vault/reference/html/
Conforme discutimos em capítulos anteriores, um Service Mesh como Istio ou Linkerd cuidam de várias necessidades de infraestrutura em uma Arquitetura de Microservices como Resiliência, Monitoramento, Load balancing e Service Discovery.
Além dessas, um Service Mesh pode cuidar de necessidades de segurança como Confidencialidade, Autenticidade, Autenticação, Autorização e Auditoria. Assim, removemos a responsabilidade da segurança dos serviços e passaríamos para a infraestrutura que os conecta.
O Istio, por exemplo, provê de maneira descomplicada:
- Mutual Authentication com TLS
- gerenciamento de chaves e rotação de credenciais com o componente Citadel
- whitelists e blacklists para restringir o acesso de certos serviços
- configuração de rate limiting, afim de evitar ataques DDoS (Distributed Denial of Service)