对于安全性要求的加强,避免出现篡改请求结果问题的出现,现对系统中所有的请求和结果响应进行加密处理。系统使用前后端分离设计架构,同时前端部分有Vue 项目也有 jQuery 项目。
遇到坑最多的地方是Axios 的get方式与jQuery的get方式
Java 后台处理
- 定义 request Filter
CustomRequestFilter
处理请求参数,拦截所有请求进行解密
/**
* 请求拦截器 -- 处理参数解密
*
* @author miniy
*/
@Setter
public class CustomRequestFilter extends UsernamePasswordAuthenticationFilter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
ParameterRequestWrapper request = new ParameterRequestWrapper(req);
filterChain.doFilter(request, servletResponse);
}
}
- 定义代理类处理参数
/**
* 请求代理类
*
* @author miniy
*/
public class ParameterRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
private Map<String, String[]> params;
public ParameterRequestWrapper(HttpServletRequest request) {
super(request);
String method = request.getMethod();
if (method.toUpperCase().equals("GET")) {
params = HttpHelper.getParamMap(request);
} else {
body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
}
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
@Override
public String getParameter(String name) {
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public Enumeration<String> getParameterNames() {
Vector<String> v = new Vector<>();
Set<Map.Entry<String, String[]>> entrySet = params.entrySet();
for (Map.Entry<String, String[]> entry : entrySet) {
v.add(entry.getKey());
}
Enumeration<String> en = v.elements();
return v.elements();
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
-
注册Filter 到spring security,注册为最外层Filter,拦截所有请求此实现方式存在问题,将自定义Filter 注册到spring security 中,spring security ignoring 后的url不会被过滤器拦截,改为spring注册
提供参数处理工具类
/**
* 工具类
*/
@Slf4j
public class HttpHelper {
/**
* 处理 post 请求体参数
*
* @param request
* @return
*/
public static String getBodyString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
String result = "";
try (InputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
result = sb.toString();
if (!StrUtils.isEmpty(result)) {
// 数据处理
String[] split = result.split(";");
String ras = split[0];
// 解密
Map<String, Object> map = YamlUtil.builder("application-config.yml");
Map<String, Object> sys = (Map<String, Object>) map.get("sys");
Map<String, String> requestKey = (Map<String, String>) sys.get("requestKey");
String privateKey = requestKey.get("privateKey");
String publicKey = requestKey.get("publicKey");
RSA rsa = SecureUtil.rsa(privateKey, publicKey);
byte[] decrypt = rsa.decrypt(ras, KeyType.PrivateKey);
// todo 如果中文乱码,前台需要使用URL encode 加密 此處使用decode 解密
String keyIV = IOUtils.toString(decrypt, "utf-8");
String[] aesAttr = keyIV.split(";");
String key = aesAttr[0];
String iv = aesAttr[1];
request.setAttribute("aesKey", key);
request.setAttribute("aesIv", iv);
if (split.length > 1) {
String bodyData = split[1];
AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes());
aes.setIv(new IvParameterSpec(iv.getBytes()));
result = aes.decryptStr(bodyData);
}
}
result = xssBaseLeach(result);
if (!isPassUrl(request.getRequestURI())) {
// xss sql 过滤
result = xssSqlLeach(result);
}
} catch (IOException e) {
log.error(e.toString());
}
return result;
}
/**
* 处理 get 方式请求参数
*
* @param request
* @return
*/
public static Map<String, String[]> getParamMap(HttpServletRequest request) {
HashMap<String, String[]> hashMap = new HashMap<>();
String result;
try {
result = request.getQueryString();
String regex = "params=";
if (result.indexOf(regex) > -1) {
// 解决+ 号变成空格的处理
result = result.replace(regex, "").replace("+", "%2B");
result = URLUtil.decode(result);
}
String[] split = result.split(";");
String ras = split[0];
// 解密
Map<String, Object> map = YamlUtil.builder("application-config.yml");
Map<String, Object> sys = (Map<String, Object>) map.get("sys");
Map<String, String> requestKey = (Map<String, String>) sys.get("requestKey");
String privateKey = requestKey.get("privateKey");
String publicKey = requestKey.get("publicKey");
RSA rsa = SecureUtil.rsa(privateKey, publicKey);
byte[] decrypt = rsa.decrypt(ras, KeyType.PrivateKey);
// todo 如果中文乱码,前台需要使用URL encode 加密 此處使用decode 解密
String keyIV = IOUtils.toString(decrypt, "utf-8");
String[] aesAttr = keyIV.split(";");
String key = aesAttr[0];
String iv = aesAttr[1];
request.setAttribute("aesKey", key);
request.setAttribute("aesIv", iv);
if (split.length > 1) {
String bodyData = split[1];
AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes());
aes.setIv(new IvParameterSpec(iv.getBytes()));
result = aes.decryptStr(bodyData);
//xss sql 注入处理
result = xssBaseLeach(result);
if (!isPassUrl(request.getRequestURI())) {
// xss sql 过滤
result = xssSqlLeach(result);
}
Map<String, Object> toMap = JSONUtils.jsonObjToMap(JSONUtils.ToJsonObj(result));
for (String mapKey : toMap.keySet()) {
hashMap.put(mapKey, new String[]{toMap.get(mapKey).toString()});
}
}
} catch (IOException e) {
log.error(e.toString());
}
return hashMap;
}
/**
* 过滤 参数中存在的 js 代码
*
* @param body
* @return
*/
private static String xssBaseLeach(String body) {
return HtmlUtils.removeHtmlTag(body, "script");
}
/**
* xss 字符替换
*
* @param body
* @return
*/
private static String xssSqlLeach(String body) {
if (body == null || body.isEmpty()) {
return body;
}
StringBuilder sb = new StringBuilder(body.length());
for (int i = 0; i < body.length(); i++) {
char c = body.charAt(i);
switch (c) {
case '>':
sb.append("》");// 转义大于号
break;
case '<':
sb.append("《");// 转义小于号
break;
case '\'':
sb.append("‘");// 转义单引号
break;
case '\"':
sb.append('"');// 转义双引号
break;
case '&':
sb.append("&");// 转义&
break;
default:
String s1 = c + "";
String s = s1.replaceAll(".*([';]+|(--)+).*", "");
sb.append(s);
break;
}
}
return sb.toString();
}
private static boolean isPassUrl(String url) {
Map<String, Object> map = YamlUtil.builder("application-config.yml");
Map<String, Object> sys = (Map<String, Object>) map.get("sys");
List<String> whitelist = (List<String>) sys.get("xssList");
return whitelist.contains(url);
}
}
- 定义 response Filter 拦截所有响应结果
/**
* 响应拦截器 -- 处理参数加密
*
* @author miniy
*/
public class CustomResponseFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
ResultResponseWrapper responseWrapper = new ResultResponseWrapper(res);
chain.doFilter(request, responseWrapper);
byte[] content = responseWrapper.getContent();
if (content.length > 0) {
// 响应结果解密
String json = IOUtils.toString(content, "utf-8");
ResponseUtil.out((HttpServletRequest) request, (HttpServletResponse) response, JSONUtils.ToJsonObj(json));
}
}
}
- 定义代理类
/**
* 响应代理类
*
* @author miniy
*/
public class ResultResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer;
private ServletOutputStream outputStream;
public ResultResponseWrapper(HttpServletResponse response) {
super(response);
buffer = new ByteArrayOutputStream();
outputStream = new WrapperOutputStream(buffer);
}
@Override
public ServletOutputStream getOutputStream() {
return outputStream;
}
@Override
public void flushBuffer() throws IOException {
if (null != outputStream) {
outputStream.flush();
}
}
public byte[] getContent()
throws IOException {
flushBuffer();
return buffer.toByteArray();
}
class WrapperOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos;
public WrapperOutputStream(ByteArrayOutputStream bos) {
this.bos = bos;
}
@Override
public void write(int b)
throws IOException {
bos.write(b);
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public void setWriteListener(WriteListener arg0) {
// TODO Auto-generated method stub
}
}
}
-
~~注册过滤器 1. spring security 最后一个过滤器 ~~此实现方式存在问题,将自定义Filter 注册到spring security 中,spring security ignoring 后的url不会被过滤器拦截,改为spring注册
- 提供工具类
/**
* 响应加密工具类
*/
@Slf4j
public class ResponseUtil {
public static void out(HttpServletRequest request, HttpServletResponse response, Object json) {
try {
// 响应结果加密
// 方式1 aes 加密返回
String key = (String) request.getAttribute("aesKey");
String iv = (String) request.getAttribute("aesIv");
AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes());
aes.setIv(new IvParameterSpec(iv.getBytes()));
String data = JSON.toJSONString(json);
String result = aes.encryptBase64(data);
// 方式2 rsa 加密返回
/* Map<String, Object> map = YamlUtil.builder("application-config.yml");
Map<String, Object> sys = (Map<String, Object>) map.get("sys");
Map<String, String> requestKey = (Map<String, String>) sys.get("responseKey");
String privateKey = requestKey.get("privateKey");
String publicKey = requestKey.get("publicKey");
RSA rsa = SecureUtil.rsa(privateKey, publicKey);
String data = JSON.toJSONString(json);
// 防止中文乱码先进行 data url 编码
String encode = URLUtil.encode(data);
String result = rsa.encryptBase64(encode, KeyType.PublicKey);*/
response.setContentType("text/html;charset=utf-8");
response.setHeader("Content-type", "application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print(result);
} catch (java.io.IOException e) {
log.error(e.toString());
}
}
}
编辑更改过滤器的注册
因注册到spring security 组件上无法拦截ignoing 的请求,更改为spring boot 方式注册,注意点为order 排序的设置,响应最简单设置为最大就好。关键点是请求filter的位置非常重要。这里要放在 spring security 内置过滤器前,spring CorsFilter 之后,此处多次测试猜的数为-100,暂未找到更科学方法。
@Bean
public FilterRegistrationBean requestFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CustomRequestFilter());
registration.addUrlPatterns("/*");
// registration.setOrder(Integer.MIN_VALUE);
registration.setOrder(-100);
registration.setName("requestFilter");
return registration;
}
@Bean
public FilterRegistrationBean responseFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CustomResponseFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Integer.MAX_VALUE);
registration.setName("responseFilter");
return registration;
}
JsonUtils 工具类
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.util.List;
import java.util.Map;
/**
* json 处理工具类
*/
public class JSONUtils {
public static JSONObject parseObj(Object object) {
return JSONUtil.parseObj(object);
}
/**
* 字符串转JSONArray
*
* @param json
* @return
*/
public static JSONArray toJsonList(String json) {
return JSON.parseArray(json);
}
/**
* 对象转化为字符串
*
* @param obj
* @return
*/
public static String JsonToString(Object obj) {
return JSON.toJSONString(obj);
}
/**
* 带类型信息的转json串
*
* @param obj
* @return
*/
public static String JsonToStringAndType(Object obj) {
return JSON.toJSONString(obj, SerializerFeature.WriteClassName);
}
/**
* Json 字符串转为 JSONObject
* JSONObject 对象类似Map
* 直接通过get()方法 获取对象
*
* @return
*/
public static com.alibaba.fastjson.JSONObject ToJsonObj(String json) {
return JSON.parseObject(json);
}
public static cn.hutool.json.JSONObject ToJsonObj(Object obj) {
return JSONUtil.parseObj(obj);
}
/**
* map 转 Bean
*
* @param map
* @param beanClass
* @param <T>
* @return
*/
public static <T> Object toBean(Map map, Class<T> beanClass) {
return JSONUtil.toBean(JSONUtil.parseFromMap(map), beanClass, true);
}
public static <T> Object toBean(com.alibaba.fastjson.JSONObject jsonObject, Class<T> beanClass) {
return JSONUtil.toBean(JSONUtil.parseObj(jsonObject.toJSONString()), beanClass, true);
}
/**
* JsonArray转java的List
*
* @param jsonArray
* @param c
* @return
*/
public static List JSONArrayToList(JSONArray jsonArray, Class c) {
String string = com.alibaba.fastjson.JSONObject.toJSONString(jsonArray, SerializerFeature.WriteClassName);
List list = com.alibaba.fastjson.JSONObject.parseArray(string, c);
return list;
}
public static Map jsonObjToMap(com.alibaba.fastjson.JSONObject jsonObject) {
return JSONUtil.toBean(JSONUtil.parseObj(jsonObject.toJSONString()), Map.class);
}
}
Java结束
前端处理
- jQuery
引入CryptoJS与jsencrypt支持
view.ajaxFilter = function () {
var ajax = $.ajax;// 修改ajax方法的默认实现
$.ajax = function (options) {
// aes 数据加密
var u32 = uuid(32);
var u16 = uuid(16);
var key = CryptoJS.enc.Latin1.parse(u32);
var iv = CryptoJS.enc.Latin1.parse(u16);
if (options.type == 'get' || options.type == 'GET') {
if (options.url.indexOf('?') > -1) {
var split = options.url.split('?');
var params = split[1];
var paramObj = params.split("&");
for (var i = 0; i < paramObj.length; i++) {
options.data[paramObj[i].split("=")[0]] = unescape(paramObj[i].split("=")[1]);
}
options.url = split[0];
}
}
// data加密
if (typeof options.data == 'object') {
options.data = JSON.stringify(options.data);
}
var encrypted = CryptoJS.AES.encrypt(options.data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
// ras 数据加密
var publicKey = ' 11';
jsencrypt = new JSEncrypt();
jsencrypt.setPublicKey(publicKey);
// todo 如果中文乱码,前台需要使用URL encode 加密 此處使用decode 解密
var ras = jsencrypt.encrypt(u32 + ";" + u16);
options.data = ras + ";" + encrypted.toString();
var dataFilter = options.dataFilter; // 对用户配置的success方法进行代理
function ns(datas, type) {
// 数据解密
// 方式1 aes 解密
var decrypt = CryptoJS.AES.decrypt(datas, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
var data = decrypt.toString(CryptoJS.enc.Utf8);
// 方式2 rsa 解密
/* var privateKey = '私密';
var decrypt = new JSEncrypt();
decrypt.setPrivateKey(privateKey);
var uncrypted = decrypt.decryptLong2(datas);
var data = decodeURIComponent(uncrypted);*/
return data;
}
options.dataFilter = ns;
return ajax(options);
}
}
- Vue
安装crypto-js与jsencrypt
npm install jsencrypt
npm install crypto-js
import Vue from 'vue'
import vueAxios from 'vue-axios'
import axios from 'axios'
import { getToken } from '@/utils/auth'
import { uuid } from '@/utils/utils'
import cryptoJs from 'crypto-js'
import JSEncrypt from 'jsencrypt'
import LE from '@/assets/config'
import store from '../store'
import router from '../router'
import { Toast } from 'vant'
Vue.use(vueAxios, axios)
Vue.use(Toast).use(cryptoJs)
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000
})
const u32 = uuid(32)
const u16 = uuid(16)
const key = cryptoJs.enc.Latin1.parse(u32)
const iv = cryptoJs.enc.Latin1.parse(u16)
// request拦截器
service.interceptors.request.use(
(config) => {
const con = config
if (store.getters.token) {
con.headers[LE.RequestTokenKey] = getToken()
}
// data数据加密
if (typeof con.data === 'object') {
con.data = JSON.stringify(con.data)
}
const encrypted = cryptoJs.AES.encrypt(con.data, key, {
iv: iv,
mode: cryptoJs.mode.CBC,
padding: cryptoJs.pad.ZeroPadding
})
// ras 数据加密
var publicKey = '123123'
const jsencrypt = new JSEncrypt()
jsencrypt.setPublicKey(publicKey)
const ras = jsencrypt.encrypt(u32 + ';' + u16)
if (con.method === 'post' || con.method === 'POST') {
con.headers['Content-Type'] = 'application/json; charset=utf-8'
con.data = ras + ';' + encrypted.toString()
} else if (con.method === 'get' || con.method === 'GET') {
con.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
const params = ras + ';' + encrypted.toString()
con.params = { params }
}
return con
},
(error) => {
Promise.reject(error)
},
)
// response 拦截器
service.interceptors.response.use(
response => {
let res = response.data
// 数据解密
// 方式1 aes 解密
const decrypt = cryptoJs.AES.decrypt(res, key, {
iv: iv,
mode: cryptoJs.mode.CBC,
padding: cryptoJs.pad.ZeroPadding
})
res = cryptoJs.enc.Utf8.stringify(decrypt).toString()
res = JSON.parse(res)
if (res.code === LE.response.NO_AUTH) {
store.dispatch('FedLogOut')
.then(() => {
router.push({ path: '/login' })
})
} else if (res.code === LE.response.OK) {
return res
} else {
const msg = '状态码:' + res.code + '异常提醒【' + res.msg + '】'
Toast.fail({
message: msg
})
}
}
,
error => {
store.dispatch('FedLogOut')
.then(() => {
router.push({ path: '/login' })
})
Promise.reject(error)
},
)
export default service
加密方式
上诉把程序以及思路提供,可以根据自己需要的加密解密方式进行处理,以下两种方式我进行了尝试最终选择了第二种。
- 方式一:RAS 加密
- 什么是RAS,RSA加密算法是一种非对称加密算法,由私钥与公钥组成一对密钥,通过公钥加密私钥界面方式处理。公钥可以暴漏在客户端,私钥必须保密存储在服务端。非对称加密算法安全性更高,但解密效率特别慢
- 在请求时,使用请求的公钥对请求参数进行加密,达到服务端过滤器时,使用私钥进行解密。完成请求参数的加密。
- 在服务端响应时,服务端采用响应的公钥加密,客户端私钥进行解密。完成结果的加密。
上述中,使用两对密钥处理。优点安全性强。缺点1 响应私钥存储在客户端 2 当出现大文本或者长文本进行加解密时效率非常慢,同时大部分工具类不支持长文本的直接解密,需要使用分段加密-分段解密处理,尽量不要这样使用
- 方式二:RAS + AES 加密
- 什么是AES,AES算法称为密码学中的高级加密标准,这个标准用来替代原先的DES。是一种对称加密的算法。加解密效率较快,但安全性相对较弱。
- 请求时,客户端生成AES的密钥Key以及偏移量IV,对文本内容进行对称加密,然后使用RAS对客户端生成AES的密钥Key以及偏移量IV进行非对称加密,进行服务端传递。
- 响应时,后台使用传递的客户端生成AES的密钥Key以及偏移量IV进行结果加密,客户端是用同一个密钥Key以及偏移量IV进行解密。
RAS + AES 方式加密是指采用RAS算法对AES的加密的密钥key(32位)以及偏移量iv(16位)进行非对称加密,使用AES算法对需要传递的文本内容进行对称加密。这种方式即可以保证安全性又能提高文本解析的效率。推荐使用。