当前位置: 首页 > 知识库问答 >
问题:

Spring Boot-需要api密钥和x509,但不是所有endpoint都需要

朱昊乾
2023-03-14

Java 11、Spring Boot 2.1.3、Spring 5.1.5

我有一个Spring Boot项目,其中某些endpoint由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/…中的endpoint

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

  • Baeldung
  • DZone
  • 协中心

然而,我遇到了一些问题:

    证书始终是必需的,而不仅仅是/v1/*endpoint
  1. API密钥过滤器不再工作

这是我更新的应用程序。属性文件:

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_url,但没有任何帮助。我还试着改变服务器。ssl。客户端身份验证想要的,但它允许客户端连接到我的/v1/*API,而无需任何证书。

不需要证书的输出示例:

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

需要证书和api密钥的输出示例:

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

共有1个答案

阎淮晨
2023-03-14

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

考虑APIKeyFilter扩展了X509身份验证过滤器,如果有一个没有有效证书的请求,那么过滤链将被破坏,并将发送403/禁止的错误响应。
如果证书是有效的,那么过滤链将继续并进行身份验证。而验证我们所拥有的只是来自身份验证对象的两种方法。如果主题是(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();
    }
}

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

出于验证目的,假设您有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,标题中有有效的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
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"}

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.

你好0.9版

你好0.8版

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

curl -ik --cert server.crt --key serverPrivateKey.pem "https://localhost:8443/v0.9/hello"

这也可以在Mozilla中进行验证(对于自签名证书),也可以在google chrome中进行验证(如果是CA认证的SSL)
第一次访问时给出的屏幕截图

添加服务器发送的证书后

 类似资料:
  • 问题内容: 以下问题与我之前问过的一个问题有关:帮助解析简单的JSON(将JSON用于JAVA ME) JSON密钥需要唯一吗?例如,我在解析以下XML(使用JSON ME)时遇到了麻烦: 并且,显然是因为密钥必须唯一。我只是想知道在所有情况下是否都是这样。例如,如果我使用的不是JSON ME,我是否可以解析所有这些名称? 谢谢。 问题答案: 如果你使用多个具有相同名称的密钥,则不会出现“错误”,

  • 我和JWT有一些安全问题。事情是这样的: 首先是创建一个只有一个密钥的令牌,并使用它来注册和验证令牌-(这是许多开发使用的规则)。 第二种是使用私钥和带有私钥的公钥进行注册,创建新的令牌,并且只使用公钥进行令牌验证。点击这里查看第二条规则中的图像 所以我的问题是哪两种方式更安全?第二条安全规则真的必要吗?谢谢

  • 我正在从https://docs.AWS.amazon.com/cli/latest/reference/kms/encrypt.html和https://docs.AWS.amazon.com/cli/latest/reference/kms/decrypt.html读取AWS加密cli文档。我发现我可以在不创建数据密钥的情况下加密/解密。当我阅读https://docs.aws.amazon.

  • 我只想知道你们是怎么做到的。 以下是我所做的: > 我分别创建了ReactJs和Springboot API。 在开发环境中,我使用npm start运行ReactJS,Springboot只运行应用程序。 使用http://localhost:3000访问reactJs 使用axios从react with:http://localhost:8080访问springboot api 由于来自re

  • 问题内容: 我想从带有JDBC的Oracle DB表中获取DATETIME列。这是我的代码: 我必须先确定列类型。我感兴趣的字段被识别为Types.DATE,但实际上它是DB中的DATETIME,因为它具有以下格式:“ 07.05.2009 13:49:32” getDate截断时间:“ 07.05.2009”,getString追加“ .0”:“ 07.05.2009 13:49:32.0” 当

  • 问题内容: 我需要使用method,但是我的minSdk是21,所以它告诉我。有什么方法可以在较低的api上使用OffsetDateTime? 问题答案: Android上的API需要API 26。 对于较旧的API级别,可以使用ThreeTenABP,它是Java 6 的JSR-310 反向端口的Android版本。