接口安全防线加解密:springboot 全局/指定接口解密(同时支持参数在body和param)
优势:通过注解形式,不需要改变原接口请求参数,在拦截器里面把加密数据解密为原接口请求参数。同时支持application/x-www-form-
urlencoded和application/json 的解密
1.原理
1.1.过滤器,过滤所有请求,利用HttpServletRequestWrapper解决request中流读取一次的处理,方便后续修改请求内容
1.2.自定义注解,通过自定义注解可以标识,指定哪些接口在拦截器中处理数据
1.3.拦截器,拦截带有指定注解的请求,把数据进行加密解密后返回处理
2.支持范围
2.1.实际可以自己改造适合多种情况处理,已支持以下
1.application/json 加密参数在body
2.application/x-www-form-urlencoded 支持参数在body或者在param
2.2.为什么不用RequestBodyAdvice
1.因为RequestBodyAdvice只支持body内容的数据加解密处理,具有局限性。
3.具体实现代码
3.1. 过滤器(目的读取request,为后续自定义数据做铺垫)
/**
* @Author: bright chen
*/
@WebFilter(value = "/*", filterName = "uriFormatFilter")
public class UriFormatFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String uri = httpServletRequest.getRequestURI();
String newUri = uri.replace("//","/");
httpServletRequest = new HttpServletRequestWrapper(httpServletRequest){
@Override
public String getRequestURI() {
return newUri;
}
};
ServletRequest requestWrapper=new RequestWrapper(httpServletRequest);
if(requestWrapper!=null){
filterChain.doFilter(requestWrapper,httpServletResponse);
}else{
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
3.2.自定义HttpServletRequestWrapper(重点1,param赋值,body赋值,param pojo读取时赋值)
/**
* @Author: bright chen
*/
public class RequestWrapper extends HttpServletRequestWrapper {
private String body;
private Map<String, String[]> params = new HashMap<String, String[]>();
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
this.params.putAll(request.getParameterMap());
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = 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 servletInputStream;
}
// 重载一个构造方法
public RequestWrapper(HttpServletRequest request, Map<String, Object> extendParams, String body) {
this(request);
if (body != null && body.length() > 0) {
setBody(body);
}
if (extendParams.size() > 0) {
addAllParameters(extendParams);// 这里将扩展参数写入参数表
}
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
// 赋值给body字段
public void setBody(String body) {
this.body = body;
}
@Override
public String getParameter(String name) {// 重写getParameter,代表参数从当前类中的map获取
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
public String[] getParameterValues(String name) {// 同上
return params.get(name);
}
/**
* 参数为pojo类型时,会通过此方法获取所有的请求参数并进行遍历,对pojo属性赋值
*
* @return
*/
@Override
public Enumeration<String> getParameterNames() {// 同上
ArrayList<String> list = list = new ArrayList<>();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
list.add(entry.getKey());
}
return Collections.enumeration(list);
}
public void addAllParameters(Map<String, Object> otherParams) {// 增加多个参数
for (Map.Entry<String, Object> entry : otherParams.entrySet()) {
addParameter(entry.getKey(), entry.getValue());
}
}
public void addParameter(String name, Object value) {// 增加参数
if (value != null) {
if (value instanceof String[]) {
params.put(name, (String[]) value);
} else if (value instanceof String) {
params.put(name, new String[]{(String) value});
} else {
params.put(name, new String[]{String.valueOf(value)});
}
}
}
}
3.3.自定义注解 @DecryptApi,后期在controller层带上这个注解则该controller层接口会被过滤器及拦截器进行解密处理。
/**
* 支持params和body的解密
* 支持
* @Author: bright chen
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface DecryptApi {
}
3.4. (核心代码) 拦截器 DecryptInterceptor,对带有@DecryptApi注解的接口数据进行解密处理后重新赋值,注意配置
拦截所有请求
/**
* @Author: bright chen
* api 解密拦截器
*/
public class DecryptInterceptor extends HandlerInterceptorAdapter {
private static Logger logger = LogManager.getLogger(DecryptInterceptor.class);
@Autowired
private EncryptProperties encryptProperties;
/**
* Controller之前执行
* preHandle:拦截于请求刚进入时,进行判断,需要boolean返回值,如果返回true将继续执行,如果返回false,将不进行执行。一般用于登录校验
* 1.当preHandle方法返回false时,从当前拦截器往回执行所有拦截器的afterCompletion方法,再退出拦截器链。也就是说,请求不继续往下传了,直接沿着来的链往回跑。
* 2.当preHandle方法全为true时,执行下一个拦截器,直到所有拦截器执行完。再运行被拦截的Controller。然后进入拦截器链,运行所有拦截器的postHandle方法,
* 完后从最后一个拦截器往回执行所有拦截器的afterCompletion方法.
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
// RequireLogin annotation = method.getMethodAnnotation(RequireLogin.class);
if (method.hasMethodAnnotation(DecryptApi.class)) {
// 需要对数据进行加密解密
// 1.对application/json类型
String contentType = request.getContentType();
if (contentType == null && !"GET".equals(request.getMethod())) {
// 请求不通过,返回错误信息给客户端
responseResult(response, response.getWriter(), TResponse.FAIL("Decrypt failed"));
return false;
}
String requestBody = null;
boolean shouldEncrypt = false;
if ((contentType != null && StringUtils.substringMatch(contentType, 0,
MediaType.APPLICATION_FORM_URLENCODED_VALUE)) || "GET".equals(request.getMethod())) {
// 1.application/x-www-form-urlencoded 支持参数在body或者在param
shouldEncrypt = true;
requestBody = convertFormToString(request);
if (requestBody == null || "{}".equals(requestBody)) {
requestBody = URLDecoder.decode(convertInputStreamToString(request.getInputStream()),
"UTF-8");
List<String> uriToList =
Stream.of(requestBody.split("&")).map(elem -> new String(elem)).collect(Collectors.toList());
Map<String, String> uriToListToMap = new HashMap<>();
for (String individualElement : uriToList) {
if (individualElement.split("=")[0] != null && !"".equals(individualElement.split("=")[0])) {
uriToListToMap.put(individualElement.split("=")[0],
individualElement.substring(individualElement.split("=")[0].length() + 1));
}
}
requestBody = JSONObject.toJSONString(uriToListToMap);
}
} else if (StringUtils.substringMatch(contentType, 0, MediaType.APPLICATION_JSON_VALUE)) {
// application/json 支持加密参数在body
shouldEncrypt = true;
requestBody = convertInputStreamToString(request.getInputStream());
}
if (requestBody == null || "{}".equals(requestBody)||!shouldEncrypt) {
return true;
} else {
String result = decodeApi(JSON.parseObject(requestBody, StdRequestApi.class),
encryptProperties.getPrivateKey());
if (result == null) {
// 请求不通过,返回错误信息给客户端
responseResult(response, response.getWriter(), TResponse.FAIL("Decrypt failed"));
return false;
}
JSONObject jasonObject = JSONObject.parseObject(result);
Map map = (Map) jasonObject;
if (request instanceof RequestWrapper) {
RequestWrapper requestWrapper = (RequestWrapper) request;
requestWrapper.setBody(result);
requestWrapper.addAllParameters(map);
// requestWrapper = new RequestWrapper(request, map, result);
return true;
}
}
} else {
String contentType = request.getContentType();
if (contentType != null && contentType.length() > 0 && StringUtils.substringMatch(contentType, 0,
MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
// 1.application/x-www-form-urlencoded 支持参数在body或者在param
String requestBody = convertFormToString(request);
if (requestBody == null || "{}".equals(requestBody)) {
// 把流数据放进param中,不解密
requestBody = URLDecoder.decode(convertInputStreamToString(request.getInputStream()),
"UTF-8");
List<String> uriToList =
Stream.of(requestBody.split("&")).map(elem -> new String(elem)).collect(Collectors.toList());
Map<String, Object> uriToListToMap = new HashMap<>();
for (String individualElement : uriToList) {
if (individualElement.split("=")[0] != null && !"".equals(individualElement.split("=")[0])) {
uriToListToMap.put(individualElement.split("=")[0],
individualElement.substring(individualElement.split("=")[0].length() + 1));
}
}
if (request instanceof RequestWrapper) {
RequestWrapper requestWrapper = (RequestWrapper) request;
requestWrapper.setBody(requestBody);
requestWrapper.addAllParameters(uriToListToMap);
return true;
}
}
}
}
return true;
}
return true;
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage() + "异常地址:" + request.getServletPath());
responseResult(response, response.getWriter(), TResponse.FAIL("Decrypt failed"));
return false;
}
}
/**
* 返回信息给客户端
*
* @param response
* @param tResponse
*/
private void responseResult(HttpServletResponse response, TResponse tResponse) throws IOException {
response.setContentType(HttpConstant.CONTENT_TYPE_JSON);
String json = JSONObject.toJSONString(tResponse);
PrintWriter out = response.getWriter();
out.print(json);
out.flush();
out.close();
}
private String convertFormToString(HttpServletRequest request) {
Map<String, String> result = new HashMap<>(8);
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
result.put(name, request.getParameter(name));
}
try {
return JSON.toJSONString(result);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private String convertInputStreamToString(InputStream inputStream) throws IOException {
return StreamUtils.copyToString(inputStream, Charset.forName("UTF-8"));
}
public String decodeApi(StdRequestApi stdRequestApi, String apiPrivateKey) {
try {
// 1.rsa解密
// 2.AES验签
// 3.AES解密
return deData;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 返回信息给客户端
*
* @param response
* @param out
* @param tResponse
*/
private void responseResult(HttpServletResponse response, PrintWriter out, TResponse tResponse) {
response.setContentType(HttpConstant.CONTENT_TYPE_JSON);
String json = JSONObject.toJSONString(tResponse);
out.print(json);
out.flush();
out.close();
}
}
3.5.公钥私钥配置读取
/**
* @Author: bright chen
*/
@Component
@ConfigurationProperties(prefix = "api")
public class EncryptProperties {
private String privateKey;
private String publicKey;
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
}
PS:本文编写于公司产品被黑客入侵后的处理方案之一,具体加密解密逻辑不在本文中讲述
下一篇:接口安全防线注解加解密(二):加密请求异常后请求/error处理
最后
从时代发展的角度看,网络安全的知识是学不完的,而且以后要学的会更多,同学们要摆正心态,既然选择入门网络安全,就不能仅仅只是入门程度而已,能力越强机会才越多。
因为入门学习阶段知识点比较杂,所以我讲得比较笼统,大家如果有不懂的地方可以找我咨询,我保证知无不言言无不尽,需要相关资料也可以找我要,我的网盘里一大堆资料都在吃灰呢。
干货主要有:
①1000+CTF历届题库(主流和经典的应该都有了)
②CTF技术文档(最全中文版)
③项目源码(四五十个有趣且经典的练手项目及源码)
④ CTF大赛、web安全、渗透测试方面的视频(适合小白学习)
⑤ 网络安全学习路线图(告别不入流的学习)
⑥ CTF/渗透测试工具镜像文件大全
⑦ 2023密码学/隐身术/PWN技术手册大全
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
扫码领取
![](https://img-blog.csdnimg.cn/img_convert/721583c77c7fc863f8751e8680f2242b.jpeg)