场景
- 现有需求是数据在传输时是明文传输,用抓包工具可对请求数据和响应数据修改后重放,不安全
- 现有架构是:前端-angular,后端-springboot+SpringSecurity
- 预期实现方式:前端-在angular 的http拦截器中,对请求和返回数据进行处理,后端-通过过滤器对数据进行处理
选择
- 后端为什么要选择过滤器进行处理
因为项目采用了SpringSecurity,SpringSecurity就是通过过滤器实现的,如果采用其他方式,
无法处理登录时候的数据包
- 如何将自定义的过滤器添加在SpringSecurity之前
- 方式一:自己注册filter,并设置order为小于-100的数,SpringSecurity的filter的顺序是-100
@Bean
public FilterRegistrationBean registerLoginCheckFilter() {
EncryptFilter requireLoginFilter = new EncryptFilter();
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(requireLoginFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.setName("EncryptFilter");
registrationBean.setOrder(-101);
return registrationBean;
}
- 方式二:将自己的filter配置到SpringSecurity中
@Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new EncryptFilter(), UsernamePasswordAuthenticationFilter.class);
- 选择哪种加密方式
最终采用的是AES加密
- MD5 : 不可逆
- RSA:加密内容长度有限制,过长的数据需要处理分段加密
- BASE64:不安全,可直接反编码看到明文
- springMvc请求中,body数据只能被读取一次
编写wapper类,将body数据保存下来,重写获取的方法,详见 实现
实现
java
过滤器
package net.rjgf.starter.config;
import com.alibaba.fastjson.JSONObject;
import net.rjtx.common.util.AESUtil;
import net.rjtx.common.util.HttpResponseUtil;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class EncryptFilter implements Filter {
// AES 加密key,16位长度,换成自己的即可
private String key = "1234567891234567";
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
MyRequestWrapper requestWrapper = new MyRequestWrapper((HttpServletRequest) servletRequest);
MyResponseWrapper responseWrapper = new MyResponseWrapper((HttpServletResponse) servletResponse);
// POST 请求才进行解析
if (requestWrapper.getMethod().equals("POST")){
String body = requestWrapper.getBody();
if (!StringUtils.isEmpty(body)) {
JSONObject jsonObject = JSONObject.parseObject(body);
Object data = jsonObject.get("data");
String s = null;
try {
s = AESUtil.aesDecrypt(data.toString(), key);
} catch (Exception e) {
e.printStackTrace();
}
requestWrapper.setBody(s);
}
}
filterChain.doFilter(requestWrapper,responseWrapper);
// 对返回数据进行加密
String responseData = responseWrapper.getResponseData("UTF-8");
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("data",AESUtil.aesEncrypt(responseData,key));
} catch (Exception e) {
e.printStackTrace();
}
// 输出
HttpResponseUtil.output((HttpServletResponse) servletResponse,jsonObject);
}
}
MyRequestWrapper
用于 request对象 body数据的重复读取
package net.rjgf.starter.config;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Map;
public class MyRequestWrapper extends HttpServletRequestWrapper {
private String body;
public MyRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = getBodyString(request);
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getBodyString(final HttpServletRequest request) throws IOException {
String contentType = request.getContentType();
String bodyString = "";
StringBuilder sb = new StringBuilder();
if (StringUtils.isNotBlank(contentType) && (contentType.contains("multipart/form-data") || contentType.contains("x-www-form-urlencoded"))) {
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> next : parameterMap.entrySet()) {
String[] values = next.getValue();
String value = null;
if (values != null) {
if (values.length == 1) {
value = values[0];
} else {
value = Arrays.toString(values);
}
}
sb.append(next.getKey()).append("=").append(value).append("&");
}
if (sb.length() > 0) {
bodyString = sb.toString().substring(0, sb.toString().length() - 1);
}
return bodyString;
} else {
return IOUtils.toString(request.getInputStream());
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public int read() {
return bais.read();
}
@Override
public void setReadListener(ReadListener readListener) { }
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
MyResponseWrapper
用于对response返回的body数据可重复读取处理
package net.rjgf.starter.config;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
public class MyResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = null;
private ServletOutputStream out = null;
private PrintWriter writer = null;
public MyResponseWrapper(HttpServletResponse response) throws IOException{
super(response);
buffer = new ByteArrayOutputStream();
out = new WapperedOutputStream(buffer);
writer = new PrintWriter(new OutputStreamWriter(buffer, "UTF-8"));
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
@Override
public PrintWriter getWriter() throws IOException {
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void reset() {
buffer.reset();
}
public String getResponseData(String charset) throws IOException {
flushBuffer();
byte[] bytes = buffer.toByteArray();
try {
return new String(bytes, charset);
} catch (UnsupportedEncodingException e) {
return "";
}
}
public void setResponseData(String charset) throws IOException {
buffer = new ByteArrayOutputStream();
buffer.write(charset.getBytes());
}
class WapperedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = null;
public WapperedOutputStream(ByteArrayOutputStream stream) throws IOException {
bos = stream;
}
@Override
public void write(int b) throws IOException {
bos.write(b);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener listener) {
}
}
}
AESUtil
AES 加解密 工具类
public class AESUtil {
/**
* 将byte[]转为各种进制的字符串
*
* @param bytes byte[]
* @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
* @return 转换后的字符串
*/
public static String binary(byte[] bytes, int radix) {
return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
}
/**
* base 64 encode
*
* @param bytes 待编码的byte[]
* @return 编码后的base 64 code
*/
public static String base64Encode(byte[] bytes) {
byte[] encode = Base64.getEncoder().encode(bytes);
return new String(encode);
}
/**
* base 64 decode
*
* @param base64Code 待解码的base 64 code
* @return 解码后的byte[]
* @throws Exception
*/
public static byte[] base64Decode(String base64Code) throws Exception {
return StringUtils.isEmpty(base64Code) ? null : Base64.getDecoder().decode(base64Code);
}
/**
* 获取byte[]的md5值
*
* @param bytes byte[]
* @return md5
* @throws Exception
*/
public static byte[] md5(byte[] bytes) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
return md.digest();
}
/**
* 获取字符串md5值
*
* @param msg
* @return md5
* @throws Exception
*/
public static byte[] md5(String msg) throws Exception {
return StringUtils.isEmpty(msg) ? null : md5(msg.getBytes());
}
/**
* 结合base64实现md5加密
*
* @param msg 待加密字符串
* @return 获取md5后转为base64
* @throws Exception
*/
public static String md5Encrypt(String msg) throws Exception {
return StringUtils.isEmpty(msg) ? null : base64Encode(md5(msg));
}
/**
* AES加密
*
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的byte[]
* @throws Exception
*/
public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
byte[] raw = encryptKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
return cipher.doFinal(content.getBytes("utf-8"));
}
/**
* AES加密为base 64 code
*
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的base 64 code
* @throws Exception
*/
public static String aesEncrypt(String content, String encryptKey) throws Exception {
return base64Encode(aesEncryptToBytes(content, encryptKey));
}
/**
* AES解密
*
* @param encryptBytes 待解密的byte[]
* @param decryptKey 解密密钥
* @return 解密后的String
* @throws Exception
*/
public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
byte[] raw = decryptKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decryptBytes = cipher.doFinal(encryptBytes);
return new String(decryptBytes);
}
/**
* 获得密钥
*
* @param secretKey
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws InvalidKeySpecException
*/
protected SecretKey generateKey(String secretKey) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("AES");
DESKeySpec keySpec = new DESKeySpec(secretKey.getBytes());
keyFactory.generateSecret(keySpec);
return keyFactory.generateSecret(keySpec);
}
/**
* 将base 64 code AES解密
*
* @param encryptStr 待解密的base 64 code
* @param decryptKey 解密密钥
* @return 解密后的string
* @throws Exception
*/
public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
}
public static void main(String[] args) throws Exception {
String content = "123456";
System.out.println("加密前:" + content);
System.out.println(content.length());
String key = "1234567891234567";
System.out.println("加密密钥和解密密钥:" + key);
String encrypt = aesEncrypt(content, key);
System.out.println("加密后:" + encrypt);
String decrypt = aesDecrypt(encrypt, key);
System.out.println("解密后:" + decrypt);
}
}
angular
主要是对http拦截器的改写
- 安装 crypto-js
npm install crypto-js
npm install --save @types/crypto-js - 使用
import { AES, mode, pad, enc } from’crypto-js’; - 方法
- 加密
encryptByEnAES(data: string): string {
let Key = enc.Utf8.parse("1234567891234567");// 秘钥长度需要是16的倍数,且需要进行Utf8转义。
let tmpAES = AES.encrypt(data, Key, {
mode: mode.ECB,
padding: pad.Pkcs7
});
return tmpAES.toString();
}
- 解密
encryptByDeAES(data: string): string {
let Key = enc.Utf8.parse("1234567891234567");// 秘钥长度需要是16的倍数,且需要进行Utf8转义。
let tmpDeAES = AES.decrypt(data, Key, {
mode: mode.ECB,
padding: pad.Pkcs7
});
return tmpDeAES.toString(enc.Utf8);
}
- 调用思路
对前端的 HttpRequest 与 HttpResponse 对象的body数据进行替换即可
// 发送数据加密
let newReq
if (req.body != null) {
newReq = req.clone({
url: url,
body: {"data": this.encryptByEnAES(JSON.stringify(req.body))}
});
}else{
newReq = req.clone({
url: url
});
}
if (rsp.url.toString().indexOf(environment.SERVER_URL) != -1){
// 处理解密
console.log(JSON.parse(this.encryptByDeAES(rsp.body["data"])))
ev = rsp.clone({
body: JSON.parse(this.encryptByDeAES(rsp.body["data"]))
});
}