WebService et HTTPS
Par Cédric Tabin le mercredi 23.07.2008, 21:00 - Java - Lien permanent
J'ai tout récemment du effectuer un appel WebService depuis un client Java via HTTPS. Voici un petit tutoriel (client-side) pas à pas pour mettre en place une communication sécurisée avec un serveur cryptant les données via certificat et qui requiert l'authentification de l'utilisateur.
A la base, j'avais monté un appel WebService standard (il n'était pas sécurisé), lorsque du jour au lendemain, l'erreur suivante remonte :
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1591) at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:187) at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:181) at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:975) at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:123) at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:516) at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:454) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:884) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1096) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1123) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1107) at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:405) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:977) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:234) at java.net.URL.openStream(URL.java:1009) at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.createReader(RuntimeWSDLParser.java:785) at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.resolveWSDL(RuntimeWSDLParser.java:236) at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.parse(RuntimeWSDLParser.java:107) ... 11 more
Il se trouve que désormais, la communication doit être sécurisée (cryptée grâce à un certificat) et que l'utilisateur doit être authentifié.
Ma situation est telle que je n'ai pas d'accès direct au certificat du serveur. Je dois donc le récupérer dynamiquement. Pour ce faire, j'ai trouvé une petite classe java (en annexe) très pratique qui fonctionne à merveille ! Elle s'utilise comme ceci (dans un terminal) :
java InstallCert localhost:8181 monMotDePasse
Cela va automatiquement récupérer le certificat envoyé par le serveur (ici localhost sur le port 8181) et créer un keystore (jssecacerts) pour java avec le mot de passe 'monMotDePasse'.
Il suffit ensuite de copier ce fichier dans le projet (ou de mettre le dossier dans le classpath) et d'ajouter les lignes de code suivantes pour que Java puisse utiliser le certificat ainsi créé :
System.setProperty("javax.net.ssl.trustStore", "jssecacerts"); System.setProperty("javax.net.ssl.trustStorePassword", "monMotDePasse");
La mise en place de ce code permet de pouvoir communiquer avec le serveur de manière sécurisée.
Après réexecution, le serveur me lance encore l'erreur suivante (le client n'est pas authentifié) :
Caused by: java.io.IOException: Server returned HTTP response code: 401 for URL: https://localhost:8181/SNV3SRV-ws-war/WSEntryPointService?wsdl at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1241) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:234) at java.net.URL.openStream(URL.java:1009) at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.createReader(RuntimeWSDLParser.java:785) at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.resolveWSDL(RuntimeWSDLParser.java:236) at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.parse(RuntimeWSDLParser.java:107) ... 11 more
Pour résoudre cela, je suis tombé sur ce post qui explique simplement qu'il faut créer une classe personnalisée pour l'authentification :
public class BasicHTTPAuthenticator extends Authenticator { private String _userName; private String _password; public BasicHTTPAuthenticator(String userName, String password) { _userName = userName; _password = password; } @Override protected PasswordAuthentication getPasswordAuthentication() { if (this.getRequestingProtocol().equalsIgnoreCase("https")) return new PasswordAuthentication(_userName, _password.toCharArray()); else return null; } public String getUserName() { return _userName; } public void setUserName(String userName) { _userName = userName; } public String getPassword() { return _password; } public void setPassword(String password) { _password = password; } }
Et il ne reste plus qu'à la mettre par défaut pour que Java s'occupe du reste :
Authenticator.setDefault(new BasicHTTPAuthenticator("username", "password"));
Pour le fun, une dernière erreur :
Caused by: java.security.cert.CertificateException: No name matching localhost found at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:210) at sun.security.util.HostnameChecker.match(HostnameChecker.java:77) at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:264) at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:250) at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:954) ... 26 more
C'est simplement qu'aucun certificat ne correspond à l'url du serveur (localhost ici). Dans ce cas, il suffit de remplacer 'localhost' avec le nom de la machine (ou du serveur). Il est possible de le faire dynamiquement grâce au code suivant :
//try/catch... String hostName = InetAddress.getLocalHost().getHostName(); urlWebService = urlWebService.replace("localhost", hostName);
Et désormais tout (re)fonctionne ! Au final, pas grand-chose à ajouter, mais c'est assez compliqué à trouver si on part de zéro et que l'on ne sait pas trop où chercher.
Commentaires
A noter que le WebService est créé de la manière suivante :
Cela permet de cibler dynamiquement le serveur sur lequel la WSDL se trouve !