Java泛型和JSON的反序列化(中)

Java泛型和JSON的反序列化(中)

上篇:https://kuyur.net/blog/archives/2729

上篇介绍了ObjectMapper的readValue(String, Class<T>)接口,这个接口的源虽然是字符串,但ObjectMapper实际上重载了众多的readValue,源可以是二进制数组(byte[])/文件(File)/资源定位符(URL)/输入流(InputStream)等,json源并不是关注重点。

Jackson专门为加注JAXBAnnotation标签的数据模型准备了JacksonJaxbJsonProvider。默认的构造函数支持JAXBAnnotation和JacksonAnnotation两种标签。但它的readFrom接口仅支持从InputStream读取json。

写成一个工具类的方法:

	@SuppressWarnings("unchecked")
	public static <T> T readJsonFromStream(InputStream is, Type genericType) {
		JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider();
		try {
			return (T)provider.readFrom(Object.class, genericType, new Annotation[0], MediaType.APPLICATION_JSON_TYPE, null, is);
		} catch (Exception e) {
			throw new RuntimeException("Deserialize from JSON failed.", e);
		}
	}

readFrom接口参数很繁杂,但对于一般转换而言,仅需要把【型】(Type genericType)和输入流(InputStream is)交给它即可。
由于Type是一切【型】的基础,因此readJsonFromStream(InputStream, Type)的转换能力要比readJsonFromString(String, Class<T>)强。

测试一下转换能力

Comment的一条json,JacksonAnnotation标签,反序列化毫无压力
{"作者": {"name": "铅球万袋", "mailAddress": "qian.qiu@wandai.com"}, "回复": "骚年よ、大志を抱け"}

	@Test
	public void testReadJsonFromString15() throws UnsupportedEncodingException {
		String jsonString = "{\"作者\": {\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, \"回复\": \"骚年よ、大志を抱け\"}";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			Comment comment = Util.readJsonFromStream(bais, Comment.class);
			System.out.println(comment);
			assertEquals(comment.getContent(), "骚年よ、大志を抱け");
			assertEquals(comment.getAuthor().getName(), "铅球万袋");
			assertEquals(comment.getAuthor().getMailAddress(), "qian.qiu@wandai.com");
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}

Stream遵循谁创建谁关闭的原则。ByteArrayInputStream事实上不需要关闭。

Archive的一条json,JAXBAnnotation标签,反序列化通过
{"ID": "1000", "标题": "18岁的那天", "正文": "18岁那天的早上...", "作者": {"name": "一桶浆糊", "mailAddress": "yi.tong@jianghu.com"}}

	@Test
	public void testReadJsonFromString3() throws UnsupportedEncodingException {
		String jsonString = "{\"ID\": \"1000\", \"标题\": \"18岁的那天\", \"正文\": \"18岁那天的早上...\", \"作者\": {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}}";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			Archive archive = Util.readJsonFromStream(bais, Archive.class);
			System.out.println(archive);
			assertEquals(archive.getBlogId(), 1000);
			assertEquals(archive.getTitle(), "18岁的那天");
			assertEquals(archive.getText(), "18岁那天的早上...");
			assertNotNull(archive.getAuthor());
			User author = archive.getAuthor();
			assertEquals(author.getName(), "一桶浆糊");
			assertEquals(author.getMailAddress(), "yi.tong@jianghu.com");
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}

再来测试泛型容器

User数组,我们希望转换为List<User>
[{"name": "铅球万袋", "mailAddress": "qian.qiu@wandai.com"}, {"name": "一桶浆糊", "mailAddress": "yi.tong@jianghu.com"}]
List的型是List.class,用反射从List.class获取构造函数构造的List,能存储Object,但Jackson并不懂得应该用User.class来构造元素,List.class包含的型信息是不足够的。
但List<User>的型是什么?List<User>.class这种语法并不存在,怎样才能获取List<User>的型?

装载了具体类型的元素的泛型容器的【型】是ParameterizedType类型,ParameterizedType是一个接口
(官方将ParameterizedType翻译成参数化类型)

	@SuppressWarnings("unused")
	private List<User> forGetType;

	@Test
	public void testParameterizedType() {
		try {
			Field userListField = getClass().getDeclaredField("forGetType");
			assertTrue(userListField.getGenericType() instanceof ParameterizedType);
			ParameterizedType userListType = (ParameterizedType)userListField.getGenericType();
			System.out.println(userListType.getClass());
			System.out.println(userListType.getRawType());
			Type[] typeArguments = userListType.getActualTypeArguments();
			for (Type type : typeArguments) {
				assertEquals(type, User.class);
				System.out.println(type);
			}
		} catch (SecurityException e) {
			e.printStackTrace();
			fail();
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
			fail();
		}
	}

输出结果
class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
interface java.util.List
class info.kuyur.jsondemo.models.User

forGetType只是为了获取List<User>的型而存在,不需要实例化。
第八行表明,List<User>的【型】确确实实是ParameterizedType
第十行透露了,Java内置的ParameterizedType实现是sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl

ParameterizedType的Type getRawType()接口,返回泛型容器自身的【型】。
ParameterizedType的Type[] getActualTypeArguments()接口,返回参与构造容器的型别参数们的【型】。
List<User>实际上是以User.class为型别参数,结合母容器List.class,构造出一种新的具体型:List<User>型,getActualTypeArguments()返回[User.class]。
同理,Map<Integer, User>实际上是以[Integer.class, User.class]为型别参数,结合母容器Map.class,构造出具体型:Map<Integer, User>型,getActualTypeArguments()返回[Integer.class, User.class]。
ParameterizedType可以反复递归,也即复杂似List<Map<Integer, User>>型同样能够用ParameterizedType表达出来。
ParameterizedType含有足够的【型】信息,让Jackson能够构造泛型容器中的元素。

(在这里说句题外话,学习Haskell对泛型的深入理解大有好处)

反序列化为List<User>,终于成功

	@Test
	public void testReadJsonFromString6() throws Exception {
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			Field userListField = UtilTest.class.getDeclaredField("forGetType");
			assertTrue(userListField.getGenericType() instanceof ParameterizedType);
			ParameterizedType userListType = (ParameterizedType)userListField.getGenericType();
			List<User> users  = Util.readJsonFromStream(bais, userListType);
			for (User user : users) {
				System.out.println(user);
				if (user.getName().equals("铅球万袋")) {
					assertEquals(user.getMailAddress(), "qian.qiu@wandai.com");
				}
				if (user.getName().equals("一桶浆糊")) {
					assertEquals(user.getMailAddress(), "yi.tong@jianghu.com");
				}
			}
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}

为了获得List<User>型信息,定义了一个从来不会使用fotGetType,代码太丑陋,还有别的方法吗?

反射为我们提供了从函数的返回类型获取参数化类型信息的另外一个方法

public class Util {
	...
	public static ParameterizedType getParameterizedReturnType(Class<?> clazz, String methodName, Class<?>... param) {
		try {
			Method method = clazz.getDeclaredMethod(methodName, param);
			Type type = method.getGenericReturnType();
			if (type instanceof ParameterizedType) {
				return (ParameterizedType) type;
			} else {
				throw new RuntimeException(type.getClass() + "is not ParameterizedType");
			}
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}
	}
}

假如有那么一个客户端需要从json反序列化,可以通过方法自身的返回值去获取参数化类型信息

package info.kuyur.jsondemo.client;

import info.kuyur.jsondemo.models.User;
import info.kuyur.jsondemo.utils.Util;

import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.List;

public class UserClient {

	public List<User> getUserList() {
		Type type = Util.getParameterizedReturnType(getClass(), "getUserList");
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			return Util.readJsonFromStream(bais, type);
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e.getMessage(), e);
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}
}

测试一下,转换无压力:

	@Test
	public void testGetUserList() {
		UserClient userClient = new UserClient();
		List<User> users = userClient.getUserList();
		for (User user : users) {
			System.out.println(user);
		}
	}

但动态从方法获取【型】还是太丑了,而且还必须有个方法才能用,这和先定义个”forGetType”没什么区别啊

于是再玩一下,实现自己的ParameterizedType
实际上sun已经给我们实现了一个,就在jaxb包中,将它拷贝出来,改改可见性

package info.kuyur.jsondemo.utils;

import java.lang.reflect.MalformedParameterizedTypeException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;

/**
 * {@link ParameterizedType} implementation.
 */
public class MoeParameterizedTypeImpl implements ParameterizedType {
	private Type[] actualTypeArguments;
	private Class<?> rawType;
	private Type ownerType;

	public MoeParameterizedTypeImpl(Class<?> rawType, Type[] actualTypeArguments, Type ownerType) {
		this.actualTypeArguments = actualTypeArguments;
		this.rawType = rawType;
		if (ownerType != null) {
			this.ownerType = ownerType;
		} else {
			this.ownerType = rawType.getDeclaringClass();
		}
		validateConstructorArguments();
	}

	private void validateConstructorArguments() {
		TypeVariable<?>[] formals = rawType.getTypeParameters();
		// check correct arity of actual type args
		if (formals.length != actualTypeArguments.length) {
			throw new MalformedParameterizedTypeException();
		}
		for (int i = 0; i < actualTypeArguments.length; i++) {
			// check actuals against formals' bounds
		}
	}

	public Type[] getActualTypeArguments() {
		return actualTypeArguments.clone();
	}

	public Class<?> getRawType() {
		return rawType;
	}

	public Type getOwnerType() {
		return ownerType;
	}

	/*
	 * From the JavaDoc for java.lang.reflect.ParameterizedType
	 * "Instances of classes that implement this interface must
	 * implement an equals() method that equates any two instances
	 * that share the same generic type declaration and have equal
	 * type parameters."
	 */
	@Override
	public boolean equals(Object o) {
		if (o instanceof ParameterizedType) {
			// Check that information is equivalent
			ParameterizedType that = (ParameterizedType) o;

			if (this == that)
				return true;

			Type thatOwner = that.getOwnerType();
			Type thatRawType = that.getRawType();

			return (ownerType == null ? thatOwner == null :	ownerType.equals(thatOwner)) &&
				(rawType == null ? thatRawType == null : rawType.equals(thatRawType)) &&
				Arrays.equals(actualTypeArguments, that.getActualTypeArguments());
		} else {
			return false;
		}
	}

	@Override
	public int hashCode() {
		return Arrays.hashCode(actualTypeArguments) ^
				(ownerType == null ? 0 : ownerType.hashCode()) ^
				(rawType == null ? 0 : rawType.hashCode());
	}

	@SuppressWarnings("rawtypes")
	public String toString() {
		StringBuilder sb = new StringBuilder();

		if (ownerType != null) {
			if (ownerType instanceof Class)
				sb.append(((Class) ownerType).getName());
			else
				sb.append(ownerType.toString());

			sb.append(".");

			if (ownerType instanceof MoeParameterizedTypeImpl) {
				// Find simple name of nested type by removing the
				// shared prefix with owner.
				sb.append(rawType.getName().replace(((MoeParameterizedTypeImpl) ownerType).rawType.getName() + "$",""));
			} else
				sb.append(rawType.getName());
		} else
			sb.append(rawType.getName());

		if (actualTypeArguments != null && actualTypeArguments.length > 0) {
			sb.append("<");
			boolean first = true;
			for (Type t : actualTypeArguments) {
				if (!first)
					sb.append(", ");
				if (t instanceof Class)
					sb.append(((Class) t).getName());
				else
					sb.append(t.toString());
				first = false;
			}
			sb.append(">");
		}

		return sb.toString();
	}
}

下面构造自己的萌萌的参数化类型(ownerType实际上可以完全无视)
List<User>型的构造:

Type[] typeArguments = new MoeParameterizedTypeImpl[1];
typeArguments[0] = new MoeParameterizedTypeImpl(User.class, new MoeParameterizedTypeImpl[0], null);
MoeParameterizedTypeImpl userListType = new MoeParameterizedTypeImpl(List.class, typeArguments, null);

User不是泛型,但用ParameterizedType仍然可以表达,第二行的等号右边即是User的ParameterizedType形式的型,可以认为User是一种退化的泛型

用萌萌的参数化类型反序列化,无压力

	@Test
	public void testReadJsonFromString7() throws UnsupportedEncodingException {
		Type[] typeArguments = new MoeParameterizedTypeImpl[1];
		typeArguments[0] = new MoeParameterizedTypeImpl(User.class, new MoeParameterizedTypeImpl[0], null);
		System.out.println(typeArguments[0]);
		MoeParameterizedTypeImpl userListType = new MoeParameterizedTypeImpl(List.class, typeArguments, null);
		System.out.println(userListType);
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			List<User> users  = Util.readJsonFromStream(bais, userListType);
			for (User user : users) {
				System.out.println(user);
				if (user.getName().equals("铅球万袋")) {
					assertEquals(user.getMailAddress(), "qian.qiu@wandai.com");
				}
				if (user.getName().equals("一桶浆糊")) {
					assertEquals(user.getMailAddress(), "yi.tong@jianghu.com");
				}
			}
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}

Map<Integer, User>型,同样毫无压力:

	@Test
	public void testReadJsonFromString10() throws UnsupportedEncodingException {
		Type[] typeArguments = new MoeParameterizedTypeImpl[2];
		typeArguments[0] = new MoeParameterizedTypeImpl(Integer.class, new MoeParameterizedTypeImpl[0], null);
		typeArguments[1] = new MoeParameterizedTypeImpl(User.class, new MoeParameterizedTypeImpl[0], null);
		MoeParameterizedTypeImpl userMapType = new MoeParameterizedTypeImpl(Map.class, typeArguments, null);
		System.out.println(userMapType);
		String jsonString = "{\"1\" : {\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, \"2\" : {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}}";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			Map<Integer, User> users = Util.readJsonFromStream(bais, userMapType);
			assertEquals(users.size(), 2);
			User user1 = users.get(Integer.valueOf(1));
			System.out.println(user1);
			User user2 = users.get(Integer.valueOf(2));
			System.out.println(user2);
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}

如果拿ObjectMapper的readValue(String, Class<T>)来转换,因为型信息不足,转换结果将会变得面目全非

	@SuppressWarnings("unchecked")
	@Test
	public void testReadJsonFromString8() {
		String jsonString = "{\"1\" : {\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, \"2\" : {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}}";
		Map<Integer, User> users = Util.readJsonFromString(jsonString, Map.class);
		assertEquals(users.size(), 2);

		User user1 = users.get(Integer.valueOf(1));
		System.out.println(user1); // null
		User user2 = users.get(Integer.valueOf(2));
		System.out.println(user2); // null

		Iterator<Integer> iter = users.keySet().iterator();
		while(iter.hasNext()) {
			Object val = users.get(iter.next());
			System.out.println(val);
			assertFalse(val instanceof User);
			assertTrue(val instanceof Map);
		}

		Object userMap1 = users.get("1"); // actually key is String type
		assertNotNull(userMap1);
		System.out.println(userMap1); // actually value is LinkedHashMap type

		Object userMap2 = users.get("2");
		assertNotNull(userMap2);
		System.out.println(userMap2);
	}

再来玩一下自定义的泛型类型

定义一个三元组:

package info.kuyur.jsondemo.models;

public class Triple<F, S, T> {

	private F first;
	private S second;
	private T third;

	public Triple() {
	}

	public Triple(F first, S second, T third) {
		this.first = first;
		this.second = second;
		this.third = third;
	}

	public F getFirst() {
		return first;
	}

	public void setFirst(F first) {
		this.first = first;
	}

	public S getSecond() {
		return second;
	}

	public void setSecond(S second) {
		this.second = second;
	}

	public T getThird() {
		return third;
	}

	public void setThird(T third) {
		this.third = third;
	}

	@Override
	public String toString() {
		return "Triple [first=" + first + ", second=" + second + ", third="
				+ third + "]";
	}
}

从下面的json反序列化出Triple<User, Archive,Comment>,注意json中的字段顺序不应该对转换有影响
{"second":{"ID":1000,"标题":"18岁的那天","正文":"18岁那天的早上...","作者":{"mailAddress":"yi.tong@jianghu.com","name":"一桶浆糊"}},"third":{"作者":{"mailAddress":"qian.qiu@wandai.com","name":"铅球万袋"},"回复":"骚年よ、大志を抱け"},"first":{"mailAddress":"yi.tong@jianghu.com","name":"一桶浆糊"}}

	@Test
	public void testReadJsonFromString16() throws UnsupportedEncodingException {
		Type[] typeArguments = new MoeParameterizedTypeImpl[3];
		typeArguments[0] = new MoeParameterizedTypeImpl(User.class, new MoeParameterizedTypeImpl[0], null);
		typeArguments[1] = new MoeParameterizedTypeImpl(Archive.class, new MoeParameterizedTypeImpl[0], null);
		typeArguments[2] = new MoeParameterizedTypeImpl(Comment.class, new MoeParameterizedTypeImpl[0], null);
		MoeParameterizedTypeImpl tripleType = new MoeParameterizedTypeImpl(Triple.class, typeArguments, null);
		System.out.println(tripleType);
		String jsonString = "{\"second\":{\"ID\":1000,\"标题\":\"18岁的那天\",\"正文\":\"18岁那天的早上...\",\"作者\":{\"mailAddress\":\"yi.tong@jianghu.com\",\"name\":\"一桶浆糊\"}},\"third\":{\"作者\":{\"mailAddress\":\"qian.qiu@wandai.com\",\"name\":\"铅球万袋\"},\"回复\":\"骚年よ、大志を抱け\"},\"first\":{\"mailAddress\":\"yi.tong@jianghu.com\",\"name\":\"一桶浆糊\"}}";
		ByteArrayInputStream bais = null;
		try {
			bais = new ByteArrayInputStream(jsonString.getBytes("UTF-8"));
			Triple<User, Archive, Comment> triple = Util.readJsonFromStream(bais, tripleType);
			System.out.println(triple.getFirst());
			System.out.println(triple.getSecond());
			System.out.println(triple.getThird());
		} finally {
			if (bais != null) {
				try {
					bais.close();
				} catch (Exception e) {}
			}
		}
	}

输出:

info.kuyur.jsondemo.models.Triple<info.kuyur.jsondemo.models.User, info.kuyur.jsondemo.models.Archive, info.kuyur.jsondemo.models.Comment>
User [name=一桶浆糊, mailAddress=yi.tong@jianghu.com]
Archive [blogId=1000, title=18岁的那天, text=18岁那天的早上..., author=User [name=一桶浆糊, mailAddress=yi.tong@jianghu.com]]
Comment [content=骚年よ、大志を抱け, author=User [name=铅球万袋, mailAddress=qian.qiu@wandai.com]]

完美转换!

User/Archive/Comment是退化的参数化类型,因此Triple<User, Archive, Comment>的参数化类型的构造过程可以简化为:

Type[] typeArguments = {User.class, Archive.class, Comment.class};
MoeParameterizedTypeImpl tripleType = new MoeParameterizedTypeImpl(Triple.class, typeArguments, null);

下篇介绍最后的杀器,Jackson提供的TypeReference。

Java泛型和JSON的反序列化(上)

开发RESTful应用的客户端时,不可避免要实现JSON或xml的反序列化和序列化。对于HTML客户端,JSON和JavaScript对象之间的转换是自然而然理所当然的事,JavaScript框架不实现就算不上框架。但对于Java语言的客户端,选择却不是那么多。Jackson JSON应该是最强大的JSON和Java对象转换工具库。

Jackson官方有个五分钟入门教程,用法非常的简洁。

下文的型都是指java.lang.reflect.Type,
java1.5引入了泛型,Type正是对一切数据类型的抽象,万物皆有型,
内置数据类型有型,如int.class,boolean.class,
对象类型有型,如Integer.class,和Integer i = 1; i.getClass();等价

Type type1 = Integer.class;
Integer i = 1;
Type type2 = i.getClass();
assertEquals(type1, type2);

同时【型】还是一种Java对象。Class是Type的实现,是【对象】(Object)这一类数据类型的型。

将反序列化功能写成一个通用的工具类:(使用ObjectMapper的接口readValue(String, Class<T>))

package info.kuyur.jsondemo.utils;

import java.io.IOException;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

public class Util {
	private Util() {}

	/**
	 * Only JacksonAnnotation supported.
	 * @param jsonString JSON string
	 * @param type Class type. Generic type is not supported.
	 * @return
	 */
	public static <T> T readJsonFromString(String jsonString, Class<T> type) {
		ObjectMapper mapper = new ObjectMapper();
		try {
			return mapper.readValue(jsonString, type);
		} catch (JsonParseException e) {
			throw new RuntimeException("Deserialize from JSON failed.", e);
		} catch (JsonMappingException e) {
			throw new RuntimeException("Deserialize from JSON failed.", e);
		} catch (IOException e) {
			throw new RuntimeException("Deserialize from JSON failed.", e);
		}
	}

反序列化函数使用了泛型(本来Jackson的ObjectMapper就使用了泛型)

定义几个数据模型,然后再测试一下这个反序列化函数的转换能力

User.java,不加注任何标签

package info.kuyur.jsondemo.models;

public class User {

	private String name;
	private String mailAddress;

	public User() {
		super();
	}

	public User(String name, String mailAddress) {
		super();
		this.name = name;
		this.mailAddress = mailAddress;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getMailAddress() {
		return mailAddress;
	}

	public void setMailAddress(String mailAddress) {
		this.mailAddress = mailAddress;
	}

	@Override
	public String toString() {
		return "User [name=" + name + ", mailAddress=" + mailAddress + "]";
	}
}

Archive.java,加注JAXBAnnotation类型的标签

package info.kuyur.jsondemo.models;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Archive {

	@XmlElement(name="ID")
	private int blogId;

	@XmlElement(name="标题")
	private String title;

	@XmlElement(name="正文")
	private String text;

	@XmlElement(name="作者")
	private User author;

	public Archive() {
		super();
	}

	public Archive(int blogId, String title, String text, User author) {
		super();
		this.blogId = blogId;
		this.title = title;
		this.text = text;
		this.author = author;
	}

	public int getBlogId() {
		return blogId;
	}

	public void setBlogId(int blogId) {
		this.blogId = blogId;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	public User getAuthor() {
		return author;
	}

	public void setAuthor(User author) {
		this.author = author;
	}

	@Override
	public String toString() {
		return "Archive [blogId=" + blogId + ", title=" + title + ", text="
				+ text + ", author=" + author + "]";
	}
}

Comment.java,加注JacksonAnnotation类型的标签

package info.kuyur.jsondemo.models;

import org.codehaus.jackson.annotate.JsonProperty;

public class Comment {

	@JsonProperty("作者")
	private User author;

	@JsonProperty("回复")
	private String content;

	public User getAuthor() {
		return author;
	}

	public void setAuthor(User author) {
		this.author = author;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	@Override
	public String toString() {
		return "Comment [content=" + content + ", author=" + author + "]";
	}
}

User的一条json字符串,成员变量名作为key,反序列化通过。
{"name": "一桶浆糊", "mailAddress": "yi.tong@jianghu.com"}

	@Test
	public void testReadJsonFromString1() {
		String jsonString = "{\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}";
		User user = Util.readJsonFromString(jsonString, User.class);
		System.out.println(user);
		assertEquals(user.getName(), "一桶浆糊");
		assertEquals(user.getMailAddress(), "yi.tong@jianghu.com");
	}

Jackson能从User的型(Class<User>)中,以json字符串中的key(name和mailAddress)来搜索成员变量名,再通过规范化的setter设置成员变量值。
含有参数的public构造子对Jackson是没意义的,因为对java而言参数变量名没有意义,有意义的只是参数类型。
以参数类型和函数名为检索条件通过反射可以确定唯一的函数,以参数类型为检索条件可以确定唯一的构造子,但Jackson不会去使用这种带参数的构造子

	public User(String name, String mailAddress) {
		super();
		this.name = name;
		this.mailAddress = mailAddress;
	}
	public User(String mingzi, String youxiang) {
		super();
		this.name = mingzi;
		this.mailAddress = youxiang;
	}

上面两个构造子完全一样,它们的函数原型相同,都是public User(String, String),你不能在一个类里同时定义两个原型相同的函数。
再次强调,参数名没有意义,如果调用这种构造子,Jackson不会懂得name和mailAddress那个先,那个后。
因此,为了让Jackson能自动反序列化,数据模型的public无参构造子以及规范化setter必须实现

Archive的一条json字符串,key在JAXBAnnotation类型的标注中定义
{"ID": "1000", "标题": "18岁的那天", "正文": "18岁那天的早上..."}

	@Test
	public void testReadJsonFromStringButFail() {
		String jsonString = "{\"ID\": \"1000\", \"标题\": \"18岁的那天\", \"正文\": \"18岁那天的早上...\"}";
		Archive archive = Util.readJsonFromString(jsonString, Archive.class);
		System.out.println(archive);
	}

很可惜,反序列化失败,ObjectMapper不支持JAXBAnnotation类型标签。

Comment的一条json字符串,key在JacksonAnnotation类型的标注中定义
{"作者": {"name": "铅球万袋", "mailAddress": "qian.qiu@wandai.com"}, "回复": "骚年よ、大志を抱け"}

	@Test
	public void testReadJsonFromString4() {
		String jsonString = "{\"作者\": {\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, \"回复\": \"骚年よ、大志を抱け\"}";
		Comment comment = Util.readJsonFromString(jsonString, Comment.class);
		System.out.println(comment);
		assertEquals(comment.getContent(), "骚年よ、大志を抱け");
		assertEquals(comment.getAuthor().getName(), "铅球万袋");
		assertEquals(comment.getAuthor().getMailAddress(), "qian.qiu@wandai.com");
	}

反序列化通过。同时Comment的成员变量author也反序列化成功。因此Jackson反序列化复杂的数据模型没有问题。

再来测试泛型

数组类型的一条json
[{"name": "铅球万袋", "mailAddress": "qian.qiu@wandai.com"}, {"name": "一桶浆糊", "mailAddress": "yi.tong@jianghu.com"}]

	@Test
	public void testReadJsonFromString5() {
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		User[] users = Util.readJsonFromString(jsonString, User[].class);
		for (User user : users) {
			System.out.println(user);
		}
	}

转换成功,ObjectMapper支持转换到特定类型的数组。
User[] users的型也即User[].class是GenericArrayType,通过getGenericComponentType()接口,可以获得User的型,这样Jackson就可以成功构造User类型的数组。

但如果我们想转换成泛型容器呢?

	@Test
	public void testReadJsonFromString5ButFail() {
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		List<User> users = Util.readJsonFromString(jsonString, List.class);
		for (User user : users) {
			System.out.println(user);
		}
	}

这种转换不会成功,List.class提供给Jackson的信息并不足够,Jackson并不懂得List里面要装的是User类型的数据。

简单的字符串数组/整型数组/布尔数组是能转换成List<String>,List<Integer>,List<Boolean>的。
例如,下面的json反序列化通过

	@Test
	public void testReadJsonFromString12() {
		String jsonString = "[1, 2]";
		List<Integer> integers = Util.readJsonFromString(jsonString, List.class);
		for (Integer i : integers) {
			System.out.println(i);
		}
	}

Jackson能从json中推导出List中所要装的东西的类型。
(注意[1, 2]是整型数组,["1", "2"]是字符串数组,json自身能携带少量的型信息)

testReadJsonFromString5ButFail用例中,事实上Jackson将(希望是User类型的)json对象转换成了LinkedHashMap类型的对象,LinkedHashMap当然不能转换到User类型,于是就抛出了ClassCastException

将容器修改为List<Object>类型:

	@Test
	public void testReadJsonFromString13() {
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		List<Object> users = Util.readJsonFromString(jsonString, List.class);
		for (Object user : users) {
			System.out.println(user);
		}
	}

或者更直接的List<LinkedHashMap>类型:

	@Test
	public void testReadJsonFromString14() {
		String jsonString = "[{\"name\": \"铅球万袋\", \"mailAddress\": \"qian.qiu@wandai.com\"}, {\"name\": \"一桶浆糊\", \"mailAddress\": \"yi.tong@jianghu.com\"}]";
		List<LinkedHashMap> users = Util.readJsonFromString(jsonString, List.class);
		for (LinkedHashMap user : users) {
			System.out.println(user);
		}
	}

可以反序列化成功,但对象已经面目全非,不是我们想要的了。

但我们很想很想要List<User>类型的数据啊,怎么办?那是下一篇的故事了。

总结一下:Jackson的ObjectMapper的使用有两个限制:
一是标签类型只能是JacksonAnnotation或者不加标签,数据模型不加标签的话,json就只能迁就数据模型,但如果json的字段名(key)不是我们所能控制的话,比如来一个"城市": "广州",麻烦可就大了;
二是对泛型容器的有限支持(这句话并不准确,事实上ObjectMapper的另外一个接口支持更加强大的【型】——ParameterizedType,可以实现我们的需求)。

11,862 次浏览 | 没有评论
2012年12月14日 | 归档于 技术, 程序

流方式创建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)的执行结果。

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

5,003 次浏览 | 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]

6,798 次浏览 | 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)时,需要重写

5,073 次浏览 | 没有评论
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."
6,192 次浏览 | 没有评论
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证书么?

3,987 次浏览 | 没有评论
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) {
				}
			}
		}
	}
}

用法:参考测试用例

6,826 次浏览 | 没有评论
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敌人中相当高的级别了吧

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