需求背景
最近项目组中接到新的需求,要求app访问服务端的接口中,需要在报文传输中对某个字段进行加密,同时若返回响应中有该字段,则需要加密返回app;为了兼容旧版本app,与客户端同事商议,在新版本中该字段传输格式为:加密文本+特定的分割符+加密方式。
解决思路
- 服务端通过传入字段判断,如果是“加密字段+特殊分割符+加密方式”这个方式拼接的,则需要对加密文本通过相对应的解密方式解密;
- app涉及的服务特别多,接口也非常多,对每个接口进行判断并解密操作,显然是不靠谱的,不但工作量大而且容易出错;
- 能否实现一个公共模块,对请求参数进行统一处理,让请求参数到达业务接口层时,已经是解密好的数据。
解决方法
- 由于项目框架是spring+ Jersey 1.x,查资料jersery看是否有对应的filter处理这种情况,果不其然Jersey的ContainerRequestFilter可以实现该功能,参考:http://codehustler.org/blog/jersey-cross-site-scripting-xss-filter-for-java-web-apps/,具体的实现代码如下:
/**
* 加密方式常量类
*/
public class EncryptContant {
public static final String ENCRYPT_BY_BASE64 = "BASE64";
public static final String ENCRYPT_BY_MD5 = "MD5";
}
public class RequestParamFilter implements ContainerRequestFilter{
@Override
public ContainerRequest filter( ContainerRequest request ){
// Clean the query strings
cleanParams( request.getQueryParameters() );
cleanParams( request.getFormParameters() );
// Return the cleansed request
return request;
}
private void cleanParams( MultivaluedMap<String, String> parameters ){
for( Map.Entry<String, List<String>> params : parameters.entrySet() ){
String key = params.getKey();
List<String> values = params.getValue();
List<String> cleanValues = new ArrayList<String>();
String value = values.get(0);
if (value.split("_").length == 2 && EncryptContant.ENCRYPT_BY_BASE64.equeals(value.split("_")[1])) {
value = Base64Utils.decryptStr(value.split("_")[0]));
}
cleanValues.add(value);
parameters.put( key, cleanValues );
}
}
然后在web.xml配置下Filter,就可以对请求参数进行过滤解密了,然而这种方式是有局限性的,对于post请求提交的参数没法解析,也就是对jersey@FormParam这种不行,通过提供的方法request.getFormParameters()获取不到参数,只能解决@QueryParam这种参数,或许jersery2中已经实现了,但由于整个项目是jersery1的,升级框架版本是不可能的,除非以后对单个项目进行重构;
- 考虑采用新的解决方案,通过springAop,扫描所有接口方法,获取参数列表,对参数进行判断解密,但是在参数列表中可能参数是@Context HttpServletRequest类型,且业务代码中有部分数据是从这个request中获取的,因此还需要对Request的所有参数判断并解密,由于J2EE的Request的数据是没法改变的,因此只能重写Request,并将重写的request做为新的参数传入接口方法;
具体的实现方法如下,示例中用"_"作为特殊分割符:
@Aspect //定义一个切面
@Configuration
@Order(3)
public class RequestParamDealAspect {
// 定义切点Pointcut
@Pointcut("execution(* com.*.controller.*Controller.*(..))")
public void excudeService() {
}
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
Object [] args = pjp.getArgs();
Object [] newArgs = new Object[args.length];
for (int i=0;i<args.length;i++) {
Object item = args[i];
if (args[i] instanceof HttpServletRequest) {
item = new MyHttpServletRequestWrapper((HttpServletRequest) item);
}
if(args[i] instanceof String){
if (item != null && args[i].toString().split("_").length == 2 && EncryptContant.ENCRYPT_BY_BASE64.equeals(item.toString().split("_")[1])) {
item = Base64Utils.decryptStr(item.toString().split("_")[0]);
}
}
newArgs[i] = item;
}
Object result = pjp.proceed(newArgs);
return result;
}
}
public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
public HttpServletRequest originalRequest;
public Map decryptParameterMap;
public MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
originalRequest = request;
decryptParameterMap = new HashMap();
Map<String, String[]> properties = request.getParameterMap();
Map<String, String> returnMap = new HashMap<String, String>();
Iterator<Map.Entry<String, String[]>> entries = properties.entrySet().iterator();
Map.Entry<String, String[]> entry;
String key = "";
String value = "";
while(entries.hasNext()){
entry = entries.next();
key = entry.getKey();
Object valueObj = entry.getValue();
if (null == valueObj) {
value = "";
} else if (valueObj instanceof String[]) {
String[] values = (String[]) valueObj;
value = values[0];
if(value.split("_").length == 2 && EncryptContant.ENCRYPT_BY_BASE64.equals(value.split("_")[1])){
value = Base64Utils.decryptStr(value.split("_")[0]);
}
} else {
value = valueObj.toString();
}
returnMap.put(key, value);
}
decryptParameterMap.putAll(returnMap);
}
@Override
public String getParameter(String s){
// 返回解密后的参数
return String.valueOf(decryptParameterMap.get(s));
}
@Override
public Enumeration getParameterNames(){
// 这里是通过实体类注入参数
return Collections.enumeration(decryptParameterMap.keySet());
}
@Override
public String[] getParameterValues(String s){
// 这里是注入参数
Object o = decryptParameterMap.get(s);
if (o == null){
return null;
} else{
return new String[] {String.valueOf(o)};
}
}
@Override
public Map getParameterMap(){
return decryptParameterMap;
}
}
通过这种方式对任何方式的传参均可以进行加密解析
- 业务中通过具体的加密方式对响应字段进行加密,通过定义filter和ThreadLocal在对request参数解析,保存加密字段和对应的加密方式的映射关系,然后在业务代码中获取要加密字段的加密方式;
public class EncryptThreadLocal {
public static ThreadLocal<Map<String,String>> threadLocalEncrypt = new ThreadLocal<Map<String,String>>();
}
public class EncryptFilter implements Filter {
public void destroy() {
EncryptThreadLocal.threadLocalEncrypt.remove();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
Map<String, String[]> parameterMap = httpRequest.getParameterMap();
Iterator<Map.Entry<String, String[]>> it = parameterMap.entrySet().iterator();
Map.Entry<String, String[]> entry = null;
String key;
String value;
Map encrytMap = new HashMap();
while (it.hasNext()){
entry = it.next();
key = entry.getKey();
value= entry.getValue()[0];
if(value.split("_").length == 2 ){
encrytMap.put(key,value.split("_")[1]);
}
}
EncryptThreadLocal.threadLocalEncrypt.set(encrytMap);
try {
chain.doFilter(request, response);
}finally {
EncryptThreadLocal.threadLocalEncrypt.remove();
}
}
}
获取加密方式,对字段进行加密
Map encryptMap = EncryptThreadLocal.threadLocalEncrypt.get();
String test="adafgaga";
String encryptType = (String) encryptMap.get("test");
if(EncryptContant.ENCRYPT_BY_BASE64.equals(encryptType )){
test = ase64Utils.ecryptStr(test);
}
总结
- 通过上面的方法,完美的解决了这个需求,并且对业务代码有很小的侵入,若返回响应中没有需要的加密的字段,就不需要配置EncryptFilter;
- 如果传入的参数值中本身就含有"_",则可以考虑先将字段escape,然后在服务端接口unescape,就不会出现问题。