【问题标题】:Spring Boot - require api key AND x509, but not for all endpointsSpring Boot - 需要 api 密钥和 x509,但不适用于所有端点
【发布时间】:2026-01-13 19:50:01
【问题描述】:

Java 11、Spring Boot 2.1.3、Spring 5.1.5

我有一个 Spring Boot 项目,其中某些端点由 API 密钥保护。目前使用此代码可以正常工作:

@Component("securityConfig")
@ConfigurationProperties("project.security")
@EnableWebSecurity
@Order(1)
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {

    private static final Logger LOG = LoggerFactory.getLogger(SecurityJavaConfig.class);
    private static final String API_KEY_HEADER = "x-api-key";

    private String apiKey;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);
        filter.setAuthenticationManager(authentication -> {
            String apiKey = (String) authentication.getPrincipal();
            if (this.apiKey != null && !this.apiKey.isEmpty() && this.apiKey.equals(apiKey)) {
                authentication.setAuthenticated(true);
                return authentication;
            } else {
                throw new BadCredentialsException("Access Denied.");
            }

        });

        httpSecurity
            .antMatcher("/v1/**")
            .csrf()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilter(filter)
            .authorizeRequests()
            .anyRequest()
            .authenticated();
    }
}

这成功地需要一个包含 API 密钥的标头,但仅适用于 /v1/... 中的端点

我有一个新要求,需要证书进行身份验证。我按照这些指南在我的项目中设置了 X.509 身份验证:

不过,我遇到了一些问题:

  1. 始终需要证书,而不仅仅是 /v1/* 端点
  2. API 密钥过滤器不再起作用

这是我更新的application.properties 文件:

server.port=8443
server.ssl.enabled=true
server.ssl.key-store-type=PKCS12
server.ssl.key-store=classpath:cert/keyStore.p12
server.ssl.key-store-password=<redacted>

server.ssl.trust-store=classpath:cert/trustStore.jks
server.ssl.trust-store-password=<redacted>
server.ssl.trust-store-type=JKS
server.ssl.client-auth=need

还有我更新的SecurityJavaConfig 类:

@Component("securityConfig")
@ConfigurationProperties("project.security")
@EnableWebSecurity
@Order(1) //Safety first.
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {

    private static final Logger LOG = LoggerFactory.getLogger(SecurityJavaConfig.class);
    private static final String API_KEY_HEADER = "x-api-key";

    private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
        new AntPathRequestMatcher("/ping")
    );

    private String apiKey;

    @Value("#{'${project.security.x509clients}'.split(',')}")
    private List<String> x509clients;

    @Override
    public void configure(final WebSecurity web) {
        web.ignoring().requestMatchers(PUBLIC_URLS);
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);
        filter.setAuthenticationManager(authentication -> {
            String apiKey = (String) authentication.getPrincipal();
            if (this.apiKey != null && !this.apiKey.isEmpty() && this.apiKey.equals(apiKey)) {
                authentication.setAuthenticated(true);
                return authentication;
            } else {
                throw new BadCredentialsException("Access Denied.");
            }
        });

        httpSecurity
            .antMatcher("/v1/**")
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilter(filter)
            .authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .x509()
            .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
            .userDetailsService(userDetailsService())
            .and()
            .csrf()
            .disable();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) {
                if (x509clients.contains(username)) {
                    return new User(
                        username,
                        "",
                        AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")
                    );
                } else {
                    throw new UsernameNotFoundException("Access Denied.");
                }
            }
        };
    }
}

我感觉httpSecurity 方法中的链顺序存在问题,但我不确定那是什么。另外,我尝试添加第二个configure() 方法,忽略PUBLIC_URLS,但这没有任何帮助。我还尝试将server.ssl.client-auth 更改为want,但它允许客户端连接到我的/v1/* API,根本不需要证书。

不需要证书的示例输出:

$ curl -k -X GET https://localhost:8443/ping
curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate

需要证书和 api-key 的示例输出:

$ curl -k -X GET https://localhost:8443/v1/clients
curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate
$ curl -k -X GET https://localhost:8443/v1/clients --cert mycert.crt --key mypk.pem 
[{"clientId":1,"clientName":"Sample Client"}]

【问题讨论】:

  • 要支持两种身份验证,您需要创建自己的身份验证提供程序并自己处理。

标签: java spring-boot spring-security x509certificate api-key


【解决方案1】:

根据您的要求,因为没有 ROLES(不同的客户端具有不同的访问级别)不需要 UserDetailService
APIKeyFilter 足以与 X509 和API 密钥。

考虑APIKeyFilter扩展X509AuthenticationFilter,如果有一个没有有效证书的请求,那么过滤器链将被破坏并发送403/Forbidden的错误响应。
如果证书有效,则过滤器链继续并进行身份验证。在验证我们所拥有的只是来自身份验证对象的两种方法
getPrincipal() - header:"x-api-key"
getCredential() - certificate subject。主题是 (EMAIL=, CN=, OU=, O=, L=, ST=, C=)
(APIKeyFilter 应配置为返回主体和凭证对象)
您可以使用主体(您的 API 密钥)来验证客户端发送的 API 密钥。和
您可以使用凭据(证书主题)作为增强功能来分别识别每个客户,如果需要,您可以为不同的客户授予不同的权限。

回顾您的要求
1. API V1 - 仅在证书和 API 密钥有效时访问。
2. 其他 API - 无限制

为实现上述要求,下面给出必要的代码

public class APIKeyFilter extends X509AuthenticationFilter
{
    private String principalRequestHeader;

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

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

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request)
    {
        X509Certificate[] certs = (X509Certificate[]) request
                .getAttribute("javax.servlet.request.X509Certificate");

        if(certs.length > 0)
        {
            return certs[0].getSubjectDN();
        }

        return super.getPreAuthenticatedCredentials(request);
    }
}
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String API_KEY_HEADER = "x-api-key";

    private String apiKey = "SomeKey1234567890";

    @Override
    protected void configure(HttpSecurity http) throws Exception 
    {
        APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);
        filter.setAuthenticationManager(authentication -> {
            if(authentication.getPrincipal() == null) // required if you configure http
            {
                throw new BadCredentialsException("Access Denied.");
            }
            String apiKey = (String) authentication.getPrincipal();
            if (authentication.getPrincipal() != null && this.apiKey.equals(apiKey)) 
            {
                authentication.setAuthenticated(true);
                return authentication;
            }
            else
            {
                throw new BadCredentialsException("Access Denied.");
            }
        });

        http.antMatcher("/v1/**")
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                .addFilter(filter)
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }

    @Bean
    public PasswordEncoder passwordEncoder() 
    {
        return new BCryptPasswordEncoder();
    }
}

验证 API 响应

https - 用于数据加密(服务器向客户端发送 ssl 证书)
X509 - 用于客户端识别(使用服务器ssl证书生成的ssl证书,不同客户端不同)
API 密钥 - 用于安全检查的共享密钥。

出于验证目的,假设您有 3 个版本,如下所示

@RestController
public class HelloController
{
    @RequestMapping(path = "/v1/hello")
    public String helloV1()
    {
        return "HELLO Version 1";
    }

    @RequestMapping(path = "/v0.9/hello")
    public String helloV0Dot9()
    {
        return "HELLO Version 0.9";
    }

    @RequestMapping(path = "/v0.8/hello")
    public String helloV0Dot8()
    {
        return "HELLO Version 0.8";
    }
}

在不同情况下给出以下响应。
CASE 1.a 版本 1,标头中包含有效的 X509 和 API 密钥

curl -ik --cert pavel.crt --key myPrivateKey.pem -H "x-api-key:SomeKey1234567890" "https://localhost:8443/v1/hello"

回应

HTTP/1.1 200
HELLO Version 1


CASE 1.b 版本 1,仅限 X509(无 API 密钥)
curl -ik --cert pavel.crt --key myPrivateKey.pem "https://localhost:8443/v1/hello"

回应

HTTP/1.1 403
{"timestamp":"2019-09-13T11:53:29.269+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/v1/hello"}


注意:
在您的情况下,有两种类型的证书
一世。 X509 客户端证书
ii:如果客户端不包含证书,则将使用服务器中使用的数据交换证书,即没有 X509 的证书

2。版本 X 没有 X509 并且在标头中没有 API 密钥。

curl "https://localhost:8443/v0.9/hello"

如果服务器证书是自签名证书(没有 CA 即证书颁发机构,证书无效)

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.


如果服务器 SSL 证书有效(CA 认证)然后
curl "https://localhost:8443/v0.9/hello"

HELLO 0.9 版

curl "https://localhost:8443/v0.8/hello"

HELLO 0.8 版

注意:如果您在开发环境中没有 CA 认证的 SSL 证书,请测试 Hack

使用 server certificate(.crt)serverPrivateKey(.pem file) 以及下面给出的请求

curl -ik --cert server.crt --key serverPrivateKey.pem "https://localhost:8443/v0.9/hello"

这也可以在 Mozilla 中进行验证(对于自签名证书),并且可以在 google chrome 中进行相同的验证(如果 CA 认证 SSL)
屏幕截图,首次访问时

添加服务器发送的证书后。

【讨论】:

  • Praveen,非常感谢您非常详细的帖子,这正是我所需要的!谢谢!
  • 我的荣幸。事实上,尝试解决这个问题教会了我很多。在尝试此操作时,很难猜测出了什么问题。我在您的问题中喜欢的一件事是演示文稿,您的问题本身就有解决方案。我只是分析学习并发布。谢谢。
  • PraveenKumar - 很好的解决方案.. 我有类似的要求.. 1 - API V1 - 访问的证书有效或 API 密钥有效。仅当证书有效或 API 密钥存在时,才能访问 API。因此,sime 请求将具有证书,而某些请求将具有 API 密钥。我们如何处理这些案件?