原址:点击打开链接
一、关键词
HTTP,HTTPS,AES,SHA-1,MD5,消息摘要,数字签名,数字加密,Java,Servlet,Bouncy Castle
二、名词解释
数字摘要:是将任意长度的消息变成固定长度的短消息,它类似于一个自变量是消息的函数,也就是Hash函数。数字摘要就是采用单项Hash函数将需要加密的明文“摘要”成一串固定长度(128位)的密文这一串密文又称为数字指纹,它有固定的长度,而且不同的明文摘要成密文,其结果总是不同的,而同样的明文其摘要必定一致。
AES:密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。是一种对称加密算法。
SHA-1:安全哈希算法(Secure Hash Algorithm)主要适用于数字签名标准 (Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。 SHA1有如下特性:不可以从消息摘要中复原信息;两个不同的消息不会产生同样的消息摘要。
MD5:Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。
三、项目背景
某合作公司需要通过互联网向我司传递一些用户数据,但是我所在项目组的外网服务器上并无部署https,只能基于http进行数据传输。为了保护双方共同的用户数据,必须对在互联网上传输的信息进行加密处理。
四、方案设计
这里涉及到两个问题,一是采用什么样的远程消息传递框架,二是如何对传输的数据进行加密。
本人平时开发所用的语言主要是Java,对于Jsp/Servlet还比较熟悉,结合去年参加过所在公司的微信公众号开发的经验,设计出了如下方案:
1.在客户端采用构造http post请求,把用户数据加密后放入request body中,并在http参数中放入调用方的签名;
2.服务端接收到请求,提取参数进行签名校验,通过后从request body中提取密文进行解密,然后进行后续处理,最终生成响应返回给客户端。
以下是具体处理的流程图:
在数据加密阶段,基于性能以及效率考虑,采用了Bouncy Castle提供的AES算法,而生成签名则采用了jdk提供的SHA-1,值得注意的是,基于安全考虑,消息密文的消息摘要也被列入到参与数字签名的参数之一。
五、代码实现
1.AES加密工具类:
- import org.apache.commons.lang.StringUtils;
- import org.apache.log4j.Logger;
- import org.bouncycastle.crypto.CipherParameters;
- import org.bouncycastle.crypto.engines.AESFastEngine;
- import org.bouncycastle.crypto.modes.CBCBlockCipher;
- import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
- import org.bouncycastle.crypto.params.KeyParameter;
- import org.bouncycastle.crypto.params.ParametersWithIV;
- import org.bouncycastle.util.encoders.Hex;
-
-
-
-
-
-
-
- public class AESTool {
- protected static final Logger log = Logger.getLogger(AESTool.class);
-
- private byte[] initVector = { 0x32, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31,
- 0x38, 0x27, 0x36, 0x35, 0x33, 0x23, 0x32, 0x31 };
-
-
-
-
-
-
-
- public String findKeyById(String appid) {
-
- String key = "123456789012345678901234567890~!";
- return key;
- }
-
-
-
-
-
-
-
-
-
-
- public String encrypt(String content, String key) throws Exception {
- if (key == null) {
- throw new IllegalArgumentException("Key cannot be null!");
- }
- String encrypted = null;
- byte[] keyBytes = key.getBytes();
- if (keyBytes.length != 32 && keyBytes.length != 24
- && keyBytes.length != 16) {
- throw new IllegalArgumentException(
- "Key length must be 128/192/256 bits!");
- }
- byte[] encryptedBytes = null;
- encryptedBytes = encrypt(content.getBytes(), keyBytes, initVector);
- encrypted = new String(Hex.encode(encryptedBytes));
- return encrypted;
- }
-
-
-
-
-
-
-
-
-
-
- public String decrypt(String content, String key) throws Exception {
- if (key == null) {
- throw new IllegalArgumentException("Key cannot be null!");
- }
- String decrypted = null;
- byte[] encryptedContent = Hex.decode(content);
- byte[] keyBytes = key.getBytes();
- byte[] decryptedBytes = null;
- if (keyBytes.length != 32 && keyBytes.length != 24
- && keyBytes.length != 16) {
- throw new IllegalArgumentException(
- "Key length must be 128/192/256 bits!");
- }
- decryptedBytes = decrypt(encryptedContent, keyBytes, initVector);
- decrypted = new String(decryptedBytes);
- return decrypted;
- }
-
-
-
-
-
-
-
-
-
-
- public byte[] encrypt(byte[] plain, byte[] key, byte[] iv) throws Exception {
- PaddedBufferedBlockCipher aes = new PaddedBufferedBlockCipher(
- new CBCBlockCipher(new AESFastEngine()));
- CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(key),
- iv);
- aes.init(true, ivAndKey);
- return cipherData(aes, plain);
- }
-
-
-
-
-
-
-
-
-
-
- public byte[] decrypt(byte[] cipher, byte[] key, byte[] iv)
- throws Exception {
- PaddedBufferedBlockCipher aes = new PaddedBufferedBlockCipher(
- new CBCBlockCipher(new AESFastEngine()));
- CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(key),
- iv);
- aes.init(false, ivAndKey);
- return cipherData(aes, cipher);
- }
-
-
-
-
-
-
-
-
-
- private byte[] cipherData(PaddedBufferedBlockCipher cipher, byte[] data)
- throws Exception {
- int minSize = cipher.getOutputSize(data.length);
- byte[] outBuf = new byte[minSize];
- int length1 = cipher.processBytes(data, 0, data.length, outBuf, 0);
- int length2 = cipher.doFinal(outBuf, length1);
- int actualLength = length1 + length2;
- byte[] result = new byte[actualLength];
- System.arraycopy(outBuf, 0, result, 0, result.length);
- return result;
- }
-
- public static void main(String[] args) throws Exception {
- AESTool aesTool = new AESTool();
- String appid = "canairport001";
- String key = aesTool.findKeyById(appid);
- String xml = "<root><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name></root>";
- String encrypted = aesTool.encrypt(xml, key);
- System.out.println("encrypted: \n" + encrypted);
- System.out.println("encrypted length: \n" + encrypted.length());
- String decrypted = aesTool.decrypt(encrypted, key);
- System.out.println("decrypted: \n" + decrypted);
- System.out.println("decrypted length: \n" + decrypted.length());
- boolean isSuccessful = StringUtils.equals(decrypted, xml);
- System.out.println(isSuccessful);
- }
- }
2.数字签名工具类:
- import java.security.MessageDigest;
- import java.security.NoSuchAlgorithmException;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
-
- import org.apache.commons.lang.StringUtils;
- import org.apache.log4j.Logger;
-
-
-
-
-
- public class SignatureUtil {
- protected static Logger log = Logger.getLogger(SignatureUtil.class);
-
- private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
-
- private String encryptionAlgorithm = "SHA-1";
-
- public String bytesToHexString(byte[] bytes) {
- char[] hexChars = new char[bytes.length * 2];
- for (int j = 0; j < bytes.length; j++) {
- int v = bytes[j] & 0xFF;
- hexChars[j * 2] = hexArray[v >>> 4];
- hexChars[j * 2 + 1] = hexArray[v & 0x0F];
- }
- return new String(hexChars);
- }
-
- public byte[] hexStringToBytes(String s) {
- int len = s.length();
- byte[] data = new byte[len / 2];
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character
- .digit(s.charAt(i + 1), 16));
- }
- return data;
- }
-
-
-
-
-
-
-
-
-
-
- public String digest(String strSrc, String encName) {
- MessageDigest md = null;
- String strDes = null;
- byte[] bt = strSrc.getBytes();
- try {
- if (encName == null || encName.equals("")) {
- encName = "MD5";
- }
- md = MessageDigest.getInstance(encName);
- md.update(bt);
- strDes = bytesToHexString(md.digest());
- } catch (NoSuchAlgorithmException e) {
- log.error("Invalid algorithm: " + encName);
- return null;
- }
- return strDes;
- }
-
-
-
-
-
-
-
-
-
-
- public String generateSignature(String appid, String token, String lol,
- long millis) {
- String timestamp = String.valueOf(millis);
- String signature = null;
- if (StringUtils.isNotBlank(token) && StringUtils.isNotBlank(timestamp)
- && StringUtils.isNotBlank(appid)) {
- List<String> srcList = new ArrayList<String>();
- srcList.add(timestamp);
- srcList.add(appid);
- srcList.add(token);
- srcList.add(lol);
-
- Collections.sort(srcList);
- Collections.reverse(srcList);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < srcList.size(); i++) {
- sb.append(srcList.get(i));
- }
- signature = digest(sb.toString(), encryptionAlgorithm);
- srcList.clear();
- srcList = null;
- }
- return signature;
- }
-
-
-
-
-
-
-
-
-
-
-
-
- public boolean isValid(String signature, String appid, String lol,
- long millis) {
- String token = findTokenById(appid);
- String calculatedSignature = generateSignature(appid, token, lol,
- millis);
- log.info("calculated signature: \n" + calculatedSignature);
- if (StringUtils.equals(calculatedSignature, signature)) {
- return true;
- } else {
- return false;
- }
- }
-
-
-
-
-
-
- public String findTokenById(String appid) {
- String token = "#@!1234567890!@#";
- return token;
- }
-
- public static void main(String[] args) {
- SignatureUtil generator = new SignatureUtil();
- String xmlString = "<root><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name></root>";
- System.out.println(xmlString.getBytes().length);
- String digest = generator.digest(xmlString, "MD5");
- System.out.println(digest);
- System.out.println(digest.getBytes().length);
- String appid = "canairport001";
- String token = generator.findTokenById(appid);
- long millis = System.currentTimeMillis();
- String signature = generator.generateSignature(appid, token, digest,
- millis);
- System.out.println(signature);
- boolean isValid = generator.isValid(signature, appid, digest, millis);
- System.out.println(isValid);
- }
- }
3.发送方代码:
- import java.io.IOException;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.Map;
- import java.util.Map.Entry;
-
- import org.apache.commons.lang.StringUtils;
- import org.apache.http.HttpEntity;
- import org.apache.http.HttpHost;
- import org.apache.http.HttpResponse;
- import org.apache.http.HttpStatus;
- import org.apache.http.auth.AuthScope;
- import org.apache.http.auth.UsernamePasswordCredentials;
- import org.apache.http.client.ClientProtocolException;
- import org.apache.http.client.methods.HttpPost;
- import org.apache.http.conn.params.ConnRoutePNames;
- import org.apache.http.entity.StringEntity;
- import org.apache.http.impl.client.DefaultHttpClient;
- import org.apache.http.message.BasicHeader;
- import org.apache.http.protocol.HTTP;
- import org.apache.http.util.EntityUtils;
- import org.apache.log4j.Logger;
-
-
-
-
-
- public class HttpclientUtil {
- protected static final Logger log = Logger.getLogger(HttpclientUtil.class);
-
-
-
-
-
-
-
-
- public String buildUri(String uri, Map<String, String> paraMap) {
- StringBuilder sb = new StringBuilder();
- uri = StringUtils.trim(uri);
- uri = StringUtils.removeEnd(uri, "/");
- uri = StringUtils.removeEnd(uri, "?");
- sb.append(uri);
- if (paraMap != null && !paraMap.isEmpty()) {
- sb.append("?");
- Iterator<Entry<String, String>> iterator = paraMap.entrySet()
- .iterator();
- while (iterator.hasNext()) {
- Map.Entry<String, String> pair = iterator.next();
- try {
- String keyString = pair.getKey();
- String valueString = pair.getValue();
- sb.append(keyString);
- sb.append("=");
- sb.append(valueString);
- sb.append("&");
- } catch (Exception e) {
- log.error(e, e);
- }
- }
- }
- return StringUtils.removeEnd(sb.toString(), "&");
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- public String postXmlString(String targetHost, int targetPort,
- String protocol, String proxyHost, int proxyPort, String proxyUser,
- String proxyPassword, String uri, Map<String, String> paraMap,
- String xml, String charset) throws ClientProtocolException,
- IOException {
- String result = null;
- DefaultHttpClient httpclient = new DefaultHttpClient();
- if (StringUtils.isNotBlank(proxyHost) && proxyPort > 0) {
-
- AuthScope authScope = new AuthScope(proxyHost, proxyPort);
- if (StringUtils.isNotBlank(proxyUser)
- && StringUtils.isNotBlank(proxyPassword)) {
-
- UsernamePasswordCredentials upc = new UsernamePasswordCredentials(
- proxyUser, proxyPassword);
- httpclient.getCredentialsProvider().setCredentials(authScope,
- upc);
- }
- HttpHost proxy = new HttpHost(proxyHost, proxyPort);
- httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY,
- proxy);
- }
- HttpHost host = new HttpHost(targetHost, targetPort, protocol);
- uri = buildUri(uri, paraMap);
- log.info("post uri: " + uri);
- log.info("post content: " + xml);
- HttpPost post = new HttpPost(uri);
- StringEntity se = new StringEntity(xml,
- StringUtils.isNotBlank(charset) ? charset : "utf-8");
- se.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
- "application/xml"));
- post.setEntity(se);
- HttpResponse response = httpclient.execute(host, post);
- if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
- HttpEntity entity = response.getEntity();
- if (entity != null) {
- result = EntityUtils.toString(entity);
- log.info("post result: " + result);
- }
- } else {
- log.error("post failed, status code: "
- + response.getStatusLine().getStatusCode());
- }
- return result;
- }
-
- public static void main(String[] args) throws Exception {
- AESTool aes = new AESTool();
- SignatureUtil signatureUtil = new SignatureUtil();
- String appid = "canairport001";
- String token = signatureUtil.findTokenById(appid);
- String key = aes.findKeyById(appid);
- long millis = System.currentTimeMillis();
- String xml = "<dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.5</version></dependency>";
- xml = aes.encrypt(xml, key);
- String lol = signatureUtil.digest(xml, "MD5");
- String signature = signatureUtil.generateSignature(appid, token, lol,
- millis);
- log.info("lol: \n" + lol);
- log.info("signature: \n" + signature);
- String uri = "http://127.0.0.1:8080/demo/psginfo.do";
- Map<String, String> paraMap = new HashMap<String, String>();
- paraMap.put("s", signature);
- paraMap.put("a", appid);
- paraMap.put("t", String.valueOf(millis));
- paraMap.put("l", lol);
- paraMap.put("o", "test");
- HttpclientUtil util = new HttpclientUtil();
- try {
- String result = util.postXmlString("127.0.0.1", 8080, "http", null,
- 0, null, null, uri, paraMap, xml, "utf-8");
- result = aes.decrypt(result, key);
- System.out.println(result);
- } catch (ClientProtocolException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
4.服务端代码:
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.io.PrintWriter;
- import java.io.UnsupportedEncodingException;
-
- import javax.servlet.ServletException;
- import javax.servlet.annotation.WebServlet;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
-
- import org.apache.commons.lang.StringUtils;
- import org.apache.log4j.Logger;
-
- import co.speedar.wechat.util.AESTool;
- import co.speedar.wechat.util.SignatureUtil;
-
-
-
-
- @WebServlet(urlPatterns = { "/psginfo.do" }, loadOnStartup = 1)
- public class PsginfoServlet extends HttpServlet {
- protected static final Logger log = Logger.getLogger(PsginfoServlet.class);
- private static final long serialVersionUID = 6536688299231165548L;
-
- private SignatureUtil signatureUtil = new SignatureUtil();
-
- private AESTool aes = new AESTool();
-
-
-
-
- public PsginfoServlet() {
- super();
- }
-
-
-
-
-
- protected void doGet(HttpServletRequest request,
- HttpServletResponse response) throws ServletException, IOException {
- String echostr = request.getParameter("e");
- log.info("echostr before echo: " + echostr);
- String signature = request.getParameter("s");
- String appid = request.getParameter("a");
- String timestamp = request.getParameter("t");
- String lol = request.getParameter("l");
- long millis = Long.valueOf(timestamp);
-
- if (signatureUtil.isValid(signature, appid, lol, millis)) {
- PrintWriter writer = response.getWriter();
- log.info("echostr after echo: " + echostr);
- writer.print(echostr);
- writer.flush();
- writer.close();
- }
- }
-
-
-
-
-
- protected void doPost(HttpServletRequest request,
- HttpServletResponse response) throws ServletException, IOException {
-
- String signature = request.getParameter("s");
- String appid = request.getParameter("a");
- String timestamp = request.getParameter("t");
- String lol = request.getParameter("l");
- String operation = request.getParameter("o");
- long millis = Long.valueOf(timestamp);
-
-
- String encoding = StringUtils
- .isNotBlank(request.getCharacterEncoding()) ? request
- .getCharacterEncoding() : "utf-8";
- String requestXmlString = getXmlStringFromHttpRequest(request);
- String digest = signatureUtil.digest(requestXmlString, "MD5");
-
-
- if (StringUtils.equals(digest, lol)) {
- if (signatureUtil.isValid(signature, appid, lol, millis)) {
- try {
- String key = aes.findKeyById(appid);
- requestXmlString = aes.decrypt(requestXmlString, key);
- log.info("received xml data:\n" + requestXmlString);
-
- String responseXmlString = doSomeThing(requestXmlString,
- operation);
- responseXmlString = aes.encrypt(responseXmlString, key);
- log.info("responsed xml data:\n" + responseXmlString);
- response.setCharacterEncoding(encoding);
- PrintWriter writer = response.getWriter();
- writer.print(responseXmlString);
- writer.flush();
- writer.close();
- } catch (Exception e) {
- log.error(e, e);
- }
- } else {
- log.error("invalid signature");
- }
- } else {
- log.error("invalid digest.");
- }
- }
-
-
-
-
-
-
-
-
- private String doSomeThing(String xml, String operation) {
- return "done";
- }
-
-
-
-
-
-
-
-
- private String getXmlStringFromHttpRequest(HttpServletRequest request) {
- String requestXmlString = "";
- try {
- InputStream inputStream = request.getInputStream();
- String encoding = StringUtils.isNotBlank(request
- .getCharacterEncoding()) ? request.getCharacterEncoding()
- : "utf-8";
- requestXmlString = getXmlStringFromInputStream(inputStream,
- encoding);
- encoding = null;
- inputStream.close();
- inputStream = null;
- } catch (IOException e) {
- log.error(e, e);
- }
-
- return requestXmlString;
- }
-
-
-
-
-
-
-
-
- private String getXmlStringFromInputStream(InputStream inputStream,
- String charsetName) {
- String resultXmlString = "";
- String tempString = null;
- BufferedReader bufferedReader;
- try {
- bufferedReader = new BufferedReader(new InputStreamReader(
- inputStream, charsetName));
- tempString = bufferedReader.readLine();
- while (tempString != null) {
- resultXmlString += tempString;
- tempString = bufferedReader.readLine();
- }
- tempString = null;
- bufferedReader.close();
- bufferedReader = null;
- } catch (UnsupportedEncodingException e) {
- log.error(e, e);
- } catch (IOException e) {
- log.error(e, e);
- }
- return StringUtils.trim(resultXmlString);
- }
-
- }
5.maven配置:
- <dependency>
- <groupId>org.bouncycastle</groupId>
- <artifactId>bcprov-jdk16</artifactId>
- <version>1.46</version>
- </dependency>
- <dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- <version>2.5</version>
- </dependency>
- <dependency>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpclient</artifactId>
- <version>4.2.5</version>
- </dependency>
- <dependency>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpmime</artifactId>
- <version>4.2.5</version>
- </dependency>
六、结语
在本方案设计实现过程中,消息传递的框架采用的是Java开发者所熟悉的Servlet技术,摘要、签名、加密所采用的算法,以及所依赖的第三方jar也是比较有口碑又大众化的货,对于有类似需要的开发者来说,本方案具有一定的参考意义。远程传递消息框架以及生成签名的环节,主要是模仿了微信公众平台的消息交互方式以及生成签名的思路,而有所创新的一小点是,把消息密文的MD5值也参与到了签名运算中,增加了被仿冒的难度,同时也便于服务方校验消息在传递过程中是否有被第三方所篡改。
基于简化工程配置的考虑,本示例项目中没有使用spring,您可以在您的生产项目中把本示例中的代码改造成春哥的单例业务bean。密钥、token建议别直接写到春哥的context配置文件中,而是写在您的生产容器的环境变量中,防止被窃取。
另外,在本方案中生成签名的参数您可以酌情增减并调换顺序,替换签名所采用的算法,或者根据您的实际需要“个性化”一下您的加密算法,以期达到更好的安全效果。
Last but not the least,在密钥以及token交换的阶段,请采取您所认可的安全有效的方式进行,譬如面对面,微信,qq,微薄私信,电话,短信,邮件(可以参考本人之前写过的一篇文章:http://lixuanbin.iteye.com/blog/1544344)
七、参考资料
【Java加密与解密的艺术】——作者:梁栋,出版日期:2010年12月,ISBN:978-7-111-29762-8
http://stackoverflow.com/questions/4243650/aes-encryption-decryption-with-bouncycastle-example-in-j2me
http://stackoverflow.com/questions/6729834/need-solution-for-wrong-iv-length-in-aes
http://baike.baidu.com/view/941329.htm?fr=aladdin
http://baike.baidu.com/subview/133041/5358738.htm?fr=aladdin
http://baike.baidu.com/view/1228622.htm?fr=aladdin
http://baike.baidu.com/view/7636.htm?fr=aladdin