9

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-authwant但它允许客户端连接到我的/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"}]
4

1 回答 1

12

在您的要求中,因为没有角色(不同的客户端具有不同的访问级别),所以不需要 UserDetailService
APIKeyFilter 足以使用 X509 和 API 密钥。

考虑APIKeyFilterextends 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 key - 用于安全检查的共享密钥。

出于验证目的,假设您有 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


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


注意:
在您的情况下,有两种类型的证书
i。带有 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"

你好 0.9 版

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

你好 0.8 版

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

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

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

于 2019-09-13T16:34:59.137 回答