JAX-RS RESTful webservice 服務端及客戶端實現(基於HTTPS雙向認證)
在ApacheCXF的Sample裡以及網上很多有關RESTful HTTPS雙向認證的文章介紹僅僅是理論,沒有涉及實際環境的實現(客戶端和服務端都是localhost);這幾天使用Apache的CXF以及 Apache portable HttpClient實現跨IP的JAXRS HTTPS雙向認證實現。
在實踐中發現tomcat版本7.0.70和7.0.68在TLS/SSL支援上也存在差異。
一,嘗試成功的環境
1,JDK7u71(Server端&服務端);
2,Service端Tomcat7.0.68
3,ServiceSDK:apache-cxf-3.1.6
二,建立自簽名服務端和客戶端金鑰庫並都加入對方匯出的授權證書
1,金鑰及證書生成和匯入過程:
(Server端IP:192.168.245.133,Client端IP:192.168.245.1)
金鑰庫生成:keytool -genkeypair -validity 730 -alias serverkey -keystore serverkeystore.jks -dname "CN=192.168.245.133"
keytool -genkeypair -validity 730 -alias clientkey -keystore clientkeystore.jks -dname "CN=merrick"
客戶端金鑰庫匯入服務端證書:
keytool -export -rfc -keystore serverkeystore.jks -alias serverkey -file myserver.cer
keytool -import -noprompt -trustcacerts -file myserver.cer -alias serverkey -keystore clientkeystore.jks
服務端金鑰庫匯入客戶端證書:
keytool -export -rfc -keystore clientkeystore.jks -alias clientkey -file myclient.cer
keytool -import -noprompt -keystore serverkeystore.jks -trustcacerts -file myclient.cer -alias clientkey
三,建立JAX-RS CXF服務端(整合Spring)
1,建立Web專案,匯入必要的庫檔案:
commons-logging-1.0.3.jar
cxf-core-3.1.6.jar
cxf-rt-frontend-jaxrs-3.1.6.jar
cxf-rt-rs-json-basic-3.1.6.jar
cxf-rt-transports-http-3.1.6.jar
javax.annotation-api-1.2.jar
javax.servlet-api-3.1.0.jar
javax.ws.rs-api-2.0.1.jar
spring-aop-4.1.9.RELEASE.jar
spring-beans-4.1.9.RELEASE.jar
spring-context-4.1.9.RELEASE.jar
spring-core-4.1.9.RELEASE.jar
spring-expression-4.1.9.RELEASE.jar
spring-web-4.1.9.RELEASE.jar
woodstox-core-asl-4.4.1.jar
xmlschema-core-2.2.1.jar
2,配置web.xml
<span style="font-size:12px;"><?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>cxfjaxrsbasicrestserver1</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/beans.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>CXFServlet</servlet-name>
<servlet-class>
org.apache.cxf.transport.servlet.CXFServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/basiccxf/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app></span>
3,配置Spring的beans.xml(路徑如上述)
<span style="font-size:12px;"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:jaxrs="http://cxf.apache.org/jaxrs"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">
<import resource="classpath:META-INF/cxf/cxf.xml"/>
<import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
<bean id="gameinfoservice" class="cxf.rest.basic.server.GameInfoServiceImpl"/>
<jaxrs:server id="gameabc" address="/">
<jaxrs:serviceBeans>
<ref bean="gameinfoservice"/>
</jaxrs:serviceBeans>
</jaxrs:server>
</beans></span>
4,建立服務介面、服務實現類、XMLBean
<span style="font-size:12px;">package cxf.rest.basic.server;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/gameinfo/")
public interface GameInfoService {
@GET
@Path("/basicinfo/")
Response getBasicinfo(@Context HttpServletRequest req);
@GET
@Path("/basicinfo/{id}/")
@Produces(MediaType.APPLICATION_XML)
GameEntity getOneGameinfo(@PathParam(value = "id") int id);
@GET
@Path("/basicinfo2/{id}/")
Response getOneGameinfo2(@PathParam("id") String id);
@GET
@Path("/basicinfo3/")
Response getOneGameinfo3();
}
</span>
<span style="font-size:12px;">package cxf.rest.basic.server;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="EGame")
public class GameEntity {
private int id;
private String name;
private String publishdate;
private String producer;
private String type;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPublishdate() {
return publishdate;
}
public void setPublishdate(String publishdate) {
this.publishdate = publishdate;
}
public String getProducer() {
return producer;
}
public void setProducer(String producer) {
this.producer = producer;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
</span>
<span style="font-size:12px;">package cxf.rest.basic.server;
import java.util.Date;
import java.util.Hashtable;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
public class GameInfoServiceImpl implements GameInfoService {
Map<Integer,GameEntity> gms = new Hashtable<Integer,GameEntity>();
public GameInfoServiceImpl() {
System.out.println("-----GameInfoServiceImpl constructor ");
GameEntity et = new GameEntity();
et.setId(123);
et.setName("HEROS");
et.setProducer("3DO ltd.");
et.setPublishdate("1995-01-01");
et.setType("Stratigy 回合遊戲");
gms.put(123, et);
}
@Override
public Response getBasicinfo(HttpServletRequest req) {
System.out.println("-------Invoking "+ this.getClass().getName() + ".getBasicinfo(), "+ req.getServletContext().getServerInfo());
Response r = Response.ok(new String("123"),MediaType.TEXT_PLAIN).build();
return r;
}
@Override
public GameEntity getOneGameinfo(int id) {
System.out.println("-------Invoking "+ this.getClass().getName() + ".getOneGameinfo(), " + id);
System.out.println(new Date().toString());
GameEntity e = gms.get(123);
System.out.println("entity null? " + (e==null));
return e;
}
@Override
public Response getOneGameinfo2(String id) {
System.out.println("-------Invoking "+ this.getClass().getName() + ".getOneGameinfo2()");
Response r = Response.ok(gms.get(123),MediaType.APPLICATION_XML_TYPE).build();
return r;
}
@Override
public Response getOneGameinfo3() {
System.out.println("-------Invoking "+ this.getClass().getName() + ".getOneGameinfo3()");
GameEntity e = gms.get(123);
Response r = Response.ok(gms.get(123),MediaType.APPLICATION_JSON_TYPE).build();
//to be continued..............................//JSON
return r;
}
}
</span>
5,RuntimeServer Tomcat7.0.68的Server.xml中配置HTTPS有關的Connector
由於是雙向認證,服務端同樣需要驗證客戶端,所以clientAuth需要配置為true
<span style="font-size:12px;"><Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="conf/serverkeystore.jks" keystorePass="passwd"
truststoreFile="conf/serverkeystore.jks" truststorePass="passwd"
/></span>
四,建立Client端
僅列出無論localhost還是跨IP的情況下都成功的code:
public static void getEntity_51() throws Throwable{//SSL client get
/**
* Fail!: Javax.net.ssl.sslhandshakeexception:
* received fatal alert: handshake_failure(when Server tomcat7.0.70, Server JDK 7u9; Client JDK 7u71)
*
* Fail!: javax.net.ssl.SSLHandshakeException:
* Received fatal alert: handshake_failure(when Server tomcat7.0.70, Server JDK 7u71; Client JDK 7u71)
*
* Success !(server tomcat7.0.68, JDK 7u71; client JDK 7u71)
*
* tomcat7.0.70的ssl支援有問題
*
* */
DefaultHttpClient httpclient = getHTTPsClient("/clientkeystore.jks","passwd",8443);
HttpGet httpget = new HttpGet("https://192.168.245.133:8443/cxfjaxrsbasicrestserver1/basiccxf/gameinfo/basicinfo2/1234/");
// httpget.addHeader(new BasicHeader("Accept" , "text/xml"));
httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
@Override
public void process(HttpRequest arg0, HttpContext arg1) throws HttpException, IOException {
Header[] h = arg0.getAllHeaders();
for (int i = 0; i < h.length; i++) {
System.out.println("---request header: "+h[i]);
}
}
});
httpclient.addResponseInterceptor(new HttpResponseInterceptor() {
@Override
public void process(HttpResponse arg0, HttpContext arg1) throws HttpException, IOException {
Header[] h = arg0.getAllHeaders();
for (int i = 0; i < h.length; i++) {
System.out.println("reponse header: "+h[i]);
}
}
});
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
String rss = EntityUtils.toString(entity, "utf-8");
System.out.println(rss);//注意字元編碼
XMLparse.parsestringtoxml(rss);//Dom4j API, be used to parse response string to xml document object//DocumentHelper.parseText(s);
// entity.writeTo(System.out);
httpclient.getConnectionManager().shutdown();
httpclient.close();
}
public static DefaultHttpClient getHTTPsClient(String jksname, String password, int httpsserverport){//success
DefaultHttpClient httpclient = null;
try {
String keyStoreLoc = Client1.class.getClassLoader().getResource("").getPath() + jksname;
System.out.println("jks path: " + keyStoreLoc);
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream(keyStoreLoc), password.toCharArray());
SSLSocketFactory sf = new SSLSocketFactory(trustStore, password, trustStore);
Scheme httpsScheme = new Scheme("https", httpsserverport, sf);
httpclient = new DefaultHttpClient();
httpclient.getConnectionManager().getSchemeRegistry().register(httpsScheme);
} catch (Throwable e) {
e.printStackTrace(); }
return httpclient;
}
測試情況:
jks path: /D:/workspace_ElipseJEE_mars2/cxfjaxrsbasicrestclient1/bin//clientkeystore.jks
---request header: Host: 192.168.245.133:8443
---request header: Connection: Keep-Alive
---request header: User-Agent: Apache-HttpClient/4.5.2 (Java/1.7.0_71)
reponse header: Server: Apache-Coyote/1.1
reponse header: Date: Tue, 18 Oct 2016 05:58:41 GMT
reponse header: Content-Type: application/xml
reponse header: Content-Length: 200
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EGame><id>123</id><name>HEROS</name><producer>3DO ltd.</producer><publishdate>1995-01-01</publishdate><type>Stratigy 回合遊戲</type></EGame>
root name: EGame
Node : id ====> 123
Node : name ====> HEROS
Node : producer ====> 3DO ltd.
Node : publishdate ====> 1995-01-01
Node : type ====> Stratigy 回合遊戲