1. 概述
安全性在 REST API 開發中發揮着至關重要的作用。一個不安全的 REST API 可能會直接提供對後端系統敏感數據的訪問。因此,組織需要關注 API 安全性。 Spring Security 提供各種機制來安全我們的 REST API。其中之一是 API 密鑰。API 密鑰是客户端在調用 API 調用時提供的令牌。 在本教程中,我們將討論 Spring Security 中基於 API 密鑰的身份驗證的實現。
2. REST API 安全
可以使用 Spring Security 來安全地保護 REST API。 REST API 具有無狀態特性。因此,它們不應使用會話或 Cookie。相反,應使用 Basic 身份驗證、API 密鑰、JWT 或基於 OAuth2 的令牌進行安全保護。
2.1. Basic 身份驗證
Basic 身份驗證是一種簡單的身份驗證方案。客户端通過包含單詞 Authorization 的 HTTP 請求發送請求,該請求包含單詞 Basic 後跟一個空格和一個 Base64 編碼的字符串 username:password。Basic 身份驗證僅在與其他安全機制(如 HTTPS/SSL)結合使用時才被認為是安全的。
2.2. OAuth2
OAuth2 是 REST API 安全的默認標準。它是一個開放的身份驗證和授權標準,允許資源所有者通過訪問令牌授權客户端訪問私有數據。
2.3. API 密鑰
一些 REST API 使用 API 密鑰進行身份驗證。API 密鑰是一個標識 API 客户端,而無需引用實際用户的令牌。令牌可以作為查詢字符串或請求頭髮送。 類似於 Basic 身份驗證,使用 SSL 隱藏密鑰也是可能的。 在本教程中,我們將重點介紹使用 Spring Security 實現 API 密鑰身份驗證。
3. Securing REST APIs with API Keys
In this section, we’ll create a Spring Boot application and secure it using API key-based authentication.
3.1. Maven Dependencies
Let’s start by declaring the spring-boot-starter-security dependency in our pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.2. Creating Custom Filter
The idea is to get the HTTP API Key header from the request and then check the secret with our configuration. In this case, we need to add a custom Filter in the Spring Security configurationclass. We’ll start by implementing the GenericFilterBean. The GenericFilterBean is a simple javax.servlet.Filter implementation that is Spring-aware. Let’s create the AuthenticationFilter class:
public class AuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
try {
Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest) request);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (Exception exp) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = httpResponse.getWriter();
writer.print(exp.getMessage());
writer.flush();
writer.close();
}
}
}
We only need to implement a doFilter() method. We evaluate the API Key header in this method and set the resulting Authentication object into the current SecurityContext instance. Then, the request is passed to the remaining filters for processing, routed toDispatcherServlet, and finally to our controller. If something goes wrong, we catch the Exception and write back to the caller without going forward with the filter chain. We delegate the evaluation of the API Key and constructing the Authentication object to the AuthenticationService class:
public class AuthenticationService {
private static final String AUTH_TOKEN_HEADER_NAME = "X-API-KEY";
private static final String AUTH_TOKEN = "Baeldung";
public static Authentication getAuthentication(HttpServletRequest request) {
String apiKey = request.getHeader(AUTH_TOKEN_HEADER_NAME);
if (apiKey == null || !apiKey.equals(AUTH_TOKEN)) {
throw new BadCredentialsException("Invalid API Key");
}
return new ApiKeyAuthentication(apiKey, AuthorityUtils.NO_AUTHORITIES);
}
}
Here, we check whether the request contains the API Key header with a secret. If the header is null or isn’t equal to secret, we throw a BadCredentialsException. If the request has the header, it performs the authentication, adds the secret to the security context, and then passes the call to the following security filter. Our getAuthentication method is quite simple – we compare the API Key header and secret with a static value. To construct the Authentication object, we must use the same approach Spring Security typically uses to build the object usingstandard authentication. So, let’s extend the AbstractAuthenticationToken class and manually trigger authentication.
3.3. Extending AbstractAuthenticationToken
To successfully implement authentication for our application, we need to convert the incoming API Key to an Authentication object such as an AbstractAuthenticationToken. The AbstractAuthenticationToken class implements the Authentication interface, representing the secret/principal for an authenticated request. Let’s create the ApiKeyAuthentication class:
public class ApiKeyAuthentication extends AbstractAuthenticationToken {
private final String apiKey;
public ApiKeyAuthentication(String apiKey, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.apiKey = apiKey;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return apiKey;
}
}
The ApiKeyAuthentication class is a type of AbstractAuthenticationToken object with the apiKey information obtained from the HTTP request. We use the setAuthenticated(true) method in the construction. As a result, the Authentication object contains apiKey and authenticated fields:
3.4. Security Config
We can register our custom filter programmatically by creating a SecurityFilterChain bean. In this case, we need to add the AuthenticationFilter before the UsernamePasswordAuthenticationFilter class using the addFilterBefore() method on an HttpSecurity instance. Let’s create the SecurityConfig class:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry.requestMatchers("/**").authenticated())
.httpBasic(Customizer.withDefaults())
.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Also, the session policy is set to STATELESS because we’ll use REST endpoints.
3.5. ResourceController
Last, we’ll create the ResourceController with a /home mapping:
@RestController
public class ResourceController {
@GetMapping("/home")
public String homeEndpoint() {
return "Baeldung !";
}
}
3.6. Disabling the Default Auto-Configuration
We need to discard the security auto-configuration. To do this, we exclude the SecurityAutoConfiguration and UserDetailsServiceAutoConfiguration classes:
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class})
public class ApiKeySecretAuthApplication {
public static void main(String[] args) {
SpringApplication.run(ApiKeySecretAuthApplication.class, args);
}
}
Now, the application is ready to test.
4. 測試
我們可以使用 curl 命令來消費受保護的應用。首先,我們嘗試請求 /home,但不提供任何安全憑據:
curl --location --request GET 'http://localhost:8080/home'
我們收到了預期的 401 Unauthorized。現在,我們請求相同的資源,但提供 API Key 和 secret 以訪問它:
curl --location --request GET 'http://localhost:8080/home' \
--header 'X-API-KEY: Baeldung'
結果,服務器的響應是 200 OK。