流方式创建zip的JAX-RS RESTful文件下载API

JAX-RS(Java API for RESTful Web Services)是JavaEE标准的一部分,规范了RESTful WEB服务的编程接口。而其中一个大名鼎鼎的实现就是jersey了。这种标注式的开发方式极大地简化了RESTful API的创建过程。WEB请求/应答中的众多变量/参数仅通过标注就可以轻松截获和自动转换。

前几天,一个新的需求来了。要求用一个RESTful API实现小文件的打包下载,并且要做到zip包中的文件名和位置(entry)可以指定。也就是说zip包中的文件名不一定和源文件名相同。
一种实现方案是,拷贝文件到指定位置,并重命名,最后从外层文件夹打包。这种方案会创建临时文件和zip文件在磁盘上,在用户下载完文件之后还要做清理工作。用户什么时候下载完成呢,天知道,龟速的网速文件又很大下几天也说不定。因此这种方案逻辑虽然简单,但不可取。
另外一种实现就是使用流。源文件的内容如同流水一样流向一个输出流,输出流的目标就是网络IO。这种方式不会有任何临时磁盘文件,连在内存中创建临时缓冲都不需要,仅需完成之后关闭流。本文的主角ZipOutputStream就是这样的一个输出流。

ZipOutputStream的构造函数:
ZipOutputStream(OutputStream out);
对象out是真正的输出目标,可以是FileOutputStream,写入磁盘文件;可以是ByteArrayOutputStream,写入到内存;还可以是网络IO的输出流。
事实上out可以是任何的OutputStream实现类,甚至是ZipOutputStream。没错,我们甚至可以将ZipOutputStream嵌套起来用。

假设要创建一个zip包,里面有两个文件,2.zip和3.zip,2.zip中有文件utf-8.txt,3.zip中有文件gbk.txt。
不创建临时文件的流实现:

final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
FileOutputStream file = null;
ZipOutputStream zip1 = null;
ZipOutputStream zip2 = null;
ZipOutputStream zip3 = null;
try {
	file = new FileOutputStream("file.zip");
	zip1 = new ZipOutputStream(file);

	zip1.putNextEntry(new ZipEntry("2.zip"));
	zip2 = new ZipOutputStream(zip1);
	zip2.putNextEntry(new ZipEntry("utf-8.txt"));
	zip2.write(UTF8_BOM);
	zip2.write("これはUTF-8のテキストです。".getBytes("UTF-8"));
	zip2.closeEntry();
	zip2.finish();
	zip1.closeEntry();

	zip1.putNextEntry(new ZipEntry("3.zip"));
	zip3 = new ZipOutputStream(zip1);
	zip3.putNextEntry(new ZipEntry("gbk.txt"));
	zip3.write("这是一段GBK文本。".getBytes("GBK"));
	zip3.closeEntry();
	zip3.finish();
	zip1.closeEntry();

	zip1.finish();

	System.out.println("DONE!");
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
} finally {
	if (zip2 != null) {
		try {
			zip2.close();
		} catch (IOException e) {}
	}
	if (zip3 != null) {
		try {
			zip3.close();
		} catch (IOException e) {}
	}
	if (zip1 != null) {
		try {
			zip1.close();
		} catch (IOException e) {}
	}
}

流zip2和zip3的输出目标都是流zip1,zip1的输出目标是磁盘文件。
zip2和zip3的输出就是2.zip和3.zip的二进制内容,倘若直接保存到磁盘上,就是文件,如果交给zip1,zip1就会进行再一次压缩,不过那是zip1的内部事情。

因此ZipOutputStream的上游和下游都是二进制内容,我们不用关心内部的压缩逻辑,也无须在出入口进行流处理。
有了这个认识,应付各种打包需求是轻而易举的事情。

例如,一个来自磁盘文件,一个压根就不是文件而是程序中的一个字符串,将这两个打包在一起(忽略异常处理):

final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
FileOutputStream output = new FileOutputStream("demo.zip");
ZipOutputStream out = new ZipOutputStream(output);

out.putNextEntry(new ZipEntry("utf-8.txt"));
out.write(UTF8_BOM);
out.write("这是一段UTF-8文本".getBytes("UTF-8"));
out.closeEntry();

out.putNextEntry(new ZipEntry("image/fav.ico"));
int len = 0;
byte[] buffer = new byte[1024];
InputStream inputStream = ClassLoader.getResourceAsStream("fav.ico");
while ((len = inputStream.read(buffer)) > 0) {
	out.write(buffer, 0, len);
}
inputStream.close();
out.closeEntry();

out.flush();
out.finish();
out.close();

假如ZipOutputStream的输出目标是网络IO,那提供zip下载的RESTful API怎么写?

很多情况下,JAX-RS的Response直接用文件对象就能返回文件的二进制内容:

Response.ok(new File(...)).type("...").header("...").build();

但本文的情况,磁盘文件并不存在,有一种使用ByteArrayOutputStream的解决方法,这是先将内容写到内存,Response再从内存读取二进制内容。
如果zip文件很大,会明显增加内存的使用量

	@GET
	@Path("/get2")
	@Produces(MediaType.APPLICATION_OCTET_STREAM)
	public Response download2() {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ZipOutputStream zos = new ZipOutputStream(bos);

		try {
			zos.putNextEntry(new ZipEntry("utf-8.txt"));
			zos.write(UTF8_BOM);
			zos.write("这是一段UTF-8文本".getBytes("UTF-8"));
			zos.closeEntry();
			zos.flush();
			zos.finish();
			return Response.ok(bos.toByteArray(), "application/zip")
				.header("Content-Disposition", "attachment; filename=demo2.zip")
				.build();
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			try {
				zos.close();
			} catch (IOException e) {}
		}
	}

为了节省内存,跳过缓冲步骤,我们需要实现JAX-RS的StreamingOutput接口。
依据StreamingOutput的说明,当我们希望输出(Response的数据源头)是流时,可以使用它作为entity直接喂给Response。它是MessageBodyWriter的轻量化选择。

A type that may be used as a resource method return value or as the entity in a Response when the application wishes to stream the output. This is a lightweight alternative to a javax.ws.rs.ext.MessageBodyWriter.

StreamingOutput唯一的write方法的参数output就是我们寻找已久的输出目标,直接将ZipOutputStream的输出目标指向它即可

public interface StreamingOutput {
    /**
     * Called to write the message body. 
     * @param output the OutputStream to write to.
     * @throws java.io.IOException if an IO error is encountered
     * @throws javax.ws.rs.WebApplicationException if a specific 
     * HTTP error response needs to be produced. Only effective if thrown prior
     * to any bytes being written to output.
     */
    void write(OutputStream output) throws IOException, WebApplicationException; 
}

于是最终代码(略过了异常处理):

package kuyur.info.zipdemo.rest;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import kuyur.info.zipdemo.servlet.filter.AuthenticationFilter;

import com.sun.jersey.spi.container.ResourceFilters;

@ResourceFilters({ AuthenticationFilter.class})
@Path("/file")
public class ZipDemoService {
	private static final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
	private static final String FAV_ICO = "fav.ico";

	@GET
	@Path("/get")
	@Produces(MediaType.APPLICATION_OCTET_STREAM)
	public Response download() {
		StreamingOutput stream = new StreamingOutput() {
			@Override
			public void write(OutputStream output) throws IOException, WebApplicationException {
				ZipOutputStream out = new ZipOutputStream(output);

				out.putNextEntry(new ZipEntry("utf-8.txt"));
				out.write(UTF8_BOM);
				out.write("这是一段UTF-8文本".getBytes("UTF-8"));
				out.closeEntry();

				out.putNextEntry(new ZipEntry("image/fav.ico"));
				int len = 0;
				byte[] buffer = new byte[1024];
				InputStream inputStream = ZipDemoService.class.getResourceAsStream(FAV_ICO);
				while ((len = inputStream.read(buffer)) > 0) {
					out.write(buffer, 0, len);
				}
				inputStream.close();
				out.closeEntry();

				out.flush();
				out.finish();
				out.close();
			}
		};
		return Response.ok(stream, "application/zip")
			.header("Content-Disposition", "attachment; filename=demo.zip")
			.build();
	}
}

完整的工程文件:zipdemo,使用了maven解决包依赖,基本上可以作为RESTful Web Service的工程模板。

Haskell学习笔记: 变态的「$」函数

有多少种写法来计算3除以4的值?
在变态的$函数上柯里化,再加上Haskell支持前缀以及中缀表示法,可以衍生出无数种写法。

Prelude> (/4) 3
0.75
Prelude> (/4) $ 3
0.75
Prelude> ($ 3) (/4)
0.75
Prelude> ($ 3) $ (/4)
0.75
Prelude> fmap ($ 3) (Just (/4))
Just 0.75
Prelude> ($) (/4) 3
0.75
Prelude> ($) (/4) $ 3
0.75
Prelude> (3/) 4
0.75
Prelude> (/) 3 4
0.75
Prelude> (/) 3 $ 4
0.75
Prelude> (3/) $ 4
0.75
Prelude> ($ 4) $ (3/)
0.75
Prelude> ($ 4) (3/)
0.75
Prelude> ($) (3/) 4
0.75
Prelude> ($ (/4)) $ ($ 3)
0.75
Prelude> ($ (/4)) ($ 3)
0.75
Prelude> ($) ($ 3) (/4)
0.75
Prelude> let func = ($ (/4)) id
Prelude> func 3
0.75

一般对$函数原型的理解是第一个参数是函数,第二个参数是为该函数提供操作数据。这种理解虽然不错,但很容易落入第二个参数只能是数字或list等纯数据的思维定势。

Prelude> :t ($)
($) :: (a -> b) -> a -> b

$函数的第二个参数可以是任何类型,haskell中函数也是一种数据类型,也能成为参数!
像表达式

($) ($ 3) (/4)

,谁是函数谁是参数?已经完全和

($) (/4) 3

反了过来。
($ 3)这个函数,它接受函数(/4)为参数,把3作为参数提供给(/4)并执行,最后返回(/4)的执行结果。

「$」的柯里化和中缀就是这么变态

3,585 次浏览 | 1 条评论
2012年9月12日 | 归档于 程序

SyntaxHighlighter Evolved的Haskell高亮插件

SyntaxHighlighter Evolved是余正在用的语法高亮插件,但还不支持Haskell
搜索一下,发现已经有人实现了
http://arieshout.me/2011/05/haskell-brush-for-syntax-highlighter.html

但如果直接上传shBrushHaskell.js,并hack SyntaxHighlighter Evolved的代码,虽然一时可以解决,但插件更新后又将恢复到原样
SyntaxHighlighter Evolved的作者提供了扩展接口,还给出了有效的wordpress插件解决方案,稍稍修改一下sample中的名字就能用了

于是为了以后省事,自己弄了这个wordpress插件
(高亮脚本来自http://arieshout.me,插件代码来自SyntaxHighlighter Evolved原作者,余干的就只是打包在一起来)

插件下载:
syntaxhighlighter-brush-haskell

使用方法:解压后上传到$BLOG_FOLDER/wp-content/plugins目录

高亮语法:
[hs][/hs]
[hask][/hask]
[haskell][/haskell]

5,175 次浏览 | 1 条评论
2012年9月11日 | 归档于 技术

Haskell新手程序:计算逆波兰表达式

计算逆波兰表达式的值是Haskell教材《Learn you a haskell》第十章的题目
关于逆波兰表达式,可以参考维基百科逆波兰表示法词条

不看教程试一下自己实现foldingFunction,最终结果变成了这样
(不考虑任何异常处理)

import Data.List

data Operator = Add | Sub | Mul | Div

main = do
    exp <- getLine
    putStrLn $ show (solveRPN exp)

solveRPN :: String -> Float
solveRPN exp = head (foldl foldingFunction [] $ words exp)

foldingFunction :: [Float] -> String -> [Float]
foldingFunction stack item
    | item == "+" = calculate stack Add
    | item == "-" = calculate stack Sub
    | item == "*" = calculate stack Mul
    | item == "/" = calculate stack Div
    | otherwise = (read item) : stack

calculate :: (Fractional a) => [a] -> Operator -> [a]
calculate stack ope =
    let num1 = stack !! 1
        num2 = stack !! 0
        num = case ope of Add -> (num1 + num2)
                          Sub -> (num1 - num2)
                          Mul -> (num1 * num2)
                          Div -> (num1 / num2)
        result = num : (tail $ tail stack)
    in result

如果只考虑四则运算,教程的实现是

solveRPN :: String -> Float  
solveRPN = head . foldl foldingFunction [] . words  
    where   foldingFunction (x:y:ys) "*" = (x * y):ys
            foldingFunction (x:y:ys) "+" = (x + y):ys
            foldingFunction (x:y:ys) "-" = (y - x):ys
            foldingFunction (x:y:ys) "/" = (y / x):ys
            foldingFunction xs numberString = read numberString:xs

余的实现的几个缺点
1.代码又长又臭,完全没有发挥Haskell模式匹配的威力
2.为了代码复用,结果又加多了一个函数原型几乎和foldingFunction一样的calculate函数,多此一举
3.自定义的数据类型Operator也是多此一举 (从命令式编程还没转过来)
4.calculate的实现为二元运算,当需要扩展到一元运算(如对数运算ln)时,需要重写

3,294 次浏览 | 没有评论
2012年9月11日 | 归档于 程序

ubuntu上安装Oracle JDK

以前写了一个为CentOS安装JDK的脚本,每次安装跑一次脚本就行了。
今天在ubuntu上安装JDK,才发现ubuntu好讨厌,相异的地方太多了

ubuntu已经从源中移除了Oracle JDK,用apt-get再也安装不了,只能手动下载安装
Oracle JDK以前还可以直接用wget下载,大概半年前起多了cookies认证,否则不给下
两家都是超讨厌的公司

用wget下载时在header中插入cookie即可

cd /home/ubuntu
wget -O jdk-6u34-linux-x64.bin --no-cookies --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2Ftechnetwork%2Fjava%2Fjavase%2Fdownloads%2Fjdk6-downloads-1637591.html;" http://download.oracle.com/otn-pub/java/jdk/6u34-b04/jdk-6u34-linux-x64.bin
chmod +x jdk-6u34-linux-x64.bin

安装到/usr/java目录下。

cd /usr
sudo mkdir java
cd java
sudo /home/ubuntu/jdk-6u34-linux-x64.bin

为当前安装的JDK创建别名是为了以后能够快速切换JDK版本(只需要改动软链接)

sudo ln -s /usr/java/jdk1.6.0_34 default

修改环境变量

sudo vim /etc/environment

注意应当往$CLASSPATH中加入.,以便支持java程序运行时从运行位置加载库

PATH=”/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/java/default/bin”
JAVA_HOME=”/usr/java/default”
CLASSPATH=”.:/usr/java/default/jre/lib/rt.jar:/usr/java/default/lib/dt.jar:/usr/java/default/lib/tools.jar”

需要退出重新登录一次以更新环境变量
检查是否正确

echo $JAVA_HOME
echo $CLASSPATH
java -version

如果安装Oracle JDK前ubuntu已经安装了Open JDK作为默认JDK,还得用update-alternatives修改默认JDK
update-alternatives和CentOS上的alternatives用法大体相同,就不再多说了

附:CentOS用JDK安装脚本

#!/bin/sh

TMP=/tmp
JDK_PACKAGE=jdk-6u32-linux-i586.bin
JDK_FOLDER=jdk1.6.0_32
JDK_LINK=/usr/bin/java
DEF_JDK_BASEDIR=/usr/java

usage() {
	cat <<EOS
Usage: $0 [OPTIONS]
  --jdk-basedir=        : JDK_BASEDIR
                          default: $DEF_JDK_BASEDIR
  --help                : What you're looking at.
EOS
}

parse_opts() {
	while [ $# -ne 0 ]; do
		opt=`echo $1 | cut -d'=' -f1`
		val=`echo $1 | cut -d'=' -f2`

		case "$opt" in
		--jdk-basedir) JDK_BASEDIR=$val;;
		--help)
			usage $0
			exit 1
			;;
		esac

		shift 1
	done
	if [ "$JDK_BASEDIR" = "" ]; then
		JDK_BASEDIR=$DEF_JDK_BASEDIR
	fi
}

parse_opts $*

# remove open-jdk
echo -n "NOTICE: open-jdk will be replaced by JDK 1.6.0(Oracle $JDK_FOLDER) as default JDK. Are you sure?[y/n]"
read YES_OR_NO
if [[ "$YES_OR_NO" != "y" && "$YES_OR_NO" != "Y" ]]; then
	exit 1
fi

#yum -y remove java-1.6.0-openjdk*

# download jdk
cd $TMP
wget -O $JDK_PACKAGE --no-cookies --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2Ftechnetwork%2Fjava%2Fjavase%2Fdownloads%2Fjdk-6u32-downloads-1594644.html;" http://download.oracle.com/otn-pub/java/jdk/6u32-b05/$JDK_PACKAGE
if [ ! -e ./$JDK_PACKAGE ]; then
	echo "ERROR: $JDK_PACKAGE is not existed."
	exit 1
fi

if [ ! -d $JDK_BASEDIR ]; then
	mkdir $JDK_BASEDIR
fi
cd $JDK_BASEDIR

# Clean job
rm -rf default
rm -rf $JDK_FOLDER

chmod +x $TMP/$JDK_PACKAGE
$TMP/$JDK_PACKAGE

if [ ! -d $JDK_FOLDER ]; then
	echo "Error: JDK version not matched."
	exit 1
fi
ln -s $JDK_FOLDER default

#export JAVA_HOME=$JDK_BASEDIR/default
#export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
#export PATH=$PATH:$JAVA_HOME/bin

BAK_FILENAME=$(date +%Y%m%d%H%M%S)
cp -p /etc/profile /etc/profile.$BAK_FILENAME
cat >> /etc/profile <<EOS

export JAVA_HOME=$JDK_BASEDIR/default
export CLASSPATH=.:\$JAVA_HOME/jre/lib/rt.jar:\$JAVA_HOME/lib/dt.jar:\$JAVA_HOME/lib/tools.jar
export PATH=\$PATH:\$JAVA_HOME/bin
EOS

# change default java jdk from open-jdk to oracle-jdk
#if [ -e $JDK_LINK ]; then
alternatives --install $JDK_LINK java $JDK_BASEDIR/default/bin/java 19000
alternatives --set java $JDK_BASEDIR/default/bin/java
#fi

source ~/.bash_profile
source /etc/profile

java -version
if [ $? != 0 ]; then
	echo "Error: JDK setup not finished."
	exit 1
fi

rm -f $TMP/$JDK_PACKAGE
echo "JDK setup finished."
echo "You can excute \"alternatives --config java\" to change JDK version."
echo "Please logout and relogin."
4,276 次浏览 | 没有评论
2012年8月29日 | 归档于 技术

不安全信道上的RIA应用的用户注册以及认证机制(2) – 用户注册

在上篇中谈了如何认证用户请求,下篇也即本文的内容是关于如何让用户安全注册帐户。

在正题之前还想补充谈一下上篇的机制能否抵挡针对服务器端数据库的攻击。
密钥是明文存放在服务器端数据库的,和用户手中持有的密钥一致。一个心怀鬼胎的数据库管理员,他瞄一眼,就能拿到了用户的密钥。骇客直接攻破数据库后果也一样。

很自然,我们想到了用哈希(hash)函数,数据库不直接保存密钥明文而只保存经计算后的摘要。非常遗憾,这种措施对安全性没有任何提高,即使hash运算再多几轮,结果还是一样。

回想起HMAC的算法,为了对消息进行署名,必须有key的参与,这个key是否已经过hash是无关紧要的。
倘若服务器端保存的key是hash过的,由于hash的不可逆,服务器端的HMAC运算必须用这个key而用不了明文密钥。客户端的逻辑也必须对应,运算前需要对用户持有的密钥进行同样的hash运算得到key后才能进行HMAC运算。

一个开源系统,或者是源码可见系统(比如JavaScript RIA应用),加密逻辑是显而易见的。心怀鬼胎的数据库管理员或者骇客拿到hash后密钥的后果,和拿到明文密钥的相比,没有任何区别。他已经能成功在客户端制造署名。

虽然对本系统的安全性没有任何提高,但从社会工程学角度出发,只保存hash后密钥仍然是值得推荐的。很多用户在不同网站使用相同用户名和密码,一个网站hash密码泄漏的危害不至于蔓延到其他网站,明文密码泄漏的危害大家用脚趾头想想都知道。

从安慰领导和客户的角度出发,改成hash也能提高他们的心理安全感。

回到本篇正题。在不使用HTTPS的情况下,如何保证注册密码不泄漏?很显然,传输的密码必须经过客户端加密。客户端能加密,服务器端能解密,这不就是公钥系统干的事嘛。本篇使用安全成熟的RSA体制。

用户注册机制:
1.服务器端持有一对RSA PublicKey和PrivateKey。如何持有,就请自行解决了。参考上一篇博客,可以在Linux下用ssh-keygen制造。服务器端用Java写的话,还可以用Java产生。其他语言余虽然不太了解,自行生产应该也不困难。
2.客户端请求服务器持有的PublicKey。(GET请求)
3.客户端用PublicKey加密用户名和密码组成的明文内容,得到密文
4.客户端将密文作为URL的参数值,发起注册请求。(POST请求)
5.服务器端用PrivateKey解密收到的密文,取出用户名和密码,如果用户名可用,在数据库写入用户信息,返回“成功”的JSON数据;如果用户名不可用,返回用户名不可用的500错误
6.客户端处理响应。

OVER。就是这么简单。

来看一下弱点。
抓包窃听是有效抵挡了,但能否抵挡中间人攻击?非常遗憾,答案还是不能。
看一下中间人的攻击流程:

1. 客户端 —— 中间人 —— 服务器端
2. 客户端 —— 中间人 ——(请求服务器端的PublicKey)——> 服务器端
3. 客户端 —— 中间人 <——(返回服务器端的PublicKey)—— 服务器端 4. 客户端 ——(请求服务器端的PublicKey)——> 中间人 —— 服务器端
5. 客户端 <——(返回中间人的PublicKey)—— 中间人 —— 服务器端 6. 客户端 ——(注册请求)——> 中间人 —— 服务器端
7. 客户端 —— 中间人(用自己的私钥解密后,再用服务器端的公钥加密) ——(注册请求)——> 服务器端
8. 客户端 —— 中间人 <——(注册成功)—— 服务器端 9. 客户端 <——(注册成功)—— 中间人 —— 服务器端

中间人成功获取了客户的注册信息

无论是上篇的认证还是下篇的注册,都无法抵挡中间人攻击。能发动中间人攻击的,在欧美除了网络提供商,在天朝要加多一个ZF。骇客的力量也恁厉害了一点。
但余想强调的是,对一般应用的安全要求,本下篇以及上篇提出的体制已经足够。更高的安全要求,你就不舍得买一个SSL证书么?

2,743 次浏览 | 没有评论
2012年8月29日 | 归档于 技术

RSA加密解密工具类

使用Amazon AWS API获取的Windows实例登录密码是经过RSA加密的,为了用Java解出登录密码,研究了一堆资料,参阅了Elasticfox的源码,最终弄出了这个工具类。
几个注意点:
1.RSA的Padding模式非常多,Amazon AWS上使用了PKCS1Padding
2.为了兼顾网络和文件两方面,使用了流获取公钥以及私钥
3.RSA加密解密是在字节上操作,和加密前的内容(一般都是字符串)的编码没有关系,加密前是这些字节,那么解密后也是这些字节,至于字节如何和字符串相互转换,那是使用者的事(网上乱七八糟将字符串混淆进加密解密过程的什么代码!)
4.公钥的格式不是X509EncodedKeySpec!公钥的格式是OpenSSH2,公钥文件内容的解码的实现以及异常处理参考了jsvnserve的代码
5.一定要添加BouncyCastleProvider库

package info.kuyur.demo.util;

import java.io.InputStream;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.StringTokenizer;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class RSACipher {

	private static final String SSH2_RSA_KEY = "ssh-rsa";
	private static final String KEY_START_WITH = "-----BEGIN RSA PRIVATE KEY-----";
	private static final String KEY_END_WITH = "-----END RSA PRIVATE KEY-----";
	public static enum PaddingMode {
		NO_PADDING,
		PKCS1_PADDING,
		OAEP_PADDING
	}

	private static final Log log = LogFactory.getLog(RSACipher.class);
	private RSACipher() {}

	/**
	 * Encrypt a message.
	 * @param raw data to be encrypted.
	 * @param publicKey
	 * @param paddingMode
	 * @return encrypted data in bytes.
	 */
	public static final byte[] encrypt(byte[] raw, byte[] publicKey, PaddingMode paddingMode) throws RSACipherException{
		if (publicKey == null) {
			throw new RSACipherException(RSACipherException.ErrorCode.NULL_PUBLIC_KEY);
		}
		try {
			SSH2DataBuffer buf = new SSH2DataBuffer(publicKey);
			String type = buf.readString();
			if (!SSH2_RSA_KEY.equals(type)) {
				throw new RSACipherException(RSACipherException.ErrorCode.CORRUPT_OPENSSH2_PUBLIC_KEY);
			}
			PublicKey key = RSACipher.decodePublicKey(buf);
			Security.addProvider(new BouncyCastleProvider());
			Cipher cipher = null;
			if (paddingMode == PaddingMode.NO_PADDING) {
				cipher = Cipher.getInstance("RSA", "BC");
			} else if (paddingMode == PaddingMode.PKCS1_PADDING) {
				cipher = Cipher.getInstance("RSA/NONE/PKCS1Padding", "BC");
			} else if (paddingMode == PaddingMode.OAEP_PADDING) {
				cipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA1AndMGF1Padding", "BC");
			}
			cipher.init(Cipher.ENCRYPT_MODE, key);
			return cipher.doFinal(raw);
		} catch (RSACipherException re) {
			throw re;
		} catch (Exception e) {
			log.error("Encrypt failed.", e);
			throw new RSACipherException(RSACipherException.ErrorCode.ENCRYPT_FAILURE, e);
		}
	}

	/**
	 * Decrypt a message.
	 * @param raw data encrypted.
	 * @param privateKey
	 * @param paddingMode
	 * @return clear data in bytes.
	 */
	public static final byte[] decrypt(byte[] raw, byte[] privateKey, PaddingMode paddingMode) throws RSACipherException {
		if (privateKey == null) {
			throw new RSACipherException(RSACipherException.ErrorCode.NULL_PRIVATE_KEY);
		}
		try {
			KeyFactory keyFactory = KeyFactory.getInstance("RSA", new BouncyCastleProvider());
			EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
			Security.addProvider(new BouncyCastleProvider());
			Cipher cipher = null;
			if (paddingMode == PaddingMode.NO_PADDING) {
				cipher = Cipher.getInstance("RSA", "BC");
			} else if (paddingMode == PaddingMode.PKCS1_PADDING) {
				cipher = Cipher.getInstance("RSA/NONE/PKCS1Padding", "BC");
			} else if (paddingMode == PaddingMode.OAEP_PADDING) {
				cipher = Cipher.getInstance("RSA/NONE/OAEPWithSHA1AndMGF1Padding", "BC");
			}
			cipher.init(Cipher.DECRYPT_MODE, keyFactory.generatePrivate(keySpec));
			return cipher.doFinal(raw);
		} catch (InvalidKeySpecException ie) {
			log.error("Corrupt RSA private key.", ie);
			throw new RSACipherException(RSACipherException.ErrorCode.CORRUPT_RSA_PRIVATE_KEY, ie);
		} catch (Exception e) {
			log.error("Decrypt failed", e);
			throw new RSACipherException(RSACipherException.ErrorCode.DECRYPT_FAILURE, e);
		}
	}

	/**
	 * Read private key from a stream encoding in UTF-8.
	 * @param stream The input stream of a file or network.
	 * @return raw data of private key
	 */
	public static byte[] getPrivateKeyRawData(InputStream stream) throws RSACipherException {
		if (stream == null) {
			throw new RSACipherException(RSACipherException.ErrorCode.NULL_SOURCE);
		}
		String keyContent = readInputStreamAsString(stream);
		return getPrivateKeyRawData(keyContent);
	}

	public static byte[] getPrivateKeyRawData(byte[] raw) throws RSACipherException {
		String keyContent = new String(raw);
		return getPrivateKeyRawData(keyContent);
	}

	public static byte[] getPrivateKeyRawData(String keyContent) throws RSACipherException{
		if (StringUtils.isEmpty(keyContent)) {
			throw new RSACipherException(RSACipherException.ErrorCode.NULL_SOURCE);
		}
		// Remove header and footer
		int startIndex = keyContent.indexOf(KEY_START_WITH);
		int endIndex = keyContent.indexOf(KEY_END_WITH);
		if (startIndex >= endIndex) {
			log.error("Invalid RSA private key file format.");
			throw new RSACipherException(RSACipherException.ErrorCode.INCORRECT_PRIVATE_KEY_FILE_FORMAT);
		}
		startIndex += KEY_START_WITH.length();
		// Get key string and remove /r/n
		String key = keyContent.substring(startIndex, endIndex).replaceAll("[^A-Za-z0-9\\+\\/\\=]", "");
		// base64 decode
		return Base64.decodeBase64(key);
	}

	/**
	 * Read public key(OpenSSH SSH-2) from a stream encoding in UTF-8.
	 * @param stream The input stream of a file or from network.
	 * @return raw data of public key
	 */
	public static byte[] getPublicKeyRawData(InputStream stream) throws RSACipherException {
		if (stream == null) {
			throw new RSACipherException(RSACipherException.ErrorCode.NULL_SOURCE);
		}
		String keyContent = readInputStreamAsString(stream);
		if (StringUtils.isEmpty(keyContent)) {
			throw new RSACipherException(RSACipherException.ErrorCode.NULL_SOURCE);
		}
		try {
			StringTokenizer st = new StringTokenizer(keyContent);
			st.nextToken();
			return Base64.decodeBase64(st.nextToken().getBytes());
		} catch (Exception e) {
			log.error("Invalid OpenSSH2 public key file format.", e);
			throw new RSACipherException(RSACipherException.ErrorCode.INCORRECT_PUBLIC_KEY_FILE_FORMAT, e);
		}
	}

	public static PublicKey decodePublicKey(SSH2DataBuffer buffer) throws RSACipherException{
		final BigInteger e = buffer.readMPint();
		final BigInteger n = buffer.readMPint();
		try {
			final KeyFactory factory = KeyFactory.getInstance("RSA");
			final RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e);
			return factory.generatePublic(spec);
		} catch (Exception ex) {
			log.error("Decode public key failed", ex);
			throw new RSACipherException(
				RSACipherException.ErrorCode.CORRUPT_OPENSSH2_PUBLIC_KEY, ex);
		}
	}

	public static final class SSH2DataBuffer {
		private final byte[] data;
		private int pos;

		public SSH2DataBuffer(final byte[] data) {
			this.data = data;
		}

		public BigInteger readMPint() throws RSACipherException {
			final byte[] raw = this.readByteArray();
			return (raw.length > 0) ? new BigInteger(raw) : BigInteger.valueOf(0);
		}

		public String readString() throws RSACipherException {
			return new String(this.readByteArray());
		}

		private int readUInt32()
		{
			final int byte1 = this.data[this.pos++];
			final int byte2 = this.data[this.pos++];
			final int byte3 = this.data[this.pos++];
			final int byte4 = this.data[this.pos++];
			return ((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0));
		}

		private byte[] readByteArray() throws RSACipherException {
			final int len = this.readUInt32();
			if ((len < 0) || (len > (this.data.length - this.pos))) {
				throw new RSACipherException(RSACipherException.ErrorCode.CORRUPT_OPENSSH2_PUBLIC_KEY);
			}
			final byte[] str = new byte[len];
			System.arraycopy(this.data, this.pos, str, 0, len);
			this.pos += len;
			return str;
		}
	}

	public static String readInputStreamAsString(InputStream input) {
		StringBuilder sb = new StringBuilder();
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));
			char[] buf = new char[1024];
			int numRead = 0;
			while ((numRead = reader.read(buf)) != -1) {
				sb.append(buf, 0, numRead);
			}
		} catch (IOException e) {
			log.error("Error while reading file", e);
		} finally {
			try {
				reader.close();
			} catch (IOException e) {
				log.error("Error while closing stream", e);
			}
		}
		return sb.toString();
	}

	public static final class RSACipherException extends RuntimeException {

		/**
		 * 
		 */
		private static final long serialVersionUID = 7705600007092255435L;

		private final ErrorCode erroCode;

		public RSACipherException(final ErrorCode errorCode) {
			super(errorCode.message);
			this.erroCode = errorCode;
		}

		public RSACipherException(final ErrorCode errorCode, final Throwable cause) {
			super(errorCode.message, cause);
			this.erroCode = errorCode;
		}

		public ErrorCode getErrorCode() {
			return this.erroCode;
		}

		public enum ErrorCode {
			NULL_SOURCE("Null source."),
			NULL_PUBLIC_KEY("Null public key."),
			NULL_PRIVATE_KEY("Null private key."),
			INCORRECT_PUBLIC_KEY_FILE_FORMAT("Incorrect public key file format (OpenSSH2)."),
			INCORRECT_PRIVATE_KEY_FILE_FORMAT("Incorrect private key file format (RSA)."),
			CORRUPT_RSA_PRIVATE_KEY("Corrupt RSA private key."),
			CORRUPT_OPENSSH2_PUBLIC_KEY("Corrupt OpenSSH2 public key."),
			DECRYPT_FAILURE("Decrypt failure."),
			ENCRYPT_FAILURE("Encrypt failure.");;
			private final String message;

			ErrorCode(final String message) {
				this.message = message;
			}
		}
	}
}

测试用例:
注意点:
1.解密和加密的测试用例中,密文需要自己产生一个来替代原来的。
2.Linux下使用ssh-keygen产生密钥对,放置到测试用的resources目录下

ssh-keygen -b 1024 -t rsa

package info.kuyur.demo.util;

import static org.junit.Assert.fail;

import java.io.IOException;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;

import javax.crypto.Cipher;

import info.kuyur.demo.util.RSACipher.SSH2DataBuffer;

import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Test;

public class RSACipherTest {

	private static String PUBLIC_KEY_FILE = "testpublickey.data";
	private static String PRIVATE_KEY_FILE = "testprivatekey.data";
	
	@Test
	public void testGetPrivateKeyFromFile() {
		InputStream inputStream = null;
		try {
			inputStream = ClassLoader.getSystemResourceAsStream(PRIVATE_KEY_FILE);
			byte[] key = RSACipher.getPrivateKeyRawData(inputStream);
			System.out.println("key content:");
			for (int i=0; i<key.length; i++) {
				System.out.print(key[i] + " ");
			}
			System.out.println();
			Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
			KeyFactory keyFactory = KeyFactory.getInstance("RSA", new BouncyCastleProvider());
			EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);
			cipher.init(Cipher.DECRYPT_MODE, keyFactory.generatePrivate(keySpec));
		} catch (Exception e) {
			e.printStackTrace();
			fail("Error happen.");
		} finally {
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (IOException e) {
				}
			}
		}
	}

	@Test
	public void testGetPublicKeyFromFile() {
		InputStream inputStream = null;
		try {
			inputStream = ClassLoader.getSystemResourceAsStream(PUBLIC_KEY_FILE);
			byte[] key = RSACipher.getPublicKeyRawData(inputStream);
			System.out.println("key content:");
			for (int i=0; i<key.length; i++) {
				System.out.print(key[i] + " ");
			}
			System.out.println();
			SSH2DataBuffer buf = new SSH2DataBuffer(key);
			String type = buf.readString();
			System.out.println(type);
			Cipher cipher = Cipher.getInstance("RSA");
			cipher.init(Cipher.ENCRYPT_MODE, RSACipher.decodePublicKey(buf));
		} catch (Exception e) {
			e.printStackTrace();
			fail("Error happen.");
		} finally {
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (IOException e) {
				}
			}
		}
	}

	@Test
	public void testEncryptAndDecrypt() {
		InputStream inputStream1 = null;
		InputStream inputStream2 = null;
		try {
			inputStream1 = ClassLoader.getSystemResourceAsStream(PUBLIC_KEY_FILE);
			byte[] publicKey = RSACipher.getPublicKeyRawData(inputStream1);
			inputStream2 = ClassLoader.getSystemResourceAsStream(PRIVATE_KEY_FILE);
			byte[] privateKey = RSACipher.getPrivateKeyRawData(inputStream2);
			if (publicKey == null || privateKey == null) {
				fail("Error happen. Key is null.");
			}

			String text = "o;ti52D6KYC";
			byte[] rawData = text.getBytes("UTF-8");
			System.out.println("Text=" + text + "; Bytes=");
			for (int i=0; i<rawData.length; i++) {
				System.out.print(rawData[i] + " ");
			}
			System.out.println("\nlength=" + rawData.length);

			System.out.println("\nCipherData=");
			byte[] cipherData =  RSACipher.encrypt(rawData, publicKey, RSACipher.PaddingMode.NO_PADDING);
			for (int i=0; i<cipherData.length; i++) {
				System.out.print(cipherData[i] + " ");
			}
			System.out.println("\nlength=" + cipherData.length);

			System.out.println("\nDecryptedData=");
			byte[] rawData2 = RSACipher.decrypt(cipherData, privateKey, RSACipher.PaddingMode.NO_PADDING);
			for (int i=0; i<rawData2.length; i++) {
				System.out.print(rawData2[i] + " ");
			}
		} catch (Exception e) {
			e.printStackTrace();
			fail("Error happen.");
		} finally {
			if (inputStream1 != null) {
				try {
					inputStream1.close();
				} catch (Exception e1) {}
			}
			if (inputStream2 != null) {
				try {
					inputStream2.close();
				} catch (Exception e2) {}
			}
		}
	}

	@Test
	public void testDecryptAndEncrypt() {
		InputStream inputStream1 = null;
		InputStream inputStream2 = null;
		try {
			inputStream1 = ClassLoader.getSystemResourceAsStream(PRIVATE_KEY_FILE);
			byte[] key = RSACipher.getPrivateKeyRawData(inputStream1);
			if (key == null) {
				fail("Error happen. Key is null.");
			}
			String ciphertext = "qyR0MJPVgsKTgdI71aocnffyg6O3qX7iihEZIi6TVHeoR+91acXIw5GPSr8vsUvEEw93fSusK2fEJjiKozzjfRVhfUe5Np+58MdFH3GFsR87uilbUnp51gqaeTp2cxPtFbOBWxg4PwGIKYV8hhcd72SCg92j0FPgZ4NBQPaXvmR+/8KRSmoU4josR3YgUIyW1AQbb4dGpdTjrE3ghlLpN4A0kmJmMLVbwKXSlcEgSy4iEtRwa1APc7a4CxJ2ihkToFjrtqJeYU/Uzn82FimiUoGChFjIr4uH5az931++3c/BLNA8xMS90OcCa68mnvC2tus+XGZGTYBDV49uPJoIYQ==";
			byte[] raw = Base64.decodeBase64(ciphertext);
			System.out.println("Ciphertext data:");
			for (int i=0; i<raw.length; i++) {
				System.out.print(raw[i] + " ");
			}
			System.out.println("\nlength=" + raw.length);

			byte[] textRaw = RSACipher.decrypt(raw, key, RSACipher.PaddingMode.PKCS1_PADDING);
			System.out.println("\nDecrypted data=");
			for (int i=0; i<textRaw.length; i++) {
				System.out.print(textRaw[i] + " ");
			}
			System.out.println("\nlength=" + textRaw.length);
			System.out.println("Data in ASCII:" + new String(textRaw, "ASCII"));

			inputStream2 = ClassLoader.getSystemResourceAsStream(PUBLIC_KEY_FILE);
			byte[] publicKey = RSACipher.getPublicKeyRawData(inputStream2);
			byte[] reEncrypt = RSACipher.encrypt(textRaw, publicKey, RSACipher.PaddingMode.PKCS1_PADDING);
			System.out.println("\nCiphertext data in Base64:");
			System.out.println(Base64.encodeBase64String(reEncrypt));
		} catch (Exception e) {
			e.printStackTrace();
			fail("Error happen.");
		} finally {
			if (inputStream1 != null) {
				try {
					inputStream1.close();
				} catch (IOException e1) {
				}
			}
			if (inputStream2 != null) {
				try {
					inputStream2.close();
				} catch (IOException e2) {
				}
			}
		}
	}
}

用法:参考测试用例

5,214 次浏览 | 没有评论
2012年8月28日 | 归档于 技术, 程序
标签: , , , , ,

不安全信道上的RIA应用的用户注册以及认证机制(1) – 客户请求的认证

首先应该认识一下RIA和传统WEB网站的在用户认证方面的不同之处。

RIA应用的客户端通过RESTful API向服务器发起GET或POST请求,服务器端完成业务逻辑处理并返回JSON或XML类型的数据。表面上看传统的WEB网站也在完成相同的事。用户通过直接的URL地址访问向服务器发起GET请求,通过提交表单向服务器发起POST请求,只不过服务器的响应是WEB页面。

但在认证方面这两者有着本质的区别。

传统的WEB网站的用户认证信息,有session保存和cookies保存两种。session随着浏览器关闭或者会话超时而失效,cookies则是被浏览器保存在文件当中。谈论到本质,在客户端保存的session信息session ID是一种内存cookie,是由服务器端写入的,浏览器关闭则内存cookie消失。所以在用户浏览器禁用cookies的情况下,解决方法就是将session ID附加到url的参数当中。

RIA的用户认证信息保存手段则依赖于实现语言。
使用HTML5的JavaScript应用,可以利用HTML5 Web Storage。不使用HTML5的JavaScript应用,就只能使用Cookies了,但Cookies会每次都发送到服务器端,在HTTP上使用实在不安全。其他语言如果支持读写本地文件,可以保存在文件中。(基于安全考虑,绝大多数RIA技术都禁止直接的本地磁盘文件操作,但可能有提供受限且安全的文件操作或存储机制)

RIA的用户认证机制不追求安全可以做得很简单,追求完美则非常麻烦。在强制只能使用HTTPS的情况下,可以将Username和Password放到URL中,每次的RESTful API请求都发送过去。但在使用HTTP的情况下,就不能这样干了,会被上级鞭死的。

终于回到主题,在不安全信道上,RIA的用户注册以及认证应当如何做?虽然密码学上不安全信道以及安全信道的区别完全不等同HTTP和HTTPS的区别,但在这个主题中,不安全信道即指HTTP。 (指鹿为马啊你)

我们的服务器端要做到HTTP请求随时随地响应,而且要适应各种乱七八糟的客户端,什么JavaScript啦,Silverlight啦,dart啦,甚至Android或者iOS上的APP都可以用。因此认证信息不可以直接放在Cookies中发送过去。

本文提出一个经过简化的使用HMAC的认证方案。在用户注册方面则需要用到RSA。在本文的解决方案中,没有通常意义的退出系统,也没有通常意义的登入系统。

API访问认证机制:
假设客户端已经持有一对用户名/密码。先不管用户名和密码存储在那里,我们决不能把密码明文发送到服务器端。当客户端访问一个API,客户端需要用密码作为加密的密钥,对由API地址,请求方式(POST/GET),用户名,时间戳组成的字符串计算摘要,并把摘要一起发送到服务器。

例如客户端要用POST方式访问API:
http://kuyur.info/blog/archive/update
约定需要计算摘要的目标字符串的组成结构如下(注意大小写敏感,先后顺序也必须维持一致):
URL=/blog/archive/update&Method=POST&Username=kuyurmoe&Timestamp=2012-08-28T04:56:26.211Z
URL是去掉域名的部分;时间戳的格式使用UNIX时间的ISO 8601规范

HMAC有多个哈希算法版本,安全性要求较高的场合可以选择sha256
各种语言应该都已经有HMAC的库,JavaScript可以使用crypto-js
以JavaScript为例:
在html文件中导入2.5.3-crypto-sha256-hmac.js

var toSign = 'URL=/blog/archive/update&Method=POST&Username=kuyurmoe&Timestamp=2012-08-28T04:56:26.211Z';
var key = readPasswordFromSomeWhere(); // for example, password = 'hogehoge'
var sign = Crypto.HMAC(Crypto.SHA256, toSign, key, {asString: true});

再使用base64算法编码这个摘要,得到可放在URL传输的字符串

var signString = base64encode(sign); // Mm4thyhnOpIOWKrifCsYcIOuVZX+pf6CGisbzWS0Wa0=

base64的库太多了,就不推荐了,自写一个也不困难

相对于一般的哈希算法,HMAC摘要的生成还多了密钥的参与。因为含有密钥认证的因素在内,这种摘要又称为署名。

我们先完成这次认证流程,再看这种机制的安全性。

将署名附加在URL的参数中,使用ajax发起POST请求,注意URL参数的参数值还需要经过URL encoded(因为还会含有“=”之类的字符)。内容或JSON对象则放在FormParams中提交。

URL encoded:

var timestampEncoded = encodeURIComponent('2012-08-28T04:56:26.211Z');

最终的URL如下:
http://kuyur.info/blog/archive/update?ArchiveId=999&Username=kuyurmoe&Timestamp=2012-08-28T04%3A56%3A26.211Z&Sign=Mm4thyhnOpIOWKrifCsYcIOuVZX%2Bpf6CGisbzWS0Wa0%3D

服务器端的验证流程:
1.根据URL,取得
API的地址:/blog/archive/update
用户名:kuyurmoe
时间戳经URL decoded后的值:2012-08-28T04:56:26.211Z
署名经URL decoded后的值:Mm4thyhnOpIOWKrifCsYcIOuVZX+pf6CGisbzWS0Wa0=
根据请求方式,取得
请求方式:POST
2.取得服务器端的当前时间(服务器端收到请求的时间戳),如果和客户端发起请求的时间戳相差太大,例如相差超过60秒,返回认证有效期已过的500错误
3.从数据库查询用户kuyurmoe的密码,如果用户不存在,返回用户不存在的500错误
4.组装出要计算署名的字符串:URL=/blog/archive/update&Method=POST&Username=kuyurmoe&Timestamp=2012-08-28T04:56:26.211Z,
使用HMAC的sha256版本计算署名(Java的情况计算结果是byte[]),再用base64算法编码署名得到可读的字符串,和从客户端传过来的字符串比较,如果相同,验证通过,如果不同,返回署名不正确的500错误
5.服务器端进行业务逻辑处理,返回JSON结果

来看安全性
URL中只含有计算后的署名,没有密钥等敏感信息。HMAC算法保证了即使用路由器截获再多的数据包,也倒推不出密钥。拿不到密钥,就伪造不了署名。假的署名在服务器端通不过验证。唯一的弱点在于署名的有效期。本文提出的机制的署名有效期不是一次有效,而是由客户端和服务器端的时间戳容许差值决定。服务器端和客户端的时间不可能完全同步,客户端请求发起到服务器接收到请求也需要时间。如果攻击者拿到完整的URL,立刻发起一模一样的请求,是能成功通过服务器端验证并获得响应的。倘若迟一点(超出容许差值)再发起请求,就通不过服务器验证。(过了这个村就没那个店)

设置更加严格的容许差值可以提高安全性
比如可以设置为:客户端时间戳不允许超前服务器端时间戳,服务器端时间戳只允许不超过客户端时间戳10秒。
从发起请求到收到请求,只是一个HTTP请求的单向传输时间,不包含服务器端的处理时间,也不包含响应发回到收到响应的时间,因此在客户端和服务端都同步过世界时后,容许差值可以设得很小

对于一般应用而言,这种安全程度已经足够,对于安全性要求高的场合当然不足够,这时应当改用署名一次有效机制,署名验证过后即无效

补充1.百度百科(http://baike.baidu.com/view/1136366.htm)上的一种一次有效署名验证机制
不使用时间戳,而先从服务器端获取一个随机数(此随机数一次有效),用密钥对随机数计算署名,将署名附在URL发送到服务器端。这种机制需要在普通请求前先获取随机数,多了一次HTTP请求。

补充2.本文的机制能否改造成一次有效机制
答案当然是可以
在服务器端,维护一个被使用过的署名先入先出队列。当发现署名被再次使用时,显然就是有人攻击了。再次被使用的署名不能通过验证。这就变相成为一次有效机制。(被否决的署名不能进入已使用队列)已使用署名队列的规模依据时间戳容许差值而定,1秒内的平均请求有1次的话,10秒的容许差值也仅需要10的规模,放大10倍,100的规模足够应付。

补充3.补充1和补充2中的机制是否能有效抵挡中间人攻击
答案是不能
想象一个更加强大的攻击者,他可以随意窃听并中断HTTP请求,还可以伪造成用户发起请求。当他截获用户的HTTP请求时,他将这个请求掐掉,然后伪造成用户发起一模一样的请求,他压根就不需要知道用户密钥,连算法都不用了解,就能拿到了服务器的响应。(本来在没加密的信道上,不掐掉用户请求他就已经能窃听到响应)
因为一次有效验证机制,只是防止了再次利用同一个署名(换句哲学的话,你不能发起两次一模一样的请求),而不能识别用户已经被替代。
IPv6中IPSec是强制要求而不是选项,IPv6网络可以有效抵挡这种攻击而无需SSL。
在天朝如果你受到这种攻击,应当已经是ZF敌人中相当高的级别了吧

2,857 次浏览 | 没有评论
2012年8月28日 | 归档于 技术

DOM元素查询函数getElementsByClassName

IE8以下的IE浏览器不支持getElementsByClassName。

参考http://www.cnblogs.com/rubylouvre/archive/2009/07/24/1529640.html的最最终实现方案,改写了一下,但这个“最最终”实现方案里有一个bug,result放在if的第一个分支里定义了。

余去掉了tag的查询,加入了安全检查
对DOM节点的检查是检测nodeType,当然刻意构造一个含有nodeType属性的非DOM节点对象,也能通过检测
这是一种鸭子测试,平衡安全性以及效率后的折衷选择
(→鸭子类型

另外className必须是字符串类型,否则返回空结果集
这和Firefox/Chrome的内置实现有点区别,className不是string的时候,Firefox/Chrome会使用toString()去取得字符串
可以打开控制台玩一下:

var obj = {};
obj.toString = function() {return 'x-grid-cell-inner';}; // 你的css类选择器
document.getElementsByClassName(obj);

最后附上源码

getElementsByClassName : function(node, className) {
    if (!node || !(node.nodeType == 1 || node.nodeType == 9)) {
        return undefined;
    }
    var result = [];
    if (typeof className !== 'string') {
        return result;
    }
    if (document.getElementsByClassName) {
        return node.getElementsByClassName(className);
    }

    var classes = className.split(' '),
        elements = (node.all) ? node.all : node.getElementsByTagName('*'),
        patterns = [],
        current,
        match;
    var i = classes.length;
    while(--i >= 0) {
        patterns.push(new RegExp('(^|\\s)' + classes[i] + '(\\s|$)'));
    }
    var j = elements.length;
    while (--j >= 0) {
        current = elements[j];
        match = false;
        for (var k=0, kl=patterns.length; k<kl; k++) {
            match = patterns[k].test(current.className);
            if (!match) {
                break;
            }
        }
        if (match) {
            result.push(current);
        }
    }
    return result;
}
2,694 次浏览 | 没有评论
2012年8月21日 | 归档于 技术, 程序

可以自由添加或删减过滤器的ExtJS4 Store

ExtJS4 Store的过滤器功能用起来巨蛋疼无比,如果写ExtJS4批判,绝对能写一篇
filter()只能增加过滤器不能减,clearFilter()又一次全部清空过滤器,filterBy()会无视过滤器列表中已存在的过滤器,没有一个令人满意的

一个gridpanel,页面上有很多ComboBox作为筛选条件,用户点击这些ComboBox,对数据进行筛选
问题在于ExtJS4的Store不能单个更换过滤器,必须先清空全部的过滤器,再一个个加回来,怎么会有这种反人类的设计啊

参考源码写了一个能自由增减和替换过滤器的派生Store

/**
 * @author kuyur
 */

Ext.define('myproject.store.BaseStore', {
	extend: 'Ext.data.Store',
	addFilter: function(key, filter, value) {
		if (Ext.isString(filter)) {
			filter = {
				property: filter,
				value: value
			};
		}
		var me = this;
		var decoded = me.decodeFilters(filter);
		if (decoded.length <= 0) {
			return;
		}
		var doLocalSort = me.sortOnFilter && !me.remoteSort;
		if (!Ext.isString(key)) {
			key = key.toString();
		}
		me.filters.add(key, decoded[0]);

		if (me.remoteFilter) {
			me.load();
		} else {
			if (me.filters.getCount()) {
				me.snapshot = me.snapshot || me.data.clone();
				me.data = me.snapshot.filter(me.filters.items);
				if (doLocalSort) {
					me.sort();
				}
				if (!doLocalSort || me.sorters.length < 1) {
					me.fireEvent('datachanged', me);
				}
			}
		}
	},
	removeFilter: function(key) {
		var me = this;
		if (!Ext.isString(key)) {
			key = key.toString();
		}
		var doLocalSort = me.sortOnFilter && !me.remoteSort;
		if (me.filters.removeAtKey(key)) {
			if (me.remoteFilter) {
				me.load();
				return;
			}
			if (me.filters.getCount()) {
				me.snapshot = me.snapshot || me.data.clone();
				me.data = me.snapshot.filter(me.filters.items);
			} else {
				me.data = me.snapshot.clone();
				delete me.snapshot;
			}
			if (doLocalSort) {
				me.sort();
			}
			if (!doLocalSort || me.sorters.length < 1) {
				me.fireEvent('datachanged', me);
			}
		}
	}
});

addFilter函数第一个参数是key,标识过滤器用
第二个参数可以是过滤器对象的一个实例,也可以是过滤器的定义{filterFn: function(record){}},还可以是数据模型的字段名,当作为字段名(字符串)使用时,需要传入第三个参数作为字段值
addFilter可以新添加一个过滤器,也可以覆盖旧的过滤器

removeFilter很简单,就是移除一个过滤器

3,202 次浏览 | 没有评论
2012年8月4日 | 归档于 技术, 程序