1. 概述
身份驗證是設計安全微服務的基本要素。我們可以通過使用基於用户憑據、證書或基於令牌的方式來實施身份驗證。
在本教程中,我們將學習如何為服務之間的通信設置身份驗證。我們將使用 Spring Security 實施解決方案。
2. 自定義身份驗證介紹
使用身份提供程序或密碼數據庫在所有情況下都可能不可行,因為私有微服務不需要基於用户的交互。然而,我們仍然應該保護應用程序免受任何無效請求的影響,而不是僅僅依賴網絡安全。
在這種情況下,我們可以通過使用自定義共享密鑰標頭來設計一種簡單的身份驗證技術。應用程序將驗證請求與預先配置的請求標頭。
我們還應該在應用程序中啓用 TLS 以安全地在網絡上傳輸共享密鑰。
我們還需要確保某些端點無需身份驗證,例如健康檢查或錯誤端點。
3. 示例應用程序
讓我們假設我們需要構建一個帶有幾個 REST API 的微服務。
3.1. Maven 依賴
首先,我們將創建一個 Spring Boot Web 項目並添加一些 Spring 依賴。
讓我們添加以下依賴:<a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web">spring-boot-starter-web</a>、<a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security">spring-boot-starter-security</a>、<a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test">spring-boot-starter-test</a> 和 <a href="https://mvnrepository.com/artifact/io.rest-assured/rest-assured">rest-assured</a> 依賴:
<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";
}
然後,我們將實現 health 端點,位於 HealthCheckController 類中:
@GetMapping(path = "/health")
public String getHealthStatus() {
return "OK";
}4. 使用 Spring Security 實現自定義身份驗證
Spring Security 提供了一組內置的過濾器類,用於實現身份驗證。 此外,我們還可以覆蓋內置的過濾器類或使用身份驗證提供者來實施自定義解決方案。
我們將配置應用程序以將 <em >AuthenticationFilter</em> 註冊到過濾器鏈中。
<h3>4.1. 實現身份驗證過濾器</h3>
<p>要實現基於頭部的身份驗證,可以使用 <em >RequestHeaderAuthenticationFilter</em> 類。 <em >RequestHeaderAuthenticationFilter</em> 是一個預身份驗證過濾器,它從請求頭中獲取 principal。 類似於任何預身份驗證場景,我們需要將身份驗證的證明轉換為具有角色的用户。</p>
<p> <span ></span><em ><span >RequestHeaderAuthenticationFilter</span></em ><span >設置 <em >Principal</em> 對象,使用請求頭。 內部,它將創建一個 <em >PreAuthenticedAuthenticationToken</em> 對象,使用來自請求頭的 <em >Principal</em> 和 <em >Credential</em>,並將令牌傳遞給身份驗證管理器。</p>
<p> <span ></span><em ><span >RequestHeaderAuthenticationFilter</span></em ><span > Bean 在 <em >SecurityConfig</em> 類中:</p>
@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;
}在上述代碼中,<em>x-auth-header-key</em> HTTP 頭部作為<em>Principal</em> 對象添加。同時,<em>AuthenticationManager</em> 對象也包含在內,用於委託實際的身份驗證。
請注意,過濾器已針對與 /api/** 路徑匹配的端點啓用。
4.2. 設置身份驗證管理器
現在,我們將創建 <span title="AuthenticationManager">AuthenticationManager</span> 並傳遞一個自定義 <span title="AuthenticationProvider">AuthenticationProvider</span> 對象,我們稍後會創建它:
@Bean
protected AuthenticationManager authenticationManager() {
return new ProviderManager(Collections.singletonList(requestHeaderAuthenticationProvider));
}4.3. 配置身份驗證提供者
為了實現自定義身份驗證提供者,我們將實現 <em >AuthenticationProvider</em> 接口。
讓我們覆蓋 <em >AuthenticationProvider</em> 接口中定義的 <em >authenticate</em> 方法:
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 匹配。如果請求頭無效,則該方法會拋出 BadCredentialsException 異常。
在成功認證後,它將返回完整的認證 PreAuthenticatedAuthenticationToken 對象。 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 保護。 鑑於此應用程序僅通過內部微服務訪問,我們將禁用 CORS 和 CSRF 保護。
請將上述 RequestHeaderAuthenticationFilter 包含在 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();
}
}請注意,會話管理設置為無狀態,因為該應用程序通過內部訪問。
4.5. 從身份驗證中排除健康端點
通過使用antMatcher 的 permitAll 方法, 我們可以排除任何公共端點中的身份驗證和授權。
讓我們在上述 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,我們將實現 API 端點的集成測試。
首先,我們通過將有效的 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());最後,我們將不添加任何標頭的情況下測試<i /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 和自定義身份驗證提供程序組合來實現基於共享密鑰的標頭身份驗證。