1、概览 {#1概览}
身份认证是微服务安全的基础。我们可以通过各种方式实现身份认证,如使用基于用户的凭证、证书或 token。
在本教程中,我们将学习如何使用 Spring Security 实现服务间的通信认证。
2、自定义身份认证简介 {#2自定义身份认证简介}
在某些情况下,使用 Oauth2 或存储在数据库中的密码可能并不可行,因为私有微服务不需要基于用户的交互。然而,我们仍然应该保护应用程序免受任何无效请求的影响。
在这种情况下,我们可以设计一种简单的身份认证技术,使用自定义 header。应用程序将根据预先配置的请求头认证请求。
我们还应在应用程序中启用 TLS,以确保 header 在网络传输中的安全。
我们可能还需要确保一些端点不需要进行任何身份认证,例如 health 或 error 端点。
3、示例应用 {#3示例应用}
假如,我们需要用几个 REST API 构建一个微服务。
3.1、Maven 依赖 {#31maven-依赖}
首先,我们将创建一个 Spring Boot Web 项目,添加 spring-boot-starter-web 和 spring-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 {#32实现-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 实现自定义身份认证 {#4使用-spring-security-实现自定义身份认证}
Spring Security 提供了多个内置 filter 类来实现身份认证。我们可以覆盖内置 filter 类或使用 authentication provider 来实现自定义的身份认证逻辑。
接下来配置应用,将 AuthenticationFilter
注册到过滤器链中。
4.1、实现 Filter {#41实现-filter}
要实现基于 header 的身份认证,我们可以使用 RequestHeaderAuthenticationFilter
类。 RequestHeaderAuthenticationFilter
是一个预认证(pre-authenticated)filter,它从请求头中获取 Principal
对象。与任何预认证场景一样,我们需要将身份认证的证明转换为具有角色的用户。
RequestHeaderAuthenticationFilter
使用请求头设置 Principal
对象。在内部,它会使用请求头中的 Principal
和 Credential
创建一个 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 {#42配置-authenticationmanager}
现在,我们将创建 AuthenticationManager
并传递一个自定义 AuthenticationProvider
对象(稍后我们将创建该对象):
@Bean
protected AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(requestHeaderAuthenticationProvider));
}
4.3、配置 AuthenticationProvider {#43配置-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 {#44配置-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 端点 {#45排除-health-端点}
使用 antMatcher
的 permitAll
方法,我们可以将任何公共端点排除在身份认证和授权之外。
让我们在上述 filterchain
方法中添加 /health
端点,将其排除在身份认证之外:
.antMatchers("/health").permitAll()
.and()
.exceptionHandling().authenticationEntryPoint((request, response, authException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED));
注意,异常处理配置了 authenticationEntryPoint
,以返回 401
未经授权状态。
5、测试 API {#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、总结 {#6总结}
在这篇文章中,我们学习了在微服务架构中如何使用 Spring Security 通过 header 来认证微服务之间的调用。
本文源码可以在此 仓库 获取。
参考:https://www.baeldung.com/spring-boot-shared-secret-authentication