Spring 启动 - 需要 api 密钥和 x509,但并非对所有端点都适用
Spring Boot - require api key AND x509, but not for all endpoints
Java11,Spring启动 2.1.3,Spring5.1.5
我有一个 Spring 引导项目,其中某些端点由 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 密钥的 header,但仅适用于 /v1/...
中的端点
我有一个新要求需要证书进行身份验证。我按照这些指南在我的项目中获得 X.509 身份验证 set-up:
我 运行 遇到了一些问题,但是:
- 证书始终是必需的,不仅仅是
/v1/*
个端点
- 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
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.");
}
}
};
}
}
我感觉我的链在 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 密钥一起使用。
考虑 APIKeyFilter
扩展 X509AuthenticationFilter
,如果请求没有有效证书,则过滤器链将被破坏,并发送 403
/Forbidden
的错误响应.
如果证书有效,则过滤器链将继续并进行身份验证。虽然验证我们所拥有的只是来自身份验证的两种方法 object
getPrincipal()
- header:"x-api-key"
getCredential()
- certificate subject
。主题是 (EMAIL=, CN=, OU=, O=, L=, ST=, C=)
(APIKeyFilter 应配置为 return 主体和凭据 object)
您可以使用主体(您的 API 密钥)来验证客户端发送的 api 密钥。和
您可以使用凭据(证书主题)作为增强功能来分别识别每个客户端,如果需要,您可以为不同的客户端授予不同的权限。
回顾您的要求
1. API V1 - 仅在证书和 API 密钥有效时访问。
2. 其他APIs - 无限制
为了达到上述要求,下面给出了必要的代码
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";
}
}
下面给出了不同情况下的响应。
CASE 1.a 具有有效 X509 和 API 键的版本 1 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 密钥)
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 且没有 API 键的版本 X header.
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 中验证(对于自签名证书)并且可以在 google chrome(如果 CA 认证的 SSL)中进行相同的验证
已提供屏幕截图,首次访问期间
添加服务器发送的证书后。
Java11,Spring启动 2.1.3,Spring5.1.5
我有一个 Spring 引导项目,其中某些端点由 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 密钥的 header,但仅适用于 /v1/...
我有一个新要求需要证书进行身份验证。我按照这些指南在我的项目中获得 X.509 身份验证 set-up:
我 运行 遇到了一些问题,但是:
- 证书始终是必需的,不仅仅是
/v1/*
个端点 - 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
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.");
}
}
};
}
}
我感觉我的链在 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 密钥一起使用。
考虑 APIKeyFilter
扩展 X509AuthenticationFilter
,如果请求没有有效证书,则过滤器链将被破坏,并发送 403
/Forbidden
的错误响应.
如果证书有效,则过滤器链将继续并进行身份验证。虽然验证我们所拥有的只是来自身份验证的两种方法 object
getPrincipal()
- header:"x-api-key"
getCredential()
- certificate subject
。主题是 (EMAIL=, CN=, OU=, O=, L=, ST=, C=)
(APIKeyFilter 应配置为 return 主体和凭据 object)
您可以使用主体(您的 API 密钥)来验证客户端发送的 api 密钥。和
您可以使用凭据(证书主题)作为增强功能来分别识别每个客户端,如果需要,您可以为不同的客户端授予不同的权限。
回顾您的要求
1. API V1 - 仅在证书和 API 密钥有效时访问。
2. 其他APIs - 无限制
为了达到上述要求,下面给出了必要的代码
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";
}
}
下面给出了不同情况下的响应。
CASE 1.a 具有有效 X509 和 API 键的版本 1 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 密钥)
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 且没有 API 键的版本 X header.
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"
注意:如果您在开发环境中没有 CA 认证的 SSL 证书,请测试 HackHELLO Version 0.8
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)中进行相同的验证
已提供屏幕截图,首次访问期间
添加服务器发送的证书后。