7.7 实例:对称加密网络应用

加密技术与网络应用密不可分,在开放的网络中进行机密数据传输少不了使用加密算法。在实际应用中,我们常常需要向合作伙伴发送XML格式的数据包。例如,数据报表、货仓清单等。这些都是机密信息,如不对其加密必将公司机密泄露无疑。

本文将使用AES算法,并配合第6章提到的SHA算法构建简单的基于对称加密算法的数据传输网络应用—DataServer。

本实例将用到开源组件包Commons Codec来协助完成本实例的构建。请读者朋友将该相关jar包部署到该应用的/WEB-INF/lib目录下。

首先,我们来构建一个用于HTTP请求的工具类—HttpUtils。当然,读者朋友可以选用成熟的开源框架—Apache Commons HttpClient替代下述代码。相信大多数读者朋友对于如何构建HTTP请求都了如指掌,为避免拖沓作者在此不对该类做详细介绍,请读者朋友参考Java API相关内容。代码实现如代码清单7-11所示。

代码清单7-11 HttpUtils


import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.net.HttpURLConnection;

import java.net.URL;

import java.util.Map;

import java.util.Properties;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

/**

*Http工具

*@author梁栋

*@version 1.0

*@since 1.0

*/

public abstract class HttpUtils{

public static final String CHARACTER_ENCODING="UTF-8";

public static final String METHOD_POST="POST";

public static final String CONTENT_TYPE="Content-Type";

/**

*打印数据

*@param response HttpServletResponse

*@param data待打印的数据

*/

public static void responseWrite(HttpServletResponse response, byte[]data)

throws IOException{

if(data!=null){

response.setContentLength(data.length);

DataOutputStream out=new DataOutputStream(response

.getOutputStream());

out.write(data);

out.flush();

out.close();

}

}

/**

*从请求中读字节流

*@param request HttpServletRequest

*@return byte[]数据

*@throws IOException

*/

public static byte[]requestRead(HttpServletRequest request)

throws IOException{

int contentLength=request.getContentLength();

byte[]data=null;

if(contentLength>0){

data=new byte[contentLength];

InputStream is=request.getInputStream();

DataInputStream dis=new DataInputStream(is);

dis.readFully(data);

dis.close();

}

return data;

}

/**

*以POST方式向指定地址发送数据包请求,并取得返回的数据包

*@param urlString请求地址

*@param requestData请求数据

*@return byte[]数据包

*@throws IOException

*/

public static byte[]postRequest(String urlString, byte[]requestData)

throws Exception{

Properties requestProperties=new Properties();

requestProperties.setProperty(CONTENT_TYPE,"application/octet-stream;

charset="+CHARACTER_ENCODING);

return postRequest(urlString, requestData, requestProperties);

}

/**

*以POST方式向指定地址发送数据包请求,并取得返回的数据包

*@param urlString请求地址

*@param requestData请求数据

*@param requestProperties请求包体

*@return byte[]数据包

*@throws IOException

*/

public static byte[]postRequest(String urlString, byte[]requestData,

Properties requestProperties)throws Exception{

byte[]responseData=null;

HttpURLConnection con=null;

try{

URL url=new URL(urlString);

con=(HttpURLConnection)url.openConnection();

if((requestProperties!=null)&&(requestProperties.size()>0)){

for(Map.Entry<Object, Object>entry:requestProperties.entrySet()){

String key=String.valueOf(entry.getKey());

String value=String.valueOf(entry.getValue());

con.setRequestProperty(key, value);

}

}

con.setRequestMethod(METHOD_POST);

con.setDoOutput(true);

con.setDoInput(true);

DataOutputStream dos=new DataOutputStream(con.getOutputStream());

if(requestData!=null){

dos.write(requestData);

}

dos.flush();

dos.close();

DataInputStream dis=new DataInputStream(con.getInputStream());

int length=con.getContentLength();

if(length>0){

responseData=new byte[length];

dis.readFully(responseData);

}

dis.close();

}finally{

//关闭流

if(con!=null){

con.disconnect();

con=null;

}

}

return responseData;

}

}


接下来,我们将构建用于加密/解密的工具类—AESCoder。此处,我们将用到第三方开源组件包—Commons Codec。

对于AESCoder类,我们参考代码清单7-5稍作调整,对密钥稍作包装以方便在存储和使用,如代码清单7-12所示。

代码清单7-12 AESCoder—密钥封装


import org.apache.commons.codec.binary.Base64;

//省略

/**

*初始化密钥

*@return String Base64编码密钥

*@throws Exception

*/

public static String initKeyString()throws Exception{

return Base64.encodeBase64String(initKey());

}

/**

*获取密钥

*@param key密钥

*@return byte[]密钥

*@throws Exception

*/

public static byte[]getKey(String key)throws Exception{

return Base64.decodeBase64(key);

}

/**

*解密

*@param data待解密数据

*@param key密钥

*@return byte[]解密数据

*@throws Exception

*/

public static byte[]decrypt(byte[]data, String key)throws Exception{

return decrypt(data, getKey(key));

}

/**

*加密

*@param data待加密数据

*@param key密钥

*@return byte[]加密数据

*@throws Exception

*/

public static byte[]encrypt(byte[]data, String key)throws Exception{

return encrypt(data, getKey(key));

}


这里我们使用Commons Codec提供Base64算法对密钥进行封装/解包。通过initKeyString()方法我们将得到密钥字符串:“Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=”。我们将在后续操作中使用到该内容。

为防止传递的机密数据在网络传递过程中被篡改,我们可以对数据进行消息摘要,并对该摘要进行验证。这里使用SHA算法对数据进行摘要/验证。完整实现如代码清单7-13所示。

代码清单7-13 摘要/验证


import org.apache.commons.codec.digest.DigestUtils;

//省略

/**

*摘要处理

*@param data待摘要数据

*@return String摘要字符串

*/

public static String shaHex(byte[]data){

return DigestUtils.md5Hex(data);

}

/**

*验证

*@param data待摘要数据

*@param messageDigest摘要字符串

*@return boolean验证结果

*/

public static boolean validate(byte[]data, String messageDigest){

return messageDigest.equals(shaHex(data));

}


AESCoder类完整实现如代码清单7-14所示。

代码清单7-14 AESCoder


import java.security.Key;

import javax.crypto.Cipher;

import javax.crypto.KeyGenerator;

import javax.crypto.SecretKey;

import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

import org.apache.commons.codec.digest.DigestUtils;

/**

*AES安全编码组件

*@author梁栋

*@version 1.0

*/

public abstract class AESCoder{

/**

*密钥算法

*/

public static final String ALGORITHM="AES";

/**

*转换密钥

*@param key二进制密钥

*@return Key密钥

*@throws Exception

*/

private static Key toKey(byte[]key)throws Exception{

//实例化AES密钥材料

SecretKey secretKey=new SecretKeySpec(key, ALGORITHM);

return secretKey;

}

/**

*解密

*@param data待解密数据

*@param key密钥

*@return byte[]解密数据

*@throws Exception

*/

public static byte[]decrypt(byte[]data, byte[]key)throws Exception{

//还原密钥

Key k=toKey(key);

//实例化

Cipher cipher=Cipher.getInstance(ALGORITHM);

//初始化,设置为解密模式

cipher.init(Cipher.DECRYPT_MODE, k);

//执行操作

return cipher.doFinal(data);

}

/**

*解密

*@param data待解密数据

*@param key密钥

*@return byte[]解密数据

*@throws Exception

*/

public static byte[]decrypt(byte[]data, String key)throws Exception{

return decrypt(data, getKey(key));

}

/**

*加密

*@param data待加密数据

*@param key密钥

*@return byte[]加密数据

*@throws Exception

*/

public static byte[]encrypt(byte[]data, byte[]key)throws Exception{

//还原密钥

Key k=toKey(key);

//实例化

Cipher cipher=Cipher.getInstance(ALGORITHM);

//初始化,设置为加密模式

cipher.init(Cipher.ENCRYPT_MODE, k);

//执行操作

return cipher.doFinal(data);

}

/**

*加密

*@param data待加密数据

*@param key密钥

*@return byte[]加密数据

*@throws Exception

*/

public static byte[]encrypt(byte[]data, String key)throws Exception{

return encrypt(data, getKey(key));

}

/**

*生成密钥

*@return byte[]二进制密钥

*@throws Exception

*/

public static byte[]initKey()throws Exception{

//实例化

KeyGenerator kg=KeyGenerator.getInstance(ALGORITHM);

//初始化256位密钥

kg.init(256);

//生成秘密密钥

SecretKey secretKey=kg.generateKey();

//获得密钥的二进制编码形式

return secretKey.getEncoded();

}

/**

*初始化密钥

*@return String Base64编码密钥

*@throws Exception

*/

public static String initKeyString()throws Exception{

return Base64.encodeBase64String(initKey());

}

/**

*获取密钥

*@param key密钥

*@return byte[]密钥

*@throws Exception

*/

public static byte[]getKey(String key)throws Exception{

return Base64.decodeBase64(key);

}

/**

*摘要处理

*@param data待摘要数据

*@return String摘要字符串

*/

public static String shaHex(byte[]data){

return DigestUtils.md5Hex(data);

}

/**

*验证

*@param data待摘要数据

*@param messageDigest摘要字符串

*@return boolean验证结果

*/

public static boolean validate(byte[]data, String messageDigest){

return messageDigest.equals(shaHex(data));

}

}


接下来,我们将构建用于提供服务的Servlet—DataServlet类。这里,DataServlet类继承了HttpServlet类,重写了init()方法并实现了doPost()方法。

我们希望将密钥可以通过web.xml文件进行配置,在DataServlet启动时加载该密钥。我们通过重写init()方法获得密钥信息。相关实现如代码清单7-15所示。

代码清单7-15 DataServlet—初始化


//省略

//密钥

private static String key;

//Servlet初始化参数—密钥

private static final String KEY_PARAM="key";

//省略

//初始化

@Override

public void init()throws ServletException{

super.init();

//初始化密钥

key=getInitParameter(KEY_PARAM);

}


我们可以调用HttpUtils类的requestRead()方法获得请求内容,调用AESCoder类的decrypt()方法对请求内容解密,并将解密后的内容输出在控制台中。同时,我们将从HTTP Header中获得此次交互数据的摘要信息,并对此验证。如果验证通过,则回复“OK”。相关实现如代码清单7-16所示。

代码清单7-16 DataServlet—处理POST请求


//HTTP Header摘要参数名

private static final String HEAD_MD="messageDigest";

//省略

//处理POST请求

protected void doPost(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException{

try{

byte[]input=HttpUtils.requestRead(request);

//对数据解密

byte[]data=AESCoder.decrypt(input, key);

System.err.println(new String(data));

//默认回复内容

byte[]output="".getBytes();

//获得此次交互数据的摘要信息

String messageDigest=request.getHeader(HEAD_MD);

//如果验证成功则回复OK

if(AESCoder.validate(data, messageDigest)){

//如果正常接收到数据则回复OK

output="OK".getBytes();

}

//加密回复

HttpUtils.responseWrite(response, AESCoder.encrypt(output, key));

}catch(Exception e){

throw new ServletException(e);

}

}


完整实现如代码清单7-17所示。

代码清单7-17 DataServlet


import java.io.IOException;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

/**

*数据服务DataServlet

*@author梁栋

*@since 1.0

*/

public class DataServlet extends HttpServlet{

private static final long serialVersionUID=-6219906900195793155L;

//密钥

private static String key;

//Servlet初始化参数—密钥

private static final String KEY_PARAM="key";

//HTTP Header摘要参数名

private static final String HEAD_MD="messageDigest";

//初始化

@Override

public void init()throws ServletException{

super.init();

//初始化密钥

key=getInitParameter(KEY_PARAM);

}

//处理POST请求

protected void doPost(HttpServletRequest request, HttpServletResponse

response)throws ServletException, IOException{

try{

//获得此次交互数据的摘要信息

String messageDigest=request.getHeader(HEAD_MD);

byte[]input=HttpUtils.requestRead(request);

//对数据解密

byte[]data=AESCoder.decrypt(input, key);

System.err.println(new String(data));

//默认回复内容

byte[]output="".getBytes();

//如果验证成功则回复OK

if(AESCoder.validate(data, messageDigest)){

//如果正常接收到数据则回复OK

output="OK".getBytes();

}

//加密回复

HttpUtils.responseWrite(response, AESCoder.encrypt(output, key));

}catch(Exception e){

throw new ServletException(e);

}

}

}


此处,我们将密钥(“Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=”)配置在web.xml文件中,完整代码如代码清单7-18所示。

代码清单7-18 web.xml


<?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"

xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

id="WebApp_ID"

version="2.5">

<display-name>DataServer</display-name>

<servlet>

<servlet-name>DataServlet</servlet-name>

<servlet-class>DataServlet</servlet-class>

<init-param>

<param-name>key</param-name>

<param-value><![CDATA[Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=]]></param-value>

</init-param>

</servlet>

<servlet-mapping>

<servlet-name>DataServlet</servlet-name>

<url-pattern>/DataServlet</url-pattern>

</servlet-mapping>

</web-app>


此处,我们将向服务器发送XML格式的数据包。并在发送数据之前对其进行摘要和加密,并校验获得的信息是否包含“OK”内容。此处,我们将密钥作为变量写入DataServletTest类中。完整实现如代码清单7-19所示。

代码清单7-19 DataServletTest


import static org.junit.Assert.*;

import java.util.Properties;

import org.junit.Test;

/**

*DataServlet测试用例

*@author梁栋

*@since 1.0

*/

public class DataServletTest{

//秘密密钥

private static final String key="Zk6tc8Gg3NVi+m6X2UmV7rd1XRRiF1sUNtxFbiFqjMs=";

//请求地址

private static final String url="http://localhost:8080/dataserver/DataServlet";

@Test

public final void test()throws Exception{

//构造数据包

StringBuilder sb=new StringBuilder();

sb.append("<?xml version=\"1.0\"encoding=\"UTF-8\"?>\r\n");

sb.append("<dataGroup>\r\n");

sb.append("\t<dataItem>\r\n");

sb.append("\t\t<id>");

sb.append("10201");

sb.append("</id>\r\n");

sb.append("\t\t<price>");

sb.append("35.0");

sb.append("</price>\r\n");

sb.append("\t\t<time>");

sb.append("2009-10-30");

sb.append("</time>\r\n");

sb.append("\t</dataItem>\r\n");

sb.append("\t<dataItem>\r\n");

sb.append("\t\t<id>");

sb.append("10301");

sb.append("</id>\r\n");

sb.append("\t\t<price>");

sb.append("55.0");

sb.append("</price>\r\n");

sb.append("\t\t<time>");

sb.append("2009-10-31");

sb.append("</time>\r\n");

sb.append("\t</dataItem>\r\n");

sb.append("</dataGroup>\r\n");

byte[]data=sb.toString().getBytes();

//向HTTP Header附加数据摘要信息

Properties requestProperties=new Properties();

requestProperties.put("messageDigest",AESCoder.shaHex(data));

//加密并发送数据

byte[]input=HttpUtils.postRequest(url, AESCoder.encrypt(data,

key),requestProperties);

//解密

input=AESCoder.decrypt(input, key);

//校验

assertEquals("OK",new String(input));

}

}


启动DataServer服务,并执行测试用例,我们将在控制台中得到如下信息:


<?xml version="1.0"encoding="UTF-8"?>

<dataGroup>

<dataItem>

<id>10201</id>

<price>35.0</price>

<time>2009-10-30</time>

</dataItem>

<dataItem>

<id>10301</id>

<price>55.0</price>

<time>2009-10-31</time>

</dataItem>

</dataGroup>


很显然,这是我们发送给服务器的数据内容。

在实际应用中,我们经常需要与合作伙伴进行机密数据交互。通常,我们都会使用对称加密算法,例如AES算法对其进行加密。并使用消息摘要算法对其内容进行摘要/验证,例如SHA算法。

对称加密算法非常适用于一般中小型加密网络应用。进行交互的两方应用可能都是定制的系统,密钥在系统设计当初就已敲定。而对于大中型加密网络应用来讲,这样的密钥管理就比较繁琐了。希望上述简单的代码实现可以为读者朋友构建加密网络应用起到抛砖引玉的作用。