java - Spring Boot - 需要 api key 和 x509,但不是所有端点

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

Java 11、Spring Boot 2.1.3、Spring 5.1.5

我有一个 Spring Boot 项目,其中某些端点由 API key 保护。目前使用这段代码效果很好:

@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 key 的 header ,但仅适用于 /v1/... 中的端点

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

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

  1. 证书始终是必需的,不仅仅是/v1/* 端点
  2. API key 过滤器不再有效

这是我更新的 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"}]

最佳答案

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

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

回顾你的要求
1. API V1 - 仅在证书和 API key 有效时访问。
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 - 用于安全检查的共享 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";
    }
}

下面给出了不同情况下的响应。
CASE 1.a header 中具有有效 X509 和 API key 的版本 1

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 Version 1 only with X509(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 的证书

<强>2。没有 X509 且 header 中没有 API key 的版本 X。

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 Version 0.9

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

HELLO Version 0.8

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

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 中验证(对于自签名证书)并且可以在谷歌浏览器中验证相同(如果 CA 认证的 SSL)
给出的屏幕截图,在第一次访问期间

enter image description here

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

enter image description here

关于java - Spring Boot - 需要 api key 和 x509,但不是所有端点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57858875/

相关文章:

spring-security - 从数据库检索的 HTML 显示不正确

java - 带有 try-catch-finally 的返回类型

java - 将日期字段与 jackson-dataformat-csv 一起使用时失败并出现 InvalidFormatException

java - 带有@SpringBootConfiguration 的 Spring Boot 多模块应用程序

spring - 为什么 Spring 在 Authentication 和 UserDetails 中有重复字段?

java - 向 Spring MVC session 添加新属性

java - 无法将自定义验证应用于 requestParam

java - XMl解析中的空指针异常

java - 在java中实例化泛型类型

java - 如何让 Spring 的 WebFlux 发出响应