一:加解密入参/出参
目的:为了防止请求参数及响应数据被暴力破解或截取,通过AES对称性加密传输规范,对请求及响应进行加解密处理
实现过程:创建请求及响应拦截器,对请求及响应源头进行数据处理,保证数据的安全传输
DecryptRequestBodyAdvice 客户端请求拦截器
EncodeResponseBodyAdvice 客户端响应拦截器
请求拦截器代码片段
@Component
如果放开注释,则表示监控所有controller请求入口
//@ControllerAdvice(basePackages = “com.ds.tech.controller”)
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Autowired
SystemConfig systemConfig;
/**
* 加载配置文件信息
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
/**
*读取请求头之前
*/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
@Override
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
Object dealData = null;
LogWriter.writeWorkLog("执行对客户端请求的数据解密");
try {
Map<String, String> dataMap = (Map) body;
if (!MapUtils.isEmpty(dataMap)) {
String srcData = dataMap.get("encryptStr");
dealData = AESUtils.encrypt(srcData, systemConfig.getEncryptKey());
LogWriter.writeWorkLog("解密后的数据:" + dealData);
}
} catch (Exception e) {
LogWriter.writeErrorLog("解密失败", e);
}
return dealData;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
响应拦截器代码片段
/**
-
@title 客户端响应拦截器
-
@author chenpengfei
-
@date 2020年04月14日
-
@param body
-
@return object
*/
@Component
//@ControllerAdvice(“com.ds.tech.controller”) 放开注释则监听所有响应
@SuppressWarnings(value = { “all” }) 剔除所有警告注解
public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {
@Autowired
SystemConfig systemConfig;@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}/**
- @title 加密响应数据
- @author chenpengfei
- @date 2020年04月14日
- @param body:最终响应的内容
- @return object
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 禁止fastjson转换循环引用
String bodyStr = JSON.toJSONString(body, SerializerFeature.DisableCircularReferenceDetect);
LogWriter.writeWorkLog(“开始加密服务器相应的数据:{}” + JSON.toJSONString(body));
ResultObject resultObject = new ResultObject();
String encodeStr = null;
JSONObject jsonData = JSONObject.parseObject(bodyStr);
try {
if (!ObjectUtils.isEmpty(jsonData)) {
encodeStr = AESUtils.encrypt(jsonData.getString(“data”).toString(),systemConfig.getEncryptKey());
LogWriter.writeWorkLog(“数据加密完成:{}” + encodeStr);
}
} catch (Exception e) {
LogWriter.writeErrorLog(AesEnum.DE_DATA_ERROR.getMessage(), e);
resultObject.setFailed(AesEnum.DE_DATA_ERROR);
return resultObject;
}
// 赋值返回值
OptASRData resultData = new OptASRData();
resultData.setResultEnData(encodeStr);
resultObject.setRetcode((int) jsonData.get(“retcode”));
resultObject.setSucceed(resultData);
resultObject.setRetmsg(jsonData.getString(“retmsg”));
return resultObject;
}
}
aes加解密方法说明
srcData:为接口请求传输的加密后的数据
encryptKey:为与接口调用者约束的对称性秘钥
AESUtils.encrypt(srcData, systemConfig.getEncryptKey());
加密及解密工具类代码
private static final String KEY_AES = “AES”;
public static String encrypt(String src, String key) throws Exception {
byte[] raw = key.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, KEY_AES);
Cipher cipher = Cipher.getInstance(KEY_AES);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(src.getBytes());
return byte2hex(encrypted);
}
public static String decrypt(String src, String key) throws Exception {
byte[] raw = key.getBytes();
SecretKeySpec skeySpec = new SecretKeySpec(raw, KEY_AES);
Cipher cipher = Cipher.getInstance(KEY_AES);
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = hex2byte(src);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString;
}
private static byte[] hex2byte(String strhex) {
if (strhex == null) {
return null;
}
int l = strhex.length();
if (l % 2 == 1) {
return null;
}
byte[] b = new byte[l / 2];
for (int i = 0; i != l / 2; i++) {
b[i] = (byte) Integer.parseInt(strhex.substring(i * 2, i * 2 + 2), 16);
}
return b;
}
private static String byte2hex(byte[] b) {
StringBuilder hs = new StringBuilder();
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1) {
hs.append("0").append(stmp);
} else {
hs.append(stmp);
}
}
return hs.toString().toUpperCase();
}
(接口请求及加密规则见附件大神接口服务验签及加密规则)
二:接口请求验证签名
目的:为了接口请求的安全性,对接口调用者和服务提供者追加延签约束
实现过程:
1.接口调用者通过指定提供的接口服务号,用户登录生成的非固定token权限,服务请求的对称性秘钥key生成base64sign码做为接口请求的参数传递
2.服务提供者通过接口调用者传递的参数进行拼接,通过服务器端生成sign并与入参携带的sign进行对比
aop验签切面方法说明
/**
* 定义Controller方法切入点
/
@Pointcut("execution( com.ds.tech.controller…*(…))")
public void controllerMethodPointcutApi() {
}
@Pointcut("controllerMethodPointcutApi()")
public void controllerMethodPointcut() {
}
/**
*
* @param pjp
* @return
*/
@Around(“controllerMethodPointcut()”)
public Object doAround(ProceedingJoinPoint pjp) {
Object result = null;
ResultObject<JSONObject> resultObject = new ResultObject<>();
try {
resultObject.setData(new JSONObject());
Object[] args = pjp.getArgs();
// 如果参数存在则进行api签名验证
if (args.length <= 0) {
resultObject.setFailed(ApiCommonEnum.INVALID_PARAMS.getCode(),
ApiCommonEnum.INVALID_PARAMS.getMessage());
result = resultObject;
return result;
}
Object iptObj = args[0];
String dtoClassName = iptObj.getClass().getName();
ComIobj<?> comIptObj = getComObject(iptObj);
resultObject = comSign(comIptObj);
if (resultObject.getRetcode() != NumberConst.INT_ZERO) {
return resultObject;
}
} catch (Exception e) {
LogWriter.writeErrorLog(e);
resultObject.setFailed(ApiCommonEnum.SIGN_EXP.getCode(), ApiCommonEnum.SIGN_EXP.getMessage());
result = resultObject;
return result;
}
try {
result = pjp.proceed();
} catch (Throwable e) {
LogWriter.writeErrorLog(e);
resultObject.setFailed(ApiCommonEnum.SIGN_EXP.getCode(), ApiCommonEnum.SIGN_EXP.getMessage());
result = resultObject;
return result;
}
return result;
}
三:入参过滤器
目的:为了防止服务端XML分析器没有进行适当的数据验证,攻击者可以通过设计特殊的输入,破坏应用程序的运行并执行某些未授权的操作带来的风险,固对请求参数进行过滤转译
实现过程:再验签方法后追加过滤代码片段
处理入参过程
try {
result = pjp.proceed(); // 将try catch 内代码替换如下
} catch (Throwable e) {
// 环绕切换方法执行后置任务
Object[] args = pjp.getArgs();
// 获取执行方法的对象
Object iptObj = args[0];
// 对象转换大神入参包装类
ClientIobj<?> clientIptObj = getClientObject(iptObj);
// 从包装类获取实际请求参数对象
Object data = clientIptObj.getData();
// 将入参转json数据
String json = JSON.toJSONString(data);
// 参数转数组(为了取每一段参数)
String[] strArray = json.split(",");
// 执行过滤方法将配置需要过滤的参数进行处理
String parameterValues = getParameterValues(strArray);
// 将转换过后的数据赋值给原始入参对象
Object resultData = JSON.parseObject(parameterValues, clientIptObj.getData().getClass());
// 利用反射给对象重新赋值
Method setReadOnly = clientIptObj.getClass().getMethod("setData", Object.class);
// 定义需要赋值的对象
Object s = resultData;
// 赋值过程
setReadOnly.invoke(clientIptObj, s);
// 重定义执行方法的对象
args[0] = clientIptObj;
// 执行后置任务
Object ret = pjp.proceed(args);
// 转发result请求
result = ret;
public String getParameterValues(String[] parameter) {
// 获取需要进行过滤的入参数量
int count = parameter.length;
// 初始化返回入参字符串
String str = null;
for (int i = 0; i < count; i++) {
if (!StringUtils.isEmpty(str)) {
// 拼接过滤后的入参
str += "," + cleanXSS(parameter[i]);
} else {
// 赋值过滤的入参
str = cleanXSS(parameter[i]);
}
}
// 返回拼接且过滤后的入参
return str;
}
private static String cleanXSS(String value) {
value = value.replaceAll("<", "<").replaceAll(">", ">");
value = value.replaceAll("%3C", "<").replaceAll("%3E", ">");
value = value.replaceAll("\\(", "(").replaceAll("\\)", ")");
value = value.replaceAll("%28", "(").replaceAll("%29", ")");
value = value.replaceAll("'", "'");
value = value.replaceAll("eval\\((.*)\\)", "");
value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
value = value.replaceAll("script", "");
value = value.replaceAll("onclick", "");
value = value.replaceAll("alert", "");
return value;
}
(随带一笔)AOP、过滤器、拦截器执行顺序
过滤器 > 拦截器 > AOP切面 (执行顺序的规则不做描述)
为了既要满足aop的验签,又要满足过滤器的特殊字符过滤,通过过滤器和aop是不可能执行的,所以将过滤器的方法放在验签执行后
四:异常referer请求/登录拦截器
目的:
referer拦截器的作用:攻击者可以构造仅高权限用户才能够进行操作的请求,诱使登录状态的高权限用户点击伪造的链接、图片,从而成功执行非法的请求,以用来执行重要的查看,修改,删除,授权等恶意操作,一般用来防止盗链。
登录拦截器的约束:验证用户登录的token时效性,服务端会对请求的用户信息进行校验,满足时效条件放行否则踢出登录。
实现过程:追加登录referer拦截器
referer拦截器过程
创建 LoginInterceptor 类,继承HandlerInterceptorAdapter
@Service
public class LoginInterceptor extends HandlerInterceptorAdapter {
/**
* 白名单
/
private String[] refererDomain = new String[] { “www.baidu.com”, “xxx.xxx.xx” };
/*
* 是否开启referer校验
*/
private Boolean check = true;
/**
* @title:拦截器开始
* @author:chenpengfei
* @date:2020-04-28
* @param:request
* @param:response
* @param:handler
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (check) {
// 校验请求referer
if (!checkReferer(request, response)) {
returnErrorMessage(response, BaseEnum.REFERER_ERROR.getCode(), BaseEnum.REFERER_ERROR.getMessage());
}
}
// 校验用户cookie
OptCmsLoginUser loginUser = cmsUserService.getLoginUser();
if (ObjectUtils.isEmpty(loginUser)) {
logout(request, response);
returnErrorMessage(response, BaseEnum.COOKIE_USER_NULL.getCode(), BaseEnum.COOKIE_USER_NULL.getMessage());
return false;
}
LogWriter.writeWorkLog("验证用户token有效性 验证的数据:" + loginUser.toString());
// 验证token
if (!cmsUserService.tokenCheck(loginUser, request)) {
logout(request, response);
returnErrorMessage(response, BaseEnum.TOKEN_OUT_TIME.getCode(), BaseEnum.TOKEN_OUT_TIME.getMessage());
return false;
}
return super.preHandle(request, response, handler);
}
private void logout(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
try {
LoginCookieHandle.logout(response);
} catch (Exception e) {
LogWriter.writeErrorLog("登录拦截logout异常", e);
}
}
private void returnErrorMessage(HttpServletResponse response, Integer code, String errorMessage)
throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
JSONObject res = new JSONObject();
res.put("retcode", code);
res.put("retmsg", errorMessage);
PrintWriter out = null;
out = response.getWriter();
out.write(res.toString());
LogWriter.writeWorkLog("拦截异常成功返回错误信息 msg:" + res.toString());
out.flush();
out.close();
}
private boolean checkReferer(HttpServletRequest request, HttpServletResponse response) {
String referer = request.getHeader("referer");
String host = request.getServerName();
LogWriter.writeWorkLog("referer请求拦截执行:host=" + host + ";" + "referer=" + referer);
// 验证非get请求
if (!"GET".equals(request.getMethod())) {
if (referer == null) {
// 状态置为404
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return false;
}
java.net.URL url = null;
try {
url = new java.net.URL(referer);
} catch (MalformedURLException e) {
LogWriter.writeErrorLog(BaseEnum.REFERER_ERROR.getMessage(), e);
// URL解析异常,也置为404
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return false;
}
// 首先判断请求域名和referer域名是否相同
if (!host.equals(url.getHost())) {
// 如果不等,判断是否在白名单中
if (refererDomain != null) {
for (String s : refererDomain) {
if (s.equals(url.getHost())) {
return check;
}
}
}
return false;
}
}
return check;
}
}