月度归档:2016年04月

分段加解密解决RSA“不正确的长度”的异常

RSA 是常用的非对称加密算法。最近使用时却出现了“不正确的长度”的异常,研究发现是由于待加密的数据超长所致。

RSA加密明文最大长度117字节,解密要求密文最大长度为128字节,所以在加密和解密的过程中需要分块进行。
RSA加密对明文的长度是有限制的,如果加密数据过大会抛出如下异常:

Exception in thread "main" javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes
at com.sun.crypto.provider.RSACipher.a(DashoA13*..)
at com.sun.crypto.provider.RSACipher.engineDoFinal(DashoA13*..)
at javax.crypto.Cipher.doFinal(DashoA13*..)

RSAUtils.java

package security;

import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;

/** *//**
* <p>
* RSA公钥/私钥/签名工具包
* </p>
* <p>
* 罗纳德·李维斯特(Ron [R]ivest)、阿迪·萨莫尔(Adi [S]hamir)和伦纳德·阿德曼(Leonard [A]dleman)
* </p>
* <p>
* 字符串格式的密钥在未在特殊说明情况下都为BASE64编码格式<br/>
* 由于非对称加密速度极其缓慢,一般文件不使用它来加密而是使用对称加密,<br/>
* 非对称加密算法可以用来对对称加密的密钥加密,这样保证密钥的安全也就保证了数据的安全
* </p>
*
* @author IceWee
* @date 2012-4-26
* @version 1.0
*/
public class RSAUtils {

/** *//**
* 加密算法RSA
*/
public static final String KEY_ALGORITHM = "RSA";

/** *//**
* 签名算法
*/
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";

/** *//**
* 获取公钥的key
*/
private static final String PUBLIC_KEY = "RSAPublicKey";

/** *//**
* 获取私钥的key
*/
private static final String PRIVATE_KEY = "RSAPrivateKey";

/** *//**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;

/** *//**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;

/** *//**
* <p>
* 生成密钥对(公钥和私钥)
* </p>
*
* @return
* @throws Exception
*/
public static Map<String, Object> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}

/** *//**
* <p>
* 用私钥对信息生成数字签名
* </p>
*
* @param data 已加密数据
* @param privateKey 私钥(BASE64编码)
*
* @return
* @throws Exception
*/
public static String sign(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64Utils.decode(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateK);
signature.update(data);
return Base64Utils.encode(signature.sign());
}

/** *//**
* <p>
* 校验数字签名
* </p>
*
* @param data 已加密数据
* @param publicKey 公钥(BASE64编码)
* @param sign 数字签名
*
* @return
* @throws Exception
*
*/
public static boolean verify(byte[] data, String publicKey, String sign)
throws Exception {
byte[] keyBytes = Base64Utils.decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64Utils.decode(sign));
}

/** *//**
* <P>
* 私钥解密
* </p>
*
* @param encryptedData 已加密数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey)
throws Exception {
byte[] keyBytes = Base64Utils.decode(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}

/** *//**
* <p>
* 公钥解密
* </p>
*
* @param encryptedData 已加密数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey)
throws Exception {
byte[] keyBytes = Base64Utils.decode(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}

/** *//**
* <p>
* 公钥加密
* </p>
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey)
throws Exception {
byte[] keyBytes = Base64Utils.decode(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}

/** *//**
* <p>
* 私钥加密
* </p>
*
* @param data 源数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String privateKey)
throws Exception {
byte[] keyBytes = Base64Utils.decode(privateKey);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}

/** *//**
* <p>
* 获取私钥
* </p>
*
* @param keyMap 密钥对
* @return
* @throws Exception
*/
public static String getPrivateKey(Map<String, Object> keyMap)
throws Exception {
Key key = (Key) keyMap.get(PRIVATE_KEY);
return Base64Utils.encode(key.getEncoded());
}

/** *//**
* <p>
* 获取公钥
* </p>
*
* @param keyMap 密钥对
* @return
* @throws Exception
*/
public static String getPublicKey(Map<String, Object> keyMap)
throws Exception {
Key key = (Key) keyMap.get(PUBLIC_KEY);
return Base64Utils.encode(key.getEncoded());
}

}

Base64Utils.java
package security;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import it.sauronsoftware.base64.Base64;

/** *//**
* <p>
* BASE64编码解码工具包
* </p>
* <p>
* 依赖javabase64-1.3.1.jar
* </p>
*
* @author IceWee
* @date 2012-5-19
* @version 1.0
*/
public class Base64Utils {

/** *//**
* 文件读取缓冲区大小
*/
private static final int CACHE_SIZE = 1024;

/** *//**
* <p>
* BASE64字符串解码为二进制数据
* </p>
*
* @param base64
* @return
* @throws Exception
*/
public static byte[] decode(String base64) throws Exception {
return Base64.decode(base64.getBytes());
}

/** *//**
* <p>
* 二进制数据编码为BASE64字符串
* </p>
*
* @param bytes
* @return
* @throws Exception
*/
public static String encode(byte[] bytes) throws Exception {
return new String(Base64.encode(bytes));
}

/** *//**
* <p>
* 将文件编码为BASE64字符串
* </p>
* <p>
* 大文件慎用,可能会导致内存溢出
* </p>
*
* @param filePath 文件绝对路径
* @return
* @throws Exception
*/
public static String encodeFile(String filePath) throws Exception {
byte[] bytes = fileToByte(filePath);
return encode(bytes);
}

/** *//**
* <p>
* BASE64字符串转回文件
* </p>
*
* @param filePath 文件绝对路径
* @param base64 编码字符串
* @throws Exception
*/
public static void decodeToFile(String filePath, String base64) throws Exception {
byte[] bytes = decode(base64);
byteArrayToFile(bytes, filePath);
}

/** *//**
* <p>
* 文件转换为二进制数组
* </p>
*
* @param filePath 文件路径
* @return
* @throws Exception
*/
public static byte[] fileToByte(String filePath) throws Exception {
byte[] data = new byte[0];
File file = new File(filePath);
if (file.exists()) {
FileInputStream in = new FileInputStream(file);
ByteArrayOutputStream out = new ByteArrayOutputStream(2048);
byte[] cache = new byte[CACHE_SIZE];
int nRead = 0;
while ((nRead = in.read(cache)) != -1) {
out.write(cache, 0, nRead);
out.flush();
}
out.close();
in.close();
data = out.toByteArray();
}
return data;
}

/** *//**
* <p>
* 二进制数据写文件
* </p>
*
* @param bytes 二进制数据
* @param filePath 文件生成目录
*/
public static void byteArrayToFile(byte[] bytes, String filePath) throws Exception {
InputStream in = new ByteArrayInputStream(bytes);
File destFile = new File(filePath);
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
destFile.createNewFile();
OutputStream out = new FileOutputStream(destFile);
byte[] cache = new byte[CACHE_SIZE];
int nRead = 0;
while ((nRead = in.read(cache)) != -1) {
out.write(cache, 0, nRead);
out.flush();
}
out.close();
in.close();
}

}

RSATester.java
package security;

import java.util.Map;

public class RSATester {

static String publicKey;
static String privateKey;

static {
try {
Map<String, Object> keyMap = RSAUtils.genKeyPair();
publicKey = RSAUtils.getPublicKey(keyMap);
privateKey = RSAUtils.getPrivateKey(keyMap);
System.err.println("公钥: \n\r" + publicKey);
System.err.println("私钥: \n\r" + privateKey);
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws Exception {
test();
testSign();
}

static void test() throws Exception {
System.err.println("公钥加密——私钥解密");
String source = "这是一行没有任何意义的文字,你看完了等于没看,不是吗?";
System.out.println("\r加密前文字:\r\n" + source);
byte[] data = source.getBytes();
byte[] encodedData = RSAUtils.encryptByPublicKey(data, publicKey);
System.out.println("加密后文字:\r\n" + new String(encodedData));
byte[] decodedData = RSAUtils.decryptByPrivateKey(encodedData, privateKey);
String target = new String(decodedData);
System.out.println("解密后文字: \r\n" + target);
}

static void testSign() throws Exception {
System.err.println("私钥加密——公钥解密");
String source = "这是一行测试RSA数字签名的无意义文字";
System.out.println("原文字:\r\n" + source);
byte[] data = source.getBytes();
byte[] encodedData = RSAUtils.encryptByPrivateKey(data, privateKey);
System.out.println("加密后:\r\n" + new String(encodedData));
byte[] decodedData = RSAUtils.decryptByPublicKey(encodedData, publicKey);
String target = new String(decodedData);
System.out.println("解密后: \r\n" + target);
System.err.println("私钥签名——公钥验证签名");
String sign = RSAUtils.sign(encodedData, privateKey);
System.err.println("签名:\r" + sign);
boolean status = RSAUtils.verify(encodedData, publicKey, sign);
System.err.println("验证结果:\r" + status);
}

}

下面是微软的.net系统下的rsa分块加密

待加密的字节数不能超过密钥的长度值除以 8 再减去 11(即:RSACryptoServiceProvider.KeySize / 8 - 11),而加密后得到密文的字节数,正好是密钥的长度值除以 8(即:RSACryptoServiceProvider.KeySize / 8)。

所以,如果要加密较长的数据,则可以采用分段加解密的方式,实现方式如下:

namespace Macroresolute.RSACryptoService
{
public static class RSACrypto
{
private static readonly Encoding Encoder = Encoding.UTF8;

public static String Encrypt(this String plaintext)
{
X509Certificate2 _X509Certificate2 = RSACrypto.RetrieveX509Certificate();
using (RSACryptoServiceProvider RSACryptography = _X509Certificate2.PublicKey.Key as RSACryptoServiceProvider)
{
Byte[] PlaintextData = RSACrypto.Encoder.GetBytes(plaintext);
int MaxBlockSize = RSACryptography.KeySize / 8 - 11;    //加密块最大长度限制

if (PlaintextData.Length <= MaxBlockSize)
return Convert.ToBase64String(RSACryptography.Encrypt(PlaintextData, false));

using (MemoryStream PlaiStream = new MemoryStream(PlaintextData))
using (MemoryStream CrypStream = new MemoryStream())
{
Byte[] Buffer = new Byte[MaxBlockSize];
int BlockSize = PlaiStream.Read(Buffer, 0, MaxBlockSize);

while (BlockSize > 0)
{
Byte[] ToEncrypt = new Byte[BlockSize];
Array.Copy(Buffer, 0, ToEncrypt, 0, BlockSize);

Byte[] Cryptograph = RSACryptography.Encrypt(ToEncrypt, false);
CrypStream.Write(Cryptograph, 0, Cryptograph.Length);

BlockSize = PlaiStream.Read(Buffer, 0, MaxBlockSize);
}

return Convert.ToBase64String(CrypStream.ToArray(), Base64FormattingOptions.None);
}
}
}

public static String Decrypt(this String ciphertext)
{
X509Certificate2 _X509Certificate2 = RSACrypto.RetrieveX509Certificate();
using (RSACryptoServiceProvider RSACryptography = _X509Certificate2.PrivateKey as RSACryptoServiceProvider)
{
Byte[] CiphertextData = Convert.FromBase64String(ciphertext);
int MaxBlockSize = RSACryptography.KeySize / 8;    //解密块最大长度限制

if (CiphertextData.Length <= MaxBlockSize)
return RSACrypto.Encoder.GetString(RSACryptography.Decrypt(CiphertextData, false));

using (MemoryStream CrypStream = new MemoryStream(CiphertextData))
using (MemoryStream PlaiStream = new MemoryStream())
{
Byte[] Buffer = new Byte[MaxBlockSize];
int BlockSize = CrypStream.Read(Buffer, 0, MaxBlockSize);

while (BlockSize > 0)
{
Byte[] ToDecrypt = new Byte[BlockSize];
Array.Copy(Buffer, 0, ToDecrypt, 0, BlockSize);

Byte[] Plaintext = RSACryptography.Decrypt(ToDecrypt, false);
PlaiStream.Write(Plaintext, 0, Plaintext.Length);

BlockSize = CrypStream.Read(Buffer, 0, MaxBlockSize);
}

return RSACrypto.Encoder.GetString(PlaiStream.ToArray());
}
}
}

private static X509Certificate2 RetrieveX509Certificate()
{
return null;    //检索用于 RSA 加密的 X509Certificate2 证书
}
}
}

注:以上加密方法返回的字符串类型为原始的 Base-64 ,若要用于 URL 传输,需另行处理!

win7下DOCKER详细安装教程

windows必须是64位的

1.下载程序包

安装包 https://github.com/boot2docker/windows-installer/releases(这个地址国内下载很慢)

用这个: https://get.daocloud.io/toolbox/

随着Docker的发展如日中天,着手安装使用Docker却成了大家的拦路虎,不过有了Toolbox,再也不用担心Docker的安装与使用了。本文对比于大家所熟知Boot2Docker命令行工具,简单的剖析了Toolbox安装器。

近日,Docker公司发布了Toolbox。Toolbox是一个安装器,目前支持Mac和Windows平台。使用它可以快速地在安装Docker工具集。本文翻译自Docker官方博客。

过去我们总听到有人说,在开发中很难使用入手使用Docker,尤其是你已经根据Compose定义过了你的应用程序,然后接下来要去单独安装Compose的情况。随着Compose、Kitematic以及Boot2Docker的普及,我们意识到我们需要让这些零碎的工具更好的在一起工作。

Toolbox可以安装你在开发中运行Docker时所需要的一切:Docker客户端、Compose(仅Mac需要)、Kitematic、Machine以及VirtualBox。Toolbox使用Machine和VirtualBox在虚拟机中创建了一个引擎来运行容器。在该虚拟机上,你可以使用Docker客户端、Compose以及Kitematic来运行容器。

它取代了Boot2Docker吗?

是的,玩转Docker,我们推荐Toolbox。

尽管Boot2Docker安装程序已经相当的受欢迎,但DockerToolbox是设计用来安装正在不断发展的Docker开发者工具集合,比如Kitematic、Machine、Swarm还有Compose。之前Boot2Docker还安装了一个叫Boot2Docker的命令行工具,以用来管理Docker虚拟机,在Toolbox中它已经被Machine取代了。

然而,在这个引擎下,Machine依然采用了Boot2DockerLinux发行版来运行容器。所不同的是,现在由Machine代替Boot2Docker命令行工具来管理这些容器。

如果你现在正在使用官方Boot2Docker(boot2docker-VM),DockerToolbox会提示你自动迁移到使用DockerMachine的虚拟机上。
 

下载最新版本的:Docker-install.exe即可。
该安装包安装完成后,系统上会多出三个软件:

Oracle VM VirtualBox
Git
Boot2Docker for Windows

以上三个默认安装即可。

2. 设置环境变量

在命令窗口中,输入ls 如果能找到命令说明环境添加正确。

3. 启动DOCKERT

在命令窗口中,切到docker的安装目录下

输入sh:
然后输入start.sh,等待启动


第一次启动中,如果有新版本会更新,时间比较长。

如果第二次启动,就非常快了。

4. 分析start.sh

#!/bin/bashset -e

# clear the MSYS MOTD
clear

cd "$(dirname "$BASH_SOURCE")"

ISO="$HOME/.boot2docker/boot2docker.iso"

if [ ! -e "$ISO" ]; then
    echo 'copying initial boot2docker.iso (run "boot2docker.exe download" to update)'
    mkdir -p "$(dirname "$ISO")"
    cp ./boot2docker.iso "$ISO"fi

echo 'initializing...'
./boot2docker.exe init
echo

echo 'starting...'
./boot2docker.exe start
echo

./boot2docker.exe ip

echo 'connecting...'
./boot2docker.exe ssh
echo

echoecho '[Press any key to exit]'read

从内容上看主要是执行,如下语句

boot2docker.exe init
boot2docker.exe start
boot2docker.exe ssh

所有在命令行下执行 sh start.sh 即可

5. 利用SSH工具管理

在windows命令行进入docker后,不能复制,而且操作也不方便,因此用支持SSH的工具来管理是很好的,比如SECURECRT, PUTTY等,推荐用SECURECRT.
在命令行下用boot2docker ip 可以查询到IP

默认的用户名和密码是: docker/tcuser

登录后的界面:

6. 下载镜像

6.1 下载地址

http://download.openvz.org/template/precreated
选择下载 ubuntu-14.04-x86_64.tar.gz

6.2 用FTP工具上传tar包

推荐使用:FileZilla

6.3 安装

命令:cat ubuntu-14.04-x86_64.tar.gz |docker import - ubuntu:ubuntu14
速度非常快,大概10几秒就完成了。

6.4 查看镜像

查看: docker images

6.5 运行

运行:docker run -i -t ubuntu:ubuntu14 /bin/bash

可以开始DOCKER旅行了。

来源: http://blog.csdn.net/zistxym/article/details/42918339

spring容器初始化和WEB项目启动加载

当spring 容器初始化和WEB项目启动加载

在做web项目开发中,尤其是企业级应用开发的时候,往往会在工程启动的时候做许多的前置检查。

比如检查是否使用了我们组禁止使用的Mysql的group_concat函数,如果使用了项目就不能启动,并指出哪个文件的xml文件使用了这个函数。

而在Spring的web项目中,我们可以介入Spring的启动过程。我们希望在Spring容器将所有的Bean都初始化完成之后,做一些操作,这个时候我们就可以实现一个接口:

package com.yk.test.executor.processor
public class InstantiationTracingBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
}
}

同时在Spring的配置文件中,添加注入:

<!-- 当Spring容器启动完成后执行下面的这个Bean -->
<bean class="com.yk.test.executor.processor.InstantiationTracingBeanPostProcessor"/>

但是这个时候,会存在一个问题,在web 项目中(spring mvc),系统会存在两个容器,一个是root application context ,另一个就是我们自己的 projectName-servlet context(作为root application context的子容器)。

这种情况下,就会造成onApplicationEvent方法被执行两次。为了避免上面提到的问题,我们可以只在root application context初始化完成后调用逻辑代码,其他的容器的初始化完成,则不做任何处理,修改后代码

如下:
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent() == null){//root application context 没有parent,他就是老大.
//需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
}
}

其实更简单的方法是使用注解:`@PostConstruct`,只需要在需要启动的时候执行的方法上标注这个注解就搞定了。

注解描述如下:

package javax.annotation;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

/**
* The PostConstruct annotation is used on a method that needs to be executed
*/
@Documented
@Retention (RUNTIME)
@Target(METHOD)
public @interface PostConstruct {
}

WEB项目启动加载的实现方式整理

方法一:
实现org.springframework.beans.factory.config.BeanPostProcessor接口:
[java] view plain copy

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}

在spring配置文件中添加:
<bean class="***.***.InstantiationTracingBeanPostProcessor"/>

方法二:
实现org.springframework.beans.factory.InitializingBean接口:

public class SysInitBean implements InitializingBean, ServletContextAware {
public void afterPropertiesSet() throws Exception {
}

@Override
public void setServletContext(ServletContext servletContext) {
}
}

在spring配置文件中添加:
<bean class="***.***.SysInitBean"/>

方法三:
实现javax.servlet.ServletContextListener:
public class RedisInitListener implements ServletContextListener {

@Override
public void contextDestroyed(ServletContextEvent sce) {

}

@Override
public void contextInitialized(ServletContextEvent sce) {
//WebApplicationContext wa = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}

在web.xml中添加listener:
<listener>
<listener-class>***.***.RedisInitListener</listener-class>
</listener>

proguard混淆器使用指南

下载地址

http://proguard.sourceforge.net/

启动proguard

解压下载的proguard4.4beta3,解压后进入lib文件把proguard.jar拷贝到自己新建的文件夹里如图:

把需要混淆的jar和jar所依赖的包也放到新建的文件夹,都放在一起,如下图。

写一个配置文件,名称自己定,举个简单配置文件例子,内容如下,其中-injars:是你需要混淆的jar,-outjars:是你混淆后输出的jar,-libraryjars:是你混淆的jar需要所依赖的jar包,后面的不在一一说明,可以参考proguard文档,配置文件可以参考文档来对自己混淆的需求来写配置文件。

-injars TheFirstDesktopApplication1.jar

-outjars TheFirstDesktopApplication1_out.jar

-libraryjars <java.home>/lib/rt.jar

-libraryjars appframework-1.0.3.jar

-libraryjars swing-worker-1.1.jar

-printmapping proguard.map

-overloadaggressively

-defaultpackage ''

-allowaccessmodification

-dontoptimize

-keep public class *

{

public protected *;

}

-keep public class org.**

-keep public class it.**

4. 把配置文件保存到你建的文件夹下,如下图。

5.点击开始,运行,输入cmd,进入你建的文件夹下,如下图。

6. 然后输入命令语句:java -jar proguard.Jar @a 然后回车,如下图。

7. 混淆成功,在产生TheFirstDesktopApplication1_out.jar如下图。

混淆器后,利用反编译器对没TheFirstDesktopApplication1_out.jar反编译,多了好多a,b,c之类的类文件,对反编译的java文件是很难编译的,即说明混淆成功。

混淆器原理功能

通常情况下,编译后的字节码仍然包含了大量的调试信息:源文件名,行号,字段名,方法名,参数名,变量名等等。这些信息使得它很容易被反编译和通过逆向工程获得完整的程序。例如像ProGuard这样的混淆器就能删除这些调试信息,并用无意义的字符序列来替换所有名字,使得它很难进行逆向工程,它进一步免费的精简代码。除了异常堆栈信息所需要的类名,方法名和行号外,程序只会保留功能上的等价。

ProGuard介绍

你可以用JB来简单的把JAVA 文件混淆一下,
然后,再发布就是,
你也可以使用。proguard

用法如下:

ProGuard是一款免费的Java类文件压缩器、优化器和混淆器。它能发现并删除无用类、字段(field)、方法和属性值(attribute)。它也能优化字节码并删除无用的指令。最后,它使用简单无意义的名字来重命名你的类名、字段名和方法名。经过以上操作的jar文件会变得更小,并很难进行逆向工程。这里提到了ProGuard的主要功能是压缩、优化和混淆,下面我就先介绍一下这些概念,然后再介绍ProGuard的基本使用方法。

l 什么是压缩:

Java 源代码(.java文件)通常被编译为字节码(.class文件)。而完整的程序或程序库通常被压缩和发布成Java文档(.jar文件)。字节码比 Java源文件更简洁,但是它仍然包含大量的无用代码,尤其它是一个程序库的时候。ProGuard的压缩程序操作能分析字节码,并删除无用的类、字段和方法。程序只保留功能上的等价,包括异常堆栈描述所需要的信息。

l 什么是混淆:

通常情况下,编译后的字节码仍然包含了大量的调试信息:源文件名,行号,字段名,方法名,参数名,变量名等等。这些信息使得它很容易被反编译和通过逆向工程获得完整的程序。有时,这是令人厌恶的。例如像ProGuard这样的混淆器就能删除这些调试信息,并用无意义的字符序列来替换所有名字,使得它很难进行逆向工程,它进一步免费的精简代码。除了异常堆栈信息所需要的类名,方法名和行号外,程序只会保留功能上的等价。通过以上的了解,你应该明白为什么需要混淆了。

l ProGuard支持那些种类的优化:

除了在压缩操作删除的无用类,字段和方法外,ProGuard也能在字节码级提供性能优化,内部方法有:

² 常量表达式求值

² 删除不必要的字段存取

² 删除不必要的方法调用

² 删除不必要的分支

² 删除不必要的比较和instanceof验证

² 删除未使用的代码

² 删除只写字段

² 删除未使用的方法参数

² 像push/pop简化一样的各种各样的peephole优化

² 在可能的情况下为类添加static和final修饰符

² 在可能的情况下为方法添加private, static和final修饰符

² 在可能的情况下使get/set方法成为内联的

² 当接口只有一个实现类的时候,就取代它

² 选择性的删除日志代码

实际的优化效果是依赖于你的代码和执行代码的虚拟机的。简单的虚拟机比有复杂JIT编译器的高级虚拟机更有效。无论如何,你的字节码会变得更小。

仍有一些明显需要优化的技术不被支持:

² 使非final的常量字段成为内联

² 像get/set方法一样使其他方法成为内联

² 将常量表达式移到循环之外

² Optimizations that require escape analysis

ProGuard 是一个命令行工具,并提供了图形化用户界面,它也可以结合Ant或J2ME Wireless Toolkit使用。通过ProGuard得到的更精简的jar文件意味着只需要更小的存储空间;网络传输更省时;装载速度更快和占用更小的内存空间。另外,ProGuard非常快速和高效,它仅仅只花费几秒钟和几兆的内存在处理程序。它处理的顺序是先压缩,然后优化,最后才进行混淆。The results section presents actual figures for a number of applications.与其他Java混淆器相比,ProGuard的主要优势可能是它的基于模版文件的简单配置。一些直观的命令行选项或一个简单的配置文件已经足够了。例如,下面的配置选项保护了jar文件里的所有applets:

-keep public class * extends java.applet.Applet

用户指南里说明了所有可用的选项,并以大量的例子为你演示这些功能强大的配置选项。

上面谈到了ProGuard的很多好处,现在我们就来看看如何在程序中使用ProGuard吧,之前也提到了ProGuard可以用命令行、图形界面、Ant等来执行和处理程序,同时也提到了配置文件,下面我们一起来看如何使用:

用命令行执行ProGuard的命令如下:

java –jar proguard.jar options……

具体的选项可以参考ProGuard的用户指南,你也可以把这些属性写在配置文件里;运行时,我们只需要指定这个配置文件就行了,例如:

java –jar proguard.jar @config.pro

而配置文件的格式也是要按照ProGuard提供的格式来写的,这个可以参考ProGuard例子里的配置文件来配置适合你的应用系统的ProGuard配置文件。ProGuard提供了图形界面的配置和运行程序,你可以在界面上配置你想要的参数,然后运行即可。前面提到的要手动写的配置文件也可以用图形界面来配置和生成。

如果你要在Ant里运行ProGuard,只需要添加一一个如下的target即可:

<target name="proguard" depends="init">

<taskdef resource="proguard/ant/task.properties" classpath="${lib.dir}/proguard/proguard.jar" />

<proguard configuration="${src.dir}/config.pro" />

</target>

你只需要制定lib.dir和src.dir属性就行了,同样的,这里也用了proguard配置文件,跟上面提到的是一样的。建议大家把ProGuardGUI当成一个生成配置文件的向导来使用,这样我们只需要修改配置文件而不用重新写一个配置文件。

如果你觉得ProGuard还不错,那就快把它加入你的项目里吧。

ProGuard工具通过移除不用的代码,用语义上混淆的名字来重命名类、字段和方法等手段来压缩、优化和混淆你的代码。结果是更小的.apk文件,并且更难于被反编译。由于ProGuard能够让你的程序难于被反编译,因此,当你的程序使用了一些机密的信息的时,使用它就显得更加重要。

ProGuard已经集成到Android的编译环境中,因此,用不着手动来触发它。ProGuard只在release模式下编译应用程序才会运行,所以,在debug模式下编译,你就不必处理混淆的代码。是否运行ProGuard是完全可选的,但强烈推荐使用。

这篇文章将描述如何启用和配置ProGuard,以及如何使用retrace工具来解码混淆过的堆栈跟踪信息。

启用ProGuard

当你创建Android工程时,proguard.cfg文件会在工程的根目录自动创建。这个文件定义了ProGuard如何优化和混淆代码,因此,理解如何定制它是非常重要的。默认的配置文件只是覆盖了一些通用的情况,所以,基本上你需要编辑它来满足你的需求。参考后面的“配置ProGuard”章节来了解如何定制ProGuard的相关信息。

启用ProGuard让它跟随Ant或Eclipse编译时一起运行,你需要在<project_root>/default.properties文件中设置proguard.config属性。路径可以是绝对路径或是工程根目录的相对路径。

如果你把proguard.cfg文件放在默认的位置(工程的根目录),你可以像这样来指定它的位置:

proguard.config=proguard.cfg

你还可以把该文件移到任何你想放的位置,然后指定绝对路径:

proguard.config=/path/to/proguard.cfg

当你在release模式下编译你的程序,不管是用ant release还是用Eclipse的导出向导,编译系统都会自动检查proguard.config属性是否设置。如果设置了,ProGuard就会在打包成.apk文件之前,自动处理应用程序的字节码。Debug模式编译,不会触发ProGuard,因为它会使得调试更加复杂累赘。

ProGuard运行结束后,输出以下文件:

dump.txt

描述.apk文件中所有类文件间的内部结构

mapping.txt

列出了原始的类,方法和字段名与混淆后代码间的映射。这个文件很重要,当你从release版本中收到一个bug报告时,可以用它来翻译被混淆的代码。

seeds.txt

列出了未被混淆的类和成员

usage.txt

列出了从.apk中删除的代码

这些文件放在以下文件夹中:

· Ant:<project_root>/bin/proguard

· Eclipse: <project_root>/proguard

注意:每当你在release模式下编译时,这些文件都会被覆盖重写,当然,是被ProGuard工具生成的最新的文件所覆盖。每次你发布你的程序时,都应该保存一份,为了将来能够解码bug报告。

配置 ProGuard

一些情况下,proguard.cfg文件中的默认配置就足够了。然而,有些情况ProGuard也很难正确分析,它可能会删除它认为不用的代码,但实际上正是你的程序所需要的。例如:

l 只在AndroidManifest.xml文件中引用的类

l 由JNI调用的方法

l 动态引用的字段和方法

默认的proguard.cfg文件努力去覆盖通用的情况,但有可能你会遇到如ClassNotFoundException这样的异常,而这正好是由于ProGuard移除了整个类造成的。

你可以修正由于ProGuard移除代码造成的错误,只需要在proguard.cfg文件中添加一行“-keep”。例如:

-keep public class <MyClass>

使用-keep选项时,有一些选项和建议,因此,强烈建议你阅读ProGuard手册来了解更多关于定制配置文件的信息。“Overview of Keep options”和“Examples section”将非常有用。而“Troubleshooting”章节也列出了一些当你的代码被删除时你可能会遇到的一些常见问题。

解码混淆过的堆栈跟踪信息

当混淆后的代码输出一个堆栈信息时,方法名是不可识别的,这使得调试变得很困难,甚至是不可能的。幸运的是,当ProGuard运行时,它都会输出一个<project_root>/bin/proguard/mapping.txt文件,而这个文件中包含了原始的类,方法和字段名被映射成的混淆名字。

retrace.bat脚本(Window)或retrace.sh脚本(Linux,Mac OS X)可以将一个被混淆过的堆栈跟踪信息还原成一个可读的信息。它位于<sdk_root>/tools/proguard文件夹中。执行retrace工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

例如:

retrace.bat -verbose mapping.txt obfuscated_trace.txt

如果你没有指定<stacktrace_file>,retrace工具会从标准输入读取。

发布程序的调试建议

每次发布程序给用户时,都应该保存一份mapping.txt。这样的话,当用户遇到一个bug并提交一个混淆的堆栈信息,能确保你能调试这个问题。工程的mapping.txt文件在你每次进行release编译时都会被覆盖,所以,你一定要小心的保存你需要的版本。

如何保存mapping.txt文件是你的事。例如,你可以用一个包含版本信息或编译号的名字来重命名文件,或者让其和你的代码一样进行版本控制。

以下面的Test.java文件为例: import java.util.*;

import java.io.*;

public class Test {

public static void main(String[] args) throws Exception {

BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));

String s = stdin.readLine();

if(s.length()%2==0)

System.out.println(showMsg1());

else

System.out.println(showMsg2());

Flight a = new Flight();

a.setName(s);

System.out.println(a.output());

Bomber b = new Bomber();

b.setName(s);

System.out.println(b.output());

s = stdin.readLine();

StringTokenizer st = new StringTokenizer(s);

int n = Integer.parseInt(st.nextToken());

System.out.println(compute(n));

}

 

public static String showMsg1() {

return "You are my sun1";

}

 

public static String showMsg2() {

return "You are my sun2";

}

 

public static int compute(int n) {

if(n>1)

return n*compute(n-1);

else

return 1;

}

 

public static class Flight{

public Flight(){

}

 

public String output(){

return this.name;

}

 

public void setName(String name){

this.name="Flight:"+name;

}

 

private String name;

}

 

public static class Bomber{

public Bomber(){

}

 

public String output(){

return this.name;

}

 

public void setName(String name){

this.name="Bomber:"+name;

}

 

private String name;

}

}

首先jar cvf a.jar *.class打包程序,然后jad -d d:\ -r -s java d:\*.class反编译程序,生成Test.java文件,通过对比可以发现,它和原来文件的内容基本是相同的。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

// Jad home page: http://www.kpdus.com/jad.html

// Decompiler options: packimports(3)

// Source File Name: Test.java

import java.io.*;

import java.util.StringTokenizer;

public class Test

{

public static class Bomber

{

public String output()

{

return name;

}

public void setName(String s)

{

name = (new StringBuilder()).append("Bomber:").append(s).toString();

}

private String name;

public Bomber()

{

}

}

public static class Flight

{

public String output()

{

return name;

}

public void setName(String s)

{

name = (new StringBuilder()).append("Flight:").append(s).toString();

}

private String name;

public Flight()

{

}

}

public Test()

{

}

public static void main(String args[])

throws Exception

{

BufferedReader bufferedreader = new BufferedReader(new InputStreamReader(System.in));

String s = bufferedreader.readLine();

if(s.length() % 2 == 0)

System.out.println(showMsg1());

else

System.out.println(showMsg2());

Flight flight = new Flight();

flight.setName(s);

System.out.println(flight.output());

Bomber bomber = new Bomber();

bomber.setName(s);

System.out.println(bomber.output());

s = bufferedreader.readLine();

StringTokenizer stringtokenizer = new StringTokenizer(s);

int i = Integer.parseInt(stringtokenizer.nextToken());

System.out.println(compute(i));

}

public static String showMsg1()

{

return "You are my sun1";

}

public static String showMsg2()

{

return "You are my sun2";

}

public static int compute(int i)

{

if(i > 1)

return i * compute(i - 1);

else

return 1;

}

}

进入Proguard的lib目录,用JDK打开proguardgui.jar,点选Input/Output标签,选择要混淆的JAR包(注意是JAR包),输出JAR包,以及用到的所有类库。

点选Obfuscation标签,选中不需要混淆的类(要被反射的类绝对不能被混淆),一般是1,4,5,9,10,11

,12这几个选项。

a.txt的文件内容为:(混淆函数名)

Gcd

b.txt的文件内容为:(混淆类名)

A

B

点选Process标签,Process按钮,生产b.jar

解压b.jar后,这时的3个class文件分别为A.class、B.class、Test.class;

重新反编译程序jad -d d:\b\ -r -s java d:\b\*.class,生成3个java文件:A.java、B.java、Test.java,具体内容如下:

A.java // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

// Jad home page: http://www.kpdus.com/jad.html

// Decompiler options: packimports(3)

public final class A

{

public A()

{

}

public final String Gcd()

{

return Gcd;

}

public final void Gcd(String s)

{

Gcd = (new StringBuilder()).append("Bomber:").append(s).toString();

}

private String Gcd;

}B.java // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

// Jad home page: http://www.kpdus.com/jad.html

// Decompiler options: packimports(3)

public final class B

{

public B()

{

}

public final String Gcd()

{

return Gcd;

}

public final void Gcd(String s)

{

Gcd = (new StringBuilder()).append("Flight:").append(s).toString();

}

private String Gcd;

}Test.java // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

// Jad home page: http://www.kpdus.com/jad.html

// Decompiler options: packimports(3)

import java.io.*;

import java.util.StringTokenizer;

public class Test

{

public Test()

{

}

public static void main(String args[])

{

String s;

if((s = (args = new BufferedReader(new InputStreamReader(System.in))).readLine()).length() % 2 == 0)

System.out.println("You are my sun1");

else

System.out.println("You are my sun2");

Object obj;

((B) (obj = new B())).Gcd(s);

System.out.println(((B) (obj)).Gcd());

((A) (obj = new A())).Gcd(s);

System.out.println(((A) (obj)).Gcd());

s = args.readLine();

args = Integer.parseInt((args = new StringTokenizer(s)).nextToken());

System.out.println(Gcd(args));

}

private static int Gcd(int i)

{

if(i > 1)

return i * Gcd(i - 1);

else

return 1;

}

}

通过对比可以发现,它和原来文件的内容有许多出入。

这里只提一下使用proguard需要注意的几个地方:

1、注意版本,如果不是项目需要,最好使用最新稳定版本,这样出错的机会小一些,学习曲线会明显缩短。

2、最好以源码的同编译版本的jvm来启动proguardgui.jar,不要直接用默认的jvm打开。

3、注意proguaardgui启动后默认加载的libraryjar中包含的rt.jar,它的版本和你使用到的jvm版本一致。

4、使用1.4的jvm打开proguard时,注意在optimization选项卡中,不要勾中keep enumerations选项,因为勾中这个选项会导致configration文件中出现java.lang.Enum,这个类

只有在1.5的环境下才有。报错信息如下:

[proguard] Note: the configuration refers to the unknown class 'java.lang.Enum'

[proguard] Note: there were 1 references to unknown classes.

[proguard] You should check your configuration for typos.

5、嵌入ant脚本时,通常会有这么一段

<target name="proguard" depends="init">

<taskdef resource="proguard/ant/task.properties" classpath="${lib.dir}/proguard/proguard.jar" />

<proguard configuration="${src.dir}/config.pro" />

</target>

task.properties在proguard.jar中,保留,config.pro是proguard的配置文件,最好使用proguardgui.jar先生成,然后再根据需求手动修改。

Nginx模块fastcgi_cache的几个注意点

在web项目中,大家都已经非常熟悉其架构流程了。都说Cache是万金油,哪里不舒服抹哪里。这些流程中,几乎每个环节都会进行cache。 从浏览器到webserver,到cgi程序,到DB数据库,会进行浏览器cache,数据cache,SQL查询的cache等等。对于fastcgi 这里的cache,很少被使用。去年年底,我对nginx的fastcgi_cache进行摸索使用。在我的测试过程中,发现一些WIKI以及网络上没被提到的注意点,这里分享一下。

从浏览器到数据库的流程图

这里是我的NGinx配置信息

01
02
03
04
05
06
07
08
09
10
11
12
13
#增加调试信息
add_header X-Cache-CFC "$upstream_cache_status - $upstream_response_time";
fastcgi_temp_path /dev/shm/nginx_tmp;
#cache设置
fastcgi_cache_path   /dev/shm/nginx_cache  levels=1:2 keys_zone=cfcache:10m inactive=50m;
fastcgi_cache_key "$request_method://$host$request_uri";
fastcgi_cache_methods GET HEAD;
fastcgi_cache   cfcache;
fastcgi_cache_valid   any 1d;
fastcgi_cache_min_uses  1;
fastcgi_cache_use_stale error  timeout invalid_header http_500;
fastcgi_ignore_client_abort on;

配置这些参数时,注意每个参数的作用域,像fastcgi_cache_path参数,只能在http配置项里配置,而 fastcgi_cache_min_uses这个参数,可以在http、server、location三个配置项里配置。这样更灵活的会每个域名、每 个匹配的location进行选择性cache了。具体的参数作用域,参考FASTCGI模块的官方WIKI。我为了调试方便,添加了一个『X-Cache-CFC』的http响应头,$upstream_cache_status 变量表示此请求响应来自cache的状态,分别为:

  • MISS 未命中
  • EXPIRED – expired, request was passed to backend Cache已过期
  • UPDATING – expired, stale response was used due to proxy/fastcgi_cache_use_stale updating Cache已过期,(被其他nginx子进程)更新中
  • STALE – expired, stale response was used due to proxy/fastcgi_cache_use_stale Cache已过期,响应数据不合法,被污染
  • HIT 命中cache

FASTCGI_CACHE $upstream_cache_status 结果为miss,一次也没命中

程序代码是Discuz!论坛, 随便开启测试了几下,发现/dev/shm/nginx_cache/下没有任何目录建立,也没有文件创建。调试的http header响应头里的X-Cache-CFC 结果一直是MISS。从服务器进程上来看,Nginx cache manager process 跟Nginx cache loader process 进程也正常运行:

1
2
3
4
root      3100     1  0 14:52 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  3101  3100  0 14:52 ?        00:00:00 nginx: worker process
www-data  3102  3100  0 14:52 ?        00:00:00 nginx: cache manager process
www-data  3103  3100  0 14:52 ?        00:00:00 nginx: cache loader process

不知道为何会这样,为何没有cache成功,我以为我配置参数有问题,只好阅读WIKI。发现fastcgi_ignore_headers 参数下解释有这么一段

fastcgi_ignore_headers
Syntax: fastcgi_ignore_headers field …
Default:
Context: http
server
location
Reference: fastcgi_ignore_headers

This directive forbids processing of the named headers from the FastCGI-server reply. It is possible to specify headers like “X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”.

也就是说这个参数的值,将会被忽略掉,同样被忽略掉的响应头比如”X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”,而nginx配置中并没有fastcgi_ignore_headers参数的设定,那么问题会不会出现在 FASTCGI响应结果里包含了类似”X-Accel-Redirect”, “X-Accel-Expires”, “Expires” or “Cache-Control”这几个响应头呢?用strace抓包,看了下nginx与fpm进程通讯的数据

1
2
3
4
5
6
####为了确保准确抓到处理该http请求的进程,我把nginx 、fpm都只开启了一个进程处理。
//strace -ff -tt -s 1000 -o xxx.log -p PHPFPM-PID
14:52:07.837334 write(3, "\1\6\0\1\0\343\5\0X-Powered-By: PHP/5.3.10-1ubuntu3.5\r\nExpires: Thu, 19 Nov 1981 08:52:00 GMT\r\nCache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\nPragma: no-cache\r\nContent-type: text/html\r\n\r\nHello cfc4n1362034327\0\0\0\0\0\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 256) = 256
//strace -ff -tt -s 1000 -o xxx.log -p Nginx-PID
15:05:13.265663 recvfrom(12, "\1\6\0\1\0\343\5\0X-Powered-By: PHP/5.3.10-1ubuntu3.5\r\nExpires: Thu, 19 Nov 1981 08:52:00 GMT\r\nCache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\nPragma: no-cache\r\nContent-type: text/html\r\n\r\nHello cfc4n1362035113\0\0\0\0\0\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 4023, 0, NULL, NULL) = 256

从抓取的数据包里可以看到,fpm确实返回了包含“Expires”、“Cache-Control”头的http 响应头信息。那么疑问来了:

  • nginx的fastcgi_cache没缓存这条http响应,是因为响应头里包含“Expires”、“Cache-Control”的原因吗?
  • 程序里并没有输出“Expires”、“Cache-Control” http header的代码,这是谁输出的呢?
  • 既然是fpm响应的时候,就已经有了,那么是php的core模块,还是其他拓展模块输出的?
  • “Expires:”时间为何是“Thu, 19 Nov 1981 08:52:00 GMT”?

疑问比较多,一个一个查起,先从Nginx的fastcgi_cache没缓存这条http响应查起。我根据测试环境nginx版本 1.1.9(ubuntu 12.04默认的),到nginx官方下了对应版本的源码,搜索了fastcgi参数使用的地方,在http\ngx_http_upstream.c找 到了。虽然不能很流程的读懂nginx的代码,但粗略的了解,根据了解的情况加以猜测,再动手测试实验,也得出了结论,确定了nginx的 fastcgi_cache的规则。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//ngx_http_upstream.c
//line 3136  当fastcgi响应包含set-cookie时,不缓存
static ngx_int_t
ngx_http_upstream_process_set_cookie(ngx_http_request_t *r, ngx_table_elt_t *h,
    ngx_uint_t offset)
{
#if (NGX_HTTP_CACHE)
    ngx_http_upstream_t  *u;
    u = r->upstream;
    if (!(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_SET_COOKIE)) {
        u->cacheable = 0;
    }
#endif
    return NGX_OK;
}
//line 3242 当响应头包含Expires时,如果过期时间大于当前服务器时间,则nginx_cache会缓存该响应,否则,则不缓存
static ngx_int_t
ngx_http_upstream_process_expires(ngx_http_request_t *r, ngx_table_elt_t *h,
    ngx_uint_t offset)
{
    ngx_http_upstream_t  *u;
    u = r->upstream;
    u->headers_in.expires = h;
#if (NGX_HTTP_CACHE)
    {
    time_t  expires;
    if (u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_EXPIRES) {
        return NGX_OK;
    }
    if (r->cache == NULL) {
        return NGX_OK;
    }
    if (r->cache->valid_sec != 0) {
        return NGX_OK;
    }
    expires = ngx_http_parse_time(h->value.data, h->value.len);
    if (expires == NGX_ERROR || expires < ngx_time()) {         u->cacheable = 0;
        return NGX_OK;
    }
    r->cache->valid_sec = expires;
    }
#endif
    return NGX_OK;
}
//line 3199  当响应头包含Cache-Control时,#####如果####这里有如果啊。。。
//【注意】如果Cache-Control参数值为no-cache、no-store、private中任意一个时,则不缓存...不缓存...
//【注意】如果Cache-Control参数值为max-age时,会被缓存,且nginx设置的cache的过期时间,就是系统当前时间 + mag-age的值
    if (ngx_strlcasestrn(p, last, (u_char *) "no-cache", 8 - 1) != NULL
        || ngx_strlcasestrn(p, last, (u_char *) "no-store", 8 - 1) != NULL
        || ngx_strlcasestrn(p, last, (u_char *) "private", 7 - 1) != NULL)
    {
        u->cacheable = 0;
        return NGX_OK;
    }
    p = ngx_strlcasestrn(p, last, (u_char *) "max-age=", 8 - 1);
    if (p == NULL) {
        return NGX_OK;
    }
    ...
    r->cache->valid_sec = ngx_time() + n;

也就是说,fastcgi响应http请求的结果中,响应头包括Expires、Cache-Control、Set-Cookie三个,都会可能 不被cache,但不只有这些,别忘了nginx配置中fastcgi_ignore_headers参数设定的部分。以及ngxin的X-ACCEL X-Accel-Redirect、X-Accel-Expires、X-Accel-Charset、X-Accel-Buffering等nginx 自定义的响应头。由于这几个不常用,我也没深入研究。通过对nginx的ngx_http_upstream模块代码模糊理解,加猜测,以及写了脚本测试 验证,可以得到结论是正确的。即Nginx fastcgi_cache在缓存后端fastcgi响应时,当响应里包含“set-cookie”时,不缓存;当响应头包含Expires时,如果过期 时间大于当前服务器时间,则nginx_cache会缓存该响应,否则,则不缓存;当响应头包含Cache-Control时,如果Cache- Control参数值为no-cache、no-store、private中任意一个时,则不缓存,如果Cache-Control参数值为max- age时,会被缓存,且nginx设置的cache的过期时间,就是系统当前时间 + mag-age的值。

nginx fastcgi_cache 响应expired

nginx fastcgi_cache hit命中

FASTCGI_CACHE $upstream_cache_status 结果为miss,一次也没命中。

01
02
03
04
05
06
07
08
09
10
//逐个测试,测试时,注释其他的
header("Expires: ".gmdate("D, d M Y H:i:s", time()+10000).' GMT');
header("Expires: ".gmdate("D, d M Y H:i:s", time()-99999).' GMT');
header("X-Accel-Expires:30");
header("Cache-Control: no-cache");
header("Cache-Control: no-store");
header("Cache-Control: private");
header("Cache-Control: max-age=10");
setcookie('cfc4n',"testaaaa");
echo 'Hello cfc4n',time();

到了这里,疑问1解决了。那么疑问2、3呢?程序里并没有输出“Expires”、“Cache-Control” http header的代码,这是谁输出的呢?既然是fpm响应的时候,就已经有了,那么是php的core模块,还是其他拓展模块输出的?我精简了代码,只输出 一个“hello world”,发现也确实被缓存了。显然,php脚本程序中并没输出http header 的“Expires”、“Cache-Control”,多次测试,最终定位到session_start函数,翻阅源码找到了这些代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//ext/session/session.c  line:1190 左右
// ...
CACHE_LIMITER_FUNC(private) /* {{{ */
{
    ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT");
    CACHE_LIMITER(private_no_expire)(TSRMLS_C);
}
/* }}} */
//再到这里3 或者上面几个 ##默认是nocache
CACHE_LIMITER_FUNC(nocache) /* {{{ */
{
    ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT");
    /* For HTTP/1.1 conforming clients and the rest (MSIE 5) */
    ADD_HEADER("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
    /* For HTTP/1.0 conforming clients */
    ADD_HEADER("Pragma: no-cache");
}
/* }}} */
//这里2
static php_session_cache_limiter_t php_session_cache_limiters[] = {
    CACHE_LIMITER_ENTRY(public)
    CACHE_LIMITER_ENTRY(private)
    CACHE_LIMITER_ENTRY(private_no_expire)
    CACHE_LIMITER_ENTRY(nocache)
    {0}
};
static int php_session_cache_limiter(TSRMLS_D) /* {{{ */
{
    php_session_cache_limiter_t *lim;
    if (PS(cache_limiter)[0] == '\0') return 0;
    if (SG(headers_sent)) {
        const char *output_start_filename = php_output_get_start_filename(TSRMLS_C);
        int output_start_lineno = php_output_get_start_lineno(TSRMLS_C);
        if (output_start_filename) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cache limiter - headers already sent (output started at %s:%d)", output_start_filename, output_start_lineno);
        } else {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cache limiter - headers already sent");
        }
        return -2;
    }
    for (lim = php_session_cache_limiters; lim->name; lim++) {
        if (!strcasecmp(lim->name, PS(cache_limiter))) {
            lim->func(TSRMLS_C);   //这里1
            return 0;
        }
    }
    return -1;
}
// ...

到了这里,知道原因了,是程序调用session_start时,php的session拓展自己输出的。session.cache_limit 参数来决定输出包含哪种Expires的header,默认是nocache,修改php.ini的session.cache_limit参数为 “none”即可让session模块不再输出这些http 响应头。或在调用session_start之前,使用session_cache_limiter函数来指定下该参数值。那为什么要在使用 session时,发Expires、Cache-Control的http response header呢?我猜测了下,需要session时,基本上是用户跟服务器有交互,那么,既然有交互,就意味着用户的每次交互结果也可能不一样,就不能 cache这个请求的结果,给返回给这个用户。同时,每个用户的交互结果都是不一样的,nginx也就不能把包含特殊Cache-Control的个人响 应cache给其他人提供了。

还有一个无聊的问题“Expires:时间为何是Thu, 19 Nov 1981 08:52:00 GMT”?我翻阅了session.c这段代码的添加时间,版本,作者信息,在php官方版本库中找到了这次提交的信息:

Revision 17092 – (view) (download) (as text) (annotate) – [select for diffs]
Modified Sun Dec 12 14:16:55 1999 UTC (13 years, 2 months ago) by sas
File length: 28327 byte(s)
Diff to previous 16964
Add cache_limiter and cache_expire options. Rename extern_referer_check
to referer_check.

对比session.c两个版本的变更,果然是这块代码。作者是sas,也就是Sascha Schumann, http://php.net/credits.php里可以看到他的大名。关于这个expires过期时间的问题,有人在stackoverflow也提问过,Why is “Expires” 1981?,别人说那天是他生日。这是真的么?如果那天是他生日的话,而他增加session.cache_limiter时是1999年,他才17岁,17岁呀。我17岁时在干嘛?还不知道电脑长啥样,正在玩『超级玛丽』呢。

好奇的不是我一个人,还有个帖子是epoch date — Expires: Thu, 19 Nov 1981 08:52:00也问了。另外两个地址虽然没问,也有人提到那天是他生日了。http://boinc.berkeley.edu/dev/forum_thread.php?id=2514、https://github.com/codeguy/Slim/issues/157,这些帖子都提到说原帖是http://www.phpbuilder.com/lists/php3-list/199911/3159.php ,我无法访问,被跳转到首页了。用http://web.archive.org找到了历史快照,发现上下文关系不大,也不能证明是他生日。 我更是好奇的发了两封邮件到他的不同邮箱里问他,不过,目前他还没回复。或许他没收到、没看到,或许懒得回了。N年后,“Expires:时间为何是Thu, 19 Nov 1981 08:52:00 GMT”这个日期,会不会又成了一段奇闻佳话了呢?

 

来源:http://www.cnxct.com/several-reminder-in-nginx-fastcgi_cache-and-php-session_cache_limiter/