问题:在Cookie value中储存特殊字符并读取。
典型案例:email地址在cookie value中的存取。
解决方法:通过将Cookie value进行Base54编码后储存,读取,并解码。
环境:Java & Javascript
问题延伸:Javascript中读取Base64值错误的解决方法。
我个人认为使用email地址来作为用户帐户登录名称是一个比较不错的方法。首先email地址本身已经具备一定程度上的唯一性。通过一定的正则表达式判断或者直接使用现成的一些判断类和Service判断后可以使数据库中的该字段价值更高。其次在同一用户需要使用多个帐户的情况下,相比“用户名”来说,更加直接并且容易记忆。
使用传统的方式创建的cookie:
Cookie cookie = new Cookie(cookieName, cookieValue);
((BridgeExternalContext)context.getExternalContext()).addCookie(cookie); // icefaces中使用ExternalContext储存cookie
此时如果cookieValue的值是包含一些特定字符,比如空格,大括号,小括号,逗号,以及@等。用直接读取的方式将不能得到正确的值。
比如上面的代码中,String cookieValue = "sam@satech.com.au"
使用下面的代码直接读取:
Cookie[] cookies = ((HttpServletRequest)((BridgeExternalContext)FacesContext
.getCurrentInstance().getExternalContext()).getRequest()).getCookies();
for(int i=0;i<cookies.length;i++){
if(cookies[i].getName().equals(loginCookieName)) {
this.loginName = cookies[i].getValue();
}
你会发现,储存后在firefox的cookie查看器中能看到正常的值,而读取出来以后,值只剩下一个sam了。这显然不是我们所想要的。
最简单的解决方式,就是使用某种编码方式避开这些不能接受的字符,之后读取出来再进行解码。这里使用Base64编码来解决。其实使用任何编码方式都是可以的。
我的环境中有很多Base64类的继承类可以使用。比较常见的是
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
其他一些继承类中的加密和解码方法,可以直接作为静态方法调用,比如Icefaces自己也在utility包里面包含了。Base64类,需要的朋友可以尝试使用,原理是一样的。我这里列出使用基本的sun的misc包中的BASE64Encoder & Decoder的方法。
编码
if (log.isDebugEnabled()) {
log.debug("Encoding string: " + this.loginName);
}
byte[] encodedBytes = this.loginName.getBytes(); // convert String into byte[]
BASE64Encoder base64encoder = new BASE64Encoder(); // Create encoder
this.base64LoginName = base64encoder.encode(encodedBytes);
Cookie nameCookie = new Cookie(loginCookieName, this.base64LoginName);
nameCookie.setMaxAge(SECONDS_PER_YEAR);
nameCookie.setComment("This is the cookie to store username");
((BridgeExternalContext)context.getExternalContext()).addCookie(nameCookie); // save cookie to context
解码
Cookie[] cookies = ((HttpServletRequest)((BridgeExternalContext)FacesContext
.getCurrentInstance().getExternalContext()).getRequest()).getCookies();
for(int i=0;i<cookies.length;i++){
if(cookies[i].getName().equals(loginCookieName)) {
this.base64LoginName = cookies[i].getValue();
BASE64Decoder decoder = new BASE64Decoder();
try {
byte[] decodedBytes = decoder.decodeBuffer(this.base64LoginName);
this.loginName = new String(decodedBytes);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
现在这个cookie value就能接受大部分的字符集了。但是,又发现一个问题。大部分的Base64都是以"="结尾。在纯Java环境中读取和储存都没有问题,但是某些时候会需要在Javascript中储存和读取这个值。在Javascript中读取已储存的Base64值的时候,最后面的"="会丢失,导致读取错误。
解决方案有2,第一,使用URLCodec urlCodec再次编码。第二,自己写一个判断函数来加上最后的"=" or "=="
方案一:
public class CookieDecoder {
private static final Log log = LogFactory.getLog(CookieDecoder.class);
/**
* @param cookieValue The value of the cookie to decode
* @return Returns the decoded string
*/
public String decode(String cookieValue) {
if (cookieValue == null || "".equals(cookieValue)) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("Decoding string: " + cookieValue);
}
URLCodec urlCodec = new URLCodec();
String b64Str;
try {
b64Str = urlCodec.decode(cookieValue);
}
catch (DecoderException e) {
log.error("Error decoding string: " + cookieValue);
return null;
}
Base64 base64 = new Base64();
byte[] encodedBytes = b64Str.getBytes();
byte[] decodedBytes = base64.decode(encodedBytes);
String result = new String(decodedBytes);
if (log.isDebugEnabled()) {
log.debug("Decoded string to: " + result);
}
return result;
}
}
JS部分:
Encode:
var encodedValue = this.base64.encode(value); document.cookie = name + "=" + escape(encodedValue) + "; expires=" + this.expires.toGMTString() + "; path=" + this.path;
Decode:
var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0)==' ') { c = c.substring(1,c.length); } if (c.indexOf(nameEQ) == 0) { var encodedValue = c.substring(nameEQ.length,c.length); return this.base64.decode(unescape(encodedValue)); } } return null;
方案二:padString()来解决最后那个等号。
private static final Log log = LogFactory.getLog(CookieDecoder.class);
/**
* @param cookieValue The value of the cookie to decode
* @return Returns the decoded string
*/
public String decode(String cookieValue) {
if (cookieValue == null || "".equals(cookieValue)) {
return null;
}
if (!cookieValue.endsWith("=")) {
cookieValue = padString(cookieValue);
}
if (log.isDebugEnabled()) {
log.debug("Decoding string: " + cookieValue);
}
Base64 base64 = new Base64();
byte[] encodedBytes = cookieValue.getBytes();
byte[] decodedBytes = base64.decode(encodedBytes);
String result = new String(decodedBytes);
if (log.isDebugEnabled()) {
log.debug("Decoded string to: " + result);
}
return result;
}
private String padString(String value) {
int mod = value.length() % 4;
if (mod <= 0) {
return value;
}
int numEqs = 4 - mod;
if (log.isDebugEnabled()) {
log.debug("Padding value with " + numEqs + " = signs");
}
for (int i = 0; i < numEqs; i++) {
value += "=";
}
return value;
}
JS部分:现在在JS中就可以按照正常方法读取了
var encodedValue = this.base64.encode(value);
document.cookie = name + "=" + encodedValue +
"; expires=" + this.expires.toGMTString() +
"; path=" + this.path;
至此,基本解决问题。