当前位置: 首页 > news >正文

【JAVA技术】mybatis 数据库敏感字段加解密方案

引言:自从有公司项目前2年做了三级等保,每年一度例行公事,昨天继续配合做等保测试。这2天比较忙,这里整理之前写的一篇等保技术文章。

正文:
现在公司项目基本用mybatis实现,但由于项目跨度年份比较久, 技术实现分为2种,早期项目是用mybatis-generator实现, 新项目是用mybatis-plus实现, 这里讲一下分别用不同的方式实现数据库敏感字段加解密。

敏感字段加解密,比如手机号、姓名、身份证、住址、邮箱,原理比较简单,在数据插入前对数据进行加密,数据查询出来的时候再解密。

1、先说加密算法, 已有的老项目,可以用AES,毕竟修复数据可以通过mysql的sql语法直接搞定。直接上sql。新项目,推荐用国密SM4

select hex(AES_ENCRYPT('hello world! 张三-123', '1234567890123456'));select AES_DECRYPT(UNHEX('42A8DAF413119F35A746CD231A955E7501C1E3E3D785CD892795EAE387B379BF'),'1234567890123456') ;

2、mybatis-generator项目,实现mybatis的typeHandler

/*** 注意不能把 BaseTypeHandler 改为 BaseTypeHandler<String> ,如果改为 BaseTypeHandler<String> 的话,* 所有 String 类型的字段都会给加密了。改为 BaseTypeHandler 在需要加密的地方加上 typeHandler 就好了*/
public class EncryptTypeHandler extends BaseTypeHandler {@SneakyThrows@Overridepublic void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) {preparedStatement.setString(i, BizUtil.encryptData_ECB((String) o));}/*** 用于在Mybatis获取数据结果集时如何把数据库类型转换为对应的Java类型** @param rs         当前的结果集* @param columnName 当前的字段名称* @return 转换后的Java对象* @throws SQLException*/@SneakyThrows@Overridepublic String getNullableResult(ResultSet rs, String columnName) {String r = rs.getString(columnName);return r == null ? null : BizUtil.decryptData_ECB(r);}/*** 用于在Mybatis通过字段位置获取字段数据时把数据库类型转换为对应的Java类型** @param rs          当前的结果集* @param columnIndex 当前字段的位置* @return 转换后的Java对象* @throws SQLException*/@SneakyThrows@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) {String r = rs.getString(columnIndex);return r == null ? null : BizUtil.decryptData_ECB(r);}/*** 用于Mybatis在调用存储过程后把数据库类型的数据转换为对应的Java类型** @param cs          当前的CallableStatement执行后的CallableStatement* @param columnIndex 当前输出参数的位置* @return* @throws SQLException*/@SneakyThrows@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) {String r = cs.getString(columnIndex);// 兼容待修复的数据return r == null ? null : BizUtil.decryptData_ECB(r);}}

mapper文件对应加密字段属性实现

<result column="user_mobile" jdbcType="VARCHAR"property="userMobile"typeHandler="com.company.group.project.util.mybatis.EncryptTypeHandler"/>

插入、更新对应属性加上

#{userMobile,jdbcType=VARCHAR, typeHandler=com.company.group.project.util.mybatis.EncryptTypeHandler},

老项目埋坑点:

a、手写的mapper 人肉处理,通过建resultMap设置属性

b、敏感字段作为参数搜索,必须加密后传递查询。 正常来说,在baseService做 selectByExample封装, 不少同学之前在extendService甚至更高层级操作了mapper, 改的苦笑不得

3、mybatis-plus项目, 之前mybatis版本之前用的低版本3.1,为了用上mybatis-plus 3.4的JsqlParserSupport等,把springboot1.5.x 升级到了springboot2.x, 这个过程走了一些弯路,所幸成功了。

mybatis-plus主要通过对象属性注解反射实现的,参照了网上的一些代码,代码改动量相对比较少。 敏感字段作为参数搜索,必须加密传递查询。

加密插件:

public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor {/*** 变量占位符正则*/private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");@Overridepublic void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {InnerInterceptor.super.beforePrepare(sh, connection, transactionTimeout);//System.out.println("================================================================================beforePrepare");}/*** 如果查询条件是加密数据列,那么要将查询条件进行数据加密。* 例如,手机号加密存储后,按手机号查询时,先把要查询的手机号进行加密,再和数据库存储的加密数据进行匹配*/@Overridepublic void beforeQuery(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {//System.out.println("================================================================================beforeQuery");if (Objects.isNull(parameterObject)) {return;}if (!(parameterObject instanceof Map)) {return;}Map paramMap = (Map) parameterObject;// 参数去重,否则多次加密会导致查询失败Set set = (Set) paramMap.values().stream().collect(Collectors.toSet());for (Object param : set) {/***  仅支持类型是自定义Entity的参数,不支持mapper的参数是QueryWrapper、String等,例如:**  支持:findList(@Param(value = "query") UserEntity query);*  支持:findPage(@Param(value = "query") UserEntity query, Page<UserEntity> page);**  不支持:findList(@Param(value = "mobile") String mobile);*  不支持:findList(QueryWrapper wrapper);*/if (param instanceof AbstractWrapper || param instanceof String) {// Wrapper、String类型查询参数,无法获取参数变量上的注解,无法确认是否需要加密,因此不做判断continue;}if (needToDecrypt(param.getClass())) {encryptEntity(param);}}}@Overridepublic void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException {if (Objects.isNull(parameterObject)) {return;}// 通过MybatisPlus自带API(save、insert等)新增数据库时if (!(parameterObject instanceof Map)) {if (needToDecrypt(parameterObject.getClass())) {encryptEntity(parameterObject);}return;}Map paramMap = (Map) parameterObject;Object param;// 通过MybatisPlus自带API(update、updateById等)修改数据库时if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) {if (needToDecrypt(param.getClass())) {encryptEntity(param);}return;}// 通过在mapper.xml中自定义API修改数据库时if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {if (needToDecrypt(param.getClass())) {encryptEntity(param);}return;}// 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) {if (param instanceof Update && param instanceof AbstractWrapper) {Class<?> entityClass = mappedStatement.getParameterMap().getType();if (needToDecrypt(entityClass)) {encryptWrapper(entityClass, param);}}return;}}/*** 校验该实例的类是否被@EncryptedTable所注解*/private boolean needToDecrypt(Class<?> objectClass) {EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);return Objects.nonNull(sensitiveData);}/*** 通过API(save、updateById等)修改数据库时** @param parameter*/private void encryptEntity(Object parameter) {//取出parameterType的类Class<?> resultClass = parameter.getClass();Field[] declaredFields = resultClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (!Objects.isNull(sensitiveField)) {field.setAccessible(true);Object object = null;try {object = field.get(parameter);} catch (IllegalAccessException e) {continue;}//只支持String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一加密try {field.set(parameter, AESUtil.encrypt(value));} catch (IllegalAccessException e) {continue;}}}}}/*** 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时** @param entityClass* @param ewParam*/private void encryptWrapper(Class<?> entityClass, Object ewParam) {AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;String sqlSet = updateWrapper.getSqlSet();if (StringUtils.isBlank(sqlSet)) {return;}String[] elArr = sqlSet.split(",");Map<String, String> propMap = new HashMap<>(elArr.length);Arrays.stream(elArr).forEach(el -> {String[] elPart = el.split("=");propMap.put(elPart[0], elPart[1]);});//取出parameterType的类Field[] declaredFields = entityClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (Objects.isNull(sensitiveField)) {continue;}String el = propMap.get(field.getName());try {Matcher matcher = PARAM_PAIRS_RE.matcher(el);if (matcher.matches()) {String valueKey = matcher.group(1);Object value = updateWrapper.getParamNameValuePairs().get(valueKey);updateWrapper.getParamNameValuePairs().put(valueKey, AESUtil.encrypt(value.toString()));}}catch (Exception e){logger.error("{}", e);}}Method[] declaredMethods = entityClass.getDeclaredMethods();for (Method method : declaredMethods) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = method.getAnnotation(EncryptedColumn.class);if (Objects.isNull(sensitiveField)) {continue;}String el = propMap.get(method.getName());try {Matcher matcher = PARAM_PAIRS_RE.matcher(el);if (matcher.matches()) {String valueKey = matcher.group(1);Object value = updateWrapper.getParamNameValuePairs().get(valueKey);updateWrapper.getParamNameValuePairs().put(valueKey, AESUtil.encrypt(value.toString()));}}catch (Exception e){logger.error("{}",e);}}}
}

解密插件:

@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Component
public class DecryptInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}if (resultObject instanceof ArrayList) {//基于selectListArrayList resultList = (ArrayList) resultObject;if (!resultList.isEmpty() && needToDecrypt(resultList.get(0))) {for (Object result : resultList) {//逐一解密decrypt(result);}}} else if (needToDecrypt(resultObject)) {//基于selectOnedecrypt(resultObject);}return resultObject;}/*** 校验该实例的类是否被@EncryptedTable所注解*/private boolean needToDecrypt(Object object) {Class<?> objectClass = object.getClass();EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);return Objects.nonNull(sensitiveData);}@Overridepublic Object plugin(Object o) {return Plugin.wrap(o, this);}private <T> T decrypt(T result) throws Exception {//取出resultType的类Class<?> resultClass = result.getClass();Field[] declaredFields = resultClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (!Objects.isNull(sensitiveField)) {field.setAccessible(true);Object object = field.get(result);//只支持String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一解密field.set(result, AESUtil.decrypt(value));}}}return result;}
}

mybatis配置

@Configuration(proxyBeanMethods = false)
public class MybatisPlusConfig {/*** 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)*/private static final Long MAX_LIMIT = 1000L;@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 加解密拦截器interceptor.addInnerInterceptor(new EncryptInterceptor());// 分页拦截PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInterceptor.setMaxLimit(MAX_LIMIT);interceptor.addInnerInterceptor(paginationInterceptor);// 乐观锁拦截interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}}

model对象demo

@EncryptedTable
@TableEventListen({SqlCommandType.INSERT, SqlCommandType.UPDATE})
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("db_demo")
public class Demo extends BaseEntity<Demo> {private static final long serialVersionUID = 1L;@EncryptedColumnprivate String mobile;@Overrideprotected Serializable pkVal() {return null;}}

遇到的坑:

1、mybatis-plus的 service 低版本和高版本默认值不一样getOne(queryWrapper, false); 高版本默认是抛异常

2、针对对象反射,比如更新后查询这种,最好是更新 和查询分别用一个对象。

原文链接:【JAVA技术】mybatis 数据库敏感字段加解密方案

相关文章:

【JAVA技术】mybatis 数据库敏感字段加解密方案

引言&#xff1a;自从有公司项目前2年做了三级等保&#xff0c;每年一度例行公事&#xff0c;昨天继续配合做等保测试。这2天比较忙&#xff0c;这里整理之前写的一篇等保技术文章。 正文&#xff1a; 现在公司项目基本用mybatis实现&#xff0c;但由于项目跨度年份比较久&…...

Collections工具类及其案例

package exercise;public class Demo1 {public static void main(String[] args) {//可变参数//方法形参的个数是可以发生变化的//格式&#xff1a;属性类型...名字//int...argsint sum getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);System.out.println(sum);}//底层&#xff1a;可…...

Duck Bro的第512天创作纪念日

Tips&#xff1a;发布的文章将会展示至 里程碑专区 &#xff0c;也可以在 专区 内查看其他创作者的纪念日文章 我的创作纪念日第512天 文章目录 我的创作纪念日第512天一、与CSDN平台的相遇1. 为什么在CSDN这个平台进行创作&#xff1f;2. 创作这些文章是为了赚钱吗&#xff1f…...

【机器学习】GPT-4中的机器学习如何塑造人类与AI的新对话

&#x1f680;时空传送门 &#x1f50d;引言&#x1f4d5;GPT-4概述&#x1f339;机器学习在GPT-4中的应用&#x1f686;文本生成与摘要&#x1f388;文献综述与知识图谱构建&#x1f6b2;情感分析与文本分类&#x1f680;搜索引擎优化&#x1f4b4;智能客服与虚拟助手&#x1…...

晨控CK-UR12-E01与欧姆龙NX/NJ系列EtherNet/IP通讯手册

晨控CK-UR12-E01与欧姆龙NX/NJ系列EtherNet/IP通讯手册 晨控CK-UR12-E01 是天线一体式超高频读写器头&#xff0c;工作频率默认为902MHz&#xff5e;928MHz&#xff0c;符合EPC Global Class l Gen 2&#xff0f;IS0-18000-6C 标准&#xff0c;最大输出功率 33dBm。读卡器同时…...

模板显式、隐式实例化和(偏)特化、具体化的详细分析

最近看了<The C Programing Language>看到了模板的特化&#xff0c;突然想起来<C Primer>上说的显式具体化、隐式具体化、特化、偏特化、具体化等概念弄得头晕脑胀&#xff0c;我在网上了找了好多帖子&#xff0c;才把概念给理清楚。 看着这么多叫法&#xff0c;其…...

软件设计师笔记-计算机系统基础知识

CPU的功能 CPU(中央处理器)是计算机的核心部件,负责执行计算机的指令和处理数据。它的功能主要可以分为程序控制、操作控制、时间控制和数据处理四个方面: 程序控制:CPU的首要任务是执行存储在内存中的程序。程序控制功能确保CPU能够按照程序的指令序列,一条一条地执行。…...

flink 作业动态维护更新,不重启flink,不提交作业

Flink任务实时获取并更新规则_flink任务流实时变更-CSDN博客 一种动态更新flink任务配置的方法_flink 数据源 动态更新-CSDN博客 Flink CEP在实时风控场景的落地与优化 最佳实践 - 在SQL任务中使用Flink CEP - 《实时计算用户手册-v4.5.0》 Flink SQL CEP详解-CSDN博客 如…...

为何数据仓库需要“分层次”?

在数据驱动的商业世界中&#xff0c;数据仓库是企业决策的心脏。然而&#xff0c;一个高效、可扩展且易于管理的数据仓库&#xff0c;需要精心设计和构建。分层是构建数据仓库的关键策略之一。本文将探讨数据仓库分层的重要性以及它如何帮助企业更好地管理数据。 数据仓库分层…...

小熊家务帮day15-day18 预约下单模块(预约下单,熔断降级,支付功能,退款功能)

目录 1 预约下单1.1 需求分析1.1.1 业务流程1.1.2 订单状态 1.2 系统设计1.2.1 订单表设计1.2.2 表结构的设置 1.3 开发远程调用接口1.3.0 复习下远程调用的开发1.3.1 查询地址簿远程接口jzo2o-api工程定义接口Customer服务实现接口 1.3.2 查询服务&服务项远程接口jzo2o-ap…...

[word] word悬挂缩进怎么设置? #经验分享#职场发展#经验分享

word悬挂缩进怎么设置&#xff1f; 在编辑Word的时候上方会有个Word标尺&#xff0c;相信很多伙伴都没使用过。其实它隐藏着很多好用的功能&#xff0c;今天就给大家分享下利用这个word标尺的悬挂缩进怎么设置&#xff0c;一起来看看吧&#xff01; 1、悬挂缩进 选中全文&…...

6-Maven的使用

6-Maven的使用 常用maven命令 //常用maven命令 mvn -v //查看版本 mvn archetype:create //创建 Maven 项目 mvn compile //编译源代码 mvn test-compile //编译测试代码 mvn test //运行应用程序中的单元测试 mvn site //生成项目相关信息的网站 mvn package //依据项目生成 …...

WPF真入门教程32--WPF数字大屏项目实干

1、项目背景 WPF (Windows Presentation Foundation) 是微软的一个框架&#xff0c;用于构建桌面客户端应用程序&#xff0c;它支持富互联网应用程序&#xff08;RIA&#xff09;的开发。在数字大屏应用中&#xff0c;WPF可以用来构建复杂的用户界面&#xff0c;展示庞大的数据…...

数据可视化Python实现超详解【数据分析】

各位大佬好 &#xff0c;这里是阿川的博客&#xff0c;祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 Python 初阶 Python–语言基础与由来介绍 Python–…...

Maxkb玩转大语言模型

Maxkb玩转大语言模型 随着国外大语言模型llama3的发布&#xff0c;搭建本地个人免费“人工智能”变得越来越简单&#xff0c;今天博主分享使用Max搭建本地的个人聊天式对话及个人本地知识域的搭建。 1.安装Maxkb开源应用 github docker快速安装 docker run -d --namemaxkb -p 8…...

React Hooks 封装可粘贴图片的输入框组件(wangeditor)

需求是需要一个文本框 但是可以支持右键或者ctrlv粘贴图片&#xff0c;原生js很麻烦&#xff0c;那不如用插件来实现吧~我这里用的wangeditor插件&#xff0c;初次写初次用&#xff0c;可能不太好&#xff0c;但目前是可以达到实现需求的一个效果啦&#xff01;后面再改进吧~ …...

Wireshark TS | 应用传输丢包问题

问题背景 仍然是来自于朋友分享的一个案例&#xff0c;实际案例不难&#xff0c;原因也就是互联网线路丢包产生的重传问题。但从一开始只看到数据包截图的判断结果&#xff0c;和最后拿到实际数据包的分析结果&#xff0c;却不是一个结论&#xff0c;方向有点跑偏&#xff0c;…...

架构设计-web项目中跨域问题涉及到的后端和前端配置

WEB软件项目中经常会遇到跨域问题&#xff0c;解决方案早已是业内的共识&#xff0c;简要记录主流的处理方式&#xff1a; 跨域感知session需要解决两个问题&#xff1a; 1. 跨域问题 2. 跨域cookie传输问题 跨域问题 解决跨域问题有很多种方式&#xff0c;如使用springboot…...

==Redis淘汰策略(内存满了触发)==

好的&#xff0c;面试官。这个问题我需要从三个方面来回答。第一个方面&#xff1a; 当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候&#xff0c;Redis 就会根据配置的内存淘汰策略。 把访问频率不高的 key 从内存中移除。maxmemory 默认情况是当前服务器的最大内存…...

2024年高考作文考人工智能,人工智能写作文能否得高分

前言 众所周知&#xff0c;今年全国一卷考的是人工智能&#xff0c;那么&#xff0c;我们来测试一下&#xff0c;国内几家厉害的人工智能他们的作答情况&#xff0c;以及能取得多少高分呢。由于篇幅有限&#xff0c;我这里只测试一个高考真题&#xff0c;我们这里用百度的文心…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

系统设计 --- MongoDB亿级数据查询优化策略

系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log&#xff0c;共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题&#xff0c;不能使用ELK只能使用…...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

【2025年】解决Burpsuite抓不到https包的问题

环境&#xff1a;windows11 burpsuite:2025.5 在抓取https网站时&#xff0c;burpsuite抓取不到https数据包&#xff0c;只显示&#xff1a; 解决该问题只需如下三个步骤&#xff1a; 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…...

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

Swagger和OpenApi的前世今生

Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章&#xff0c;二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑&#xff1a; &#x1f504; 一、起源与初创期&#xff1a;Swagger的诞生&#xff08;2010-2014&#xff09; 核心…...