开放接口

- 通信方式采用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链下一层使用

SpringBoot前后端接口加解密--解决方案的更多相关文章

  1. vue+springboot前后端分离实现单点登录跨域问题处理

    最近在做一个后台管理系统,前端是用时下火热的vue.js,后台是基于springboot的.因为后台系统没有登录功能,但是公司要求统一登录,登录认证统一使用.net项目组的认证系统.那就意味着做单点登 ...

  2. BugPhobia准备篇章:Beta阶段前后端接口文档

    0x00:序言 Two strangers fell in love, Only one knows it wasn’t by chance. To the searching tags, you m ...

  3. docker-compose 部署 Vue+SpringBoot 前后端分离项目

    一.前言 本文将通过docker-compose来部署前端Vue项目到Nginx中,和运行后端SpringBoot项目 服务器基本环境: CentOS7.3 Dokcer MySQL 二.docker ...

  4. Springboot前后端分离开发

    .1.springboot前后端分离开发之前要配置好很多东西,这周会详细补充博客内容和遇到的问题的解析 2,按照下面流程走一遍 此时会加载稍等一下 pom.xml显示中加上阿里云镜像可以加速下载配置文 ...

  5. 解决SpringBoot前后端集成项目导出jar包运行访问页面资源报错“Whitelabel Error Page”问题

    一.SpringBoot前后端集成项目导出jar包后运行访问页面资源报错"Whitelabel Error Page"问题 二.解决方案 1.将Controller层移入com.x ...

  6. vue菜鸟从业记:公司项目里如何进行前后端接口联调

    最近我的朋友王小闰进入一家新的公司,正好公司项目采用的是前后端分离架构,技术栈是王小闰非常熟悉的vue全家桶,后端用的是Java语言. 在前后端开发人员碰面之后,协商确定好了前端需要的数据接口(扯那么 ...

  7. 基于SpringBoot前后端分离的点餐系统

    基于SpringBoot前后端分离的点餐系统 开发环境:主要采用Spring boot框架和小程序开发 项目简介:点餐系统,分成卖家端和买家端.买家端使用微信小程序开发,实现扫码点餐.浏览菜单.下单. ...

  8. SpringCloud SpringBoot 前后端分离企业级微服务架构源码赠送

    基于SpringBoot2.x.SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务敏捷开发系统架构.并引入组件化的思想实现高内聚低耦合,项目代码简洁注释丰富上手 ...

  9. JEECG-Boot 项目介绍——基于代码生成器的快速开发平台(Springboot前后端分离)

    Jeecg-Boot 是一款基于代码生成器的智能开发平台!采用前后端分离架构:SpringBoot,Mybatis,Shiro,JWT,Vue&Ant Design.强大的代码生成器让前端和后 ...

  10. .Net Core与Vue.js模块化前后端分离快速开发解决方案(NetModular)

    NetModular是什么? NetModular不仅仅是一个框架,它也是一整套的模块化与前后端分离的快速开发的解决方案,目标是致力于开箱即用,让开发人员完全专注于业务开发,不需要关心底层封装和实现. ...

随机推荐

  1. 即时通讯技术文集(第33期):IM开发综合技术合集(Part6) [共12篇]

    为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第33 期. [- 1 -] IM开发技术学习:揭秘微信朋友圈这种信息推流背后的系统设计 [链接 ...

  2. 不为人知的网络编程(十五):深入操作系统,一文搞懂Socket到底是什么

    1.引言 我相信大家刚开始学网络编程中socket的时候,都跟我一样对书上所讲的socket概念云里雾里的.似懂非懂,很是困扰. 这篇文章我打算从初学者的角度,用通俗易懂的文字,跟大家分享下我所理解的 ...

  3. 常见的运行窗口命令和命令提示符(DOS)命令

    常见的运行窗口命令和命令提示符(DOS)命令 常见的运行窗口命令 Win + R 打开运行窗口 基础应用程序启动命令 calc:启动计算器. notepad:打开记事本. mspaint:启动画图工具 ...

  4. 变分推断(VI)、随机梯度变分推断(SGVI/SGVB)、变分自编码器(VAE)串讲

    参考资料: VI参考:PRML Chapter 10. SGVI原文:Auto-Encoding Variational Bayes -- Kingma. VAE参考1:Tutorial on Var ...

  5. [LC1302] 层数最深叶子节点的和

    题目概述 给你一棵二叉树的根节点 root ,请你返回 层数最深的叶子节点的和 . 基本思路 这是一个简单的树的遍历的问题,可以用bfs或者dfs来解题.这里采用dfs来解,在遍历的过程中,只需要用全 ...

  6. 一致性hash和普通hash和hash槽

    普通hash Hash函数:一般翻译做散列.杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值.碰撞(冲突):如果两个关键字通过 ...

  7. 史上最强Dubbo面试28题答案详解

    1.Dubbo是什么? Dubbo 是一个分布式.高性能.透明化的 RPC 服务框架,提供服务自动注册.自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成. RPC 指的是远程调用协议, ...

  8. 「TC SRM625 D1L3」Seatfriends

    思路 首先,对于计数题,不是 \(\text{dp}\) 就是排列组合,这题多思考一会儿就发现单纯 \(\text{dp}\) 和排列组合是做不出来的.然后激动人心地发现,这题是 \(\text{dp ...

  9. Kali Linux 安装教程

    Kali Linux 安装教程 下载镜像文件 Kali官网下载 访问Kali官网(https://www.kali.org/ ),根据下图所示进行下载 清华大学开源软件镜像站下载 访问清华大学开源软件 ...

  10. 硬件设计:POE--POE基础

    参考资料:POE供电基础知识:PSE PD检测过程详解 POE供电简介 以太网供电 一.POE相关介绍 POE(Power Over Ethernet)是指在现有的以太网Cat.5布线基础架构上不做任 ...