概述
在REST API开发中,安全性至关重要。不安全的REST API可能会直接暴露后端系统的敏感数据,因此组织必须重视API安全。Spring Security提供了多种机制来保护我们的REST API,其中之一是API密钥。API密钥是在调用API时客户端提供的令牌。本教程将讨论如何在Spring Security中实现基于API密钥的身份验证。
REST API安全
Spring Security可用于保护REST API。由于REST API是无状态的,不应使用会话或cookie。相反,应该使用基本认证、API密钥、JWT(/spring-security-oauth-jwt)或基于OAuth2(/spring-security-oauth)的令牌进行加密。
2.1. 基本认证
基本认证是一种简单的身份验证方案。客户端在HTTP请求头中发送包含单词Basic
,后面跟着一个由 Base64 编码的字符串username:password
的Authorization
字段。只有在配合HTTPS/SSL等其他安全机制的情况下,基本认证才被视为安全。
2.2. OAuth2
OAuth2是REST API安全的事实标准。它是一个开放的认证和授权标准,允许资源所有者通过访问令牌向客户端委派对私有数据的访问权限。
2.3. API密钥
有些REST API使用API密钥进行身份验证。API密钥是一个标识API客户端的令牌,而不涉及实际用户。密钥可以作为查询字符串或请求头发送。与基本认证类似,可以通过SSL隐藏密钥。在本教程中,我们将专注于使用Spring Security实现基于API密钥的身份验证。
3. 使用API密钥保护REST API
在这个部分,我们将创建一个Spring Boot应用,并使用API密钥进行身份验证。
3.1. Maven依赖项
首先,在pom.xml
中声明spring-boot-starter-security
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.2. 创建自定义过滤器
思路是获取HTTP请求头中的API密钥,然后与我们的配置进行比较。在这种情况下,我们需要在Spring Security配置类中添加一个自定义过滤器。我们将从实现GenericFilterBean
开始。GenericFilterBean
是一个简单的javax.servlet.Filter
实现,具有Spring意识。让我们创建AuthenticationFilter
类:
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);
} 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();
}
filterChain.doFilter(request, response);
}
}
我们只需要实现doFilter()
方法。在这个方法中,我们评估API密钥头,并将结果的Authentication
对象设置到当前SecurityContext
实例中。然后,请求传递给剩余的过滤器处理,最后路由到DispatcherServlet
,最终到达控制器。我们委托API密钥的评估和Authentication
对象的构建给AuthenticationService
类:
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);
}
}
在这里,我们检查请求是否包含带有秘密的API密钥头。如果头为空或不等于秘密,我们抛出BadCredentialsException
。如果请求包含头,它将执行身份验证,将秘密添加到安全上下文中,然后将请求传递给下一个安全过滤器。我们的getAuthentication
方法非常简单,只是比较HTTP头中的API密钥和秘密与静态值。为了构建Authentication
对象,我们必须使用Spring Security通常用于构建标准身份验证时所采用的方法。所以,我们需要扩展AbstractAuthenticationToken
类并手动触发身份验证。
3.3. 扩展AbstractAuthenticationToken
为了成功为我们的应用程序实施身份验证,我们需要将接收到的API密钥转换为Authentication
对象,如AbstractAuthenticationToken
。AbstractAuthenticationToken
类实现了Authentication
接口,代表已认证请求的密钥/主体。让我们创建ApiKeyAuthentication
类:
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;
}
}
ApiKeyAuthentication
类是AbstractAuthenticationToken
对象的一种类型,其中包含从HTTP请求获取的apiKey
信息。我们在构造时使用setAuthenticated(true)
方法。因此,Authentication
对象包含apiKey
和authenticated
字段:
3.4. 安全配置
我们可以程序性地注册自定义过滤器,通过创建一个SecurityFilterChain
bean。在这种情况下,我们需要使用HttpSecurity
实例的addFilterBefore()
方法,在UsernamePasswordAuthenticationFilter
类之前添加AuthenticationFilter
。让我们创建SecurityConfig
类:
@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();
}
}
另外,设置会话策略为STATELESS
,因为我们使用的是REST端点。
3.5. ResourceController
最后,我们将创建具有/home
映射的ResourceController
:
@RestController
public class ResourceController {
@GetMapping("/home")
public String homeEndpoint() {
return "Baeldung !";
}
}
3.6. 禁用默认自动配置
我们需要丢弃安全自动配置。为此,我们排除SecurityAutoConfiguration
和UserDetailsServiceAutoConfiguration
类:
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class})
public class ApiKeySecretAuthApplication {
public static void main(String[] args) {
SpringApplication.run(ApiKeySecretAuthApplication.class, args);
}
}
现在,应用程序准备测试。
4. 测试
我们可以使用curl命令来消费受保护的应用。首先,尝试不提供任何安全凭据请求/home
:
curl --location --request GET 'http://localhost:8080/home'
我们得到预期的401 Unauthorized
响应。现在,让我们提供API密钥和秘密以访问资源:
curl --location --request GET 'http://localhost:8080/home' \
--header 'X-API-KEY: Baeldung'
服务器的响应是200 OK
。
5. 总结
在本教程中,我们讨论了REST API的安全机制。然后,我们在Spring Boot应用中实现了Spring Security,使用API密钥的身份验证机制保护我们的REST API。如往常一样,代码示例可在GitHub上找到。