1. 概述
身份驗證是設計安全微服務的基本要素。我們可以通過多種方式實現身份驗證,例如使用基於用户憑據、證書或基於令牌的方式。
在本教程中,我們將學習如何為服務之間的通信設置身份驗證。我們將使用 Spring Security 實施解決方案。
2. 自定義身份驗證介紹
使用身份提供程序或密碼數據庫在某些情況下可能不可行,因為私有微服務不需要基於用户的交互。但是,我們仍然應該保護應用程序免受任何無效請求的影響,而不是僅僅依賴網絡安全。
在這種情況下,我們可以通過使用自定義共享密鑰標頭來設計一種簡單的身份驗證技術。應用程序將驗證請求與預先配置的請求標頭。
我們還應該在應用程序中啓用 TLS 以確保網絡上共享密鑰的安全。
我們還需要確保一些端點在沒有身份驗證的情況下可以工作,例如健康檢查或錯誤端點。
3. 示例應用程序
設想一下,我們需要構建一個帶有少量 REST API 的微服務。
3.1. Maven 依賴
首先,我們將創建一個 Spring Boot Web 項目幷包含一些 Spring 依賴項。
讓我們添加 spring-boot-starter-web, spring-boot-starter-security, spring-boot-starter-test, 以及 rest-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 類中實現 health 端點:
@GetMapping(path = "/health")
public String getHealthStatus() {
return "OK";
}
4. Implement the Custom Authentication With Spring Security
Spring Security provides several in-built filter classes to implement authentication. We can also override the built-in filter class or use an authentication provider to implement a custom solution.
We’ll configure the application to register an AuthenticationFilter into the filter chain.
4.1. Implement the Authentication Filter
To implement a header-based authentication, we can use the RequestHeaderAuthenticationFilter class. The RequestHeaderAuthenticationFilter is a pre-authenticated filter that obtains the principal from a request header. As with any pre-auth scenarios, we’ll need to convert the proof of authentication into a user with a role.
The RequestHeaderAuthenticationFilter sets the Principal object with the request header. Internally, it’ll create a PreAuthenticedAuthenticationToken object using the Principal and Credential from the request header and pass the token to the authentication manager.
Let’s add the RequestHeaderAuthenticationFilter Bean in the SecurityConfig class:
@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;
}
In the above code, the x-auth-header-key header is added as the Principal object. Also, the AuthenticationManager object is included to delegate the actual authentication.
We should note that the filter is enabled for the endpoints matching with the /api/** path.
4.2. Setup the Authentication Manager
Now, we’ll create the AuthenticationManager and pass a custom AuthenticationProvider object, which we’ll create later:
@Bean
protected AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(requestHeaderAuthenticationProvider));
}
4.3. Configure the Authentication Provider
To implement the custom authentication provider, we’ll implement the AuthenticationProvider interface.
Let’s override the authenticate method defined in the AuthenticationProvider interface:
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<>());
}
}
In the above code, the authSecretkey value matched with the Principal. In case the header is not valid, the method throws a BadCredentialsException.
On successful authentication, it’ll return the fully authenticated PreAuthenticatedAuthenticationToken object. The PreAuthenticatedAuthenticationToken object can be treated as a user for role-based authorization.
Also, we’ll need to override the supports method defined in the AuthenticationProvider interface:
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(PreAuthenticatedAuthenticationToken.class);
}
The supports method checks for the Authentication class type supported by this authentication provider.
4.4. Configure Filter With Spring Security
To enable Spring Security in the application, we’ll add the @EnableWebSecurity annotation. Also, we need to create a SecurityFilterChain object.
Also, Spring Security enables CORS and CSRF protection by default. As this application is only accessible by internal microservice, we’ll disable the CORS and CSRF protection.
Let’s include the above RequestHeaderAuthenticationFilter in the SecurityFilterChain:
@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();
}
}
We should note that the session management is set as STATELESS since the application is accessed internally.
4.5. Exclude Health Endpoint From Authentication
Using the antMatcher’s permitAll method, we can exclude any public endpoints from authentication and authorization.
Let’s add the /health endpoint in the above filterchain method to exclude from authentication:
.requestMatchers(HttpMethod.GET, "/health").permitAll()
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint((request, response, authException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)));
We should note that the exception handling is configured to include the authenticationEntryPoint for returning 401 Unauthorized status.
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 過濾器和自定義身份驗證提供程序。