Spring Boot要求api密钥和x509,但不是对所有的端点都要求。[英] Spring Boot - require api key AND x509, but not for all endpoints

本文是小编为大家收集整理的关于Spring Boot要求api密钥和x509,但不是对所有的端点都要求。的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

Java 11,Spring Boot 2.1.3,Spring 5.1.5

我有一个弹簧启动项目,其中某些端点被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"}]

推荐答案

在您的要求中,由于没有角色(不同的客户的访问级别不同) userDetailservice不需要.
ApikeyFilter足够与X509和API键一起工作.

考虑APIKeyFilter扩展X509AuthenticationFilter,如果有没有有效证书的请求,则将断开过滤链,并且将发送403/Forbidden的错误响应.
如果证书有效,则将继续过滤链,并将进行身份验证.在验证我们拥有的时,只有两种来自身份验证对象的方法
getPrincipal() - header:"x-api-key"
getCredential() - certificate subject.主题是(email =,cn =,ou =,o =,l =,st =,c =)
(应将ApikeyFilter配置为返回主体和凭据对象)
您可以使用principal(您的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";
    }
}

下面给定反应在不同情况下.
案例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


案例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证书,则测试黑客

使用服务器证书(.crt)和 serverprivatekey(.pem file)以及如下给出的请求

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

也可以在Mozilla(用于自签名的证书)中进行验证,并且可以在Google Chrome(如果CA认证的SSL)中进行验证
给出的屏幕截图,在第一次访问

在此处输入图像说明

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

在此处输入图像说明

本文地址:https://www.itbaoku.cn/post/1937873.html

问题描述

Java 11, Spring Boot 2.1.3, Spring 5.1.5

I have a Spring Boot project in which certain endpoints are guarded by an API key. This works just fine at the moment with this code:

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

This successfully requires a header containing an API key, but only for endpoints in /v1/...

I have a new requirement to require certificate for authentication. I followed these guides to get X.509 authentication set-up in my project:

I am running into a few problems, however:

  1. Cert is ALWAYS required, not just for /v1/* endpoints
  2. API key filter no longer works

Here's my updated application.properties file:

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

And my updated SecurityJavaConfig class:

@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.");
                }
            }
        };
    }
}

I have a feeling that there's an issue with the order of my chain in httpSecurity methods, but I'm not sure what that is. Also, I tried adding the second configure() method ignoring the PUBLIC_URLS, but that did not help whatsoever. I also tried changing server.ssl.client-auth to want but it allows clients to connect to my /v1/* APIs with no cert at all.

Example output that should not require a cert:

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

Example output that should require a cert AND an 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"}]

推荐答案

In your requirement, as there is no ROLES(Different client's having deifferent access level) UserDetailService is not required.
APIKeyFilter is enough to work with X509 and API key.

Consider APIKeyFilter extends X509AuthenticationFilter, If there is a request without valid certificate then filter chain will be broken and error response of 403/Forbidden will be sent.
If certificate is valid then filter chain continues and authentication will be carried out. While validating what we have is only two methods from authentication object
getPrincipal() - header:"x-api-key"
getCredential() - certificate subject. Where subject is (EMAIL=, CN=, OU=, O=, L=, ST=, C=)
(APIKeyFilter should be configured to return principal and credential object)
You can use principal(Your API key) for validating api key sent by client. and
You can use credentials(certificate subject) as a enhancement to identify each client seperately and if required you can grant different authorities for different client.

Recalling your requirement
1. API V1 - Accessed only if Certificate and API key valid.
2. Other APIs - No restrictions

To achieve the above said requirement, necessary codes given below

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

Verifying API Response

https - used for data encryption (ssl certificate sent by server to client)
X509 - used for client identification (ssl certificates generated by using server ssl certificate but different for different clients)
API key - shared secret key for security check.

For verification purpose lets assume you have 3 versions as given below

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

Below given response in different cases.
CASE 1.a Version 1 with valid X509 and API key in header

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

Response

HTTP/1.1 200
HELLO Version 1


CASE 1.b Version 1 with X509 only(No API Key)

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

Response

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


Note:
In your case, there are two types of certificate
i. Client Certificate with X509
ii: If client not including certificate then for data exchange certificate used in server will be used i.e, certificate without X509

2. Version X without X509 and without API key in header.

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

If server certificate is self signed certificate(Certificate is invalid without CA i.e, Certification Authority)

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.


If server SSL certificate is valid(CA certified) then

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

HELLO Version 0.9

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

HELLO Version 0.8

Note: Testing Hack if you don't have CA certified SSL certificate in dev environment

Use the server certificate(.crt) and serverPrivateKey(.pem file) along with request as given below

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

This can also be verified in Mozilla(for self signed certificate) and can be verified the same in google chrome(if CA certified SSL)
Screen shot given, During first time access

enter image description here

After adding certificate sent by server.

enter image description here