使用API密钥和密钥保护Spring Boot API

101

我想保护Spring Boot API,使其只能被拥有有效API密钥和密钥的客户端访问。但是程序内部没有身份验证(使用用户名和密码的标准登录)因为所有数据都是匿名的。我想要实现的只是所有API请求仅能用于特定的第三方前端。

我找到了很多关于如何通过用户身份验证来保护Spring Boot API的文章。但我不需要用户身份验证。我所考虑的是只提供客户端API密钥和密钥,以便他可以访问终点。

请问您能否建议我如何实现这一点?谢谢!


1
你是说唯一的区别就是你称之为API密钥而不是用户名,还是还有其他的区别? - dur
以下回答是否解决了您的问题?您是如何管理用户和每个用户的 API 密钥的? - Rasool Ghafari
5个回答

114
创建一个过滤器,以获取您用于身份验证的任何标头。
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

    private String principalRequestHeader;

    public APIKeyAuthFilter(String principalRequestHeader) {
        this.principalRequestHeader = principalRequestHeader;
    }

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        return request.getHeader(principalRequestHeader);
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        return "N/A";
    }

}

在您的Web安全配置中配置过滤器。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${yourapp.http.auth-token-header-name}")
    private String principalRequestHeader;

    @Value("${yourapp.http.auth-token}")
    private String principalRequestValue;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        APIKeyAuthFilter filter = new APIKeyAuthFilter(principalRequestHeader);
        filter.setAuthenticationManager(new AuthenticationManager() {

            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String principal = (String) authentication.getPrincipal();
                if (!principalRequestValue.equals(principal))
                {
                    throw new BadCredentialsException("The API key was not found or not the expected value.");
                }
                authentication.setAuthenticated(true);
                return authentication;
            }
        });
        httpSecurity.
            antMatcher("/api/**").
            csrf().disable().
            sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
            and().addFilter(filter).authorizeRequests().anyRequest().authenticated();
    }

}

1
这很有帮助。我有一个应用程序需要支持基于用户名/密码和ApiKey的身份验证。我已经让用户名/密码工作了,阅读了您的文章后,我也能够让ApiKey工作了。不幸的是,我似乎破坏了用户名/密码。我怀疑是我的过滤器顺序或者同时使用相同的AuthenticationManager进行用户名/密码和ApiKey身份验证。有什么建议吗? - Phillip Stack
2
@PhillipStack 你应该能够配置两个具有不同身份验证管理器的WebSecurityConfigurerAdapter,比如: https://dev59.com/tVwX5IYBdhLWcg3wnwgy - MarkOfHall
1
如果我理解正确的话,APIKey 不是私有的。任何使用客户端的人都可以打开开发者控制台并检查头部内容。是这样吗? - marcellorvalle
5
通常,使用API密钥保护的API的客户端是另一个服务。如果你认为这个API的客户端会是Web浏览器,我建议你研究OAuth / JWT令牌以实现用户授权。 - MarkOfHall
2
有用的必读内容:REST安全防护清单/API密钥 - Guillaume Husta
显示剩余4条评论

29

我意识到在这件事上我有些晚,但我还是成功地让API密钥与Spring Boot一起使用用户名/密码认证。我对使用AbstractPreAuthenticatedProcessingFilter的想法并不是很满意,因为在阅读JavaDoc时,它似乎是对该特定类的误用。

最终,我创建了一个新的ApiKeyAuthenticationToken类,并编写了一个非常简单的原始servlet过滤器来实现这个目标:

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;

@Transient
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {

    private String apiKey;
    
    public ApiKeyAuthenticationToken(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;
    }
}

过滤器

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;

public class ApiKeyAuthenticationFilter implements Filter {

    static final private String AUTH_METHOD = "api-key";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {
        if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            String apiKey = getApiKey((HttpServletRequest) request);
            if(apiKey != null) {
                if(apiKey.equals("my-valid-api-key")) {
                    ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
                    SecurityContextHolder.getContext().setAuthentication(apiToken);
                } else {
                    HttpServletResponse httpResponse = (HttpServletResponse) response;
                    httpResponse.setStatus(401);
                    httpResponse.getWriter().write("Invalid API Key");
                    return;
                }
            }
        }
        
        chain.doFilter(request, response);
        
    }

    private String getApiKey(HttpServletRequest httpRequest) {
        String apiKey = null;
        
        String authHeader = httpRequest.getHeader("Authorization");
        if(authHeader != null) {
            authHeader = authHeader.trim();
            if(authHeader.toLowerCase().startsWith(AUTH_METHOD + " ")) {
                apiKey = authHeader.substring(AUTH_METHOD.length()).trim();
            }
        }
        
        return apiKey;
    }
}

此时所剩的就是在链中适当位置注入过滤器。在我的情况下,我希望API密钥身份验证在任何用户名/密码身份验证之前进行评估,以便它可以在应用程序尝试重定向到登录页面之前对请求进行身份验证:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
            .disable()
        .addFilterBefore(new ApiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest()
                .fullyAuthenticated()
                .and()
        .formLogin();
}

还有一件事我要说的是,你需要注意的是,API密钥认证请求不会在你的服务器上创建并丢弃大量的HttpSession


1
API密钥通常比OAuth不太安全。但是它们更简单,这也是其吸引力的一部分。是否值得进行这种权衡取决于您的需求以及应用程序的部署方式。我的特定应用程序是一个内部应用程序,不接受来自外部世界的连接,因此在我的情况下这种权衡是值得的。但是,例如,我不会将API密钥部署到移动应用程序作为唯一的安全机制,因为应用程序的任何用户都可以获得该API密钥。 - matt forsythe
1
@mattforsythe 您是正确的,但 API 密钥通常是用于私有环境中。理论上,在移动应用程序中使用它,您需要在后端创建某种代理。 - Wanny Miarelli
1
@WannyMiarelli,对的。如我在评论中所提到的那样,我的应用程序是一种私有的、内部的应用程序,不接受来自外部互联网的连接。这就是为什么它在我这种情况下非常理想的原因。我想我们说的是同一件事,对吧? - matt forsythe
明白了。将AuthorityUtils.NO_AUTHORITIES更改为您想要赋予API密钥的任何角色。 - daydr3amer
2
@MuhammadAbuBakr,“@Transient”指示Spring不为此请求创建HttpSession。在使用API密钥的无状态请求的情况下,您不需要创建HttpSession,如果客户端忽略从服务器返回的cookie,则每个请求都将在服务器上创建全新的HttpSession。如果您需要在请求之间存储状态,并且您的客户端保存并遵守cookie,请随意删除它。 - matt forsythe
显示剩余4条评论

12
继@MarkOfHall的回答之后,WebSecurityConfigurerAdapter已被弃用(请参见https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter)。因此,他的APISecurityConfig版本现在将如下所示:
package com.fasset.ledger.auth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig {

@Value("${yourapp.http.auth-token-header-name}")
private String principalRequestHeader;

@Value("${yourapp.http.auth-token}")
private String principalRequestValue;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ApiKeyAuthFilter filter = new ApiKeyAuthFilter(principalRequestHeader);
    filter.setAuthenticationManager(new AuthenticationManager() {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String principal = (String) authentication.getPrincipal();
            if (!principalRequestValue.equals(principal))
            {
                throw new BadCredentialsException("The API key was not found or not the expected value.");
            }
            authentication.setAuthenticated(true);
            return authentication;
        }
    });
    http.antMatcher("/api/**").
            csrf().disable().
            sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
            and().addFilter(filter).authorizeRequests().anyRequest().authenticated();

    return http.build();
  }
}

9

@MarkOfHall的回答是正确的,我只想再添加一些细节。在您拥有代码之后,您需要将属性值添加到application.properties文件中,如下所示:

yourapp.http.auth-token-header-name=X-API-KEY
yourapp.http.auth-token=abc123

在 Postman 中设置身份验证值,步骤如下:

enter image description here

你可以使用Postman,但如果你使用请求,它会类似于以下提供的请求:
$ curl -H "X-API-KEY: abc123" "http://localhost:8080/api/v1/property/1"

除非提供正确的键和值,否则该应用程序将无法正常工作。

6

在 @zawar 和 @MarkOfHall 的回答以及来自 https://github.com/gregwhitaker/springboot-apikey-example 的资料基础上,截至2022年12月8日,一种现代的解决方案如下:

package com.mygloriousapp.auth;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

/**
 * Filter responsible for getting the api key off of incoming requests that need to be authorized.
 */
public class ApiKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

  private final String headerName;

  public ApiKeyAuthFilter(final String headerName) {
    this.headerName = headerName;
  }

  @Override
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    return request.getHeader(headerName);
  }

  @Override
  protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
    // No credentials when using API key
    return null;
  }
}




package com.mygloriousapp.config;

import com.mygloriousapp.auth.ApiKeyAuthFilter;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig {

  @Value("${app.http.auth-token-header-name}")
  private String principalRequestHeader;

  @Value("${app.http.auth-token}")
  private String principalRequestValue;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ApiKeyAuthFilter filter = new ApiKeyAuthFilter(principalRequestHeader);
    filter.setAuthenticationManager(
        authentication -> {
          String principal = (String) authentication.getPrincipal();
          if (!Objects.equals(principalRequestValue, principal)) {
            throw new BadCredentialsException(
                "The API key was not found or not the expected value.");
          }
          authentication.setAuthenticated(true);
          return authentication;
        });
    http.antMatcher("/**")
        .csrf()
        .disable()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .addFilter(filter)
        .authorizeRequests()
        .anyRequest()
        .authenticated();

    return http.build();
  }
}

在 application.properties 中需要进行必要的配置:

app.http.auth-token-header-name=X-API-Key
app.http.auth-token=109353c6-6432-4acf-8e77-ef842f64a664

在pom.xml中的依赖项:

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

如果您正在使用Postman,请单击集合并编辑授权选项卡:Postman autorization

3
感谢您更新并将这些内容整合到实际的代码风格中。 - Marquee
截至2023年8月,HttpServletRequest的依赖关系为import jakarta.servlet.http.HttpServletRequest; - M Smith
https://stackoverflow.com/questions/74609057/how-to-fix-spring-authorizerequests-is-deprecated - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接