Spring Boot - 需要 api 密钥和 x509,但不适用于所有端点 [英] Spring Boot - require api key AND x509, but not for all endpoints

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

问题描述

Java 11、Spring Boot 2.1.3、Spring 5.1.5

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

@Component("securityConfig")@ConfigurationProperties("project.security")@EnableWebSecurity@订单(1)公共类 SecurityJavaConfig 扩展了 WebSecurityConfigurerAdapter {私有静态最终记录器日志 = LoggerFactory.getLogger(SecurityJavaConfig.class);private static final String API_KEY_HEADER = "x-api-key";私有字符串 apiKey;@覆盖protected void configure(HttpSecurity httpSecurity) 抛出异常 {APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);filter.setAuthenticationManager(身份验证 -> {String apiKey = (String) authentication.getPrincipal();if (this.apiKey != null && !this.apiKey.isEmpty() && this.apiKey.equals(apiKey)) {authentication.setAuthenticated(true);返回认证;} 别的 {throw new BadCredentialsException("访问被拒绝.");}});http安全.antMatcher("/v1/**").csrf().禁用().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).和().addFilter(过滤器).authorizeRequests().anyRequest().authenticated();}}

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

中的端点

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

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

    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

    After adding certificate sent by server.

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

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