1. 概述

身份验证是设计安全微服务的基本要素。我们可以通过用户凭据、证书或令牌等多种方式实现身份验证。在这个教程中,我们将学习如何为服务间通信设置身份验证,我们将使用Spring Security来实施这个解决方案。

2. 自定义身份验证简介

对于私有微服务而言,不一定总是需要基于用户的交互式身份验证。然而,我们仍应保护应用免受无效请求,而不仅仅是依赖网络安全。

在这种情况下,我们可以设计一种简单的自定义身份验证方法,通过使用预配置的请求头共享密钥。应用程序将根据这个头部对请求进行验证。

同时,我们也需要在应用中启用TLS以确保共享密钥在网络传输中的安全性。部分端点可能需要无身份验证访问,例如健康检查或错误处理端点。

3. 示例应用

假设我们需要构建一个带有多个REST API的微服务。

3.1. Maven依赖项

首先,我们创建一个Spring Boot Web项目,并添加一些Spring依赖项。我们需要添加spring-boot-starter-webspring-boot-starter-securityspring-boot-starter-testrest-assured

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
</dependency>

3.2. 实现REST控制器

我们的应用有两个端点,一个使用共享密钥头可访问,另一个对网络内的所有服务开放。

首先,我们在APIController类中实现一个/hello端点:

@GetMapping(path = "/api/hello")
public String hello(){
    return "hello";
}

然后,在HealthCheckController类中实现健康检查端点:

@GetMapping(path = "/health")
public String getHealthStatus() {
   return "OK";
}

4. 使用Spring Security实现自定义身份验证

Spring Security提供了内置过滤器来实现身份验证。我们也可以覆盖内置过滤器或使用自定义的身份验证提供者。

我们将配置应用,将一个AuthenticationFilter注册到过滤链中。

4.1. 实现身份验证过滤器

为了实现基于头像的身份验证,我们可以使用RequestHeaderAuthenticationFilter类。RequestHeaderAuthenticationFilter是一个预认证过滤器,它从请求头中获取主体。对于预认证场景,我们需要将身份证明转换为具有角色的用户。

RequestHeaderAuthenticationFilter将请求头设置为Principal对象。内部,它会使用请求头中的PrincipalCredential创建一个PreAuthenticatedAuthenticationToken对象,并将其传递给认证管理器。

SecurityConfig类中添加RequestHeaderAuthenticationFilter的 Bean:

@Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() {
    RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
    filter.setPrincipalRequestHeader("x-auth-secret-key");
    filter.setExceptionIfHeaderMissing(false);
    filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/**"));
    filter.setAuthenticationManager(authenticationManager());

    return filter;
}

在这段代码中,我们设置了x-auth-header-key作为Principal对象。同时,包含了AuthenticationManager对象来委托实际的身份验证。

值得注意的是,该过滤器仅针对路径匹配/api/*的端点启用。

4.2. 配置认证管理器

现在,我们将创建AuthenticationManager并传递一个自定义的AuthenticationProvider对象,稍后我们会实现它:

@Bean
protected AuthenticationManager authenticationManager() {
    return new ProviderManager(Collections.singletonList(requestHeaderAuthenticationProvider));
}

4.3. 配置认证提供者

为了实现自定义认证提供者,我们将实现AuthenticationProvider接口。

让我们重写AuthenticationProvider接口中定义的authenticate方法:

public class RequestHeaderAuthenticationProvider implements AuthenticationProvider {
     
    @Value("${api.auth.secret}")
    private String apiAuthSecret;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String authSecretKey = String.valueOf(authentication.getPrincipal());

        if(StringUtils.isBlank(authSecretKey) || !authSecretKey.equals(apiAuthSecret) {
            throw new BadCredentialsException("Bad Request Header Credentials");
        }

        return new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), null, new ArrayList<>());
    }
}

上述代码中,如果authSecretkeyPrincipal匹配,则通过。如果头部无效,方法将抛出BadCredentialsException

成功认证后,它将返回一个完全经过身份验证的PreAuthenticatedAuthenticationToken对象,该对象可以用于基于角色的授权。

此外,我们还需要重写AuthenticationProvider接口中的supports方法:

@Override
public boolean supports(Class<?> authentication) {
    return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}

supports方法检查此身份验证提供者支持的Authentication类类型。

4.4. 配置Spring Security过滤器

为了在应用中启用Spring Security,我们将添加@EnableWebSecurity注解。同时,我们需要创建一个SecurityFilterChain对象。

默认情况下,Spring Security启用了跨域资源共享(CORS)和跨站请求伪造(CSRF)保护。由于此应用仅限内部微服务访问,我们将禁用这些保护。

SecurityFilterChain中包含上述RequestHeaderAuthenticationFilter

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http.cors(Customizer.withDefaults()).csrf(AbstractHttpConfigurer::disable)
          .sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
          .addFilterAfter(requestHeaderAuthenticationFilter(), HeaderWriterFilter.class)
          .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry
                      .requestMatchers("/api/**").authenticated());

        return http.build();
    }
}

我们注意到,会话管理设置为 STATELESS,因为应用由内部访问。

4.5. 从认证中排除健康端点

使用antMatcherpermitAll方法,我们可以排除任何公共端点的认证和授权。

让我们在上述filterchain方法中添加/health端点,使其不受身份验证影响:

.requestMatchers(HttpMethod.GET, "/health").permitAll()
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> 
       httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint((request, response, authException) -> 
                                                          response.sendError(HttpServletResponse.SC_UNAUTHORIZED)));

我们还应该注意,异常处理已配置包含authenticationEntryPoint,以便返回401 Unauthorized状态。

5. 实现API的集成测试

我们将使用TestRestTemplate来实现端点的集成测试。

首先,通过传递有效的x-auth-secret-key头来实现测试 /hello端点:

HttpHeaders headers = new HttpHeaders();
headers.add("x-auth-secret-key", "test-secret");

ResponseEntity<String> response = restTemplate.exchange(new URI("http://localhost:8080/app/api"),
  HttpMethod.GET, new HttpEntity<>(headers), String.class);

assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("hello", response.getBody());

然后,通过传递无效的头实现测试:

HttpHeaders headers = new HttpHeaders();
headers.add("x-auth-secret-key", "invalid-secret");

ResponseEntity<String> response = restTemplate.exchange(new URI("http://localhost:8080/app/api"),
  HttpMethod.GET, new HttpEntity<>(headers), String.class);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());

最后,我们测试无需头验证的/health端点:

HttpHeaders headers = new HttpHeaders();
ResponseEntity<String> response = restTemplate.exchange(new URI(HEALTH_CHECK_ENDPOINT),
  HttpMethod.GET, new HttpEntity<>(headers), String.class);

assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("OK", response.getBody());

正如预期,身份验证对所需的端点有效。/health端点无需头验证即可访问。

6. 总结

在这篇文章中,我们学习了如何使用共享密钥头的自定义身份验证来增强服务间通信的安全性。

我们还了解了如何通过结合RequestHeaderAuthenticationFilter和自定义身份验证提供者实现共享密钥头的身份验证。

如往常一样,示例代码可以在GitHub上找到。