Răsfoiți Sursa

Add an example and basic integration test for x509 authentication

[gh #5038]
Alexey Nesterov 6 ani în urmă
părinte
comite
a21fa1494a

+ 11 - 0
samples/boot/webflux-x509/spring-security-samples-boot-webflux-x509.gradle

@@ -0,0 +1,11 @@
+apply plugin: 'io.spring.convention.spring-sample-boot'
+
+dependencies {
+    compile project(':spring-security-core')
+    compile project(':spring-security-config')
+    compile project(':spring-security-web')
+    compile 'org.springframework.boot:spring-boot-starter-webflux'
+
+    testCompile project(':spring-security-test')
+    testCompile 'org.springframework.boot:spring-boot-starter-test'
+}

+ 40 - 0
samples/boot/webflux-x509/src/main/java/sample/MeController.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample;
+
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author Alexey Nesterov
+ * @since 5.2
+ */
+@RestController
+@RequestMapping("/me")
+public class MeController {
+
+	@GetMapping
+	public Mono<String> me() {
+		return ReactiveSecurityContextHolder.getContext()
+				.map(SecurityContext::getAuthentication)
+				.map(authentication -> "Hello, " + authentication.getName());
+	}
+}

+ 58 - 0
samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+
+/**
+ * @author Alexey Nesterov
+ * @since 5.2
+ */
+@SpringBootApplication
+public class WebfluxX509Application {
+
+	@Bean
+	public ReactiveUserDetailsService reactiveUserDetailsService() {
+		return new MapReactiveUserDetailsService(
+			User.withUsername("client").password("").authorities("ROLE_USER").build()
+		);
+	}
+
+	@Bean
+	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+		// @formatter:off
+		http
+			.x509()
+				.and()
+				.authorizeExchange()
+				.anyExchange().authenticated();
+		// @formatter:on
+
+		return http.build();
+	}
+
+	public static void main(String[] args) {
+		SpringApplication.run(WebfluxX509Application.class);
+	}
+}

+ 8 - 0
samples/boot/webflux-x509/src/main/resources/application.yml

@@ -0,0 +1,8 @@
+server:
+  port: 8443
+  ssl:
+    key-store: 'classpath:./certs/server.p12'
+    key-store-password: 'password'
+    client-auth: need
+    trust-store: 'classpath:./certs/server.p12'
+    trust-store-password: 'password'

+ 2 - 0
samples/boot/webflux-x509/src/main/resources/certs/curl_app.sh

@@ -0,0 +1,2 @@
+ curl -vvvv --cacert out/DevCA.crt --cert out/localhost.crt --key out/localhost.key https://localhost:8443/me
+

BIN
samples/boot/webflux-x509/src/main/resources/certs/server.p12


+ 90 - 0
samples/boot/webflux-x509/src/test/java/sample/WebfluxX509ApplicationTest.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sample;
+
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SslContextBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import reactor.netty.http.client.HttpClient;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class WebfluxX509ApplicationTest {
+
+	@LocalServerPort
+	int port;
+
+	@Test
+	public void shouldExtractAuthenticationFromCertificate() throws Exception {
+		WebTestClient webTestClient = createWebTestClientWithClientCertificate();
+		webTestClient
+				.get().uri("/me")
+				.exchange()
+				.expectStatus().isOk()
+				.expectBody()
+				.consumeWith(result -> {
+					String responseBody = new String(result.getResponseBody());
+					assertThat(responseBody).contains("Hello, client");
+				});
+	}
+
+	private WebTestClient createWebTestClientWithClientCertificate() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableEntryException {
+		ClassPathResource serverKeystore = new ClassPathResource("/certs/server.p12");
+
+		KeyStore keyStore = KeyStore.getInstance("PKCS12");
+		keyStore.load(serverKeystore.getInputStream(), "password".toCharArray());
+
+		X509Certificate devCA = (X509Certificate) keyStore.getCertificate("DevCA");
+
+		X509Certificate clientCrt = (X509Certificate) keyStore.getCertificate("client");
+		KeyStore.Entry keyStoreEntry = keyStore.getEntry("client",
+				new KeyStore.PasswordProtection("password".toCharArray()));
+		PrivateKey clientKey = ((KeyStore.PrivateKeyEntry) keyStoreEntry).getPrivateKey();
+
+		SslContextBuilder sslContextBuilder = SslContextBuilder
+				.forClient().clientAuth(ClientAuth.REQUIRE)
+				.trustManager(devCA)
+				.keyManager(clientKey, clientCrt);
+
+		HttpClient httpClient = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContextBuilder));
+		ClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpClient);
+
+		return WebTestClient
+				.bindToServer(httpConnector)
+				.baseUrl("https://localhost:" + port)
+				.build();
+	}
+}