使用 API Key 和 Secret 保护 Spring Boot API

1、概览

安全在 REST API 开发中起着至关重要的作用。不安全的 REST API 可以直接访问后端系统的敏感数据。因此,企业需要关注 API 的安全性。

Spring Security 提供了各种机制来保护我们的 REST API。其中之一就是 API key。API key 是客户端在调用 API 时提供的 Token。

在本教程中,我们将讨论 Spring Security 中基于 API key 的身份认证的实现。

2、REST API 安全

Spring Security 可用于保护 REST API。REST API 是无状态的。因此,它们不应使用会话或 cookie。相反,它们应该使用 Basic authentication、API key、JWT 或基于 OAuth2 的 token 来确保安全。

2.1、Basic Authentication

Basic authentication 是一种简单的身份认证方案。客户端发送 HTTP 请求时,Authorization 包含 Basic 字样,后面跟一个空格和一个 Base64 编码的字符串 username:password。Basic authentication 只有在使用 HTTPS/SSL 等其他安全机制时才被认为是安全的。

2.2、OAuth2

OAuth2 是 REST API 安全性的事实标准。它是一个开放的身份认证和授权标准,允许资源所有者通过 access token 权客户端访问私有数据。

2.3. API Key

有些 REST API 使用 API Key 进行身份认证。API Key 是一种令牌,用于识别 API 客户端与 API 之间的关系,而无需引用实际用户。Token 可以在查询字符串中发送,也可以作为请求头发送。与 Basic authentication 一样,也可以使用 SSL 通信来保证 Key 的传输是加密的。

在本教程中,我们将重点介绍使用 Spring Security 实现 API Key 身份认证。

3、使用 API Key 确保 REST API 安全

在本节中,我们将创建一个 Spring Boot 应用程序,并使用基于 API Key 的身份认证确保其安全。

3.1、Maven 依赖

首先,让我们在 pom.xml 中声明 spring-boot-starter-security 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

3.2,创建自定义 Filter

这样做的目的是从请求中获取 HTTP API Key header 信息,然后根据我们的配置检查密文。在这种情况下,我们需要在 Spring Security 配置类中添加一个自定义 Filter。

我们将从实现 GenericFilterBean 开始。GenericFilterBean 是一个简单的 javax.servlet.Filter 实现。

创建 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 Key header,并将得到的 Authentication 对象设置到当前的 SecurityContext 实例中。 然后,request 被传递到其余 filter 进行处理,然后路由到 DispatcherServlet,最后到达我们的 controller。

我们将鉴别 API key 和构建 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 Key header。如果 header 为空或不等于预定义的 token,我们就会抛出 BadCredentialsException 异常。如果请求包含该 header,则会执行身份认证,将 header 值添加到 security context 中,然后将调用传递给下一个 security filter。我们的 getAuthentication 方法非常简单,只需将 API Key header 信息和静态 token 值进行比较即可。

要构建 Authentication 对象,我们必须使用与 Spring Security 通常在标准 authentication 上构建对象相同的方法。因此,让我们继承 AbstractAuthenticationToken 类并手动触发 authentication。

3.3、继承 AbstractAuthenticationToken

要成功地为应用程序实现 authentication(认证),我们需要将输入的 API Key 转换成一个 Authentication 对象,如 AbstractAuthenticationTokenAbstractAuthenticationToken 类实现了 Authentication 接口,代表了身份认证请求的 secret/principal。

创建 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 对象包含 apiKeyauthenticated 字段:

ApiKeyAuthentication class

3.4、Security Config

我们可以通过创建 SecurityFilterChain Bean 以编程方式注册自定义 Filter。在这种情况下,我们需要使用 HttpSecurity 实例上的 addFilterBefore() 方法将 AuthenticationFilter 添加到 UsernamePasswordAuthenticationFilter 类之前。

创建 SecurityConfig 类:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
          .disable()
          .authorizeRequests()
          .antMatchers("/**")
          .authenticated()
          .and()
          .httpBasic()
          .and()
          .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
          .and()
          .addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}

此外,session policy(会话策略)设置为 STATELESS(无状态),因为我们将使用 REST 端点。

3.5,ResourceController

最后,我们将创建具有 /home mapping 的 ResourceController

@RestController
public class ResourceController {
    @GetMapping("/home")
    public String homeEndpoint() {
        return "Baeldung !";
    }
}

3.6、禁用默认自动配置

我们需要放弃 security 自动配置。为此,我们要排除 SecurityAutoConfigurationUserDetailsServiceAutoConfiguration 类:

@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 Key:

curl --location --request GET 'http://localhost:8080/home' \
--header 'X-API-KEY: Baeldung'

因此,服务器的响应为 200 OK

5、总结

在本教程中,我们讨论了 REST API 安全机制。然后,我们在 Spring Boot 应用程序中实现了 Spring Security,以使用 API Key 身份认证机制确保 REST API 的安全。


原文:https://www.baeldung.com/spring-boot-api-key-secret