使用 API 密钥和 secret 保护 Spring 启动 API

我希望保护 SpringBootAPI,以便只有拥有有效 API 密钥和机密的客户端才能访问它。但是,由于所有数据都是匿名的,所以在程序内没有身份验证(带有用户名和密码的标准登录)。所有我试图实现的是,所有的 API 请求只能用于特定的第三方前端。

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

你能告诉我怎样才能做到这一点吗? 谢谢!

99903 次浏览

Create a filter that grabs what ever header(s) you're using for authentication.

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";
}


}

Configure the filter in your Web Security config.

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();
}


}

I realize I am a little late to the game on this one, but I also managed to get API keys working with Spring Boot in tandem with user-name/password authentication. I wasn't crazy about the idea of using AbstractPreAuthenticatedProcessingFilter because in reading the JavaDoc, it seemed like a misuse of that particular class.

I ended up creating a new ApiKeyAuthenticationToken class along with a pretty simple raw servlet filter to accomplish this:

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;
}
}

And the filter

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;
}
}

All that is left at this point is to inject the filter at the proper location in the chain. In my case, I wanted API key authentication to be evaluated before any user-name / password authentication so that it could authenticate the request before the application tried to redirect to a login page:

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

One other thing I will say you should watch out for is that your API key authenticated requests don't create and abandon a bunch of HttpSessions on your server.

The answer from the @MarkOfHall is correct and I just want to add a little more details. After you have the code, you will need to add the property values to the application.properties file as below:

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

The set the authentication value in the Postman as below:

enter image description here

You can use Postman but if you use cURL request will be similar provided below:

$ curl -H "X-API-KEY: abc123" "http://localhost:8080/api/v1/property/1"

Unless if provide the correct key and value, the app will not work.

To build on the answer by @MarkOfHall, WebSecurityConfigurerAdapter has been deprecated (see https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter). So his version of APISecurityConfig will now look like:

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();
}

}

Building upon the answers of @zawar and @MarkOfHall, and from https://github.com/gregwhitaker/springboot-apikey-example

a contemporary solution as of December 8th 2022 would look like this:

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();
}
}

The required configuration in application.properties:

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

The dependeny in pom.xml:

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

If you are using Postman, click on the collection and edit the Authorization tab: Postman autorization