通过SpringAop、Filter对Request参数解密和对Response内容加密

java 同时被 2 个专栏收录
21 篇文章 0 订阅
3 篇文章 0 订阅

需求背景

最近项目组中接到新的需求,要求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,就不会出现问题。
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页

打赏作者

冰封之骑士

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值