從 HttpServletRequest 取得 HTTP 基本驗證
1. 引言
基本驗證是 HTTP 服務最常用的安全機制。它的流行源於其簡單易用和易於實現。在本教程中,我們將探討 HTTP 基本驗證的工作原理,以及如何在基於 Spring 的應用程式中從傳入的 HTTP 請求中提取憑證,特別是密碼。
2. HTTP 基本驗證
HTTP 基本驗證是一種簡單的驗證方案,用戶端在 HTTP 請求頭中傳送憑證。用戶端發送的請求中包含憑證,這些憑證包含在Authorization標頭中,然後伺服器對其進行驗證。標頭格式如下:
Authorization: Basic <base64(username:password)>
使用者名稱和密碼會合併成一個字串,中間用冒號分隔。然後,該字串會使用 Base64 編碼。例如,如果使用者名稱是admin ,密碼是secret ,則合併後的字串為admin:secret 。此字串的 Base64 編碼版本為YWRtaW46c2VjcmV0 。最終的 HTTP 頭部如下所示:
Authorization: Basic YWRtaW46c2VjcmV0
需要注意的是,Base64 是一種編碼,而非加密。它很容易被逆向破解,因此在使用基本驗證時必須使用 HTTPS。
3. Maven 依賴項
首先,讓我們將spring-boot-starter-web依賴項匯入pom.xml檔案中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
4. 從授權標頭檢索憑證
在本節中,我們將逐步介紹從傳入的 HTTP 請求中擷取原始使用者名稱和密碼的過程。
4.1. 從 HTTP 請求中手動提取
取得密碼最直接的方法是直接從HttpServletRequest中讀取Authorization標頭並對其進行解碼。讓我們建立一個BasicAuthExtractor類別和一個實用方法來實現這一點:
public class BasicAuthExtractor {
public static String[] extractCredentials(String authHeader) {
if (authHeader != null && authHeader.startsWith("Basic ")) {
String base64Credentials = authHeader.substring("Basic ".length()).trim();
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
final String[] values = credentials.split(":", 2);
if (values.length == 2) {
return values;
}
}
return null;
}
}
` extractCredentials()方法先檢查Authorization標頭是否存在。它還會驗證該標頭是否以「 Basic 」前綴開頭。接下來,它會從標頭值中移除該前綴。然後,它使用Base64.getDecoder()解碼剩餘的 Base64 編碼字串。解碼後,位元組被轉換為 UTF-8 字串。產生的字串應遵循username:password格式。最後,該方法以第一個冒號分割字串。它會傳回一個包含使用者名稱和密碼的雙元素陣列。如果標頭無效或格式錯誤,則該方法將傳回null 。我們可以在RestController中輕鬆使用extractCredentials()方法:
@GetMapping("/extract")
public ResponseEntity<String> extract(@RequestHeader("Authorization") String authHeader) {
String[] credentials = BasicAuthExtractor.extractCredentials(authHeader);
if (credentials != null) {
String username = credentials[0];
String password = credentials[1];
return ResponseEntity.ok("Extracted Username: " + username +
" Extracted Password: " + password);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
我們使用@RequestHeader註解從傳入的請求中取得Authorization標頭值。
4.2 自訂 Servlet 過濾器
另一種方法是建立自訂過濾器,讀取請求頭,提取密碼,並將其儲存在請求屬性中以供後續使用。讓我們建立一個AuthFilter類別來實現這一點:
@Component
public class AuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String[] credentials = BasicAuthExtractor.extractCredentials(authHeader);
if (credentials != null) {
request.setAttribute("rawPassword", credentials[1]);
}
filterChain.doFilter(request, response);
}
}
此過濾器會攔截每個傳入的 HTTP 請求並讀取Authorization標頭。然後,它使用extractCredentials()方法提取密碼,並將其作為請求屬性存儲,然後再繼續執行過濾鏈。之後,在我們的服務或控制器中,我們可以安全地檢索它:
String rawPassword = (String) request.getAttribute("rawPassword");
5. 測試 BasicAuthExtractor
我們先來測試核心提取邏輯。首先,我們需要確保它能正確處理有效的基本驗證標頭,並傳回預期的憑證陣列:
@Test
void givenValidHeader_whenExtract_thenReturnCredentialsArray() {
// Given
String header = encodeCredentials("admin", "secret");
// When
String[] credentials = BasicAuthExtractor.extractCredentials(header);
// Then
assertThat(credentials).isNotNull();
assertThat(credentials).hasSize(2);
assertThat(credentials[0]).isEqualTo("admin");
assertThat(credentials[1]).isEqualTo("secret");
}
為了保持測試程式碼的簡潔性和可讀性,我們使用了一個輔助方法來處理 Base64 編碼和標頭格式化:
private String encodeCredentials(String username, String password) {
String credentials = username + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
}
此外,我們還需要驗證該方法對於無效標頭(例如缺少Basic前綴或使用不同身份驗證方案的標頭)是否能夠安全地傳回null :
@Test
void givenMissingBasicPrefix_whenExtract_thenReturnNull() {
// Given
String header = "Bearer some-token";
// When
String[] credentials = BasicAuthExtractor.extractCredentials(header);
// Then
assertThat(credentials).isNull();
}
6. 結論
本文介紹了 HTTP 基本驗證如何對憑證進行編碼,以及如何使用 Java 的 Base64 工具手動解碼。我們也探討如何使用自訂濾鏡來擷取原始密碼。雖然提取密碼有時對於舊版整合是必要的,但由於其固有的安全風險,應盡可能避免這樣做。與往常一樣,原始碼已發佈在 GitHub 上。