我们在日常开发过程中,经常会有在filter中对请求参数进行统一修改,然后对响应数据也要做统一修改的需求。比如出于安全考虑,我们会将请求参数和响应数据进行加密,这个时候就需要在filter中将请求参数进行解密,给后续的接口使用,而且还要将接口响应的数据进行加密,返回给请求端,这里面就涉及到两个问题:数据的加解密和请求数据的修改。
一般我们都会使用https来保证我们和服务端的通信安全,https协议可以保证我们通信的数据是加密的,不会被别人抓包,但是对于一些对安全性要求比较高的公司,https协议还不够,这个时候就需要我们自己来对通信数据加密了。
一般来说非对称加密要比对称加密的安全性要更高一些,但是非对称加密的加密算法效率较低,会严重影响我们系统的响应时间和QPS,总结来说:
一种非对称的加密算法,它的密钥分为公钥和私钥,公钥是大家都知道的,用来加密,私钥只有我们自己知道,用来解密,公钥和私钥是成对的,这个不是重点,直接上一下代码实现吧。
public static List genKeyPair() throws NoSuchAlgorithmException {
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,最小为512
keyPairGen.initialize(512,new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥
List result = new ArrayList<>(2);
//公钥
result.add(new String(Base64Util.encode(publicKey.getEncoded())));
//私钥
result.add(new String(Base64Util.encode((privateKey.getEncoded()))));
return result;
}
public static String encrypt(String str, String publicKey) throws Exception{
//base64编码的公钥
byte[] decoded = Base64Util.decodeb(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
String outStr = Base64Util.encode(cipher.doFinal(str.getBytes("UTF-8")));
return outStr;
}
public static String decrypt(String str, String privateKey) throws Exception{
//64位解码加密后的字符串
byte[] inputByte = Base64Util.decodeb(str);
//base64编码的私钥
byte[] decoded = Base64Util.decodeb(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
String outStr = new String(cipher.doFinal(inputByte));
return outStr;
}
这个代码实现网上很多,随便一搜就有。
一种对称加密算法,它对密钥的长度有一定的要求,还有五种加密模式,还需要指的固定的偏移量,这些在实现的时候一定要注意,也直接上实现代码吧。
public static byte[] encrypt(String content, String secretKey) throws Exception {
if(secretKey == null || secretKey.length() != 16) {
return null;
}
byte[] encrypted = getCipher(Cipher.ENCRYPT_MODE, secretKey).doFinal(content.getBytes());
return encrypted;
// return Base64Util.encode(encrypted);
}
public static String decrypt(String encryptContent, String secretKey) throws Exception {
return decrypt(Base64.getDecoder().decode(encryptContent), secretKey);
}
public static String decrypt(byte[] encryptContent, String secretKey) throws Exception {
logger.info("encryptContent: " + encryptContent + ", secretKey: " + secretKey);
if(secretKey == null || secretKey.length() != 16) {
return SECRET_KEY_ERROR;
}
logger.info("encryptContent length: " + encryptContent.length);
byte[] original = getCipher(Cipher.DECRYPT_MODE, secretKey).doFinal(encryptContent);
return new String(original);
}
private static Cipher getCipher(int mode, String secretKey) throws Exception {
IvParameterSpec iv = new IvParameterSpec(INIT_VECTOR.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(mode, skeySpec, iv);
return cipher;
}
还有一点:我们一般都会把结果进行Base64编码,因为加密出来的数据是二进制的,不能打印出来,但是有时也会觉得Base64编码是多余的,因为编码之后,其他端还要解码,会直接使用二进制通信,这个时候就要直接使用加密出来的二进制数据了。
附一个在线加密的网站:http://tool.chacuo.net/
一般我们想到的修改请求数据的流程:
这里有几个问题:
java.io.IOException: Stream closed
java.lang.IllegalStateException: getReader() has already been called for this request
针对这些问题,一般会创建一个自己的类继承与HttpServletRequestWrapper类,重写里面的getInputStream()方法,具体代码如下:
public class MyRequestWapper extends HttpServletRequestWrapper {
//请求数据
private String body;
//请求二进制数据
private byte[] bytes;
public MyRequestWapper(HttpServletRequest request) throws IOException {
super(request);
try (ServletInputStream inputStream = request.getInputStream()){
bytes = new byte[request.getContentLength()];
inputStream.read(bytes);
body = new String(bytes);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
//这一步最关键
//这一步使得后续获取InputStream都是这个对象
//而在此时我们也把我们自定义的数据塞进去了
//也就是说后续处理中获取到的参数就是我们此时塞进去的数据
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ServletInputStream inputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
//这里流的读取,就是从我们自定义流中读取数据
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return inputStream;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public byte[] getBytes() {
return bytes;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
}
简单介绍下上面的代码吧,主要有几点:
整个流程和修改请求数据差不多,继承HttpServletResponseWrapper类,重写getOutputStream()方法,先上代码吧:
public class MyResponseWapper extends HttpServletResponseWrapper {
//我们获取响应数据的流
private ByteArrayOutputStream byteArrayOutputStream;
public MyResponseWapper(HttpServletResponse response) {
super(response);
byteArrayOutputStream = new ByteArrayOutputStream();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
//这个最重要,是在这里将响应数据写到这个类的byteArrayOutputStream中的,
//这样我们后续才能拿到响应数据
//也正是因为这样,实际的HttpResponse对象中是没有被写入数据的
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
};
}
public byte[] toByteArray(){
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(bytes.length);
return bytes;
}
}
代码很简单,创建了我们自己的一个输出流,然后在ServletOutputStream类中的write(int b)方法中,用这个输出流来接收原本应该写入HttpServletResponse中的响应数据,这里要注意几点:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
MyRequestWapper requestWapper = new MyRequestWapper((HttpServletRequest) servletRequest);
MyResponseWapper responseWapper = new MyResponseWapper((HttpServletResponse) servletResponse);
//获取请求参数的二进制数据
byte[] requestByte = requestWapper.getBytes();
//获取请求参数字符串
String requestBody = requestWapper.getBody();
System.out.println("requestBody: " + requestBody);
//替换请求参数
String newBody = "{\"name\": \"aa\", \"age\": 21}";
requestWapper.setBytes(newBody.getBytes());
//继续后续请求处理
filterChain.doFilter(requestWapper, responseWapper);
//获取响应数据
String response = new String(responseWapper.toByteArray());
System.out.println(response);
byte[] rewriteResult = "{\"code\": 1000, \"msg\": success}".getBytes();
//往原始的HttpResponse对象中写入数据
responseWapper.setContentLength(rewriteResult.length);
try(OutputStream out = servletResponse.getOutputStream()){
out.write(rewriteResult);
out.flush();
}
}
这个代码里就有我上面提到的几个点了:
注意responseWapper.setContentLength(rewriteResult.length);这行代码,如果少了这行代码,可能会使客户端接收到的数据少了,或者一直处于等待数据的状态,原因是在http协议中,content-length表示数据内容的长度,客户端会根据这个字段的值来接收这个值大小的数据,而如果我们修改了响应数据,但是没有修改这个字段的值的话,如果我们修改的数据比原本的数据少的话,客户端会一直处于等待数据的状态,它会以为还有数据没有传送过来;而如果我们修改的数据多的话,客户端也只是接收一部分数据,所以在修改响应结果时,一定要记得修改下这个参数的值。