Spring Cloud - Serviços de proteção

Spring Cloud - Serviços de proteção

1. Visão geral

No artigo anterior,Spring Cloud – Bootstrapping, criamos um aplicativoSpring Cloud básico. Este artigo mostra como protegê-lo.

Naturalmente, usaremosSpring Security para compartilhar sessões usandoSpring SessioneRedis. Esse método é simples de configurar e fácil de estender a muitos cenários de negócios. Se você não estiver familiarizado comSpring Session, verifiquethis article.

O compartilhamento de sessões nos dá a capacidade de registrar usuários em nosso serviço de gateway e propagar essa autenticação para qualquer outro serviço de nosso sistema.

Se você não estiver familiarizado comRedis orSpring Security, é uma boa ideia fazer uma revisão rápida desses tópicos neste momento. Embora grande parte do artigo esteja pronta para copiar e colar para um aplicativo, não há substituto para entender o que acontece sob o capô.

Para obter uma introdução aRedis, leia o tutorialthis. Para uma introdução aSpring Security, leiaspring-security-login,role-and-privilege-for-spring-security-registration espring-security-session. Para obter uma compreensão completa deSpring Security,, dê uma olhada emlearn-spring-security-the-master-class.

2. Configuração do Maven

Vamos começar adicionando a dependênciaspring-boot-starter-security a cada módulo no sistema:


    org.springframework.boot
    spring-boot-starter-security

Como usamos o gerenciamento de dependênciasSpring, podemos omitir as versões das dependênciasspring-boot-starter.

Como uma segunda etapa, vamos modificarpom.xml de cada aplicativo comspring-session,spring-boot-starter-data-redis dependências:


    org.springframework.session
    spring-session


    org.springframework.boot
    spring-boot-starter-data-redis

Apenas quatro de nossos aplicativos serão associados aSpring Session:discovery,gateway,book-service erating-service.

Em seguida, adicione uma classe de configuração de sessão nos três serviços no mesmo diretório que o arquivo principal do aplicativo:

@EnableRedisHttpSession
public class SessionConfig
  extends AbstractHttpSessionApplicationInitializer {
}

Por último, adicione essas propriedades aos três arquivos*.properties em nosso repositório git:

spring.redis.host=localhost
spring.redis.port=6379

Agora vamos pular para a configuração específica do serviço.

3. Protegendo o serviço de configuração

O serviço de configuração contém informações confidenciais geralmente relacionadas a conexões com o banco de dados e chaves de API. Não podemos comprometer essas informações, então vamos mergulhar de cabeça e proteger este serviço.

Vamos adicionar propriedades de segurança ao arquivoapplication.properties emsrc/main/resources do serviço de configuração:

eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/
security.user.name=configUser
security.user.password=configPassword
security.user.role=SYSTEM

Isso configurará nosso serviço para efetuar login com a descoberta. Além disso, estamos configurando nossa segurança com o arquivoapplication.properties.

Vamos agora configurar nosso serviço de descoberta.

4. Protegendo o serviço de descoberta

Nosso serviço de descoberta contém informações confidenciais sobre a localização de todos os serviços no aplicativo. Ele também registra novas instâncias desses serviços.

Se os clientes mal-intencionados obtiverem acesso, eles aprenderão a localização da rede de todos os serviços em nosso sistema e poderão registrar seus próprios serviços maliciosos em nosso aplicativo. É fundamental que o serviço de descoberta esteja protegido.

4.1. Configuração de segurança

Vamos adicionar um filtro de segurança para proteger os endpoints que os outros serviços usarão:

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth) {
       auth.inMemoryAuthentication().withUser("discUser")
         .password("discPassword").roles("SYSTEM");
   }

   @Override
   protected void configure(HttpSecurity http) {
       http.sessionManagement()
         .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
         .and().requestMatchers().antMatchers("/eureka/**")
         .and().authorizeRequests().antMatchers("/eureka/**")
         .hasRole("SYSTEM").anyRequest().denyAll().and()
         .httpBasic().and().csrf().disable();
   }
}

Isso configurará nosso serviço com um usuário ‘SYSTEM’. Esta é uma configuração básicaSpring Security com algumas variações. Vamos dar uma olhada nessas reviravoltas:

  • @Order(1) - diz aSpring para conectar este filtro de segurança primeiro para que seja tentado antes de qualquer outro

  • .sessionCreationPolicy - diz aSpring para sempre criar uma sessão quando um usuário efetuar login neste filtro

  • .requestMatchers - limita a quais pontos finais este filtro se aplica

O filtro de segurança, que acabamos de configurar, configura um ambiente de autenticação isolado que pertence apenas ao serviço de descoberta.

4.2. Protegendo o painel Eureka

Uma vez que nosso aplicativo de descoberta tem uma bela IU para visualizar os serviços registrados atualmente, vamos expor isso usando um segundo filtro de segurança e vinculá-lo à autenticação para o resto do nosso aplicativo. Lembre-se de que nenhuma tag@Order() significa que este é o último filtro de segurança a ser avaliado:

@Configuration
public static class AdminSecurityConfig
  extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) {
   http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
     .and().httpBasic().disable().authorizeRequests()
     .antMatchers(HttpMethod.GET, "/").hasRole("ADMIN")
     .antMatchers("/info", "/health").authenticated().anyRequest()
     .denyAll().and().csrf().disable();
   }
}

Adicione esta classe de configuração dentro da classeSecurityConfig. Isso criará um segundo filtro de segurança que controlará o acesso à nossa interface do usuário. Este filtro tem algumas características incomuns, vejamos:

  • httpBasic().disable() - diz ao spring security para desabilitar todos os procedimentos de autenticação para este filtro

  • sessionCreationPolicy - definimos comoNEVER para indicar que exigimos que o usuário já tenha se autenticado antes de acessar os recursos protegidos por este filtro

Este filtro nunca definirá uma sessão de usuário e depende deRedis para preencher um contexto de segurança compartilhado. Como tal, depende de outro serviço, o gateway, para fornecer autenticação.

4.3. Autenticação com serviço de configuração

No projeto de descoberta, vamos acrescentar duas propriedades aobootstrap.properties em src / main / resources:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword

Essas propriedades permitirão que o serviço de descoberta seja autenticado com o serviço de configuração na inicialização.

Vamos atualizar nossodiscovery.properties em nosso repositório Git

eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Adicionamos credenciais de autenticação básicas ao nosso serviçodiscovery para permitir que ele se comunique com o serviçoconfig. Além disso, configuramosEureka para ser executado no modo autônomo, dizendo ao nosso serviço para não se registrar com ele mesmo.

Vamos enviar o arquivo para o repositóriogit. Caso contrário, as alterações não serão detectadas.

5. Securing Gateway Service

Nosso serviço de gateway é a única parte do nosso aplicativo que queremos expor para o mundo. Como tal, precisará de segurança para garantir que apenas usuários autenticados possam acessar informações confidenciais.

5.1. Configuração de segurança

Vamos criar uma classeSecurityConfig como nosso serviço de descoberta e substituir os métodos com este conteúdo:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth.inMemoryAuthentication().withUser("user").password("password")
      .roles("USER").and().withUser("admin").password("admin")
      .roles("ADMIN");
}

@Override
protected void configure(HttpSecurity http) {
    http.authorizeRequests().antMatchers("/book-service/books")
      .permitAll().antMatchers("/eureka/**").hasRole("ADMIN")
      .anyRequest().authenticated().and().formLogin().and()
      .logout().permitAll().logoutSuccessUrl("/book-service/books")
      .permitAll().and().csrf().disable();
}

Essa configuração é bem direta. Declaramos um filtro de segurança com login de formulário que protege uma variedade de pontos de extremidade.

A segurança em / eureka / ** é para proteger alguns recursos estáticos que serviremos de nosso serviço de gateway para a página de statusEureka. Se você estiver construindo o projeto com o artigo, copie a pastaresource/static do projeto de gateway emGithub para o seu projeto.

Agora modificamos a anotação@EnableRedisHttpSession em nossa classe de configuração:

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

Definimos o modo de liberação como imediato para persistir quaisquer alterações na sessão imediatamente. Isso ajuda na preparação do token de autenticação para redirecionamento.

Finalmente, vamos adicionar umZuulFilter que encaminhará nosso token de autenticação após o login:

@Component
public class SessionSavingZuulPreFilter
  extends ZuulFilter {

    @Autowired
    private SessionRepository repository;

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpSession httpSession = context.getRequest().getSession();
        Session session = repository.getSession(httpSession.getId());

        context.addZuulRequestHeader(
          "Cookie", "SESSION=" + httpSession.getId());
        return null;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }
}

Este filtro irá capturar a solicitação conforme ela é redirecionada após o login e adicionar a chave da sessão como um cookie no cabeçalho. Isso propagará a autenticação para qualquer serviço de backup após o login.

5.2. Autenticação com Config and Discovery Service

Vamos adicionar as seguintes propriedades de autenticação ao arquivobootstrap.properties emsrc/main/resources do serviço de gateway:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

A seguir, vamos atualizar nossogateway.properties em nosso repositório Git

management.security.sessions=always

zuul.routes.book-service.path=/book-service/**
zuul.routes.book-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.book-service.execution.isolation.thread
    .timeoutInMilliseconds=600000

zuul.routes.rating-service.path=/rating-service/**
zuul.routes.rating-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.rating-service.execution.isolation.thread
    .timeoutInMilliseconds=600000

zuul.routes.discovery.path=/discovery/**
zuul.routes.discovery.sensitive-headers=Set-Cookie,Authorization
zuul.routes.discovery.url=http://localhost:8082
hystrix.command.discovery.execution.isolation.thread
    .timeoutInMilliseconds=600000

Adicionamos gerenciamento de sessões para sempre gerar sessões, porque só temos um filtro de segurança que podemos definir no arquivo de propriedades. Em seguida, adicionamos nossas propriedadesRedis de host e servidor.

Além disso, adicionamos uma rota que redirecionará solicitações ao nosso serviço de descoberta. Como um serviço de descoberta independente não se registra, precisamos localizá-lo com um esquema de URL.

Podemos remover a propriedadeserviceUrl.defaultZone do arquivogateway.properties em nosso repositório git de configuração. Este valor está duplicado no arquivobootstrap.

Vamos enviar o arquivo para o repositório Git, caso contrário, as alterações não serão detectadas.

6. Protegendo Serviço de Livro

O servidor do serviço de livro manterá as informações confidenciais controladas por vários usuários. Este serviço deve ser protegido para evitar vazamentos de informações protegidas em nosso sistema.

6.1. Configuração de segurança

Para proteger nosso serviço de livros, copiaremos a classeSecurityConfig do gateway e substituiremos o método com este conteúdo:

@Override
protected void configure(HttpSecurity http) {
    http.httpBasic().disable().authorizeRequests()
      .antMatchers("/books").permitAll()
      .antMatchers("/books/*").hasAnyRole("USER", "ADMIN")
      .authenticated().and().csrf().disable();
}

===

6.2. Propriedades

Adicione essas propriedades ao arquivobootstrap.properties emsrc/main/resources do serviço de livro:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

Vamos adicionar propriedades ao nosso arquivobook-service.properties em nosso repositório git:

management.security.sessions=never

Podemos remover a propriedadeserviceUrl.defaultZone do arquivobook-service.properties em nosso repositório git de configuração. Este valor está duplicado no arquivobootstrap.

Lembre-se de confirmar essas alterações para que o serviço de livros as compreenda.

7. Serviço de classificação de segurança

O serviço de classificação também precisa ser protegido.

7.1. Configuração de segurança

Para proteger nosso serviço de classificação, copiaremos a classeSecurityConfig do gateway e substituiremos o método com este conteúdo:

@Override
protected void configure(HttpSecurity http) {
    http.httpBasic().disable().authorizeRequests()
      .antMatchers("/ratings").hasRole("USER")
      .antMatchers("/ratings/all").hasAnyRole("USER", "ADMIN").anyRequest()
      .authenticated().and().csrf().disable();
}

Podemos excluir o métodoconfigureGlobal() do serviçogateway.

===

7.2. Propriedades

Adicione essas propriedades ao arquivobootstrap.properties emsrc/main/resources do serviço de classificação:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

Vamos adicionar propriedades ao nosso arquivo de rating-service.properties em nosso repositório git:

management.security.sessions=never

Podemos remover a propriedadeserviceUrl.defaultZone do arquivo rating-service.properties em nosso repositório git de configuração. Este valor está duplicado no arquivobootstrap.

Lembre-se de confirmar essas alterações para que o serviço de classificação as compreenda.

8. Executando e testando

InicieRedise todos os serviços para o aplicativo:config, discovery,gateway, book-service,erating-service. Agora vamos testar!

Primeiro, vamos criar uma classe de teste em nosso projetogateway e criar um método para nosso teste:

public class GatewayApplicationLiveTest {
    @Test
    public void testAccess() {
        ...
    }
}

A seguir, vamos configurar nosso teste e validar se podemos acessar nosso recurso/book-service/books desprotegido adicionando este snippet de código dentro de nosso método de teste:

TestRestTemplate testRestTemplate = new TestRestTemplate();
String testUrl = "http://localhost:8080";

ResponseEntity response = testRestTemplate
  .getForEntity(testUrl + "/book-service/books", String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

Execute este teste e verifique os resultados. Se observarmos falhas, confirme se todo o aplicativo foi iniciado com êxito e se as configurações foram carregadas do nosso repositório git de configuração.

Agora vamos testar se nossos usuários serão redirecionados para fazer login ao visitar um recurso protegido como um usuário não autenticado, anexando este código ao final do método de teste:

response = testRestTemplate
  .getForEntity(testUrl + "/book-service/books/1", String.class);
Assert.assertEquals(HttpStatus.FOUND, response.getStatusCode());
Assert.assertEquals("http://localhost:8080/login", response.getHeaders()
  .get("Location").get(0));

Execute o teste novamente e confirme se foi bem-sucedido.

A seguir, vamos realmente fazer login e usar nossa sessão para acessar o resultado protegido pelo usuário:

MultiValueMap form = new LinkedMultiValueMap<>();
form.add("username", "user");
form.add("password", "password");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

Agora, vamos extrair a sessão do cookie e propagá-la para a seguinte solicitação:

String sessionCookie = response.getHeaders().get("Set-Cookie")
  .get(0).split(";")[0];
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
HttpEntity httpEntity = new HttpEntity<>(headers);

e solicite o recurso protegido:

response = testRestTemplate.exchange(testUrl + "/book-service/books/1",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

Execute o teste novamente para confirmar os resultados.

Agora, vamos tentar acessar a seção de administração com a mesma sessão:

response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());

Execute o teste novamente e, como esperado, somos impedidos de acessar áreas administrativas como um usuário antigo comum.

O próximo teste confirmará que podemos fazer login como administrador e acessar o recurso protegido por administrador:

form.clear();
form.add("username", "admin");
form.add("password", "admin");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

sessionCookie = response.getHeaders().get("Set-Cookie").get(0).split(";")[0];
headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
httpEntity = new HttpEntity<>(headers);

response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

Nosso teste está ficando grande! Mas podemos ver quando executamos que, ao fazer login como administrador, obtemos acesso ao recurso de administração.

Nosso teste final está acessando nosso servidor de descoberta através do nosso gateway. Para fazer isso, adicione este código ao final de nosso teste:

response = testRestTemplate.exchange(testUrl + "/discovery",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());

Execute este teste uma última vez para confirmar que tudo está funcionando. Sucesso!!!

Você sentiu falta disso? Porque fizemos logon no nosso serviço de gateway e visualizamos o conteúdo em nossos serviços de livros, classificações e descobertas sem precisar fazer login em quatro servidores separados!

Ao utilizarSpring Session para propagar nosso objeto de autenticação entre os servidores, podemos fazer login uma vez no gateway e usar essa autenticação para acessar controladores em qualquer número de serviços de apoio.

9. Conclusão

A segurança na nuvem certamente se torna mais complicada. Mas com a ajuda deSpring Security eSpring Session, podemos resolver facilmente esse problema crítico.

Agora, temos um aplicativo em nuvem com segurança em torno de nossos serviços. UsandoZuuleSpring Session, podemos registrar usuários em apenas um serviço e propagar essa autenticação para todo o nosso aplicativo. Isso significa que podemos facilmente dividir nosso aplicativo em domínios adequados e proteger cada um deles como acharmos melhor.

Como sempre, você pode encontrar o código-fonte emGitHub.