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链下一层使用
SpringBoot前后端接口加解密--解决方案的更多相关文章
- vue+springboot前后端分离实现单点登录跨域问题处理
最近在做一个后台管理系统,前端是用时下火热的vue.js,后台是基于springboot的.因为后台系统没有登录功能,但是公司要求统一登录,登录认证统一使用.net项目组的认证系统.那就意味着做单点登 ...
- BugPhobia准备篇章:Beta阶段前后端接口文档
0x00:序言 Two strangers fell in love, Only one knows it wasn’t by chance. To the searching tags, you m ...
- docker-compose 部署 Vue+SpringBoot 前后端分离项目
一.前言 本文将通过docker-compose来部署前端Vue项目到Nginx中,和运行后端SpringBoot项目 服务器基本环境: CentOS7.3 Dokcer MySQL 二.docker ...
- Springboot前后端分离开发
.1.springboot前后端分离开发之前要配置好很多东西,这周会详细补充博客内容和遇到的问题的解析 2,按照下面流程走一遍 此时会加载稍等一下 pom.xml显示中加上阿里云镜像可以加速下载配置文 ...
- 解决SpringBoot前后端集成项目导出jar包运行访问页面资源报错“Whitelabel Error Page”问题
一.SpringBoot前后端集成项目导出jar包后运行访问页面资源报错"Whitelabel Error Page"问题 二.解决方案 1.将Controller层移入com.x ...
- vue菜鸟从业记:公司项目里如何进行前后端接口联调
最近我的朋友王小闰进入一家新的公司,正好公司项目采用的是前后端分离架构,技术栈是王小闰非常熟悉的vue全家桶,后端用的是Java语言. 在前后端开发人员碰面之后,协商确定好了前端需要的数据接口(扯那么 ...
- 基于SpringBoot前后端分离的点餐系统
基于SpringBoot前后端分离的点餐系统 开发环境:主要采用Spring boot框架和小程序开发 项目简介:点餐系统,分成卖家端和买家端.买家端使用微信小程序开发,实现扫码点餐.浏览菜单.下单. ...
- SpringCloud SpringBoot 前后端分离企业级微服务架构源码赠送
基于SpringBoot2.x.SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务敏捷开发系统架构.并引入组件化的思想实现高内聚低耦合,项目代码简洁注释丰富上手 ...
- JEECG-Boot 项目介绍——基于代码生成器的快速开发平台(Springboot前后端分离)
Jeecg-Boot 是一款基于代码生成器的智能开发平台!采用前后端分离架构:SpringBoot,Mybatis,Shiro,JWT,Vue&Ant Design.强大的代码生成器让前端和后 ...
- .Net Core与Vue.js模块化前后端分离快速开发解决方案(NetModular)
NetModular是什么? NetModular不仅仅是一个框架,它也是一整套的模块化与前后端分离的快速开发的解决方案,目标是致力于开箱即用,让开发人员完全专注于业务开发,不需要关心底层封装和实现. ...
随机推荐
- [转]CLion 2022.2.4破解教程详细图解mac,windows,linux均适用(2022.11.10亲测有效)
前言 此教程为CLion 2022.2.4 破解教程,且此教程以及下面提供的破解补丁适用与2022.2以后的新版本.2022年11月10日亲测有效,mac与windows均测试完美破解 CLion ...
- 特殊数据类型的深度分析:JSON、数组和 HSTORE 的实用价值
title: 特殊数据类型的深度分析:JSON.数组和 HSTORE 的实用价值 date: 2025/1/4 updated: 2025/1/4 author: cmdragon excerpt: ...
- 深入解析 Spring AI 系列:解析OpenAI接口对接
今天我们将主要探讨OpenAI是如何进行接口对接的,虽然我们不打算深入细节,但会对整体流程进行一个大概的了解.后续会逐步分析其中的具体细节,大家可以耐心等待,逐步展开.好的,现在让我们开始,下面是我简 ...
- CAS实现原理
一.什么是CAS? 在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令. 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新 ...
- w3cschool-Spark 编程指南
https://www.w3cschool.cn/spark/ Spark 编程指南 spark特性: 提供了java scala python 和R的api支持. 在生产环境上扩展超过8000个节点 ...
- biancheng-Spring MVC
MVC设计模式简介 http://c.biancheng.net/spring_mvc/ MVC 设计模式一般指 MVC 框架,M(Model)指数据模型层,V(View)指视图层,C(Control ...
- Pipe pg walkthrough Intermediate
NAMP ┌──(root?kali)-[~] └─# nmap -p- -A 192.168.128.45 Starting Nmap 7.95 ( https://nmap.org ) at 20 ...
- Oracle数据快照设置
1.1 手册目的 该手册主要目的是用于生产环境排查问题及恢复用户误操作删除数据及程序错误导致数据丢失使用. 1.2 查看Undo表空间参数 在命令窗口查询Undo表空间的快照参数 1 show par ...
- 从存钱罐到子数组:一个关于累加和的精妙问题|LeetCode 560 和为K的子数组
LeetCode 560 和为K的子数组 点此看全部题解 LeetCode必刷100题:一份来自面试官的算法地图(题解持续更新中) 生活中的算法 你有没有这样的经历:每天往存钱罐里存一些零钱,某一天突 ...
- PostgreSQL:数据库迁移与版本控制
title: PostgreSQL:数据库迁移与版本控制 date: 2025/2/6 updated: 2025/2/6 author: cmdragon excerpt: 在现代软件开发中,数据库 ...