Pre-Shared Keys sind mit Spring Boot eine Geschichte voller Missverständnisse. Hier jeweils meine:
HTTP/HTTPS
In der Theorie scheint die Verwendung von https “überflüssig”, denn wenn ich eh nur vorher als “vertrauenswürdig” zertifizierte Clients zulasse, warum dann noch die Verbindung absichern. Naheliegende Antwort: Austausch von Identitäten über http ist nie gut – ⚠
Also https in den application.properties (/application.yml) der Server-Anwendung einschalten:
1 2 3 4 |
server.port=8443 server.ssl.enabled=true server.ssl.client-auth=need server.ssl.protocol=TLS |
Zertifikatserstellung
Die eigentliche Idee ist jetzt: Jeder Client muss ein “trusted” Zertifikat vorweisen. Diese Zertifikate werden in einem “Trust-Store” abgelegt, den der Server bekommt. Der Client bekommt sein Zertifikat ebenfalls, dort in einem “Key-Store”, um sich auszuweisen. Andersrum analog: Das Serverzertifikat bekommt der Client in einem Trust-Store, um den https-Server zu verifizieren, der Server legt das in seinem Key-Store ab.
Insgesamt sieht die Erstellung so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Generate server key and self signed server certificate keytool -genkey -alias serverkey -keystore serverkeystore.p12 -keyalg RSA -storetype PKCS12 # Generate client key and self signed client certificate keytool -genkey -alias clientkey -keystore clientkeystore.p12 -keyalg RSA -storetype PKCS12 # Export the server certificate keytool -export -alias serverkey -file servercert.cer -keystore serverkeystore.p12 # Export the client certificate keytool -export -alias clientkey -file clientcert.cer -keystore clientkeystore.p12 # Import the server certificate into client truststore keytool -importcert -file servercert.cer -keystore clienttruststore.p12 -alias servercert # Import the client certificate into server truststore keytool -importcert -file clientcert.cer -keystore servertruststore.p12 -alias clientcert |
Wichtig dabei: Die verwendeten Passwörter (klar), aber auch der verwendete Name, beides brauchen wir unten.
PS, siehe unten und hier ⚠
Zertifikate einbinden
Die Zertifikate kann man dem Server jetzt über einen TomcatConnector bekannt machen, muss man in Spring Boot aber nicht (⚠; Danke, Nils). Stattdessen genügt Folgendes, ebenfalls in den application.properties:
1 2 3 4 5 6 7 8 |
server.ssl.key-store=classpath:certificates/serverkeystore.p12 server.ssl.key-password=123456 server.ssl.key-alias=serverkey server.ssl.key-store-password=123456 server.ssl.key-store-type=pkcs12 server.ssl.trust-store=classpath:certificates/servertruststore.p12 server.ssl.trust-store-password=123456 |
Hier verweise ich auf Key-Stores in der .jar, was seit Tomcat 7.0.66+ funktioniert. ⚠
In der Client-Anwendung sieht das etwas anders aus, zuerst die application.properties:
1 2 3 4 5 6 |
# custom keys, might be anything: http.client.ssl.key-store=classpath:certificates/clientkeystore.p12 http.client.ssl.trust-store=classpath:certificates/clienttruststore.p12 # I use the same pw for all key-stores: http.client.ssl.trust-store-password=123456 |
Verwendung dann beispielhaft in einem RestTemplate (org.apache.http.* gibt’s hier)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import org.apache.http.client.HttpClient; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Value("${http.client.ssl.key-store}") private Resource keyStore; @Value("${http.client.ssl.trust-store}") private Resource trustStore; @Value("${http.client.ssl.trust-store-password}") private String keyStorePassword; private RestTemplate getRestTemplate() { try { SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial( keyStore.getFile(), // pass twice, for key-store AND certificate: keyStorePassword.toCharArray(), keyStorePassword.toCharArray()) .loadTrustMaterial( trustStore.getURL(), keyStorePassword.toCharArray(), // use this for self-signed certificates only: new TrustSelfSignedStrategy()) .build(); HttpClient httpClient = HttpClients.custom() .setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier())) .build(); return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); } catch (IOException | NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException | CertificateException | KeyManagementException e) { throw new RuntimeException(e); } } // ... RestTemplate restTemplate = getRestTemplate(); String url = https://example.com:8443/some/where/{myParam} MyObject myObject = restTemplate.getForObject(url, MyObject.class, Maps.newHashMap("myParam", 42)); |
ACHTUNG: Der NoopHostnameVerifier war hier nötig, da ich das Zertifikat nicht mit expliziten Hostnames erstellt habe, in Production würde man das wohl tun. ⚠
Userverwaltung
Natürlich kann man mehrere Client-Zertifikate verwenden, und natürlich kann man jedem Client separate Rechte zuweisen. In der Server-Anwendung:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String USER1 = "USER_1"; private static final String USER2 = "USER_2"; // ... @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().hasAnyAuthority(USER1, USER2) .and().x509() // extracts the user field from the certificate; "CN" is "common name": .subjectPrincipalRegex("CN=(.*?)(?:,|$)") .userDetailsService(userDetailsService()); } @Bean public UserDetailsService userDetailsService() { return username -> { if (username.equals("Name from certificate")) { return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList(USER1)); } // else ... throw new SecurityException("user " + username + " is unknown"); }; } } |
(Quelle und Stack Overflow)
btw: Spring Security bringt einen default User mit (“user”, Passwort wird auf der Konsole beim Hochfahren ausgegeben). Deaktivierung in den application.properties:
1 |
security.basic.enabled=false |
hth