SpringBoot前后端接口加解密--解决方案
开放接口
- 通信方式采用HTTP+JSON或消息中间件进行通信。
- 调用接口之前需要使用登录鉴权接口获得token。
- 当鉴权成功之后才能调用其他接口(携带Token)。
登录接口:
Code 说明
200 成功
401 未授权,请先登录。
403 没有访问权限
404 接口不存在
500 接口内部错误
但是开放接口报文密文篡改问题
传入报文加密 :但系统已经很臃肿--很多的业务接口了,不可能每一个接口都去实现一遍报文解密;所以抽离到通用服务上去,然后业务接口无感实现
所以在网关层处理就最合适了
代码示例---基于Filter实现报文解密,然后转给业务接口
/**
* @author Administrator
* @apiNote 移动设备传输数据解密
* @date 2024/9/4 11:23
*/
@Component
public class MobileDevicesReqDataDecryptFilter implements Filter, InitializingBean {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 是否开启解密
*/
@Value("${cztech.manage.gateway.mobile-devices.enable-decrypt:true}")
private boolean enable;
@Value("${cztech.manage.gateway.mobile-devices-iv:56542a855c2756e9}")
private String aesIV;
@Value("${cztech.manage.gateway.mobile-devices-encrypt-key:C9JUQeuOmEu5ZvJKprEMuA==}")
private String aesKey;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
/**
* application/json
* application/x-www-form-urlencoded。
* 移动设备的 前端和后端只针对Content-Type 为上述格式数据进行加解密。
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
StopWatch stopWatch = new StopWatch();
stopWatch.start();
HttpServletRequest request = (HttpServletRequest) servletRequest;
String contentType = request.getContentType();
// app原生请求数据时需要在http header中添加operateSystem(取值为1表示ANDROID,2-表示IOS)
if (handlerDecryptRequest(servletRequest, servletResponse, filterChain, response, request, contentType)) {
stopWatch.stop();
if (logger.isDebugEnabled()) {
logger.debug("MobileDevicesReqDataDecryptFilter cost time: {} ms", stopWatch.getTotalTimeMillis());
}
return;
}
stopWatch.stop();
if (logger.isDebugEnabled()) {
logger.debug("MobileDevicesReqDataDecryptFilter cost time: {} ms", stopWatch.getTotalTimeMillis());
}
filterChain.doFilter(servletRequest, servletResponse);
}
public boolean handlerDecryptRequest(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain, HttpServletResponse response, HttpServletRequest request, String contentType) throws IOException {
// 判断开关是否开启
if (!enable) {
return false;
}
if(StringUtils.isEmpty(contentType)){ // content-type 为空则不解密
return false;
}
// 如果开启解密开关就走以下逻辑
if (request.getMethod().equalsIgnoreCase("POST") &&
(contentType.contains("application/json")
|| contentType.contains("application/x-www-form-urlencoded"))) {
String operateSystem = request.getHeader("operateSystem");
if (StringUtils.isNotEmpty(operateSystem) && ("1".equals(operateSystem) || "2".equals(operateSystem))) {
ServletInputStream inputStream = servletRequest.getInputStream();
String originBodyStr = null;
try {
originBodyStr = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
if (StringUtils.isEmpty(originBodyStr)) {
filterChain.doFilter(new MobileDevicesReqServletRequestWrapper((HttpServletRequest) servletRequest, originBodyStr), servletResponse);
return true;
}
//解密数据
String decryptData = decryptData(originBodyStr);
logger.debug("decryptData is :" + decryptData);
filterChain.doFilter(new MobileDevicesReqServletRequestWrapper((HttpServletRequest) servletRequest, decryptData), servletResponse);
} catch (Exception e) {
response.sendError(HttpStatus.BAD_REQUEST.value(), "非法请求");
}
return true;
}
}
return false;
}
/**
* 请求参数进行解密(加密算法为AES,模式为CBC,填充方式为PKCS7)
*
* @param originBodyStr originBodyStr
* @return String
*/
public String decryptData(String originBodyStr) {
AES aes = new AES("CBC", "PKCS7Padding",
aesKey.getBytes(StandardCharsets.UTF_8),
aesIV.getBytes(StandardCharsets.UTF_8));
return aes.decryptStr(originBodyStr);
}
@Override
public void destroy() {
logger.info("MobileDevicesReqDataDecryptFilter destroy");
Filter.super.destroy();
}
@Override
public void afterPropertiesSet() throws Exception {
}
/**
* http request包装器。
* 数据解密之后 body 已经发生变化,所以重写{@link #getInputStream()} 业务接口才能获取解密后的数据。
* 包装器只针对 content-type为json格式(其他格式可能会有大数据导致能溢出)。
* 为了使spring 自动解析参数所以需要重写{@link #getParameterNames()} 和{@link #getParameterValues(String)}
*/
class MobileDevicesReqServletRequestWrapper extends HttpServletRequestWrapper {
private String body;
private HttpServletRequest request;
/**
* 参数是否已经解析,true-解析,false-未解析
*/
private boolean parseParam;
/**
* 请求参数
*/
private Map<String, List<String>> parameters = new HashMap<>();
public MobileDevicesReqServletRequestWrapper(HttpServletRequest request, String body) {
super(request);
this.body = body;
this.request = request;
}
@Override
public Map<String, String[]> getParameterMap() {
if (!parseParam){
parseParameter(request.getContentType());
}
HashMap<String,String[]> parametersArray = new HashMap<>();
parameters.forEach((key,value)->{
String[] data =new String[value.size()];
value.toArray(data);
parametersArray.put(key,data);
});
return parametersArray;
// return super.getParameterMap();
}
@Override
public Enumeration<String> getParameterNames() {
// 判断 content-type 是否为 form_urlEncoded; ,如果是再解析参数
if (parseParam) {
return Collections.enumeration(parameters.keySet());
}
String contentType = request.getContentType();
if (StringUtils.isNotEmpty(contentType) && StringUtils.isNotEmpty(body)) {
parseParameter(contentType);
}
parseParam = true;
return Collections.enumeration(parameters.keySet());
}
@Override
public int getContentLength() {
return (int) getContentLengthLong();
}
@Override
public long getContentLengthLong() {
return body.getBytes().length;
}
private void parseParameter(String contentType) {
if (contentType.equals(ContentType.FORM_URLENCODED.getValue())) { //
String[] bodyParameters = body.split("&");
for (String bodyParameter : bodyParameters) {
if (StringUtils.isNotEmpty(bodyParameter)) {
String[] parameter = bodyParameter.split("="); //解析键值对
if (parameter.length == 2) {
String name = parameter[0];
String value = parameter[1];
if (StringUtils.isNotEmpty(name)) {
List<String> values = parameters.get(name);
if (values == null) {
values = new ArrayList<>();
parameters.put(name, values);
}
if (StringUtils.isNotEmpty(value)) {
//参数值需要使用urlDecode 。
values.add(URLDecoder.decode(value, Charset.forName("UTF-8")));
}
}
}
}
}
}
}
@Override
public String[] getParameterValues(String name) {
if (StringUtils.isEmpty(name)) {
return null;
}
getParameterNames();
List<String> values = parameters.get(name);
if (ListUtil.isBlank(values)) { // 参数值判断
return null;
}
String[] valueArray = new String[]{};
return values.toArray(valueArray);
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
}
核心就是从Request拿到加密报文:在doFilter方法里解密
但是Request的getInputStream流后就不可再用了,整个链路的Request IO 传给下一场时,下一层无法使用
解决方案就是:new 一个自定义的 HttpServletRequestWrapper 替代原来的 Request 传给过Filter链下一层使用
作者:隔壁老郭
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
Java入门到入坟
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!