Написание интеграционных тестов для приложения Spring Boot. - Аутентификация MTLSJAVA

Программисты JAVA общаются здесь
Ответить
Anonymous
 Написание интеграционных тестов для приложения Spring Boot. - Аутентификация MTLS

Сообщение Anonymous »

У меня есть приложение Spring Boot, где я реализовал аутентификацию MTLS, включив следующие свойства: < /p>

Код: Выделить всё

server:
port: 8080
ssl:
enabled: true
client-auth: need
key-store: 'classpath:keystore.p12'
key-store-password: serverkeystore
key-store-type: PKCS12
trust-store:  'classpath:truststore.p12'
trust-store-type: PKCS12
trust-store-password: servertruststore
subject-validation:
enabled: true
allowed-list: test1.com,test.test1.com
cn-pattern: "CN=([^,]+)"
< /code>
Также я добавил следующую реализацию для сертификата CN и SAN Validation: < /p>
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@ConditionalOnProperty(
name = "server.ssl.subject-validation.enabled",
havingValue = "true"
)
@RequiredArgsConstructor
public class ClientCertificateValidationFilter implements Filter {

@Value("#{'${server.ssl.subject-validation.allowed-list:[]}'.split(',')}")
private final List clientCnOrSanAllowedList;
@Value("${server.ssl.subject-validation.cn-pattern:CN=([^,]+)}")
private String cnValidationPattern;
private Pattern cnPattern;

@SneakyThrows
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws PatternSyntaxException {

compileValidationPattern(this.cnValidationPattern);

X509Certificate[] x509Certificates = extractCertificateFromRequest(servletRequest);

boolean isClientSanOrCnMatched = verifyAllowedClients(x509Certificates);

if (isClientSanOrCnMatched) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Client Request: Not Allowed");
}
}

private void compileValidationPattern(String cnValidationPattern) {
try {
log.debug("[compileValidationPatterns()] Compiling validation pattern: \"{}\"", cnValidationPattern);
cnPattern = Pattern.compile(cnValidationPattern);
} catch (PatternSyntaxException exception) {
throw new PatternSyntaxException(exception.getDescription(), exception.getPattern(), exception.getIndex());
}
}

private X509Certificate[] extractCertificateFromRequest(ServletRequest servletRequest) {
log.debug("[extractCertificateFromRequest()] Extracting certificate from request");
HttpServletRequest httpServletRequest = ((HttpServletRequest) servletRequest);
return (X509Certificate[]) httpServletRequest.getAttribute("jakarta.servlet.request.X509Certificate");
}

private boolean verifyAllowedClients(X509Certificate[] x509Certificates) throws CertificateParsingException {
boolean isClientSanOrCnMatched = false;
if (Objects.nonNull(x509Certificates) && x509Certificates.length >  0) {
X509Certificate x509Certificate = x509Certificates[0];
isClientSanOrCnMatched = validateClientCn(x509Certificate);
if (!isClientSanOrCnMatched) {
isClientSanOrCnMatched = validateClientSan(x509Certificate);
}
}
return isClientSanOrCnMatched;
}

private boolean validateClientCn(X509Certificate x509Certificate) {
String subjectDN = x509Certificate.getSubjectX500Principal().getName();
log.debug("[validateClientCn()] Client Subject: {}", subjectDN);
Matcher commonNameMatcher = cnPattern.matcher(subjectDN);
if(commonNameMatcher.find()){
String clientCommonName = commonNameMatcher.group(1);
log.debug("[validateClientCn()] Client CN: {}", clientCommonName);
return clientCnOrSanAllowedList.stream().anyMatch(clientCommonName::equals);
}
return false;
}

private boolean validateClientSan(X509Certificate x509Certificate) throws CertificateParsingException {
if(Objects.nonNull(x509Certificate.getSubjectAlternativeNames())) {
log.debug("[validateClientSan()] Client SANs: {}", x509Certificate.getSubjectAlternativeNames());
return x509Certificate.getSubjectAlternativeNames().stream()
.anyMatch(sanList -> sanList.stream()
.anyMatch(clientCnOrSanAllowedList::equals));
}
return false;
}
}
Я пытаюсь создать интеграционные тесты, либо по mockmvc , либо testresttemplate , но, к сожалению, я не нашел ни одного примера в Интернете, который моделирует такой случай Полем Я некоторое время застрял с этим и ничего не достиг.

Код: Выделить всё

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ClientCertificateValidationFilterTests {

@LocalServerPort
private int port;

@Autowired
private TestRestTemplate restTemplate;

@BeforeEach
public void setup() throws Exception {
String TRUSTSTORE_PATH = "truststore.p12";
String KEYSTORE_PATH = "keystore.p12";
String KEYSTORE_PASSWORD = "clientkeystore";
String TRUSTSTORE_PASSWORD = "servertruststore";

SSLContext sslContext = createSSLContext(KEYSTORE_PATH, KEYSTORE_PASSWORD, TRUSTSTORE_PATH, TRUSTSTORE_PASSWORD);

SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);

HttpClientConnectionManager connectionManager =
PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(socketFactory).build();

CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);

RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder()
.requestFactory(() -> factory)
.rootUri("https://localhost:" + port);

this.restTemplate = new TestRestTemplate(restTemplateBuilder,
null,
null,
TestRestTemplate.HttpClientOption.SSL);
}

@Test
public void test() throws Exception {
String url = "https://localhost:" + port + "/api/test";

ResponseEntity  response = this.restTemplate.exchange(
url,
HttpMethod.GET,
null,
String.class
);
Assertions.assertEquals(200, response.getStatusCode().value());
}

private SSLContext createSSLContext(String keystorePath, String keystorePassword,
String truststorePath, String truststorePassword) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream(new ClassPathResource(keystorePath).getFile()), keystorePassword.toCharArray());

KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(new FileInputStream(new ClassPathResource(truststorePath).getFile()), truststorePassword.toCharArray());

return SSLContexts.custom()
.loadKeyMaterial(keyStore, keystorePassword.toCharArray())
.loadTrustMaterial(trustStore, null)
.build();
}
}
< /code>
Вот вывод консоли при запуске предыдущего теста: < /p>
org.springframework.web.client.ResourceAccessException: I/O error on HEAD request for "https://localhost:56110/test": Received fatal alert: bad_certificate

at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:915)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:895)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:790)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:672)
at org.springframework.boot.test.web.client.TestRestTemplate.exchange(TestRestTemplate.java:710)
at com.ahmed.demo.ClientCertificateValidationFilterTests.test(ClientCertificateValidationFilterTests.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert:  bad_certificate
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:130)
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:370)
at java.base/sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:287)
at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:209)
at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:172)
at java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1509)
at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1480)
at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1066)
at org.apache.hc.core5.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:149)
at org.apache.hc.core5.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280)
at org.apache.hc.core5.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:247)
at org.apache.hc.core5.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:54)
at org.apache.hc.core5.http.impl.io.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:304)
at org.apache.hc.core5.http.impl.io.HttpRequestExecutor.execute(HttpRequestExecutor.java:175)
at org.apache.hc.core5.http.impl.io.HttpRequestExecutor.execute(HttpRequestExecutor.java:218)
at org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager$InternalConnectionEndpoint.execute(PoolingHttpClientConnectionManager.java:712)
at org.apache.hc.client5.http.impl.classic.InternalExecRuntime.execute(InternalExecRuntime.java:216)
at org.apache.hc.client5.http.impl.classic.MainClientExec.execute(MainClientExec.java:116)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:188)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:192)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.HttpRequestRetryExec.execute(HttpRequestRetryExec.java:96)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:152)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:115)
at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:170)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:87)
at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:55)
at org.apache.hc.client5.http.classic.HttpClient.executeOpen(HttpClient.java:183)
at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:99)
at org.springframework.http.client.AbstractStreamingClientHttpRequest.executeInternal(AbstractStreamingClientHttpRequest.java:70)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:889)
... 7 more
Имейте в виду, что я прикрепляю правильный TrustStore.p12 , keystore.p12 , и я уверен, что они работают правильно, как они есть Уже протестировано на моем терминале с использованием команды Curl в запущенном приложении, и те же значения, используемые внутри Application.yaml , используются для Test/Application.yaml .

Я попробовал Mockmvc Но, к сожалению, не нашел способ настроить его на использование контекста SSL.

Подробнее здесь: https://stackoverflow.com/questions/793 ... entication
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «JAVA»