欢迎来到:http://observer.blog.51cto.com
webservcie是可以跨语言跨平台开发的一种技术,各种计算机语言都可以搭建服务器,同时各种计算机语言也可以开发客户端。只要有服务器,不管是用java还是C++抑或是php搭建的,其他语言都可以根据其开放的wsdl开发好客户端,然后调用其方法就像调用本地本项目的方法一样。
本文使用httpclient开发客户端,服务器例子依赖于文章:http://observer.blog.51cto.com/4267416/1231205,如果有不明白的可以先看这些文章再看本文。本文在原来例子的基础上加上安全验证与session的使用与权限管理。
webservice的安全验证有很多方法,实现soapheader认证、集成windows身份验证、ip绑定验证等等。在此博主觉得,基础的,往往却是最有效的。本文使用过滤器验证登录身份,然后就像平时web登录后一样,在session中存放信息达到保持登录与权限的管理。
第一:编写webservice客户端模板
首先咱们使用httpclient写一个能够访问webservice的客户端,详情可下载源码查看。
在此,因为wsdl、url、encoding还有就是返回的字段名在后面修改的可能性很大,所以使用配置文件存放他们的信息。
webservicetest.properties:
webservice.wsdl=http://localhost:8080/webServiceCXF/services/Service?wsdl webservice.url=http://www.observer.com/service webservice.encoding=utf-8 webservice.fieldname=result
然后包装一个类读取他们:com.observer.webservcie.util.ServicePropertiesUtil详情可以下载代码查看,咱们说正点。
在我的上一篇文章:http://observer.blog.51cto.com/4267416/1244274中,介绍了js的webservice客户端的相关内容,其中XMLHttpRequest所起到的作用其实就相当于浏览器访问一个web服务器的作用一样,只不过它访问的时候向服务器推送了一段xml,并且访问的是webservice的url而已。
既然知道了这个原理,那么跟他一样在java中能够起到浏览器作用的httpclient能不能访问webservice的url进而访问webservice的方法呢?咱们做一下尝试,拭目以待。
因为后面加上session验证之后,客户端封装就要有所变化,所以在此先将客户端模板写出来,其中发送的xml模板完全可以使用soupui自动生成,soupri的使用参看介绍js的那篇文章。
TestWebServiceClient:
public class TestWebServiceClient { public static void main(String[] args) throws FormatException { ServicePropertiesUtil servicePropertiesUtil = new ServicePropertiesUtil("webservicetest"); //获取读取配置文件的ServicePropertiesUtil类 StringBuffer sb = new StringBuffer(); sb.append("<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:ser='"+ servicePropertiesUtil.getWebServiceUrl()+"'>"); sb.append("<soapenv:Header/>"); sb.append("<soapenv:Body>"); sb.append("<ser:getGradeName>"); sb.append("<toid>"); sb.append(123); sb.append("</toid>"); sb.append("</ser:getGradeName>"); sb.append("</soapenv:Body>"); sb.append("</soapenv:Envelope>"); //访问webservcie时需要用到的xml HttpClient httpClient = new HttpClient();//新建一个HttpClient类 PostMethod postMethod=new PostMethod(servicePropertiesUtil.getWebServiceWsdl()); postMethod.setRequestHeader("SOAPAction","http://dao.wfservice.ws.emolay.com"); String html = null; try { String out = sb.toString(); postMethod.setRequestEntity(new StringRequestEntity(out, "text/xml", servicePropertiesUtil.getWebServiceEncoding())); //设置需要推送的数据 httpClient.executeMethod(postMethod);//访问webservcie html=postMethod.getResponseBodyAsString();//获取访问webserivce反馈的xml postMethod.releaseConnection(); } catch (HttpException e) { e.printStackTrace(); throw new FormatException("连接异常1", "服务器正在维护中,请稍后再试,或者联系服务器维护人员进行处理"); } catch (IOException e) { throw new FormatException("连接异常2", "服务器正在维护中,请稍后再试,或者联系服务器维护人员进行处理"); } Document doc=Jsoup.parse(html);//使用jsoup解析xml,得到webservice返回的数据 Elements elements=doc.select(servicePropertiesUtil.getWebServiceFieldname()); //根据webservcie的配置,解析xml中的数据 String [] datas = new String[elements.size()]; int i = 0; for(Element ele:elements){ datas[i] = ele.text(); i++; } //输出数据 for(String d : datas){ System.out.println(d); } } }
技术上的东西在注释已经写的很清楚了,这里就不多说了,服务器使用依赖的服务器webServiceCXF,部署服务器到tomcat后,运行这里的main方法,的到结果:“is succeed”;证明客户端代码编写正确,同时也证明了,httpclient是可以访问webservice的。
第二:改造服务器
首先咱们将服务器改个名,改为:webServiceCXFSession
在此,也许会有人想,如果在webservice中定义一个登录的方法,webserivce访问这个方法的时候就必须不能被拦截,但是过滤器中是不可能知道你的webservice是要访问什么方法的,只有到了webservice解析完传过来的xml才能知道,这时候就矛盾了,怎么办呢?在此,博主起初是想在传过来的头文件中加上一个参数(如islogin),然后在过滤器中得到这个头文件信息,然后根据这个头文件信息进行判断是否放行。但是这时候又出现问题了,要是别人重写了客户端,把所有的方法传过来的这个islogin全部都改为true,其他都不变,那也可以不用验证就进入webservcie。最后,博主觉得,直接在过滤器中进行判断登录即可,而登录的用户名密码可以使用头文件的形式传过来。
好了,登录的问题解决了,使用过滤器登录了之后,我们就可以在islogin为false时判断session中是否已经登录了,session中也的确放进了用户信息,但是要怎样才能在webservice的方法中的到session中的信息,进而判断其权限呢?在此就要用到博主的上一篇文章中提到的一个类了,该类为:java.lang.ThreadLocal,简单的解释就是:每个不同线程访问的都是不同的副本(原理上是否这样暂且不管,只要知道达到了这个效果就成),详情可看API或上一篇文章:http://observer.blog.51cto.com/4267416/1263577;我们在访问webservice的过程其实就像访问servlet,以多线程的方式访问,但是一旦进到了服务器,那么访问过滤器与访问webservice的是同一个线程。那么好,只要我们在webservcie实现类中定义一个ThreadLocal,其中放着session,然后在webservcie进入与出来时把session放进与移除,那么在不同用户访问webservcie都可以得到不同的自己的session,然后在方法里面判断其权限即可,如下所示
GradeServiceImpl中:
private static final ThreadLocal<HttpSession> threadLocal = new ThreadLocal<HttpSession>(); public static void setHttpSession(HttpSession session){ threadLocal.set(session); } public static void removeSession(){ threadLocal.remove(); } /** * 验证权限,如果不是admin用户,返回状态码"-1"表示没有权限访问该方法 */ public String getGradeName(long toid) { HttpSession session = threadLocal.get(); String user = (String)session.getAttribute("user"); if("admin".equals(user)){ System.out.println("业务逻辑"); }else{ return "-1"; } return "is succeed"; }
然后在web.xml中添加过滤器:
<!-- webserivce过滤器 --> <filter> <filter-name>webserviceFilter</filter-name> <filter-class>com.observer.cxf.login.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>webserviceFilter</filter-name> <url-pattern>/services/*</url-pattern> </filter-mapping> <!-- webserivce过滤器 end -->
新建过滤器的实现类:com.observer.cxf.login.filter.LoginFilter:
public class LoginFilter implements Filter{ public void destroy() { } /** * 过滤器的过滤方法 * 当传过来的html头文件中有islogin并且为true时表示用户需要登录,那么就验证登录 * 当传过来的html头文件中有islogin并且为false时表示用户需要调用webservice, * 那么首先验证是否已经登录,如果已经登录就继续,如果没有登录就直接返回notlogin提示用户没有登录 */ public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)arg0; HttpServletResponse response = (HttpServletResponse)arg1; HttpSession session = request.getSession(); String islogin = request.getHeader("islogin"); if("false".equals(islogin)){//判断是要登录还是要访问webservice String user = (String) session.getAttribute("user"); if(user != null){//判断是否已经登录 GradeServiceImpl.setHttpSession(session); //访问webservice之前将session放到webservice实现类中 arg2.doFilter(request, response); GradeServiceImpl.removeSession(); //webservice访问完成后移除里面的session return; }else{ PrintWriter out = response.getWriter(); out.println("notlogin"); out.flush(); out.close(); } }else if("true".equals(islogin)){ String username = request.getHeader("username"); String password = request.getHeader("password"); //的到头文件中传过来的username与password String user = LoginManger.login(username, password); PrintWriter out = response.getWriter(); if(user!=null){ session.setAttribute("user", user); out.println("islogin"); }else{ out.println("notlogin"); } out.flush(); out.close(); } } public void init(FilterConfig arg0) throws ServletException { } }
以上代码中的LoginManger.login(username, password);是登录的业务逻辑管理类,一般要检查数据库,在此就不多做了,详情可下载源码看即可。
第三:封装客户端
服务器已经写好了,究竟能不能用,写好客户端就知道了。
在此,博主使用模板模式编写客户端,这样在我们的服务器修改了之后,只要大体上没改,我们就可以只修改实现类就可以了,当然,服务器方法添加了的话,抽象类还是要写的,一般情况下,服务器修改的就是一些配置文件上的东西,这时候,相应的xml就会有所改变,那么你就只要修改实现类就可以了。还有这里使用了可变长参数列表,这样如果服务器的webservice方法要是参数改变了的话,也只要修改实现类中的方法内容即可。
上代码:
抽象类com.observer.webservcie.httpclient.WebServiceClient:
public abstract class WebServiceClient { public static ServicePropertiesUtil servicePropertiesUtil = new ServicePropertiesUtil("webservice"); private static String tmpcookies=""; /** * 向webservcie发送请求 * @param outXml 请求所需要推送的xml数据 * @return 请求反馈的信息 * @throws FormatException 当webservice服务器关闭或链接不可以等原因造成请求失败时, * 抛出com.observer.webservcie.error.FormatException */ private String executeMethod(String outXml) throws FormatException{ HttpClient httpClient = new HttpClient(); PostMethod postMethod=new PostMethod(servicePropertiesUtil.getWebServiceWsdl()); postMethod.setRequestHeader("Cookie", tmpcookies); postMethod.setRequestHeader("islogin","false"); postMethod.setRequestHeader("SOAPAction","http://dao.wfservice.ws.emolay.com"); String html = null; try { String out = outXml; postMethod.setRequestEntity(new StringRequestEntity(out, "text/xml", servicePropertiesUtil.getWebServiceEncoding())); httpClient.executeMethod(postMethod); html=postMethod.getResponseBodyAsString(); } catch (HttpException e) { e.printStackTrace(); throw new FormatException("连接异常1", "服务器正在维护中,请稍后再试,或者联系服务器维护人员进行处理"); } catch (IOException e) { throw new FormatException("连接异常2", "服务器正在维护中,请稍后再试,或者联系服务器维护人员进行处理"); }finally{ postMethod.releaseConnection(); } return html; } /** * 向webservcie服务器发送登录请求 * @param username 用户名 * @param password 用户密码 * @return 登录成功后返回服务器反馈的信息:islogin * @throws FormatException 当webservice服务器关闭或链接不可以等原因造成请求失败或登录失败时, * 抛出com.observer.webservcie.error.FormatException */ public String login(String username, String password) throws FormatException{ HttpClient httpClient = new HttpClient(); PostMethod postMethod=new PostMethod(servicePropertiesUtil.getWebServiceWsdl()); postMethod.setRequestHeader("islogin","true"); postMethod.setRequestHeader("username",username); postMethod.setRequestHeader("password",password); String html = null; try { httpClient.executeMethod(postMethod); html=postMethod.getResponseBodyAsString(); if(html!=null && "notlogin".equals(html.trim())){ throw new FormatException("登录失败", "用户名密码错误"); } Cookie[] cookies = httpClient.getState().getCookies(); StringBuffer sb = new StringBuffer(); for (Cookie c : cookies) { sb.append(c.toString() + ";"); } synchronized(this){ tmpcookies = sb.toString(); } } catch (HttpException e) { e.printStackTrace(); throw new FormatException("连接异常1", "服务器正在维护中,请稍后再试,或者联系服务器维护人员进行处理"); } catch (IOException e) { throw new FormatException("连接异常2", "服务器正在维护中,请稍后再试,或者联系服务器维护人员进行处理"); }finally{ postMethod.releaseConnection(); } return html; } /** * 将服务器反馈回来的xml解析出结果来 * @param xml 服务器返回的xml * @return 解析出来的结果集 */ private String[] parseData(String xml){ Document doc=Jsoup.parse(xml); Elements elements=doc.select(servicePropertiesUtil.getWebServiceFieldname()); String [] datas = new String[elements.size()]; int i = 0; for(Element ele:elements){ datas[i] = ele.text(); i++; } return datas; } /** * 想webservice方式请求,并且解析webservice反馈的xml得到结果 * @param outXml 需要请求发送的xml * @return 返回webservice反馈的xml解析之后的结果 * @throws FormatException 当用户没有登录或者 * 服务器已经关闭时抛出com.observer.webservcie.error.FormatException */ public String[] send(String outXml) throws FormatException{ String xml = executeMethod(outXml); if("notlogin".equals(xml.trim())){ throw new FormatException("登陆异常", "对不起您没有登录"); } return parseData(xml); } /** * 获取webservice中getGradeName方法所需要的xml的抽象方法,该方法让子类重写, * 让其可以在服务器参数更改是灵活的更改 * @param data 获取webservice中getGradeName所需要的参数 * @return webservice中getGradeName方法所需要的xml * @throws FormatException 当参数不符合规则时 * 抛出com.observer.webservcie.error.FormatException */ public abstract String getGradeNameXml(String... data) throws FormatException; /** * 想webservice请求getGradeName方法的模板方法 * @param data 获取webservice中getGradeName所需要的参数 * @return 返回webservice的getGradeName方法返回的数据 * @throws FormatException 当连接失败或者没有登录或者没有那个权限进行访问该方法时 * 抛出com.observer.webservcie.error.FormatException */ public String[] getGradeName(String... data) throws FormatException{ String outXml = getGradeNameXml(data); String[] datas = send(outXml); if(datas.length>0 && "-1".equals(datas[0])){ throw new FormatException("操作异常", "对不起你没有权限进行此操作"); } return datas; } }
实现类:com.observer.webservcie.httpclient.impl.WebServiceClientImpl:
public class WebServiceClientImpl extends WebServiceClient { /** * 获取webservice中getGradeName方法所需要的xml的实现方法 * @param data 获取webservice中getGradeName所需要的参数 * @return webservice中getGradeName方法所需要的xml * @throws FormatException 当参数不符合规则时 * 抛出com.observer.webservcie.error.FormatException */ public String getGradeNameXml(String... toid) throws FormatException{ int id; try{ id = Integer.valueOf(toid[0]); }catch (Exception e) { throw new FormatException("参数错误","输入的参数必须为整数字符串"); } StringBuffer sb = new StringBuffer(); sb.append("<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:ser='"+ servicePropertiesUtil.getWebServiceUrl()+"'>"); sb.append("<soapenv:Header/>"); sb.append("<soapenv:Body>"); sb.append("<ser:getGradeName>"); sb.append("<toid>"); sb.append(id); sb.append("</toid>"); sb.append("</ser:getGradeName>"); sb.append("</soapenv:Body>"); sb.append("</soapenv:Envelope>"); return sb.toString(); } }
以上代码中,因为登录跟访问webservice是两个不同的与代码,所以写了两个连接webservice的方法,然后,因为不同的httpclient访问服务器都会产生不同的session,解决的办法就是将上一次访问的Cookie传到下一次访问的httpclient中,这样服务器就会认为你这一次访问的依然是同一个session;在此,用户登录过后,就会将cookie的有关信息存放在一个静态的变量(tmpcookies)中,然后如果下次要进行访问,就将这个变量的cookie放到httpclient的头文件中,让服务器知道你需要使用刚才的session进行访问。
第四:测试
好了,服务器跟客户端都已经编写完毕,该测试一下了,先登录后访问,不登录就访问等等:
public class TestClient { public static void main(String[] args) { WebServiceClient webServiceClient = new WebServiceClientImpl(); String[] datas1 = null; String[] datas2 = null; try { String login = webServiceClient.login("admin", "admin"); System.out.println(login); datas1 = webServiceClient.getGradeName("123"); datas2 = webServiceClient.getGradeName("2222"); } catch (FormatException e) { e.printStackTrace(); System.out.println(e.getDetails()); return; } for(String data : datas1){ System.out.println(data); } for(String data : datas2){ System.out.println(data); } } }
在此,博主测试是通过的,如果哪位朋友修改客户端测出了bug,可以给在下个信息,咱们看看能否解决。
第五:打包客户端
详细的打包客户端这里就不说了,因为在这一篇文章已经有了:http://observer.blog.51cto.com/4267416/1238972,不明白的可以参看它后面的打包测试。
在这里要说明的是,因为这里的客户端用到了第三方依赖包,所以,你可以直接将依赖包一起打进自己的jar中,但是如果这样做的话,当别人导进你的客户端的同时也导进了依赖包的话就会出现冲突,所以,一般是写一个说明文档,说明该客户端依赖于什么什么第三方的包,想要使用此客户端必须在项目下导进第三方的包,然后将所需要的包列出来,在自己压缩出来的压缩包中也像其他如ssh的框架一样将依赖包也一起放进去。
到了这里使用httpclient制作客户端与使用session进行用户的验证的方法就已经完成了,详细的可以下载附件代码查看。
在此,博主是在maven的服务器的项目下进行改造的,因为使用jar包实在是太大,上传实在太慢了,所以有不会用maven的朋友,实在是抱歉了,不过相信能够做java到这一步的,看着本文都能够自己写出项目了,把项目中的代码跟配置移植到普通web项目应该更是没有问题的。