CA认证——使用吉大正元认证系统为门户把关

最近使用JeeCMS的系统需要用到CA认证,之前很是苦逼了一段时间毕竟是第一次接触类似的东西,没有专业的工程师指导,只给了一个demo和几个文档,当时真的是无从下手。幸好通过两个熬夜,不断的尝试终于成功了,下面就把具体内容分享一下。

关于CA认证是什么东西,我想大多数人都知道的,网上银行使用的key宝或者U盾都属于此列,使用数字证书和密钥加固重要密码的认证流程。也就是说,我手里有个钥匙盘,插入钥匙盘,输入钥匙盘的密码,才能通过该认证,否则拒绝通行。具体原理大家可以去百度百科上去熟悉(http://baike.baidu.com/view/356572.htm),此处不再赘言。

CA认证首先要有认证服务器,服务器可以配置认证关键字(相当于应用ID),产生认证原文,进行密码验证,返回认证结果和证书信息。本文主要说明的是客户端的配置和服务器端的交互过程。

涉及到的文件:前台登陆界面,后台两个servlet。

所采用认证方式登陆,则进行以下流程:

前台代码如下:



random对应RandomServlet,代码如下:
public class RandomServlet extends HttpServlet {

	private static final long serialVersionUID = 3923090461076418525L;

	private String tempURL = null,propertiesURL = null;
	
	private Properties props = null;
	
	/** 认证地址 */
	private final String KEY_AUTHURL = "authURL";

	/** 应用标识 */
	private final String KEY_APP_ID = "appId";

	/**
	 * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
	 */
	public void init(ServletConfig cfg) throws ServletException {
		// 初始化程序跳转页面
		tempURL = cfg.getInitParameter("url");
		propertiesURL = cfg.getInitParameter("propertiesURL");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest
	 * , javax.servlet.http.HttpServletResponse)
	 */
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;
		//System.out.println("已进入RandomServlet!!!");
		// 设置页面不缓存
		response.setHeader("Pragma", "No-cache");
		response.setHeader("Cache-Control", "no-cache");
		response.setDateHeader("Expires", 0);
		// 初始化属性文件路径
//		String parentPath = request.getSession().getServletContext()
//				.getRealPath("/WEB-INF");
		// 产生认证原文
		String randNum = generateRandomNum();

		if (randNum == null || randNum.trim().equals("")) {
			System.out.println("证书认证数据不完整!");
			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			return;
		}

		/**************************
		 * 第三步 服务端返回认证原文   *
		 **************************/
		// 设置认证原文到session,用于程序向后传递,通讯报文中使用
		//System.out.println("设置认证原文到session开始");
		HttpSession session = request.getSession();
		session.setAttribute("original_data", randNum);
		//System.out.println("设置认证原文到session结束");
		// 设置认证原文到页面,给页面程序提供参数,用于产生认证请求数据包
		request.setAttribute("original", randNum);

		// 设置跳转页面
		//request.getRequestDispatcher(tempURL).forward(request, response);
		response.getWriter().write(randNum);
		return;
	}

	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		doGet(req, resp);
	}
	/**
	 * 产生认证原文
	 */
	private String generateRandomNum() {
		/**************************
		 * 第二步 服务端产生认证原文   *
		 **************************/
		String num = "1234567890abcdefghijklmnopqrstopqrstuvwxyz";
		int size = 10;
		char[] charArray = num.toCharArray();
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < size; i++) {
			sb.append(charArray[((int) (Math.random() * 10000) % charArray.length)]);
		}
		return sb.toString();
	}


	/**
	 * 获取文件中的属性值
	 */
	private String getProperties(String key) {
		return props.get(key) == null ? null : (String) props.get(key);
	}
}

auth对应AuthenServlet,代码如下:

/**
 * Copyright © 1999-2008 JIT Co,Ltd. 
 * All right reserved.
 */
package cn.com.jit.cinas;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.PostMethod;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;

import sun.misc.BASE64Encoder;

public class AuthenServlet extends HttpServlet {
	private static final long serialVersionUID = -1686835672374220173L;

	private String tempURL = null, propertiesURL = null;
	private Properties props = null;

	public void init(ServletConfig cfg) throws ServletException {
		tempURL = cfg.getInitParameter("url");
		propertiesURL = cfg.getInitParameter("propertiesURL");
	}

	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;
		response.setCharacterEncoding("UTF-8");
		/***************************************************************************
		 * isSuccess 认证是否成功,true成功/false失败;errCode 错误码;errDesc 错误描述 *
		 * ************************************************************************/
        //第四步:客户端认证
		//第五步:服务端验证认证原文
		//第六步:应用服务端认证
		//第七步:网关返回认证响应
		//第八步:服务端处理
		
		/***********************************
		 * 获取应用标识及网关认证地址 *
		 ***********************************/
		
		boolean isSuccess = true;
		String errCode = null, errDesc = null;

		// 初始化属性文件路径
		String parentPath = request.getSession().getServletContext()
				.getRealPath("/WEB-INF");

		// 初始化配置文件属性
		InputStream in = new FileInputStream(parentPath + propertiesURL);
		props = new Properties();
		props.load(in);


		// 可以根据需求使用不同的获取方法
		String appId = this.getProperties(KEY_APP_ID);
		String authURL = this.getProperties(KEY_AUTHURL);

		if (!isNotNull(appId) || !isNotNull(authURL)) {
			isSuccess = false;
			errDesc = "应用标识或网关认证地址不可为空";
			System.out.println("应用标识或网关认证地址不可为空\n");
		}

		String original_data = null, signed_data = null,original_jsp = null , username = null , password = null;
		/**************************
		 * 获取认证数据信息 *
		 **************************/
		if (isSuccess) {
			//System.out.println("应用标识及网关的认证地址读取成功!\n应用标识:" + appId + "\n认证地址:"+ authURL + "\n");
			//System.out.println("最先的原文ORIGINAL_DATA="+(String) request.getSession().getAttribute(KEY_ORIGINAL_DATA));
			//System.out.println("从客户端返回的原文="+(String) request.getParameter(KEY_ORIGINAL_JSP));
			//System.out.println("从客户的返回的认证报文="+(String) request.getParameter(KEY_SIGNED_DATA));
			if (isNotNull((String) request.getSession().getAttribute(KEY_ORIGINAL_DATA))
					&& isNotNull((String) request.getParameter(KEY_SIGNED_DATA))
					&&isNotNull((String) request.getParameter(KEY_ORIGINAL_JSP))) {
				// 获取session中的认证原文
				original_data = (String) request.getSession().getAttribute(KEY_ORIGINAL_DATA);
				// 获取request中的认证原文
				original_jsp = (String) request.getParameter(KEY_ORIGINAL_JSP);
				
				/**************************
				 * 第五步:服务端验证认证原文 *
				 **************************/
				if(!original_data.equalsIgnoreCase(original_jsp)){
					isSuccess = false;
					errDesc = "客户端提供的认证原文与服务端的不一致";
					System.out.println("客户端提供的认证原文与服务端的不一致!\n");
					
				}else{
					// 获取证书认证请求包
					signed_data = (String) request.getParameter(KEY_SIGNED_DATA);

					/* 随机密钥 */
					original_data = new BASE64Encoder().encode(original_jsp
							.getBytes());
					//System.out.println("读取认证原文和认证请求包成功!\n认证原文:" + original_jsp+ "\n认证请求包:" + signed_data + "\n");
				}
				

			} else {
				isSuccess = false;
				errDesc = "证书认证数据不完整";
				System.out.println("证书认证数据不完整!\n");
			}
		}
		

		
		/**************************
		 * 第六步:应用服务端认证 *
		 **************************/
		// 认证处理
		try {
			byte[] messagexml = null;
			if (isSuccess) {


				/*** 1 组装认证请求报文数据 ** 开始 **/
				Document reqDocument = DocumentHelper.createDocument();
				Element root = reqDocument.addElement(MSG_ROOT);
				Element requestHeadElement = root.addElement(MSG_HEAD);
				Element requestBodyElement = root.addElement(MSG_BODY);
				/* 组装报文头信息 */
				requestHeadElement.addElement(MSG_VSERSION).setText(
						MSG_VSERSION_VALUE);
				requestHeadElement.addElement(MSG_SERVICE_TYPE).setText(
						MSG_SERVICE_TYPE_VALUE);

				/* 组装报文体信息 */

				// 组装应用标识信息
				requestBodyElement.addElement(MSG_APPID).setText(appId);

				Element authenElement = requestBodyElement.addElement(MSG_AUTH);

				Element authCredentialElement = authenElement
						.addElement(MSG_AUTHCREDENTIAL);
				
				// 组装证书认证信息
				authCredentialElement.addAttribute(MSG_AUTH_MODE,
						MSG_AUTH_MODE_CERT_VALUE );
				authCredentialElement.addElement(MSG_DETACH).setText(
						signed_data);
				authCredentialElement.addElement(MSG_ORIGINAL).setText(
						original_data);
				
				// 组装口令认证信息
				//username = request.getParameter( "" );//获取认证页面传递过来的用户名/口令
				//password = request.getParameter( "" ); 
				//authCredentialElement.addAttribute(MSG_AUTH_MODE,MSG_AUTH_MODE_PASSWORD_VALUE );
				//authCredentialElement.addElement( MSG_USERNAME ).setText(username);
				//authCredentialElement.addElement( MSG_PASSWORD ).setText(password);

				// 组装属性查询列表信息
				Element attributesElement = requestBodyElement
						.addElement(MSG_ATTRIBUTES);

				attributesElement.addAttribute(MSG_ATTRIBUTE_TYPE,
						MSG_ATTRIBUTE_TYPE_PORTION);

				// TODO 取公共信息
				addAttribute(attributesElement, "X509Certificate.SubjectDN",
						"http://www.jit.com.cn/cinas/ias/ns/saml/saml11/X.509");
				addAttribute(attributesElement, "UMS.UserID",
						"http://www.jit.com.cn/ums/ns/user");

				/*** 1 组装认证请求报文数据 ** 完毕 **/

				StringBuffer reqMessageData = new StringBuffer();
				try {
					/*** 2 将认证请求报文写入输出流 ** 开始 **/
					ByteArrayOutputStream outStream = new ByteArrayOutputStream();
					XMLWriter writer = new XMLWriter(outStream);
					writer.write(reqDocument);
					messagexml = outStream.toByteArray();
					/*** 2 将认证请求报文写入输出流 ** 完毕 **/

					reqMessageData.append("请求内容开始!\n");
					reqMessageData.append(outStream.toString() + "\n");
					reqMessageData.append("请求内容结束!\n");
					//System.out.println(reqMessageData.toString() + "\n");
				} catch (Exception e) {
					isSuccess = false;
					errDesc = "组装请求时出现异常";
					System.out.println("组装请求时出现异常");
				}
			}

			/****************************************************************
			 * 创建与网关的HTTP连接,发送认证请求报文,并接收认证响应报文*
			 ****************************************************************/
			/*** 1 创建与网关的HTTP连接 ** 开始 **/
			System.out.println("/*** 1 创建与网关的HTTP连接 ** 开始 **/");
			int statusCode = 500;
			HttpClient httpClient = null;
			PostMethod postMethod = null;
			if (isSuccess) {
				// HTTPClient对象
				httpClient = new HttpClient();
				postMethod = new PostMethod(authURL);

				// 设置报文传送的编码格式
				postMethod.setRequestHeader("Content-Type",
						"text/xml;charset=UTF-8");
				/*** 2 设置发送认证请求内容 ** 开始 **/
				postMethod.setRequestBody(new ByteArrayInputStream(messagexml));
				/*** 2 设置发送认证请求内容 ** 结束 **/
				// 执行postMethod
				try {
					/*** 3 发送通讯报文与网关通讯 ** 开始 **/
				//	System.out.println("/*** 3 发送通讯报文与网关通讯 ** 开始 **/");
					statusCode = httpClient.executeMethod(postMethod);
				//	System.out.println("/*** 3 发送通讯报文与网关通讯 ** 结束 **/");
					/*** 3 发送通讯报文与网关通讯 ** 结束 **/
				} catch (Exception e) {
					isSuccess = false;
					errCode = String.valueOf(statusCode);
					errDesc = e.getMessage();
					System.out.println("与网关连接出现异常\n");
				}
			}
			/****************************************************************
			 * 	第七步:网关返回认证响应*
			 ****************************************************************/
			//System.out.println("第七步:网关返回认证响应*");
			StringBuffer respMessageData = new StringBuffer();
			String respMessageXml = null;
			if (isSuccess) {
				// 当返回200或500状态时处理业务逻辑
				if (statusCode == HttpStatus.SC_OK
						|| statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
					// 从头中取出转向的地址
					try {
						/*** 4 接收通讯报文并处理 ** 开始 **/
						System.out.println("/*** 4 接收通讯报文并处理 ** 开始 **/");
						byte[] inputstr = postMethod.getResponseBody();

						ByteArrayInputStream ByteinputStream = new ByteArrayInputStream(
								inputstr);
						ByteArrayOutputStream outStream = new ByteArrayOutputStream();
						int ch = 0;
						try {
							while ((ch = ByteinputStream.read()) != -1) {
								int upperCh = (char) ch;
								outStream.write(upperCh);
							}
						} catch (Exception e) {
							isSuccess = false;
							errDesc = e.getMessage();
						}

						if (isSuccess) {
							System.out.println("// 200 表示返回处理成功");
							// 200 表示返回处理成功
							if (statusCode == HttpStatus.SC_OK) {
								respMessageData.append("响应内容开始!\n");
								respMessageData.append(new String(outStream
										.toByteArray(), "UTF-8")
										+ "\n");
								respMessageData.append("响应内容开始!\n");
								respMessageXml = new String(outStream
										.toByteArray(), "UTF-8");
							} else {
								// 500 表示返回失败,发生异常
								respMessageData.append("响应500内容开始!\n");
								respMessageData.append(new String(outStream
										.toByteArray())
										+ "\n");
								respMessageData.append("响应500内容结束!\n");
								isSuccess = false;
								errCode = String.valueOf(statusCode);
								errDesc = new String(outStream.toByteArray());
							}
							//System.out.println(respMessageData.toString()+ "\n");
						}
						/*** 4 接收通讯报文并处理 ** 结束 **/
					} catch (IOException e) {
						isSuccess = false;
						errCode = String.valueOf(statusCode);
						errDesc = e.getMessage();
						//System.out.println("读取认证响应报文出现异常!");
					}
				}
			}

			/*** 1 创建与网关的HTTP连接 ** 结束 **/

			/**************************
			 *第八步:服务端处理 *
			 **************************/
			Document respDocument = null;
			Element headElement = null;
			Element bodyElement = null;
			if (isSuccess) {
				//把string转换为xml
				respDocument = DocumentHelper.parseText(respMessageXml);

				headElement = respDocument.getRootElement().element(MSG_HEAD);
				bodyElement = respDocument.getRootElement().element(MSG_BODY);

				/*** 1 解析报文头 ** 开始 **/
				if (headElement != null) {
					boolean state = Boolean.valueOf(
							headElement.elementTextTrim(MSG_MESSAGE_STATE))
							.booleanValue();
					if (state) {
						isSuccess = false;
						errCode = headElement.elementTextTrim(MSG_MESSAGE_CODE);
						errDesc = headElement.elementTextTrim(MSG_MESSAGE_DESC);
						//System.out.println("认证业务处理失败!\t" + errDesc + "\n");
					}
				}
			}

			if (isSuccess) {
				//System.out.println("解析报文头成功!\n");
				/* 解析报文体 */
				// 解析认证结果集
				Element authResult = bodyElement.element(MSG_AUTH_RESULT_SET)
						.element(MSG_AUTH_RESULT);

				isSuccess = Boolean.valueOf(
						authResult.attributeValue(MSG_SUCCESS)).booleanValue();
				if (!isSuccess) {
					errCode = authResult
							.elementTextTrim(MSG_AUTH_MESSSAGE_CODE);
					errDesc = authResult
							.elementTextTrim(MSG_AUTH_MESSSAGE_DESC);
					//System.out.println("身份认证失败,失败原因:" + errDesc);
				}
			}
			
			if (isSuccess) {
				//System.out.println("身份认证成功!\n");
				// 解析用户属性列表
				Element attrsElement = bodyElement.element(MSG_ATTRIBUTES);
				if (attrsElement != null) {
					List attributeNodeList = attrsElement
							.elements(MSG_ATTRIBUTE);
					for (int i = 0; i < attributeNodeList.size(); i++) {
						Element userAttrNode = (Element) attributeNodeList
								.get(i);
						String name = userAttrNode.attributeValue(MSG_NAME);
						String value = userAttrNode.getTextTrim();
						request.setAttribute(name, value);
						//System.out.println("属性名:" + name + "\t属性值:" + value
							//	+ "\n");
						//this.password = value;
						this.getUsername(value);
					}
				}
			}

		} catch (Exception e) {
			isSuccess = false;
			errDesc = e.getMessage();
		}

		if (!isSuccess) {
			if (isNotNull(errCode)) {
				request.setAttribute("errCode", errCode);
			}
			if (isNotNull(errDesc)) {
				request.setAttribute("errDesc", errDesc);
			}
			System.out.println("处理数据结束,业务处理失败,失败原因:" + errDesc + "\n");
		}else {
			System.out.println("处理数据结束,一切正常!\n");
		}
		
		
		request.setAttribute("isSuccess", new Boolean(isSuccess).toString());
		//request.getRequestDispatcher(tempURL).forward(request, response);
		if(isSuccess){			
			response.getWriter().write("success,"+this.username+","+this.realname);
		}else{
			response.getWriter().write("failure,"+errCode+","+errDesc);	
		}
	}

	private void getUsername(String value) {
		
		String[] users = value.split(",");
		
		String usercodeTemp= users[0].trim();
		String usernameTemp = users[1].trim();
		
		this.realname = usercodeTemp.substring(usercodeTemp.indexOf('=')+1, usercodeTemp.length());
		this.username = usernameTemp.substring(usernameTemp.indexOf('=')+1, usernameTemp.length());
	}

	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		doGet(req, resp);
	}

	/**
	 * 判断是否是空串
	 */
	private boolean isNotNull(String str) {
		if (str == null || str.trim().equals(""))
			return false;
		else
			return true;
	}

	/**
	 * 获取文件中的属性值
	 */
	private String getProperties(String key) {
		return props.get(key) == null ? null : (String) props.get(key);
	}

	/**
	 * 向xml插入结点
	 */
	private void addAttribute(Element attributesElement, String name,
			String namespace) {
		Element attr = attributesElement.addElement(MSG_ATTRIBUTE);
		attr.addAttribute(MSG_NAME, name);
		attr.addAttribute(MSG_NAMESPACE, namespace);
	}

	/******************************* 报文公共部分 ****************************/
	/** 报文根结点 */
	private final String MSG_ROOT = "message";

	/** 报文头结点 */
	private final String MSG_HEAD = "head";

	/** 报文体结点 */
	private final String MSG_BODY = "body";

	/** 服务版本号 */
	private final String MSG_VSERSION = "version";

	/** 服务版本值 */
	private final String MSG_VSERSION_VALUE = "1.0";

	/** 服务类型 */
	private final String MSG_SERVICE_TYPE = "serviceType";

	/** 服务类型值 */
	private final String MSG_SERVICE_TYPE_VALUE = "AuthenService";

	/** 报文体 认证方式 */
	private final String MSG_AUTH_MODE = "authMode";

	/** 报文体 证书认证方式 */
	private final String MSG_AUTH_MODE_CERT_VALUE = "cert";
	
	/** 报文体 口令认证方式 */
	private final String MSG_AUTH_MODE_PASSWORD_VALUE = "password";

	/** 报文体 属性集 */
	private final String MSG_ATTRIBUTES = "attributes";

	/** 报文体 属性 */
	private final String MSG_ATTRIBUTE = "attr";

	/** 报文体 属性名 */
	private final String MSG_NAME = "name";

	/** 报文体 属性空间 */
	private final String MSG_NAMESPACE = "namespace";
	/*********************************************************************/

	/******************************* 请求报文 ****************************/
	/** 报文体 应用ID */
	private final String MSG_APPID = "appId";

	/** 报文体 认证结点 */
	private final String MSG_AUTH = "authen";

	/** 报文体 认证凭据 */
	private final String MSG_AUTHCREDENTIAL = "authCredential";

	/** 报文体 detach认证请求包 */
	private final String MSG_DETACH = "detach";

	/** 报文体 原文 */
	private final String MSG_ORIGINAL = "original";
	
	/** 报文体 用户名 */
	private final String MSG_USERNAME = "username";
	
	/** 报文体 口令 */
	private final String MSG_PASSWORD = "password";

	/** 报文体 属性类型 */
	private final String MSG_ATTRIBUTE_TYPE = "attributeType";

	/** 指定属性 */
	private final String MSG_ATTRIBUTE_TYPE_PORTION = "portion";
	
	
	/*********************************************************************/

	/******************************* 响应报文 ****************************/
	/** 报文体 认证结果集状态 */
	private final String MSG_MESSAGE_STATE = "messageState";

	/** 响应报文消息码 */
	private final String MSG_MESSAGE_CODE = "messageCode";

	/** 响应报文消息描述 */
	private final String MSG_MESSAGE_DESC = "messageDesc";

	/** 报文体 认证结果集 */
	private final String MSG_AUTH_RESULT_SET = "authResultSet";

	/** 报文体 认证结果 */
	private final String MSG_AUTH_RESULT = "authResult";

	/** 报文体 认证结果状态 */
	private final String MSG_SUCCESS = "success";

	/** 报文体 认证错误码 */
	private final String MSG_AUTH_MESSSAGE_CODE = "authMessageCode";

	/** 报文体 认证错误描述 */
	private final String MSG_AUTH_MESSSAGE_DESC = "authMessageDesc";
	/*********************************************************************/
	
	/**应用数据库用户名**/
	private String username;
	/**应用数据库真实姓名**/
	private String realname;
	/**应用数据库密码**/
	private String password;
	/**************************** 业务处理常量 ****************************/
	/** 认证地址 */
	private final String KEY_AUTHURL = "authURL";

	/** 应用标识 */
	private final String KEY_APP_ID = "appId";

	/** session中原文 */
	private final String KEY_ORIGINAL_DATA = "original_data";
	
	/** 客户端返回的认证原文,request中原文 */
	private final String KEY_ORIGINAL_JSP = "original_jsp";

	/** 证书认证请求包 */
	private final String KEY_SIGNED_DATA = "signed_data";
	/*********************************************************************/
}

代码显得有些繁琐冗余,不花费一定量的时间是无法真正掌握的,这是吉大正元给的demo,没有经过大的改动直接拿来用了。需要注意以下几点:

1,需要在服务器上配置和应用相关的ID,用于识别不同的应用。

2,需要安装吉大正元自带的驱动程序,否则无法识别key盘,还要使用object标签加载一个动态链接库,如上:

    id="JITDSignOcx" width="0" codebase="/${res}/jit/JITDSign.cab#version=2,0,24,18">

为了加载一个类,JITDSign可以直接使用类里面的方法。

3,在进行ajax提交的时候注意保持同步,否则可能出现不按顺序执行的情况。

关于文档和demo可以到http://115.com/file/c2gqtrxh下载。

你可能感兴趣的:(我的技术博客)