前后端报文传输加密方案

前后端报文传输加密方案开发人员联系方式:251746034@qq.com代码库:https://github.com/chenjia/vue-desktop代码库:https://github.com/chenjia/vue-app代码库:https://github.com/chenjia/lxt示例:http://47.100.119.102/vue-desktop示例:http://47.100.119…

大家好,又见面了,我是你们的朋友全栈君。

开发人员联系方式:251746034@qq.com
代码库:https://github.com/chenjia/vue-desktop
代码库:https://github.com/chenjia/vue-app
代码库:https://github.com/chenjia/lxt
示例:http://47.100.119.102/vue-desktop
示例:http://47.100.119.102/vue-app
目的:前后端传输报文进行加密处理。

一、开发环境
前端技术:vue + axios
后端技术:java
加密算法:AES

为什么选择采用AES加密算法?作者在各种加密算法都进行过尝试,发现AES有以下特点比较符合要求:
1、加密解密执行速度快,相对DES更安全(原来采用的DES,结果部门的安全扫描建议用AES)
2、对称加密
3、被加密的明文长度可以很大,最多测试过10万长度的字符串。

java端AES加密示例,参考 lxt/lxt-common/com/lxt/ms/common/utils/SecurityUtils.java

public class SecurityUtils { 
   
    public final static String letters = "abcdefghijklmnopqrstuvwxyz0123456789";

    public final static String key = "ed26d4cd99aa11e5b8a4c89cdc776729";

    private static String Algorithm = "AES";

    private static String AlgorithmProvider = "AES/ECB/PKCS5Padding";

    private final static String encoding = "UTF-8";

    public static String encrypt(String src) throws NoSuchAlgorithmException, NoSuchPaddingException,
            InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException, InvalidAlgorithmParameterException { 
   
        SecretKey secretKey = new SecretKeySpec(key.getBytes("utf-8"), Algorithm);
        //IvParameterSpec ivParameterSpec = getIv();
        Cipher cipher = Cipher.getInstance(AlgorithmProvider);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] cipherBytes = cipher.doFinal(src.getBytes(Charset.forName("utf-8")));
        return Base64Utils.encodeToString(cipherBytes);
    }

    public static String decrypt(String src) throws Exception { 
   
        SecretKey secretKey = new SecretKeySpec(key.getBytes("utf-8"), Algorithm);

        //IvParameterSpec ivParameterSpec = getIv();
        Cipher cipher = Cipher.getInstance(AlgorithmProvider);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] hexBytes = Base64Utils.decodeFromString(src);
        byte[] plainBytes = cipher.doFinal(hexBytes);
        return new String(plainBytes, "utf-8");
    }

    public static String md5Encrypt(String str) { 
   
        try { 
   
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            byte[] byteDigest = md.digest();
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < byteDigest.length; offset++) { 
   
                i = byteDigest[offset];
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            //32位加密
            return buf.toString();
            // 16位的加密
            //return buf.toString().substring(8, 24);
        } catch (NoSuchAlgorithmException e) { 
   
            e.printStackTrace();
            return null;
        }
    }

    public static String encryptKey(String key) throws Exception { 
   
        String encryptedKey = "";

        String[] array = key.split("");

        Random random = new Random();

        for (int i = 0; i < array.length; i++) { 
   
            encryptedKey += array[i];
            for (int j = 0; j < i % 2 + 1; j++) { 
   
                int index = random.nextInt(letters.length());
                encryptedKey += letters.substring(index, index + 1);
            }
        }
        return Base64Utils.encodeToString(new StringBuilder(encryptedKey).reverse().toString().getBytes(encoding)).replaceAll("\n", "");
    }

    public static String decryptKey(String encryptedKey) { 
   
        encryptedKey = new String(Base64Utils.decodeFromString(encryptedKey));
        String key = "";

        char[] c = new StringBuilder(encryptedKey).reverse().toString().toCharArray();

        for (int i = 0, j = 0; i < encryptedKey.length(); i++) { 
   
            key += c[i];
            i += (j++ % 2 + 1);
        }

        return key;
    }

前端AES加密,参考 vue-app/src/utils/security.js 或 vue-desktop/src/utils/security.js

var CryptoJS = require("crypto-js");

const encryptByAES = (message, key) => { 
   
    var keyHex = CryptoJS.enc.Utf8.parse(key);
    var encrypted = CryptoJS.AES.encrypt(message, keyHex, { 
   
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return encrypted.ciphertext.toString(CryptoJS.enc.Base64).replace(/[\r\n]/g, '');
}

const decryptByAES = (ciphertext, key) => { 
   
    var keyHex = CryptoJS.enc.Utf8.parse(key);
    var decrypted = CryptoJS.AES.decrypt({ 
   
        ciphertext: CryptoJS.enc.Base64.parse(ciphertext.replace(/[\r\n]/g, ''))
    }, keyHex, { 
   
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return decrypted.toString(CryptoJS.enc.Utf8);
}

const encryptKey = key => { 
   
  let array = key.split('')
  let letters = 'abcdefghijklmnopqrstuvwxyz0123456789'
  let encryptedKey = ''
  for(let i=0;i<array.length;i++){ 
   
    encryptedKey += array[i]
    for(let j=0;j<i%2+1;j++){ 
   
      encryptedKey += letters.substr(parseInt(Math.random()*letters.length),1)
    }
  }
  return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptedKey.split('').reverse().join('')))
}

const decryptKey = encryptedKey => { 
   
  encryptedKey = CryptoJS.enc.Base64.parse(encryptedKey).toString(CryptoJS.enc.Utf8).split('').reverse().join('')
  let str = ''
  for(let i=0,j=0;i<encryptedKey.length;i++){ 
   
    str += encryptedKey[i]
    i += (j++ % 2 + 1)
  }
  return str
}

export { 
   encryptByAES,decryptByAES,encryptKey,decryptKey}

好了,加密算法都有了,那怎么对报文进行加密呢?
前端利用axios的拦截器就可以轻松实现。

import axios from 'axios'
import cache from './cache'
import store from '../vuex/store'
import { 
   encryptByAES,decryptByAES,encryptKey,decryptKey} from './security'
var CryptoJS = require("crypto-js");
window.axios = axios

let instance = axios.create({ 
   
  method: 'post',
  timeout: 60000,
  withCredentials: true,
  headers: { 
   
    post: { 
   
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  },
  transformRequest: [function(data) { 
   
    let ret = ''
    for (let it in data) { 
   
      ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
    }
    return ret
  }]
})

instance.interceptors.request.use(function(config) { 
   
  let user = cache.get('user')
  let data = { 
   
    head: { 
   
      url: config.url,
      debug: true,
      userId: user ? user.userId : null,
      token: cache.get('token'),
      timestamp:new Date().getTime()
    },
    body: { 
   
      data: config.data
    }
  }
  console.log('\n【request:'+config.url+'】', data, '\n\n')
  config.url = window.Config.server + config.url

  config.data = { 
   
    request: encryptByAES(JSON.stringify(data), decryptKey(Config.key))
  }
  return config
}, function(error) { 
   
  console.log(error)
  return Promise.reject(error)
})

instance.interceptors.response.use(function(response) { 
   
  let resp = decryptByAES(response.data.response, decryptKey(Config.key))
  response.data = JSON.parse(resp)
  console.log('\n【response:'+response.config.url+'】',response, '\n\n')
  if(response.data.head.status != 200){ 
   
    store.commit('TOGGLE_POPUP', { 
   visible: true, text: response.data.head.msg, duration: 3000})
  }
  let token = response.data.head.token
  cache.set('token', token || cache.get('token'))
  return response
}, function(error) { 
   
  console.log(error)
  return Promise.reject(error)
})

export default instance

注意上面 request 和 response 两个拦截器,在拦截 request 的时候,以下是对请求进行加密

config.data = {
  request: encryptByAES(JSON.stringify(data), decryptKey(Config.key))
}

在拦截 response 的时候,以下是对响应的解密

let resp = decryptByAES(response.data.response, decryptKey(Config.key))
response.data = JSON.parse(resp)

这样,前端只要是通过 instance 这个模版发出去的请求,就能自动在请求时加密,响应时解密了。注意,这里的decryptKey(Config.key)是对进行简单混淆后的密钥进行反处理,才能得到最初的AES密钥。

前端部分好了,后台部分怎么做呢?其实思路都是类似的,后台是用的springcloud里面的zuul进行统一拦截的,当然你如果不是使用的微服务体系,后台通过最原始的过滤器也是可以的。

public class RequestFilter extends ZuulFilter{ 
   
	@Value("#{'${filterUrls.services}'.split(',')}")
	private String[] services;

	@Value("${filterUrls.apis}")
	private String apis;

	@Value("#{'${filterUrls.excludes}'.split(',')}")
	private String[] excludes;

	@Override
	public Object run() throws ZuulException{ 
   
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletRequest request = ctx.getRequest();
		
		System.out.println("【contextPath】"+request.getContextPath());
		System.out.println("【requestURI】"+request.getRequestURI());
		
		String contextPath = request.getContextPath();
		String uri = request.getRequestURI().replaceAll(contextPath, "");

		String encryptedText = request.getParameter("request");
		Packages pkg = new Packages();
		String decryptedText = null;
		try { 
   
			decryptedText = SecurityUtils.decrypt(encryptedText);
			pkg = JSONUtils.json2Obj(decryptedText, Packages.class);
		} catch (Exception e) { 
   
			e.printStackTrace();
			pkg.getHead().setStatus(500);
			pkg.getHead().setMsg("报文解密异常!");
		}

		if (pkg.getHead().getStatus() == 200 && apis.indexOf(uri) == -1) { 
   
			String token = pkg.getHead().getToken();
			String userId = pkg.getHead().getUserId();

			if (StringUtils.isNotEmpty(userId)) { 
   
				try { 
   
					Map<String, Object> map = JWTUtils.parse(token);
					if(userId.equals(map.get("userId"))){ 
   
						Set<Object> resourceSet = CacheUtils.sGet("RESOURCE_"+userId);
						if(resourceSet == null || !resourceSet.contains(uri)){ 
   
							System.out.println("forbidden:"+uri);
						}
// if(resourceSet == null || !resourceSet.contains(uri)){ 
   
// pkg.getHead().setStatus(500);
// pkg.getHead().setMsg("未授权的访问,请联系管理员!");
// }
					}else { 
   
						pkg.getHead().setStatus(500);
						pkg.getHead().setMsg("token验证失败!");
					}
				} catch (Exception e) { 
   
					e.printStackTrace();
					pkg.getHead().setStatus(500);
					pkg.getHead().setMsg("token转换失败!");
				}
			}
		}

		InputStream in = (InputStream) ctx.get("requestEntity");
		if (in == null) { 
   
			try { 
   
				in = ctx.getRequest().getInputStream();
				String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
				body = "request=" + JSONUtils.obj2Json(pkg);

				final byte[] reqBodyBytes = body.getBytes();
				ctx.setRequest(new HttpServletRequestWrapper(ctx.getRequest()) { 
   
					@Override
					public ServletInputStream getInputStream() throws IOException { 
   
						return new ServletInputStreamWrapper(reqBodyBytes);
					}

					@Override
					public int getContentLength() { 
   
						return reqBodyBytes.length;
					}

					@Override
					public long getContentLengthLong() { 
   
						return reqBodyBytes.length;
					}
				});
			} catch (IOException e) { 
   
				e.printStackTrace();
				throw new ZuulException(e, 500, "获取输入流失败");
			}
		}

		return null;
	}

	@Override
	public boolean shouldFilter() { 
   
		boolean shouldFilter = false;
		
		HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
		String uri = request.getRequestURI();
		for(String url : services){ 
   
			if(uri.startsWith(url)){ 
   
				shouldFilter = true;
				break;
			}
		}

		for(String exclude : excludes){ 
   
			if(uri.startsWith(exclude)){ 
   
				shouldFilter = false;
				break;
			}
		}
		
		return shouldFilter;
	}

	@Override
	public int filterOrder() { 
   
		return FilterConstants.PRE_DECORATION_FILTER_ORDER;
	}

	@Override
	public String filterType() { 
   
		return "pre";
	}
}
public class ResponseFilter extends ZuulFilter { 
   
	@Value("#{'${filterUrls.services}'.split(',')}")
	private String[] services;

	@Value("#{'${filterUrls.origins}'.split(',')}")
	private Set<String> origins;

	@Value("#{'${filterUrls.excludes}'.split(',')}")
    private String[] excludes;

	@Override
	public Object run() throws ZuulException { 
   
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletRequest request = ctx.getRequest();
		HttpServletResponse response = ctx.getResponse();

		response.setCharacterEncoding("UTF-8");
		response.setContentType("application/json;charset=utf-8");

		String origin = request.getHeader("Origin");
		if (origins.contains(origin)) { 
   
			response.setHeader("Access-Control-Allow-Origin", origin);
			response.setHeader("Access-Control-Allow-Methods",
					"POST,GET,OPTIONS");
			response.setHeader("Access-Control-Allow-Headers",
					"Origin,X-Requested-With,Content-Type,Accept,token");
			response.setHeader("Access-Control-Allow-Credentials", "true");
		}else { 
   
			System.out.println("【origin】"+origin);
		}
		
		try { 
   
            InputStream stream = ctx.getResponseDataStream();
            String body = StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
            String encryptedText = SecurityUtils.encrypt(body);
            ctx.setResponseBody("{\"response\":\""+ encryptedText.replaceAll("\r\n|\n", "") +"\"}");
        } catch (Exception e) { 
   
            throw new ZuulException(e, 500, "报文加密异常");
        }
		return null;
	}

	@Override
	public boolean shouldFilter() { 
   
		boolean shouldFilter = false;
		
		HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
		String uri = request.getRequestURI();
		for(String url : services){ 
   
			if(uri.startsWith(url)){ 
   
				shouldFilter = true;
				break;
			}
		}

		for(String exclude : excludes){ 
   
			if(uri.startsWith(exclude)){ 
   
				shouldFilter = false;
				break;
			}
		}

		return shouldFilter;
	}

	@Override
	public int filterOrder() { 
   
		return FilterConstants.SEND_RESPONSE_FILTER_ORDER;
	}

	@Override
	public String filterType() { 
   
		return "post";
	}
}
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication { 
   
	public static void main(String[] args) { 
   
		SpringApplication.run(GatewayApplication.class, args);
	}
	
	@Bean
    public RequestFilter requestFilter() { 
   
        return new RequestFilter();
    }

	@Bean
    public ResponseFilter responseFilter() { 
   
        return new ResponseFilter();
    }
}

记得在启动类里面注册这两个过滤器(拦截器)。
作者的实现里面在拦截器里面加了大量的逻辑,可以根据自己的需要酌情删减。
比如:控制权限、控制需要拦截的接口前缀、控制拦截的例外。
再加上一个统一的熔断,可以更加友好的提醒前端。

@Component
public class FallbackConfig implements FallbackProvider { 
   
	Logger logger = LoggerFactory.getLogger(FallbackConfig.class);

	@Override
	public String getRoute() { 
   
		return "*";
	}

	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause) { 
   
// if (cause != null && cause.getCause() != null) { 
   
// System.out.println(cause.getMessage());
// String reason = cause.getCause().getMessage();
// System.out.println("\n[fallback]"+reason+"\n");
// }

		if(cause != null){ 
   
			System.out.println("【fallback msg】"+cause.getMessage());
		}

		if (cause.getCause() != null) { 
   
			System.out.println("【fallback cause】"+cause.getCause().getMessage());
		}

		return new ClientHttpResponse() { 
   
			
			@Override
			public HttpHeaders getHeaders() { 
   
				HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
			}
			
			@Override
			public InputStream getBody() throws IOException { 
   
				Packages pkg = new Packages();
				pkg.getHead().setStatus(500);
				pkg.getHead().setMsg("服务器正在开小差");
				return new ByteArrayInputStream(JSONUtils.obj2Json(pkg).replace("\r\n", "").replace("\n", "").getBytes());
			}
			
			@Override
			public String getStatusText() throws IOException { 
   
				return "OK";
			}
			
			@Override
			public HttpStatus getStatusCode() throws IOException { 
   
				return HttpStatus.OK;
			}
			
			@Override
			public int getRawStatusCode() throws IOException { 
   
				return 200;
			}
			
			@Override
			public void close() { 
   
				
			}
		};
	}

}

当前端某个接口调用异常的时候,后台统一返回提醒内容:服务器正在开小差,这样即使你的后台挂了,或者是在重启中(springcloud微服务重启单个服务很正常),前端都不会受影响。

最后提醒一句,任何前端加密都不能做到绝对的安全,毕竟代码都是暴露在浏览器的,特别是你的加密解密密钥,建议密钥也不要直明文暴露出来,而是对密钥进行简单的混淆处理后使用,再加上现在前后端都是分离的,前端一般都是es6或typescript使用webpack打包进行ugly处理,这样安全性也能提高不少。

好了,最后附上效果图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/145849.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号