一、前言
上一篇已經完整的講解了spring boot 與cxf整合輸出日誌的示例工程,並針對線上生成環境提出了加解密及驗籤等安全技術。
本文將帶領大家實現Spring Boot 整合CXF實現數字證書籤名、SSL會話功能。
專案中涉及數字證書的製作,相關示例請參考:
數字簽名簡介 https://www.toutiao.com/i6930517389587218957/
專案原始碼: https://github.com/ouyushan/spring-webservice-samples
spring-cxf參考原始碼: https://github.com/code-not-found/cxf-jaxws
spring-ws參考原始碼: https://github.com/code-not-found/spring-ws
二、開發環境IDE::IDEA
JDK:1.8
maven:3.6.2
spring boot:2.4.0
cxf:3.4.1
三、實現步驟具體程式碼實現步驟和上篇一樣。本章在之前基礎上講解新增內容。
3.1、實現服務端和客戶端新建服務端和客戶端spring-cxf-client-ssl、spring-cxf-server-ssl,並完成服務部署和客戶端訪問介面,具體步驟見上一篇。
3.2、新增SSL依賴spring boot與cxf整合後要進行SSL會話只需引入cxf-rt-ws-security,並實現相關配置即可。
服務端、客服端pom檔案中新增cxf依賴包
<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-ws-security</artifactId> <version>3.4.1</version></dependency>
3.3、服務端啟用SSL在服務端application.yml中啟用SSL,並指定證書資訊
server: port: 8080 # 開啟SSL ssl: enabled: true key-store: classpath:jks/webclient.p12 key-store-password: ouyushan.pass key-alias: webclient key-password: ouyushan.pass
3.4、客戶端配置SSL資訊
在客戶端application.yml中配置證書資訊,並在配置類中對SSLContext進行初始化配置
server: port: 7070 serviceUrl: https://localhost:8080/ws/userclient: keystore-alias: ouyushan.pass key-store: classpath:jks/webclient.p12 trust-store: classpath:jks/webclient.p12 store-password: ouyushan.pass
CxfClientConfig.java
package org.ouyushan.cxf.config;import org.apache.cxf.configuration.jsse.TLSClientParameters;import org.apache.cxf.configuration.security.FiltersType;import org.apache.cxf.endpoint.Client;import org.apache.cxf.ext.logging.LoggingInInterceptor;import org.apache.cxf.ext.logging.LoggingOutInterceptor;import org.apache.cxf.frontend.ClientProxy;import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;import org.apache.cxf.transport.http.HTTPConduit;import org.apache.cxf.transport.https.httpclient.DefaultHostnameVerifier;import org.ouyushan.cxf.ws.UserService;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.Resource;import sun.security.util.HostnameChecker;import javax.net.ssl.KeyManager;import javax.net.ssl.KeyManagerFactory;import javax.net.ssl.TrustManager;import javax.net.ssl.TrustManagerFactory;import java.io.IOException;import java.io.InputStream;import java.security.KeyStore;import java.security.KeyStoreException;import java.security.NoSuchAlgorithmException;import java.security.UnrecoverableKeyException;import java.security.cert.CertificateException;/** * @Description: * @Author: ouyushan * @Email: [email protected] * @Date: 2020/11/26 16:52 */@Configurationpublic class CxfClientConfig { /** * SSL配置引數 */ @Value("${client.key-store}") private Resource keyStoreResource; @Value("${client.trust-store}") private Resource trustStoreResource; @Value("${client.store-password}") private String storePassword; @Value("${server.serviceUrl}") private String serviceUrl; @Bean public UserService userService() throws Exception { JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean(); jaxWsProxyFactoryBean.setServiceClass(UserService.class); jaxWsProxyFactoryBean.setAddress(serviceUrl); // log the request and response messages jaxWsProxyFactoryBean.getInInterceptors() .add(loggingInInterceptor()); jaxWsProxyFactoryBean.getOutInterceptors() .add(loggingOutInterceptor()); UserService userService = (UserService) jaxWsProxyFactoryBean.create(); return userService; } @Bean public LoggingInInterceptor loggingInInterceptor() { return new LoggingInInterceptor(); } @Bean public LoggingOutInterceptor loggingOutInterceptor() { return new LoggingOutInterceptor(); } /** * SSL 相關配置 * * @return * @throws Exception */ @Bean public HTTPConduit httpConduit() throws Exception { Client client = ClientProxy.getClient(userService()); HTTPConduit httpConduit = (HTTPConduit) client.getConduit(); httpConduit.setTlsClientParameters(tlsClientParameters()); return httpConduit; } @Bean public TLSClientParameters tlsClientParameters() throws Exception { TLSClientParameters tlsClientParameters = new TLSClientParameters(); // 指定協議 tlsClientParameters.setSecureSocketProtocol("TLS"); // should NOT be used in production tlsClientParameters.setDisableCNCheck(true); // 設定信任證書(對方證書) tlsClientParameters.setTrustManagers(trustManagers()); // 設定keystore(自己證書) // tlsClientParameters.setKeyManagers(keyManagers()); tlsClientParameters.setCipherSuitesFilter(cipherSuitesFilter()); // 會覆蓋setDisableCNCheck 或自定義HostnameVerifier //tlsClientParameters.setHostnameVerifier(new DefaultHostnameVerifier()); // 根據證書資訊獲取指定的域名資訊,等效於setHostnameVerifier(new DefaultHostnameVerifier()); //tlsClientParameters.setUseHttpsURLConnectionDefaultHostnameVerifier(true); // 當證書資訊包含服務域名時可使用此配置 //tlsClientParameters.setUseHttpsURLConnectionDefaultSslSocketFactory(true); return tlsClientParameters; } @Bean public TrustManager[] trustManagers() throws NoSuchAlgorithmException, KeyStoreException, IOException { TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore()); return trustManagerFactory.getTrustManagers(); } /** * @return * @throws NoSuchAlgorithmException * @throws KeyStoreException * @throws CertificateException * @throws IOException * @throws UnrecoverableKeyException *//* @Bean public KeyManager[] keyManagers() throws NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException { KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore(), storePassword.toCharArray()); return keyManagerFactory.getKeyManagers(); }*/ @Bean public KeyStore trustStore() throws KeyStoreException, IOException { KeyStore trustStore = KeyStore.getInstance("PKCS12"); InputStream inputStream = trustStoreResource.getInputStream(); try { trustStore.load(inputStream, storePassword.toCharArray()); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } return trustStore; } @Bean public KeyStore keyStore() throws KeyStoreException, IOException { KeyStore trustStore = KeyStore.getInstance("PKCS12"); InputStream inputStream = keyStoreResource.getInputStream(); try { trustStore.load(inputStream, storePassword.toCharArray()); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } return trustStore; } @Bean public FiltersType cipherSuitesFilter() { FiltersType filter = new FiltersType(); filter.getInclude().add("TLS_ECDHE_RSA_.*"); filter.getInclude().add("TLS_DHE_RSA_.*"); return filter; }}
3.5、功能驗證分別啟動服務端和客戶端,訪問客戶端請求介面:
http://localhost:7070/user/get?userId=1
客戶端控制檯輸出如下:
2020-12-03 16:57:26.434 INFO 1500 --- [nio-7070-exec-2] o.a.cxf.services.UserService.REQ_OUT : REQ_OUT Address: https://localhost:8080/ws/user HttpMethod: POST Content-Type: text/xml ExchangeId: a2b51140-15a6-4982-a4e7-5375435ba482 ServiceName: UserServiceService PortName: UserServicePort PortTypeName: UserService Headers: {SOAPAction="", Accept=*/*} Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:getUserName xmlns:ns2="http://ws.cxf.ouyushan.org/"><userId>1</userId></ns2:getUserName></soap:Body></soap:Envelope>2020-12-03 16:57:26.488 INFO 1500 --- [nio-7070-exec-2] o.a.cxf.services.UserService.RESP_IN : RESP_IN Address: https://localhost:8080/ws/user Content-Type: text/xml;charset=UTF-8 ResponseCode: 200 ExchangeId: a2b51140-15a6-4982-a4e7-5375435ba482 ServiceName: UserServiceService PortName: UserServicePort PortTypeName: UserService Headers: {Keep-Alive=timeout=60, connection=keep-alive, content-type=text/xml;charset=UTF-8, Content-Length=220, Date=Thu, 03 Dec 2020 08:57:26 GMT} Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:getUserNameResponse xmlns:ns2="http://ws.cxf.ouyushan.org/"><return>tom</return></ns2:getUserNameResponse></soap:Body></soap:Envelope>
服務端控制檯輸出如下:
2020-12-03 16:57:26.482 INFO 9588 --- [nio-8080-exec-5] o.a.cxf.services.UserService.REQ_IN : REQ_IN Address: https://localhost:8080/ws/user HttpMethod: POST Content-Type: text/xml; charset=UTF-8 ExchangeId: 23f1bff5-af63-4346-88ef-d1d5834266e3 ServiceName: UserService PortName: UserServiceImplPort PortTypeName: UserService Headers: {SOAPAction="", Accept=*/*, host=localhost:8080, connection=keep-alive, content-type=text/xml; charset=UTF-8, cache-control=no-cache, Content-Length=202, pragma=no-cache, user-agent=Apache-CXF/3.4.1} Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:getUserName xmlns:ns2="http://ws.cxf.ouyushan.org/"><userId>1</userId></ns2:getUserName></soap:Body></soap:Envelope>2020-12-03 16:57:26.483 INFO 9588 --- [nio-8080-exec-5] o.a.cxf.services.UserService.RESP_OUT : RESP_OUT Address: https://localhost:8080/ws/user Content-Type: text/xml ResponseCode: 200 ExchangeId: 23f1bff5-af63-4346-88ef-d1d5834266e3 ServiceName: UserService PortName: UserServiceImplPort PortTypeName: UserService Headers: {} Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:getUserNameResponse xmlns:ns2="http://ws.cxf.ouyushan.org/"><return>tom</return></ns2:getUserNameResponse></soap:Body></soap:Envelope>
從日誌可以看出完整的請求鏈路資訊,且服務端地址已變為
https://localhost:8080/ws/user
四、總結透過本篇教材,您應該已掌握Spring Boot整合CXF實現SSL會話。整個過程可分為兩步:
服務端啟用SSL;客戶端配置SSL相關資訊,包括httpConduit和tlsClientParameters等。需要注意的是,自制證書由於沒有指定CN為域名或ip,在配置客戶端時需要注意以下事宜:
// 會覆蓋setDisableCNCheck 或自定義HostnameVerifier//tlsClientParameters.setHostnameVerifier(new DefaultHostnameVerifier());// 根據證書資訊獲取指定的域名資訊,等效於setHostnameVerifier(new DefaultHostnameVerifier());//tlsClientParameters.setUseHttpsURLConnectionDefaultHostnameVerifier(true);// 當證書資訊包含服務域名時可使用此配置//tlsClientParameters.setUseHttpsURLConnectionDefaultSslSocketFactory(true);
至此Spring Boot 整合CXF實現SSL會話已整合完成,證書的製作以及服務的配置文中已詳細講解。下一步是在此教材的基礎上實現簽名功能。