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:
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:
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:
# 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 :
security . basic . enabled = false
hth