Spring Boot-需要api键和x509,但不是所有端点 [英] Spring Boot - require api key AND x509, but not for all endpoints

查看:109
本文介绍了Spring Boot-需要api键和x509,但不是所有端点的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Java 11,Spring Boot 2.1.3,Spring 5.1.5

Java 11, Spring Boot 2.1.3, Spring 5.1.5

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

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

这成功需要包含API密钥的标头,但仅适用于/v1/...

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

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

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

  • Baeldung
  • DZone
  • Codecentric

但是,我遇到了一些问题:

I am running into a few problems, however:

  1. 始终需要证书,而不仅仅是/v1/*个端点
  2. API密钥过滤器不再起作用
  1. Cert is ALWAYS required, not just for /v1/* endpoints
  2. API key filter no longer works

这是我更新的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.

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.

示例输出不需要证书:

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

示例输出需要证书和api键:

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

推荐答案

在您的要求中,由于没有ROLES(不同客户端的访问级别不同),因此不需要UserDetailService .
APIKeyFilter足够可以使用X509和API密钥.

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.

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

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.

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

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

验证API响应

https-用于数据加密(服务器发送到客户端的SSL证书)
X509-用于客户端标识(使用服务器ssl证书生成的ssl证书,但对于不同的客户端而言是不同的)
API密钥-用于安全检查的共享密钥.

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

在不同情况下给出以下响应.
案例1.a版本1,其中有效的X509和API密钥位于标头中

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"

响应

HTTP/1.1 200
HELLO Version 1


CASE 1.b版本1仅具有X509(无API密钥)


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

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


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.版本X,不含X509,标头中不含API密钥.

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

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

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.


如果服务器SSL证书有效(已通过CA认证),则


If server SSL certificate is valid(CA certified) then

HELLO版本0.9

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

HELLO版本0.8

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

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

使用服务器证书(.crt) serverPrivateKey(.pem文件)以及以下给出的请求

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"


这也可以在Mozilla中进行验证(用于自签名证书),也可以在google chrome中进行验证(如果经过CA认证的SSL)
给出了屏幕截图,首次访问期间


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

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

After adding certificate sent by server.

这篇关于Spring Boot-需要api键和x509,但不是所有端点的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆