使用 Spring Security 通过 Header 认证服务之间的调用

1、概览

身份认证是微服务安全的基础。我们可以通过各种方式实现身份认证,如使用基于用户的凭证、证书或 token。

在本教程中,我们将学习如何使用 Spring Security 实现服务间的通信认证。

2、自定义身份认证简介

在某些情况下,使用 Oauth2 或存储在数据库中的密码可能并不可行,因为私有微服务不需要基于用户的交互。然而,我们仍然应该保护应用程序免受任何无效请求的影响。

在这种情况下,我们可以设计一种简单的身份认证技术,使用自定义 header。应用程序将根据预先配置的请求头认证请求。

我们还应在应用程序中启用 TLS,以确保 header 在网络传输中的安全。

我们可能还需要确保一些端点不需要进行任何身份认证,例如 health 或 error 端点。

3、示例应用

假如,我们需要用几个 REST API 构建一个微服务。

3.1、Maven 依赖

首先,我们将创建一个 Spring Boot Web 项目,添加 spring-boot-starter-webspring-boot-starter-test 依赖。

<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>

3.2、实现 REST Controller

我们的应用有两个端点,一个端点可通过 secret header 访问,另一个端点可被网络中的所有用户访问。

首先,在 APIController 中实现 /hello 端点:

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

然后,我们将在 HealthCheckController 类中实现 /health 端点:

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

4、使用 Spring Security 实现自定义身份认证

Spring Security 提供了多个内置 filter 类来实现身份认证。我们可以覆盖内置 filter 类或使用 authentication provider 来实现自定义的身份认证逻辑。

接下来配置应用,将 AuthenticationFilter 注册到过滤器链中。

4.1、实现 Filter

要实现基于 header 的身份认证,我们可以使用 RequestHeaderAuthenticationFilter 类。 RequestHeaderAuthenticationFilter 是一个预认证(pre-authenticated)filter,它从请求头中获取 Principal 对象。与任何预认证场景一样,我们需要将身份认证的证明转换为具有角色的用户。

RequestHeaderAuthenticationFilter 使用请求头设置 Principal 对象。在内部,它会使用请求头中的 PrincipalCredential 创建一个 PreAuthenticedAuthenticationToken 对象,并将该 token 传递给身份认证管理器(AuthenticationManager)。

让我们在 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 header 添加为 Principal 对象。此外,还包括了 AuthenticationManager 对象,用于实际的身份认证操作。

我们需要注意的是,该 filter 仅对与 /api/** 路径匹配的端点启用。

4.2、配置 AuthenticationManager

现在,我们将创建 AuthenticationManager 并传递一个自定义 AuthenticationProvider 对象(稍后我们将创建该对象):

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

4.3、配置 AuthenticationProvider

为实现自定义 authentication provider,我们将实现 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<>());
    }
}

在上述代码中,authSecretkey 值与 Principal 匹配。如果 header 无效,该方法会抛出 BadCredentialsException 异常。

认证成功后,它将返回经过完全认证的 PreAuthenticatedAuthenticationToken 对象,PreAuthenticatedAuthenticationToken 对象可被视为基于角色授权的用户。

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

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

supports 方法会检查该 authentication provider 所支持的 Authentication class 类型。

4.4、配置 Filter

要在应用程序中启用 Spring Security,我们需要添加 @EnableWebSecurity 注解。此外,我们还需要创建一个 SecurityFilterChain 对象。

另外,Spring Security 默认启用 CORS 和 CSRF 保护。由于此应用程序只能由内部微服务访问,我们将禁用 CORS 和 CSRF 保护。

让我们在 SecurityFilterChain 中加入上述 RequestHeaderAuthenticationFilter

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and()
          .csrf()
          .disable()
          .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
          .and()
          .addFilterAfter(requestHeaderAuthenticationFilter(), HeaderWriterFilter.class)
          .authorizeHttpRequests()
          .antMatchers("/api/**").authenticated();

        return http.build();
    }
}

值得注意的是,session 管理被设置为 STATELESS,因为这是内部应用程序,用不着 session。

4.5、排除 Health 端点

使用 antMatcherpermitAll 方法,我们可以将任何公共端点排除在身份认证和授权之外。

让我们在上述 filterchain 方法中添加 /health 端点,将其排除在身份认证之外:

.antMatchers("/health").permitAll()
.and()
.exceptionHandling().authenticationEntryPoint((request, response, authException) ->
  response.sendError(HttpServletResponse.SC_UNAUTHORIZED));

注意,异常处理配置了 authenticationEntryPoint,以返回 401 未经授权状态。

5、测试 API

使用 TestRestTemplate 测试我们的端点。

首先,让我们通过向 /hello 端点传递有效的 x-auth-secret-key header 来进行测试:

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());

然后,让我们通过传递一个无效的 header 来测试端点:

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());

最后,我们不添加任何 header,测试请求 /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());

不出所料,只有传递了正确的 x-auth-secret-key header 才能访问 /hello 端点,而 /health 不需要任何认证。

6、总结

在这篇文章中,我们学习了在微服务架构中如何使用 Spring Security 通过 header 来认证微服务之间的调用。

本文源码可以在此 仓库 获取。


参考:https://www.baeldung.com/spring-boot-shared-secret-authentication