前端技术
HTML
CSS
Javascript
前端框架和UI库
VUE
ReactJS
AngularJS
JQuery
NodeJS
JSON
Element-UI
Bootstrap
Material UI
服务端和客户端
Java
Python
PHP
Golang
Scala
Kotlin
Groovy
Ruby
Lua
.net
c#
c++
后端WEB和工程框架
SpringBoot
SpringCloud
Struts2
MyBatis
Hibernate
Tornado
Beego
Go-Spring
Go Gin
Go Iris
Dubbo
HessianRPC
Maven
Gradle
数据库
MySQL
Oracle
Mongo
中间件与web容器
Redis
MemCache
Etcd
Cassandra
Kafka
RabbitMQ
RocketMQ
ActiveMQ
Nacos
Consul
Tomcat
Nginx
Netty
大数据技术
Hive
Impala
ClickHouse
DorisDB
Greenplum
PostgreSQL
HBase
Kylin
Hadoop
Apache Pig
ZooKeeper
SeaTunnel
Sqoop
Datax
Flink
Spark
Mahout
数据搜索与日志
ElasticSearch
Apache Lucene
Apache Solr
Kibana
Logstash
数据可视化与OLAP
Apache Atlas
Superset
Saiku
Tesseract
系统与容器
Linux
Shell
Docker
Kubernetes
[--rotate-pages 参数功能解...]的搜索结果
这里是文章列表。热门标签的颜色随机变换,标签颜色没有特殊含义。
点击某个标签可搜索标签相关的文章。
点击某个标签可搜索标签相关的文章。
Tesseract
...act:图像旋转角度参数设置无效的深度解析与解决策略 引言(1) 亲爱的开发者们,我们都知道Tesseract作为一款强大的开源OCR(光学字符识别)工具,在处理和识别图像中的文本信息时,展现出了非凡的能力。然而,在实际应用过程中,我们可能遇到过这样的困扰:“哎呀,我明明设置了图像旋转角度参数,为啥Tesseract就是不听话,无法正确地识别出旋转后的文字呢?”今天,我们就一起来揭开这个谜团,探讨一下“图像旋转角度参数设置无效”的问题及其解决方案,让我们一起走进Tesseract的世界,感受其背后的逻辑与奥秘。 问题阐述(2) 首先,让我们明确一下问题现象。在使用Tesseract进行图像识别时,有时候由于图片本身存在一定的倾斜角度,因此需要预先对图像进行旋转校正。其实呢,理论上讲,咱们可以通过调整--psm参数或者直接操作API接口来给图片“拧个角度”,但有时候你会发现,就算你把角度调得准准的,可识别出来的结果还是让人挠头,不太对劲儿。这正是我们今天要坐下来好好唠一唠的问题。 python import pytesseract from PIL import Image 假设我们有一张倾斜45度的图片 img = Image.open('rotated_text.jpg') rotated_img = img.rotate(45) 尝试设置旋转角度为45度进行识别 text = pytesseract.image_to_string(rotated_img, config='--psm 6 -c tessedit_pageseg_mode=6 --oem 3 --rotate-pages 45') print(text) 尽管我们已经尝试将图像旋转回正,并在配置中指定了旋转角度,但输出的识别结果却并不理想,这确实令人费解且头疼。 原因分析(3) 原因一:预处理的重要性 Tesseract对于图像的识别并非简单依赖于用户设定的旋转参数,而是基于内部的页面分割算法(Page Segmentation Mode)。如果原始图片质量不咋地,或者背景乱七八糟的,光靠调整旋转角度这一招,可没法保证一定能识别得准准的。在调用Tesseract前,往往需要对图像进行一系列预处理操作,比如灰度化、二值化、降噪等。 原因二:旋转参数的误解 --rotate-pages参数主要用于PDF文档旋转,而非单个图像的旋转矫正。对于单个图像,我们应先自行完成旋转操作后再进行识别。 解决方案(4) 策略一:手动预处理与旋转 正确的做法是先利用Python Imaging Library(Pillow)或其他图像处理库对图像进行旋转校正,然后再交给Tesseract进行识别: python 正确的做法:手动旋转图像并进行识别 corrected_img = img.rotate(-45, expand=True) 注意这里旋转的角度是负数,因为我们要将其逆向旋转回正 corrected_text = pytesseract.image_to_string(corrected_img, config='--psm 6') print(corrected_text) 策略二:结合Tesseract的内部矫正功能 Tesseract从v4版本开始支持自动检测并矫正文本方向,可通过--deskew-amount参数开启文本行的去斜功能,但这并不能精确到每个字符,所以对于严重倾斜的图像,仍需先进行手动旋转。 python 使用Tesseract的去斜功能 auto_corrected_text = pytesseract.image_to_string(img, config='--psm 6 --deskew-amount 0.2') print(auto_corrected_text) 结语(5) 总而言之,“图像旋转角度参数设置无效”这个问题,其实更多的是我们在理解和使用Tesseract时的一个误区。我们需要深入了解其工作原理,并结合恰当的预处理手段来提升识别效果。在这一趟探索的旅程中,我们又实实在在地感受了一把编程那让人着迷的地方——就是那种面对棘手问题时,不断挠头苦思、积极动手实践,然后欢呼雀跃地找到解题钥匙的时刻。而Tesseract,就像一位沉默而睿智的朋友,等待着我们去发掘它更多的可能性和潜力。
2023-05-04 09:09:33
80
红尘漫步
转载文章
...库服务器上,可以将该参数设置为机器物理内存大小的80%。但是,不要将它设置得太大,因为物理内存的竞争可能会导致操作系统中的分页。注意,在32位系统上,每个进程的用户级内存可能被限制在2-3.5G,所以不要设置得太高。 innodb_buffer_pool_size=20M Size of each log file in a log group. You should set the combined size of log files to about 25%-100% of your buffer pool size to avoid unneeded buffer pool flush activity on log file overwrite. However, note that a larger logfile size will increase the time needed for the recovery process. 日志组中每个日志文件的大小。您应该将日志文件的合并大小设置为缓冲池大小的25%-100%,以避免在覆盖日志文件时出现不必要的缓冲池刷新活动。但是,请注意,较大的日志文件大小将增加恢复过程所需的时间。 innodb_log_file_size=48M Number of threads allowed inside the InnoDB kernel. The optimal value depends highly on the application, hardware as well as the OS scheduler properties. A too high value may lead to thread thrashing. InnoDB内核中允许的线程数。最优值在很大程度上取决于应用程序、硬件以及OS调度程序属性。过高的值可能导致线程抖动。 innodb_thread_concurrency=9 The increment size (in MB) for extending the size of an auto-extend InnoDB system tablespace file when it becomes full. 增量大小(以MB为单位),用于在表空间满时扩展自动扩展的InnoDB系统表空间文件的大小。 innodb_autoextend_increment=128 The number of regions that the InnoDB buffer pool is divided into. For systems with buffer pools in the multi-gigabyte range, dividing the buffer pool into separate instances can improve concurrency, by reducing contention as different threads read and write to cached pages. InnoDB缓冲池划分的区域数。对于具有多gb缓冲池的系统,将缓冲池划分为单独的实例可以提高并发性,因为不同的线程对缓存页面的读写会减少争用。 innodb_buffer_pool_instances=8 Determines the number of threads that can enter InnoDB concurrently. 确定可以同时进入InnoDB的线程数 innodb_concurrency_tickets=5000 Specifies how long in milliseconds (ms) a block inserted into the old sublist must stay there after its first access before it can be moved to the new sublist. 指定插入到旧子列表中的块必须在第一次访问之后停留多长时间(毫秒),然后才能移动到新子列表。 innodb_old_blocks_time=1000 It specifies the maximum number of .ibd files that MySQL can keep open at one time. The minimum value is 10. 它指定MySQL一次可以打开的.ibd文件的最大数量。最小值是10。 innodb_open_files=300 When this variable is enabled, InnoDB updates statistics during metadata statements. 当启用此变量时,InnoDB会在元数据语句期间更新统计信息。 innodb_stats_on_metadata=0 When innodb_file_per_table is enabled (the default in 5.6.6 and higher), InnoDB stores the data and indexes for each newly created table in a separate .ibd file, rather than in the system tablespace. 当启用innodb_file_per_table(5.6.6或更高版本的默认值)时,InnoDB将每个新创建的表的数据和索引存储在单独的.ibd文件中,而不是系统表空间中。 innodb_file_per_table=1 Use the following list of values: 0 for crc32, 1 for strict_crc32, 2 for innodb, 3 for strict_innodb, 4 for none, 5 for strict_none. 使用以下值列表:0表示crc32, 1表示strict_crc32, 2表示innodb, 3表示strict_innodb, 4表示none, 5表示strict_none。 innodb_checksum_algorithm=0 The number of outstanding connection requests MySQL can have. This option is useful when the main MySQL thread gets many connection requests in a very short time. It then takes some time (although very little) for the main thread to check the connection and start a new thread. The back_log value indicates how many requests can be stacked during this short time before MySQL momentarily stops answering new requests. You need to increase this only if you expect a large number of connections in a short period of time. MySQL可以有多少未完成连接请求。当MySQL主线程在很短的时间内收到许多连接请求时,这个选项非常有用。然后,主线程需要一些时间(尽管很少)来检查连接并启动一个新线程。back_log值表示在MySQL暂时停止响应新请求之前的短时间内可以堆多少个请求。只有当您预期在短时间内会有大量连接时,才需要增加这个值。 back_log=80 If this is set to a nonzero value, all tables are closed every flush_time seconds to free up resources and synchronize unflushed data to disk. This option is best used only on systems with minimal resources. 如果将该值设置为非零值,则每隔flush_time秒关闭所有表,以释放资源并将未刷新的数据同步到磁盘。这个选项最好只在资源最少的系统上使用。 flush_time=0 The minimum size of the buffer that is used for plain index scans, range index scans, and joins that do not use 用于普通索引扫描、范围索引扫描和不使用索引执行全表扫描的连接的缓冲区的最小大小。 indexes and thus perform full table scans. join_buffer_size=200M The maximum size of one packet or any generated or intermediate string, or any parameter sent by the mysql_stmt_send_long_data() C API function. 由mysql_stmt_send_long_data() C API函数发送的一个包或任何生成的或中间字符串或任何参数的最大大小 max_allowed_packet=500M If more than this many successive connection requests from a host are interrupted without a successful connection, the server blocks that host from performing further connections. 如果在没有成功连接的情况下中断了来自主机的多个连续连接请求,则服务器将阻止主机执行进一步的连接。 max_connect_errors=100 Changes the number of file descriptors available to mysqld. You should try increasing the value of this option if mysqld gives you the error "Too many open files". 更改mysqld可用的文件描述符的数量。如果mysqld给您的错误是“打开的文件太多”,您应该尝试增加这个选项的值。 open_files_limit=4161 If you see many sort_merge_passes per second in SHOW GLOBAL STATUS output, you can consider increasing the sort_buffer_size value to speed up ORDER BY or GROUP BY operations that cannot be improved with query optimization or improved indexing. 如果在SHOW GLOBAL STATUS输出中每秒看到许多sort_merge_passes,可以考虑增加sort_buffer_size值,以加快ORDER BY或GROUP BY操作的速度,这些操作无法通过查询优化或改进索引来改进。 sort_buffer_size=1M The number of table definitions (from .frm files) that can be stored in the definition cache. If you use a large number of tables, you can create a large table definition cache to speed up opening of tables. The table definition cache takes less space and does not use file descriptors, unlike the normal table cache. The minimum and default values are both 400. 可以存储在定义缓存中的表定义的数量(来自.frm文件)。如果使用大量表,可以创建一个大型表定义缓存来加速表的打开。与普通的表缓存不同,表定义缓存占用更少的空间,并且不使用文件描述符。最小值和默认值都是400。 table_definition_cache=1400 Specify the maximum size of a row-based binary log event, in bytes. Rows are grouped into events smaller than this size if possible. The value should be a multiple of 256. 指定基于行的二进制日志事件的最大大小,单位为字节。如果可能,将行分组为小于此大小的事件。这个值应该是256的倍数。 binlog_row_event_max_size=8K If the value of this variable is greater than 0, a replication slave synchronizes its master.info file to disk. (using fdatasync()) after every sync_master_info events. 如果该变量的值大于0,则复制奴隶将其主.info文件同步到磁盘。(在每个sync_master_info事件之后使用fdatasync())。 sync_master_info=10000 If the value of this variable is greater than 0, the MySQL server synchronizes its relay log to disk. (using fdatasync()) after every sync_relay_log writes to the relay log. 如果这个变量的值大于0,MySQL服务器将其中继日志同步到磁盘。(在每个sync_relay_log写入到中继日志之后使用fdatasync())。 sync_relay_log=10000 If the value of this variable is greater than 0, a replication slave synchronizes its relay-log.info file to disk. (using fdatasync()) after every sync_relay_log_info transactions. 如果该变量的值大于0,则复制奴隶将其中继日志.info文件同步到磁盘。(在每个sync_relay_log_info事务之后使用fdatasync())。 sync_relay_log_info=10000 Load mysql plugins at start."plugin_x ; plugin_y". 开始时加载mysql插件。“plugin_x;plugin_y” plugin_load The TCP/IP Port the MySQL Server X Protocol will listen on. MySQL服务器X协议将监听TCP/IP端口。 loose_mysqlx_port=33060 本篇文章为转载内容。原文链接:https://blog.csdn.net/mywpython/article/details/89499852。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-10-08 09:56:02
129
转载
转载文章
...749Ad 发现一个参数在链接中 0Q8mMY0xXDL749Ad 通过抓包发现了一个很重要的文件:playlist.m3u8 里面包含了数个 .ts 的网络地址; .ts 文件是可以播放的视频片段; 发现 可以通过合并 .ts 片段可以得到完整视频; 出现一个问题:playlist.m3u8 怎么获取? 发现:https://v.douyu.com/api/stream/getStreamUrl 可以获取 playlist.m3u8 文件地址; 需要POST传入一些参数才行,发现: sign 参数是一种签名,一般通过JS生成,找了半天没有方法生成 sign 参数; 通过查阅大佬文献发现:手机端的斗鱼视频有接口可以直接获取 playlist.m3u8 文件地址,成功越过 sign 签名防线; 手机端斗鱼视频链接:https://vmobile.douyu.com/show/0Q8mMY0xXDL749Ad 通过抓包发现:https://vmobile.douyu.com/video/getInfo?vid=0Q8mMY0xXDL749Ad; 这就解决了playlist.m3u8 文件获取问题:json[‘data’][‘video_url’] 第一个难题解决!!; 综上所述,整理一下具体采集流程: 获取vid = 0Q8mMY0xXDL749Ad (就是链接中的参数); 通过 https://vmobile.douyu.com/video/getInfo?vid=0Q8mMY0xXDL749Ad 获取 playlist.m3u8 文件地址; 解析 playlist.m3u8 文件提取所有 .ts文件; 下载所有 .ts 文件; 合并 .ts 成视频文件输出; Python实现 不要开启线程池,因为会有一些问题 app.py config 中可以配置 import requestsimport reimport jsonimport timeimport pymongoimport psutilfrom hashlib import md5from moviepy.editor import from multiprocessing import Pool基本配置config = {'UID':'gKpdxKRWXwaW',用户ID'CID':104,栏目ID'TYPE':1, 1=>按用户id采集列表,2=>按栏目ID采集列表'TIME_START':1,起始时间'TIME_ENT':500,结束时间'PAGE_START':1,起始页'PAGE_END':10,结束页'TIME_GE':0,每个下载间隔时间'POOL':False,是否开启线程池'CHECKID':True, True 过滤已经下载过的视频 False 不过滤'FILE_PATH':'F:/ceshi/',下载目录,【会自动创建文件夹】'TS_PATH':'F:/ceshi/download/',缓存文件目录,【会自动创建文件夹】'DB_URL':'localhost',数据库地址'DB_NAME':'douyu',数据库名称''DB_TABLE':'douyu'数据库表}MongoDB初始化client = pymongo.MongoClient(config['DB_URL'])mango_db = client[config['DB_NAME']]MongoDB存储def save_to_mango(result):if mango_db[config['DB_TABLE']].insert_one({'vid':result}):print('成功存储到MangoDB')return Truereturn FalseMongoDB验证重复def check_to_mongo(vid):count = mango_db[config['DB_TABLE']].find({'vid':vid}).count()if count==0:return Falsereturn True删除文件def del_file(page):if os.path.exists(page): 删除文件,可使用以下两种方法。os.remove(page) os.unlink(my_file)else:print('no such file:%s' % page)循环列表删除文件def loop_del_file(arr):for item in arr:del_file(item)请求器def get_content_requests(url):headers = {}headers['user-agent']='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'headers['cookie'] = 'dy_did=07f83a57d1d2e22942e0883200001501; acf_did=07f83a57d1d2e22942e0883200001501; Hm_lvt_e99aee90ec1b2106afe7ec3b199020a7=1556514266,1557050422,1557208315; acf_auth=; acf_auth_wl=; acf_uid=; acf_nickname=; acf_username=; acf_own_room=; acf_groupid=; acf_notification=; acf_phonestatus=; _dys_lastPageCode=page_video,page_video; Hm_lpvt_e99aee90ec1b2106afe7ec3b199020a7=1557209469; _dys_refer_action_code=click_author_video_cate2'try:req_content = requests.get(url,headers = headers)if req_content.status_code == 200:return req_contentprint('请求失败:',url)return Noneexcept:print('请求失败:', url)return None把时间换算成秒def str_to_int(time):try:time_array = time.split(':')time_int = (int(time_array[0])60)+int(time_array[1])return time_intexcept:print('~~~~~计算视频时间失败~~~~~')return None提取需要采集的数据def get_list(html,type = 1):data = []try:list_json = json.loads(str(html))for om in list_json['data']['list']:gtime = str_to_int(om['video_str_duration'])if gtime > config['TIME_START'] and gtime < config['TIME_ENT']:if type == 2:data.append({'title': om['title'], 'vid': om['url'].split('show/')[1]})else:data.append({'title': om['title'], 'vid': om['hash_id']})return dataexcept:print('~~~~~数据提取失败~~~~~')return None解析playlist.m3u8def get_ts_list(m3u8):data = []try:html_m3u8_json = json.loads(m3u8)m3u8_text = get_content_requests(html_m3u8_json['data']['video_url'])m3u8_vurl =html_m3u8_json['data']['video_url'].split('playlist.m3u8?')[0]if m3u8_text:get_text = re.findall(',\n(.?).ts(.?)\n',m3u8_text.text,re.S)for item in get_text:data.append(m3u8_vurl+item[0]+'.ts'+item[1])return datareturn Noneexcept:print('~~~~~解析playlist.m3u8失败~~~~~')return None 杀死moviepy产生的特定进程def killProcess(): 处理python程序在运行中出现的异常和错误try: pids方法查看系统全部进程pids = psutil.pids()for pid in pids: Process方法查看单个进程p = psutil.Process(pid) print('pid-%s,pname-%s' % (pid, p.name())) 进程名if p.name() == 'ffmpeg-win64-v4.1.exe': 关闭任务 /f是强制执行,/im对应程序名cmd = 'taskkill /f /im ffmpeg-win64-v4.1.exe 2>nul 1>null' python调用Shell脚本执行cmd命令os.system(cmd)except:pass下载.ts文件def download_ts(m3u8_list,name):try:if not os.path.exists(config['FILE_PATH']):os.makedirs(config['FILE_PATH'])if not os.path.exists(config['TS_PATH']):os.makedirs(config['TS_PATH'])if os.path.exists(config['FILE_PATH']+name+'.mp4'):name = name+'_'+str(int(time.time()))print('开始下载:',name)L = []R = []for p in m3u8_list:ts_find = get_content_requests(p)file_ts = '{0}{1}.ts'.format(config['TS_PATH'],md5(ts_find.content).hexdigest())with open(file_ts,'wb') as f:f.write(ts_find.content)R.append(file_ts)hebing = VideoFileClip(file_ts)L.append(hebing)killProcess()print('下载完成:',file_ts)mp4file = '{0}{1}.mp4'.format(config['FILE_PATH'],name)final_clip = concatenate_videoclips(L)final_clip.to_videofile(mp4file, fps=24, remove_temp=True)killProcess()loop_del_file(R)print('\n下载完成:',name)print('')return Trueexcept:print('~~~~~合成.ts文件失败~~~~~')return None下载视频列表def list_get_kong(list_json):for item in list_json:y = Trueif config['CHECKID']:if check_to_mongo(item['vid']):print('~~~~~检测到重复项~~~~~')y = Falseif y:get_show_html = get_content_requests('https://vmobile.douyu.com/video/getInfo?vid=' + item['vid'])if get_show_html:m3u8_list = get_ts_list(get_show_html.text)if m3u8_list:download = download_ts(m3u8_list, item['title'])if download: save_to_mango(item['vid'])time.sleep(config['TIME_GE'])控制器def main(page):if config['TYPE']==1:print('~~~~~按用户ID采集~~~~~')listurl = 'https://v.douyu.com/video/author/getAuthorVideoListByNew?up_id={0}&cate2_id=0&limit=30&page={1}'.format(config['UID'],page)get_list_html = get_content_requests(listurl)if get_list_html:list_json = get_list(get_list_html.text,1)if list_json:list_get_kong(list_json)else:print('~~~~~按列表ID采集~~~~~')listurl = 'https://v.douyu.com/video/video/listData?page={1}&cate2Id={0}&action=new'.format(config['CID'],page)get_list_html = get_content_requests(listurl)if get_list_html:list_json = get_list(get_list_html.text,2)if list_json:list_get_kong(list_json)初始化if __name__=='__main__':if config['POOL']:groups = [x for x in range(config['PAGE_START'],config['PAGE_END']+1)]pool = Pool()pool.map(main, groups)else:for item in range(config['PAGE_START'],config['PAGE_END']+1):main(item)print('~~~~~已经完成【所有操作】~~~~~') 总结:众所周知,BiliBili是一个学习的网站! 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_35875470/article/details/89857445。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-12-18 11:34:00
119
转载
转载文章
...全中心中的内存完整性功能可能导致非Hyper-V虚拟机无法启动。 近期,微软官方发布了关于如何在启用内存完整性功能的同时,确保其他虚拟机软件兼容性的最新指南。该指南建议用户在运行非Hyper-V虚拟化解决方案时,可尝试通过Windows设置中“设备安全性”选项暂时关闭内存完整性保护。此外,对于专业用户而言,深入理解并合理配置Windows Hypervisor Platform的各项参数也是至关重要的,这包括通过Powershell命令行工具对hypervisorlaunchtype进行灵活调整。 值得注意的是,部分IT专业媒体针对这一现象进行了深度解析和实战演示,指导用户如何在确保系统安全的前提下,充分挖掘硬件资源潜力以支持多类型虚拟机的共存与高效运行。同时,一些第三方虚拟机软件也在不断更新适配,力求在Windows 11等新环境下实现更稳定的性能表现。 综上所述,在处理虚拟机启动失败这类问题时,不仅需要了解基本的排查步骤,还需关注操作系统更新动态及第三方软件的兼容性改进,以便及时采取相应措施,避免潜在的冲突影响到日常的开发测试或生产环境的正常运行。
2023-02-22 23:03:19
177
转载
VUE
...A)提供了强大的路由功能。在Vue项目中,Vue Router通过定义和配置不同的路由规则,帮助实现页面之间的跳转、参数传递等功能,并支持多种模式如History和Hash模式,以适应不同服务器环境下的部署需求。 Single Page Application (SPA) , SPA是一种现代Web应用开发模式,特点是用户与应用交互过程中,大部分内容和视图变化无需重新加载整个页面,而是通过异步请求更新局部视图或数据。Vue.js构建的应用通常采用SPA形式,只需加载一次HTML文件后,后续的导航和交互都在同一页面内完成,极大地提高了用户体验和应用性能。 Nginx , Nginx是一款高性能的HTTP和反向代理服务器,同时也是一个IMAP/POP3/SMTP代理服务器。在本文语境下,Nginx被用来托管和配置Vue项目部署后的静态资源服务。通过正确配置Nginx,可以处理Vue项目的SPA特性,将所有非静态资源请求重定向至index.html,从而避免因路由机制导致的404错误问题。
2023-10-10 14:51:55
76
青山绿水_
Kibana
...世界。它的仪表板定制功能也是超级灵活,让用户们爱不释手,直呼过瘾,就像DIY自己的专属数据空间一样,倍儿爽!不过,在实际操作的时候,我们偶尔也会碰上Kibana仪表板刷新速度抽风的问题,这样一来,实时更新就有点“罢工”了。本文将针对这一问题进行深入探讨,并通过实例代码演示解决方法。 2. 问题描述与现象分析 当你发现Kibana仪表板上的图表或数据显示不再实时更新,或者刷新频率明显低于预期时,这可能是由于多种原因造成的。可能的原因包括但不限于: - Elasticsearch索引滚动更新策略设置不当,导致Kibana无法获取最新的数据。 - Kibana自身配置中的时间筛选条件或仪表板刷新间隔设置不正确。 - 网络延迟或系统资源瓶颈,影响数据传输和处理速度。 3. 示例与排查步骤 示例1:检查Elasticsearch滚动索引配置 假设你的日志数据是通过Logstash写入Elasticsearch并配置了基于时间的滚动索引策略,而Kibana关联的索引模式未能动态更新至最新索引。 yaml Logstash输出到Elasticsearch的配置段落 output { elasticsearch { hosts => ["localhost:9200"] index => "logstash-%{+YYYY.MM.dd}" 其他相关配置... } } 在Kibana中,你需要确保索引模式包含了滚动创建的所有索引,例如logstash-。 示例2:调整Kibana仪表板刷新频率 Kibana仪表板默认的自动刷新间隔为5分钟,若需要实时更新,可以在仪表板编辑界面调整刷新频率。 markdown 在Kibana仪表板编辑模式下 1. 找到右上角的“自动刷新”图标(通常是一个循环箭头) 2. 点击该图标并选择你期望的刷新频率,比如“每秒” 示例3:检查网络与系统资源状况 如果你已经确认上述配置无误,但依然存在实时更新失效的问题,可以尝试监控网络流量以及Elasticsearch和Kibana所在服务器的系统资源(如CPU、内存和磁盘I/O)。过高的负载可能导致数据处理和传输延迟。 4. 解决策略与实践 面对这个问题,我们需要根据实际情况采取相应的措施。如果问题是出在配置上,那就好比是你的Elasticsearch滚动索引策略或者Kibana刷新频率设置有点小打小闹了,这时候咱们就得把这些参数调整一下,调到最合适的节奏。要是遇到性能瓶颈这块硬骨头,那就得从根儿上找解决方案了,比如优化咱系统的资源配置,让它们更合理地分工协作;再不然,就得考虑给咱的硬件设备升个级,换个更强力的装备,或者琢磨琢磨采用那些更高效、更溜的数据处理策略,让数据跑起来跟飞一样。 5. 总结与思考 在实际运维工作中,我们会遇到各种各样的技术难题,如同Kibana仪表板刷新频率异常一样,它们考验着我们的耐心与智慧。只有你真正钻进去,把系统的工作原理摸得门儿清,像侦探一样抽丝剥茧找出问题的根儿,再结合实际业务需求,拿出些接地气、能解决问题的方案来,才能算是把这些强大的工具玩转起来,让它们乖乖为你服务。每一次我们成功解决一个问题,就像是对知识和技术的一次磨砺和淬炼,同时也像是在大数据的世界里打怪升级,这就是推动我们在这一领域不断向前、持续进步的原动力。 以上仅为一种可能的问题解析与解决方案,实践中还可能存在其他复杂因素。因此,我们要始终保持敏锐的洞察力和求知欲,不断探寻未知,以应对更多的挑战。
2023-10-10 23:10:35
277
梦幻星空
HessianRPC
...ception:传入参数不合法”的问题 在分布式系统开发中,HessianRPC作为一种轻量级的远程过程调用协议被广泛应用。然而,在实际动手开发的时候,我们免不了会撞上一个常见的小插曲:“IllegalArgumentException”这个家伙,它跑出来告诉你:喂喂,你传的参数不达标,比如说,方法签名跟我期待的样子对不上号。这篇东西会手把手地,用详尽的步骤解析和实实在在的例子演示,让大家都能更接地气地理解,更能轻松上手解决这个问题。 1. HessianRPC简介 首先,让我们回顾一下HessianRPC的基本概念。Hessian是一种高效、紧凑的二进制RPC协议,由Caucho公司开发,特别适合于互联网传输。这个东西超级实用,它能够让Java和其他一些好兄弟语言(比如.NET、Python这些)毫无障碍地远程互相调用对方的方法,就跟在本地调用一样方便。你只需要稍微捣鼓一下配置,写点简单的代码,就能轻松实现服务端和客户端的顺畅交流啦! 2. 方法签名的重要性 在HessianRPC中,每个远程方法都有其独特的“方法签名”,它包括方法名以及参数类型列表。当客户端调用服务器端的方法时,Hessian会根据这个签名来匹配和校验参数。如果客户端传过来的参数“不按套路出牌”,跟服务器端方法要求的参数类型或数量对不上号,那可就得闹脾气了,会直接抛出一个“IllegalArgumentException”异常。 java // 服务器端接口示例 public interface MyService { String process(String input, int num); } // 客户端错误调用示例 MyService service = (MyService) hessianProxyFactory.create(MyService.class, serverUrl); String result = service.process("Hello", "World"); // 这里第二个参数应该是int类型,而非String类型,会导致IllegalArgumentException 3. “IllegalArgumentException:传入参数不合法”问题解析 上述代码中的客户端尝试以一个字符串参数代替整型参数去调用process方法,这就导致了"IllegalArgumentException"。在进行序列化和反序列化的时候,Hessian这家伙发现传过来的参数类型跟预先给定的方法签名对不上号儿,于是它就毫不客气地抛出了一个异常。 4. 解决方案及预防措施 面对这种问题,我们需要从以下几个方面着手: 4.1 检查并确保参数类型正确 在编写客户端调用代码时,应仔细核对每个参数是否符合服务端方法签名的要求。比如上例中,我们需要将第二个参数修改为整型数值: java String result = service.process("Hello", 123); // 正确的调用方式 4.2 强化代码审查与测试 在项目开发过程中,建议采用自动化测试工具和单元测试,覆盖所有RPC方法调用,确保参数类型的准确无误。同时,代码审查也是防止此类问题的有效手段。 4.3 提供清晰的API文档 对于对外提供的服务接口,应该编写详尽且易于理解的API文档,明确指出每个方法的签名,包括方法名、参数类型和返回值类型,以便开发者在调用时有据可依。 4.4 利用IDE的智能提示 现代集成开发环境(IDE)如IntelliJ IDEA或Eclipse都具有强大的智能提示功能,能自动识别和匹配方法签名,利用好这些特性也能有效避免参数类型不匹配的问题。 总结起来,遭遇HessianRPC的“IllegalArgumentException:传入参数不合法”异常,本质上是对方法签名的理解和使用不到位的结果。在编程实战中,只要我们足够细心、步步为营,像侦探破案那样运用各种工具和策略,完全可以把这些潜在问题扼杀在摇篮里,让系统的运行稳如磐石。记住了啊,解决任何技术难题都得像咱们看侦探小说那样,得瞪大眼睛仔仔细细地观察,用脑子冷静地分析推理,动手实践去验证猜想,最后才能拨开层层迷雾,看到那片晴朗的蓝天。
2024-01-16 09:18:32
542
风轻云淡
ActiveMQ
...制:线程池大小配置全解析 1. 引言 在分布式系统中,消息队列作为异步解耦的重要组件,其性能和稳定性直接影响着整个系统的健壮性。Apache ActiveMQ,作为一个成熟的开源消息中间件,它的高效运行离不开对其内部各项参数的精准配置。这篇东西,咱们要重点聊聊ActiveMQ里一个至关重要的配置细节——线程池的大小。咱会手把手教你如何根据实际业务需求,把这个参数调校得恰到好处,从而让你的系统性能噌噌噌地往上窜。 2. 线程池与ActiveMQ的关系 在ActiveMQ中,线程池承担着处理网络连接、消息发送接收、消息持久化等多种任务的核心角色。如果你的线程池开得太小,就好比是收银台只开了一个窗口,结果大家伙都得排队等着处理请求,这样一来,消息传递的速度自然就慢下来了,延迟也就跟着增加。反过来,要是线程池弄得过大,就像是商场里开了一堆收银台,虽然看起来快,但其实每个窗口都在拼命消耗系统资源,就像每台收银机都在疯狂“吃电”。这样一来,整体性能就会被拖累,反而适得其反。因此,理解并适配合适的线程池大小至关重要。 3. 默认线程池配置及查看 首先,我们先看看ActiveMQ默认的线程池配置。打开ActiveMQ的配置文件(如conf/activemq.xml),可以看到如下片段: xml ... 10 2 ... 这里展示了默认的最大线程数(maxThreads)和最小线程数(minThreads),通常情况下,初始值可能并不完全适应所有应用场景。 4. 调整线程池大小 - 增大线程池大小:当发现消息堆积或处理速度慢时,可以尝试适当增大线程池的大小。例如,我们将最大线程数调整为20: xml 20 - 动态调整策略:实际上,ActiveMQ还支持动态调整线程池大小,可以根据系统负载自动扩缩容。例如,使用pendingTaskSize属性设置触发扩容的待处理任务阈值: xml 20 100 5. 调整线程池大小的思考过程 调整线程池大小并非简单的“越大越好”,而是需要结合实际应用环境和压力测试结果来综合判断。比如,在人多手杂的情况下,你发现电脑虽然还没使出全力(CPU利用率不高),但消息处理的速度还是跟不上趟,这时候,我们或许可以考虑把线程池扩容一下,就像增加更多的小帮手来并行干活,很可能就能解决这个问题了。不过呢,假如咱们的系统都已经快被内存撑爆了,这时候还盲目地去增加线程数量,那就好比在拥堵的路上不断加塞更多的车,反而会造成频繁的“切换车道”,让整个系统的运行效率变得更低下。 6. 结论与实践建议 调整ActiveMQ线程池大小是一项细致且需反复试验的工作。务必遵循“观察—调整—验证”的循环优化过程,并密切关注系统监控数据。另外,别忘了要和其他系统参数一起“团队协作”,像是给内存合理分配额度、调整磁盘读写效率这些小细节,这样才能让整个系统的性能发挥到极致。 最后,每个系统都是独一无二的,所以对于ActiveMQ线程池大小的调整没有绝对的“黄金法则”。作为开发者,咱们得摸透自家业务的脾性,像个理智的大侦探一样剖析问题。这可不是一蹴而就的事儿,得靠咱一步步地实操演练,不断摸索、优化,最后才能找到那个和咱自身业务最对味儿、最合拍的ActiveMQ配置方案。
2023-02-24 14:58:17
502
半夏微凉
Apache Solr
...致数据插入失败:深入解析与应对策略 1. 引言 Apache Solr,作为一款高性能、可扩展的全文搜索引擎,在处理大规模数据索引和搜索需求时表现出色。然而,在那种很多人同时挤在一个地方,都对着Solr进行写操作的繁忙情况下,就有点像大家抢着往一个本子上记东西,一不留神就会出现“手忙脚乱”的并发写入冲突问题。这样一来,就像有几笔记录互相打架,最后可能导致某些数据无法成功插入的情况。本文将深入探讨这一问题,并通过实例代码及解决方案来帮助你理解和解决此类问题。 2. 并发写入冲突原理浅析 在Solr中,每个文档都有一个唯一的标识符——唯一键(uniqueKey),当多个请求尝试同时更新或插入同一唯一键的文档时,就可能出现并发写入冲突。Solr默认采用了像乐天派一样的乐观锁机制,也就是版本号控制这一招儿,来巧妙地应对这个问题。具体来说呢,就像每一份文档都有自己的身份证号码一样,它们各自拥有一个版本号字段,这个字段就叫做 _version_。每次我们对文档进行更新的时候,这个版本号就会往上加一,就像咱们小时候玩游戏升级打怪一样,每次升级都会经验值往上涨。要是有两个请求,它们各自带的版本号对不上茬儿,那么后到的那个请求就会被我们无情地拒之门外。这么做是为了避免数据被不小心覆盖或者丢失掉,就像你不会同时用两支笔在同一份作业上写字,以防搞乱一样。 java // 示例:尝试更新一个文档,包含版本号控制 SolrInputDocument doc = new SolrInputDocument(); doc.addField("id", "1"); // 唯一键 doc.addField("_version_", 2); // 当前版本号 doc.addField("content", "new content"); UpdateRequest req = new UpdateRequest(); req.add(doc); req.setCommitWithin(1000); // 设置自动提交时间 solrClient.request(req); 3. 并发写入冲突引发的问题实例 设想这样一个场景:有两个并发请求A和B,它们试图更新同一个文档。假设请求A先到达,成功更新了文档并增加了版本号。这时,请求B才到达,但由于它携带的是旧的版本号信息,因此更新操作会失败。 java // 请求B的示例代码,假设携带的是旧版本号 SolrInputDocument conflictingDoc = new SolrInputDocument(); conflictingDoc.addField("id", "1"); // 同一唯一键 conflictingDoc.addField("_version_", 1); // 这是过期的版本号 conflictingDoc.addField("content", "conflicting content"); UpdateRequest conflictReq = new UpdateRequest(); conflictReq.add(conflictingDoc); solrClient.request(conflictReq); // 此请求将因为版本号不匹配而失败 4. 解决策略与优化方案 面对这种并发写入冲突导致的数据插入失败问题,我们可以从以下几个方面入手: - 重试策略:当出现版本冲突时,可以设计一种重试机制,让客户端获取最新的版本号后重新发起更新请求。但需要注意避免无限循环和性能开销。 - 分布式事务:对于复杂业务场景,可能需要引入分布式事务管理,如使用Solr的TransactionLog功能实现ACID特性,确保在高并发环境下的数据一致性。 - 应用层控制:在应用层设计合理的并发控制策略,例如使用队列、锁等机制,确保在同一时刻只有一个请求在处理特定文档的更新。 - 合理设置Solr配置:比如调整autoCommit和softCommit的参数,以减少因频繁提交而导致的并发冲突。 5. 总结与思考 在实际开发过程中,我们不仅要了解Apache Solr提供的并发控制机制,更要结合具体业务场景灵活运用,适时采取合适的并发控制策略。当碰上并发写入冲突,导致数据插不进去的尴尬情况时,咱们得主动出击,找寻并实实在在地执行那些能解决问题的好法子,这样才能确保咱们系统的平稳运行,保证数据的准确无误、前后一致。在摸爬滚打的探索旅程中,我们不断吸收新知识,理解奥秘,改进不足,这正是技术所散发出的独特魅力,也是咱们这群开发者能够持续进步、永不止步的原动力。
2023-12-03 12:39:15
536
岁月静好
Kotlin
...lin:一场关于无效参数的冒险 引子:一次突如其来的挑战 在编程的世界里,Kotlin以其简洁明了的语法、强大的类型安全机制以及对Java语言的兼容性,赢得了无数开发者的心。哎呀,兄弟,你这语言用得确实牛批,但就像开车一样,再溜的车也难免会碰上坑坑洼洼。在这堆问题里头,有一种特别让人头疼的家伙,叫 IllegalArgumentException。这家伙就像是突然冒出来的路障,让你措手不及,一不小心就踩中了,结果就是程序卡壳,半天解不开。这不就是我们在编程路上的“小麻烦”嘛!今天,我们就来一起探索一下这个“非法参数异常”背后的故事。 第一章:何为 IllegalArgumentException 在Kotlin中,当我们尝试调用一个方法时,如果传入的参数不符合该方法的要求或者类型不匹配,就会抛出 IllegalArgumentException。这事儿就像你去参加一个超级认真的补习班,老师布置了一道题目让你做,结果你交上去的答案全错了,那肯定得被老师好好点名批评一番了。 第二章:深入剖析 IllegalArgumentException 假设我们有一个简单的函数 calculateAge,它接受一个人的出生年份作为参数,并计算出当前年龄: kotlin fun calculateAge(birthYear: Int): Int { val currentYear = 2023 return currentYear - birthYear } 如果我们不小心传入了一个非整数类型的参数,比如一个字符串,Kotlin会立即察觉到这一点,并优雅地抛出 IllegalArgumentException: kotlin fun test() { val age = calculateAge("2000") println("Your age is $age.") } // 运行结果:编译错误,因为calculateAge接受的是Int类型参数,而"2000"是String类型。 第三章:如何避免 IllegalArgumentException 避免 IllegalArgumentException 的关键在于确保所有传入函数的参数都符合预期的类型和格式。我们可以利用Kotlin的静态类型系统来帮助我们进行这一工作: - 类型检查:确保所有输入的参数都是正确的类型。例如,可以使用 assert 函数在运行时验证类型: kotlin fun safeCalculateAge(birthYear: Any): Int { assert(birthYear is Int) { "Expected an Integer for birthYear" } val currentYear = 2023 return currentYear - birthYear.toInt() } // 使用示例: val age = safeCalculateAge(2000) println("Your age is $age.") - 函数参数验证:在定义函数时就加入类型检查逻辑: kotlin fun calculateAgeWithValidation(birthYear: Int): Int { if (birthYear < 0 || birthYear > 2023) { throw IllegalArgumentException("Birth year must be within the range of 0 to 2023.") } val currentYear = 2023 return currentYear - birthYear } 第四章:实战演练:创建一个更复杂的示例 假设我们要构建一个简单的日历应用,其中包含一个用于计算天数的函数。为了增加复杂性,我们添加了对月份和年份的验证: kotlin data class Date(val day: Int, val month: Int, val year: Int) fun calculateDaysSinceBirthday(dateOfBirth: Date): Int { val currentYear = Calendar.getInstance().get(Calendar.YEAR) val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1 // 注意月份是从0开始的 val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) val birthday = dateOfBirth.day to dateOfBirth.month to dateOfBirth.year val birthDate = Date(birthday) val daysSinceBirthday = (currentYear - birthDate.year) 365 + (currentMonth - birthDate.month) 30 + (currentDay - birthDate.day) return daysSinceBirthday } fun main() { val birthDate = Date(day = 1, month = 1, year = 2000) val days = calculateDaysSinceBirthday(birthDate) println("Days since your birthday: $days") } 在上面的代码中,我们通过 Calendar 类获取当前日期,并与生日日期进行比较,计算出天数差值。嘿,兄弟!咱们就拿一年有365天,一个月有30天来打个比方,这可是咱们简化了一下,方便大家理解。实际上啊,生活里头可没这么简单,得分清闰年和普通年是怎么回事,这样日子才过得有模有样呢! 结语:面对挑战,拥抱学习 每一次遇到 IllegalArgumentException 都是一次学习的机会。它们提醒我们,即使在看似完美的代码中,也可能隐藏着一些小错误。通过仔细检查和验证我们的参数,我们可以编写出更加健壮、可维护的代码。哎呀,你瞧这Kotlin,它可真是个能手呢!它那一大堆好用的工具和特性,就像是魔法一样,帮我们解决了好多麻烦事儿。比如说,静态类型这一招,就像是一道坚固的防线,能提前发现那些可能出错的地方。还有函数注解,就像是给代码贴上了标签,让我们一眼就能看出这是干啥的。而模式匹配嘛,简直就是解谜神器,轻轻松松就能解开那些复杂的逻辑难题。这些玩意儿合在一起,就形成了一个强大的武器库,帮我们防患于未然,解决问题更是不在话下。你说是不是,这Kotlin,简直就是程序员的好伙伴!让我们带着好奇心和探索精神,继续在编程的海洋中航行吧! --- 在这篇文章中,我们不仅探讨了 IllegalArgumentException 的由来和解决方法,还通过一系列的代码示例展示了如何在实践中应用这些知识。嘿,兄弟!读完这篇文章后,希望你对Kotlin里的异常处理方式有了一番全新的领悟。别担心,这不像是AI在跟你说话,就像跟老朋友聊天一样轻松。你得尝试将这些小技巧应用到你的实际项目中,让代码不仅好看,而且超级稳定,就像是给你的程序穿上了一件坚固的盔甲。这样,无论遇到什么问题,它都能稳如泰山。所以,拿起你的键盘,动手实践吧!记住,编程是一场持续的学习之旅,每一次遇到困难都是成长的机会。加油!
2024-09-18 16:04:27
112
追梦人
转载文章
...txt文件内容并传入参数hal=1000 HTTP/1.1表示HTTP版本是1.1 2、服务端传回HTTP响应 HTTP/1.0200OK Server:ReageWebServer Content-Type:text/html <!DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <htmlxmlns="http://www.w3.org/1999/xhtml"> <!--Copyright(c)2000-2008QuadralayCorporation.Allrightsreserved.--> <head> <title>WebWorksHelp5.0</title> </head> <body>wuff</body> </html> 前面四行(包括空行)是消息体,后面是消息。一般要指明消息体的长度,方便客户端的接收处理。 三、示例程序 ====================================================================== / 主要实现功能,处理浏览器的get请求信息,发送网页文件。处理404、403等错误。 1.实现绑定本机机器的1024端口作为ReageWeb服务提供网页服务的端口。(避免与机器上装有web服务器产生端口冲突) 2.实现get获取网页方式。 3.实现index.html作为网站的首页面 作者:Reage blog:http://blog.csdn.net/rentiansheng / include<stdio.h> include<stdlib.h> include<string.h> include<sys/types.h> include<sys/socket.h> include<sys/un.h> include<netinet/in.h> include<arpa/inet.h> include<fcntl.h> include<string.h> include<sys/stat.h> include<signal.h> defineMAX1024 intres_socket; voidapp_exit(); / @description:开始服务端监听 @parameter ip:web服务器的地址 port:web服务器的端口 @result:成功返回创建socket套接字标识,错误返回-1 / intsocket_listen(charip,unsignedshortintport){ intres_socket;//返回值 intres,on; structsockaddr_inaddress; structin_addrin_ip; res=res_socket=socket(AF_INET,SOCK_STREAM,0); setsockopt(res_socket,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); memset(&address,0,sizeof(address)); address.sin_family=AF_INET; address.sin_port=htons(port); address.sin_addr.s_addr=htonl(INADDR_ANY);//inet_addr("127.0.0.1"); res=bind(res_socket,(structsockaddr)&address,sizeof(address)); if(res){printf("portisused,nottorepeatbind\n");exit(101);}; res=listen(res_socket,5); if(res){printf("listenportiserror;\n");exit(102);}; returnres_socket; } / @description:向客户端发送网页头文件的信息 @parameter conn_socket:套接字描述符。 status:http协议的返回状态码。 @s_status:http协议的状态码的含义 @filetype:向客户端发送的文件类型 / voidsend_http_head(intconn_socket,intstatus,chars_status,charfiletype){ charbuf[MAX]; memset(buf,0,MAX); sprintf(buf,"HTTP/1.0%d%s\r\n",status,s_status); sprintf(buf,"%sServer:ReageWebServer\r\n",buf); sprintf(buf,"%sContent-Type:%s\r\n\r\n",buf,filetype); write(conn_socket,buf,strlen(buf)); } / @description:向客户端发送错误页面信息 @parameter conn_socket:套接字描述符。 status:http协议的返回状态码。 @s_status:http协议的状态码的含义 @filetype:向客户端发送的文件类型 @msg:错误页面信息内容 / voidsend_page_error(intconn_socket,intstatus,chars_status,charmsg){ charbuf[MAX]; sprintf(buf,"<html><head></head><body><h1>%s</h1><hr>ReageWebServer0.01</body></head>",msg); send_http_head(conn_socket,status,s_status,"text/html"); write(conn_socket,buf,strlen(buf)); } / @description:向客户端发送文件 @parameter conn_socket:套接字描述符。 @file:要发送文件路径 / intsend_html(intconn_socket,charfile){ intf; charbuf[MAX]; inttmp; structstatfile_s; //如果file为空,表示发送默认主页。主页暂时固定 if(0==strlen(file)){ strcpy(file,"index.html"); } //如果获取文件状态失败,表示文件不存的,发送404页面,暂时404页面内容固定。 if(stat(file,&file_s)){ send_page_error(conn_socket,404,"Notfound","Notfound<br/>Reagedoesnotimplementthismothod\n"); return0; } //如果不是文件或者无读权限,发送无法读取文件 if(!(S_ISREG(file_s.st_mode))||!(S_IRUSR&file_s.st_mode)){ send_page_error(conn_socket,403,"Forbidden","Forbidden<br/>Reagecouldn'treadthefile\n"); return0; } //发送头文件,现在只提供html页面 send_http_head(conn_socket,200,"OK","text/html"); f=open(file,O_RDONLY); if(0>f){ //打开文件失败,发送404页面,其实感觉发送5xx也可以的,服务器内部错误 send_page_error(conn_socket,404,"Notfound","Notfound<br/>Reagecouldn'treadthefile\n"); return0; } buf[MAX-1]=0;//将文件内容缓冲区最后的位设置位结束标志。 //发送文件的内容 while((tmp=read(f,buf,MAX-1))&&EOF!=tmp){ write(conn_socket,buf,strlen(buf)); } } / @description:提取url中可用的信息。访问的网页和数据访问方式 @parameter: conn_socket:与客户端链接的套接字 uri:要处理的url,注意不是浏览器中的url,而是浏览器发送的http请求 @resutl: / intdo_uri(intconn_socket,charuri){ charp; p=strchr(uri,'?'); if(p){p=0;p++;} send_html(conn_socket,uri); } voidulog(charmsg){} voidprint(charmsg){ ulog(msg); printf(msg); } intmain(intargc,charargv[]){ intconn_socket; inttmp; intline; structsockaddr_inclient_addr; charbuf[MAX]; intlen=sizeof(client_addr); charmethod[100],uri[MAX],version[100]; charpwd[1024]; res_socket=socket_listen("127.0.0.1",1024); //当按ctrl+c结束程序时调用,使用app_exit函数处理退出过程 signal(SIGINT,app_exit); while(1){ conn_socket=accept(res_socket,(structsockaddr)&client_addr,&len); printf("reage\n"); line=0; //从客户端获取请求信息 while(0==(tmp=read(conn_socket,buf,MAX-1))||tmp!=EOF){ buf[MAX-1]=0; break;//我只使用了第一行的请求信息,所以丢弃其他的信息 } //send_http_head(conn_socket,200,"text/html"); sscanf(buf,"%s%s%s",method,uri,version); //目前只处理get请求 if(!strcasecmp(method,"get")) //send_html(conn_socket,"h.html"); do_uri(conn_socket,uri+1); close(conn_socket); } } voidapp_exit(){ //回复ctrl+c组合键的默认行为 signal(SIGINT,SIG_DFL); //关闭服务端链接、释放服务端ip和端口 close(res_socket); printf("\n"); exit(0); } ====================================================================== 本篇文章为转载内容。原文链接:https://blog.csdn.net/iteye_9368/article/details/82520401。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-05-30 18:31:58
90
转载
转载文章
...环和,但如果有删除为避免错误必须使用迭代器 foreach遍历不允许改变变量的地址,java的参数是值传递,修改了形参的地址并不影响原来的参数,故即使你修改了值也不会同步到原变量中,故操作的变量都显式或者隐式的定义为final JSON fastjson parseArray(String text, Class<T> clazz) 解析List parseObject(String text, Class<T> clazz) 解析Object JSON对于null、空白字符串、“null”会返回nullif (text == null) {return null;} else {DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance());JSONLexer lexer = parser.lexer;int token = lexer.token();ArrayList list;if (token == 8) {lexer.nextToken(); // nextToken() => ...if ("null".equalsIgnoreCase(ident)) this.token = 8;list = null;} } String toJSONString(Object object) 将对象转为String toJSONBytes(Object object, SerializerFeature... features) 将对象转为byte[] @JSONField() 可以忽略字段serialize ,别名映射name,日期格式化format等 jackson @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 设置Date到前台的格式 @JsonIgnore SpringMVC不会向前台传递该字段 ObjectMapper mapper = new ObjectMapper();String str = mapper.writeValueAsString(admin); // 对象转JSON字符串mapper.readValue(s,Admin.class ); // JSON字符串转对象 EasyExcel 官方API https://www.yuque.com/easyexcel/doc 使用类注解@ExcelIgnoreUnannotated配合@ExcelProperty操作 @ExcelProperty可以指定表头列名,列顺序和表头的合并 @ColumnWidth(10)可以指定列宽,其长度约为(中文length3+英文length1) @DateTimeFormat(value="yyyy-MM-dd HH:mm:ss")可以指定日期格式 自定义策略实现SheetWriteHandler工作表回调接口,在afterSheetCreate()工作表创建之后方法可以 设置列宽 自定义表头 新建单元格 自定义策略实现RowWriteHandler行回调接口,在afterRowDispose()行操作完之后方法可以 设置行高 设置行样式 自定义策略实现CustomerCellHandler单元格回调接口,在afterCellDispose()单元格操作完之后方法可以 根据行号,列宽甚至是单元格的值来设置单元格样式 可以对单元格的值获取和修改 样式通常包括内容格式、批注、背景色、自动换行、平和垂直居中、边框大小和颜色、字体实例(格式,颜色,大小,加粗等)等 自定义策略继承AbstractMergeStrategy单元格合并抽象类,在merge()方法中可以通过CellRangeAddress合并单元格 过于复杂的表格可以使用模板,配合写出write和填充fill一起使用 Mybatis 在mapper方法的@select中也是可以直接书写动态SQL的,但要使用<script></script>包裹,这样就不用在java文件和xml文件切换了,将@select中包裹的代码直接放到浏览器的控制台输出后会自动转义\n,\t,+,"等 动态sql中“<” 和 “>” 号要用转义字符 “<” 和 ”>“ (分号要带) 动态sql中test中表达式通常使用 test=“id != null and id != ‘’”,要注意的是字符串不能直接识别单引号,有两种方法使用id==“1001"或者id==‘1001’.toString(),另外参数如果是boolean,可以直接使用test=”!flag",如果判定集合的话可以使用 test=“list != null and list.size>0” 返回数据类型为Map只能接收一条记录,字段为键名,字段值为值,但通常是用实体类接收,或是使用注解@MapKey来进行每条记录的映射,效果等同于List用Stream流转Map foreach遍历list collection=“list” item=“vo” separator="," open="(" close=")"> {vo.id} foreach遍历map collection=“map” index=“key” item=“value”,{key}获取建,{value}获取值,$亦可 collection=“map.entrySet()” index=“key” item=“value”,同上 collection=“map.keys” item=“key”,{key}为键 不要使用where 1=1,使用动态where拼接,会自动剔除where后多余的and和or 单个参数时无论基本和引用并且未使用在动态SQL可以不加参数注解@Param,但一旦参数大于一个或者参数在动态SQL中使用就必须加@Param 并不是直接把参数加引号,而是变成?的形式交给prepareStatement处理,$直接使用值,当ORDER BY诸如此类不需要加引号的参数时,使用$代替,但为避免sql注入,该参数不能交由用户控制 Plus 官方API https://baomidou.com/guide/ @TableName 表名 @TableField(strategy = FieldStrategy.IGNORED) 更新不会忽略NULL值 @TableField(exist = false)表明该字段非数据字段,否则新增更新会报错 MybatisPlus对于单表的操作还是非常优秀的,在对单表进行新增或者更新的时候经常使用,但对于单表的查询业务上很少出现仅仅查询一张表的情况,但也会有,如果条件不大于3个还是可以使用的,多了倒没有直接写SQL来的方便了 MybatisPlus的批量插入也是通过for循环插入的,还是建议使用Mybatis的动态foreach进行批量插入 MybatisPlus的分页器会对方法中的参数判断,如果存在分页对象就先查询总数看是否大于0,然后拼接当前的数据库limit语句,所以如果我们分页对象为null,就可以实现不分页查询 Object paramObj = boundSql.getParameterObject();IPage page = null;if (paramObj instanceof IPage) { ……public static String getOriginalCountSql(String originalSql) {return String.format("SELECT COUNT(1) FROM ( %s ) TOTAL", originalSql);} ……originalSql = DialectFactory.buildPaginationSql(page, buildSql, dbType, this.dialectClazz); ……public String buildPaginationSql(String originalSql, long offset, long limit) {StringBuilder sql = new StringBuilder(originalSql);sql.append(" LIMIT ").append(offset).append(",").append(limit);return sql.toString();} IDEA 插件 Lombok : 快速生成getter、setter等 Alibaba Java Coding Guidelines :阿里规约扫描 Rainbow Brackets :彩色括号 HighlightBracketPair :高亮提示 MyBatisX :mabatisPlus提供的xml和mapper转换的插件,小鸟图标 CamelCase :大小写、驼峰、下划线、中划线转换插件 使用shift+Alt+u进行转换(很方便) 可以在Editor中设置CamelCase的转换,一般只保留下划线和驼峰两种 String Manipulation :字符串工具(未使用) RestfulToolkit http :Restful请求工具 打开idea,在右侧边栏会有一个标签(RestServices),打开可以看到里面是url路径 ctrl+\或者ctrl+alt+n会检索路径 Ctrl + Enter格式化json 没有记忆功能,也不能加token,只是查找请求路径使用 easycode :代码生成工具(个人觉得很好用,常用于生成实体类) 支持自定义模板 支持添加自定义列,不影响数据库 支持多表同时生成 支持自定义类型映射 支持配置导入导出 支持动态调试 支持自定义属性 Power Mode 11 :打字特效(纯属装逼) Nyan Progress Bar :漂亮的进度条(纯属装逼) Other Vo:数据持久化模型 Query:数据查询模型 Dto:数据传输模型 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_40910781/article/details/111416185。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-05-26 23:30:52
268
转载
转载文章
...交付时间) 极大程度避免低级错误 (减少大量内容合并到主干分支的请看看,避免代码合并冲突和无法预料的行为) 低级错误:编译错误,安装问题,接口问题,性能问题等 难点 迁移遗留代码到现有CI系统,需要的投入通常爱预料之外 在文化和组织上如果没有采用敏捷原则或DecOps的工作方式,那么很可能没有持续不断的提交,那么CI的存在意义不大 随着业务增长、工具的更替、技术的演进。CI系统也必然随之改动,往往会导致阶段性的不稳定和人力物力的耗费 如果CI的基本设定不到位,开发流程将会增加特别的开销 注意点 CI流程的触发方式 跟踪触发式:在每次提交到源码版本管理系统时触发 计划任务:预配置好的计划 手动:无论是通过CI服务器的管理界面还是脚本,用户可以手工执行CI工作流 代码审核 可在持续集成服务器里使用代码分析工具(例如Sonar)来执行自动代码审查 自动代码审查通过后,可发起一个人工代码审查,揪出那些自动审查无法找出的问题,即验证业务需求,架构问题,代码是否可读,以及是否易于扩展。 可灵活配置代码审核策略,例如:如果某些人没有审查代码便阻止对主干分支的任何提交。 最常用的工具是Gerrit 持续交付 简述 持续交付简称CD或CDE,是一种能够使得软件在较短的循环中可靠的发布的软件工程方法 与持续集成相比,持续交付的重点在于 交付,其核心对象不在于代码,而在于可交付的产物。 由于持续集成仅仅针对于新旧代码的集成过程执行来了一定的测试,其变动到持续交付后还需要一些额外的流程 持续交付可以看作为是持续集成的下一步,它强调的是,不敢怎么更新,软件是随时随快可以交付的 有图可看出,持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实的运行环境的[类生产环境]中 目的 持续交付永爱确保让代码能够快速、安全的部署到产品环境中,它通过将每一次改动都会提交到一个模拟产品环境中,使用严格的自动化测试,确保业务应用和服务能符合预期 好处 持续交付和持续集成的好处非常相似: 快速发布。能够应对业务需求,并更快地实现软件价值 编码→测试→上线→交付的频繁迭代周期缩短,同时获得迅速反馈 高质量的软件发布标准。整个交付过程标准化、可重复、可靠 整个交付过程进度可视化,方便团队人员了解项目完成度 更先进的团队协作方式。从需求分析、产品的用户体验到交互、设计、开发、测试、运维等角色密切协作,相比于传统的瀑布式软件团队,更少浪费 持续部署 简述 持续部署 意味着:通过自动化部署的手段将软件功能频繁的进行交付 持续部署是持续交付的下一步,指的是代码通过审批以后,自动化部署到生产环境。 持续部署是持续交付的最高阶段,这意味着,所有通过了一系列的自动化测试的改动都将自动部署到生产环境。它也可以被称为“Continuous Release” 持续化部署的目标是:代码在任何时候都是可部署的,可以进入生产阶段。 持续部署的前提是能自动化完成测试、构建、部署等步骤 注:持续交付不等于持续集成 与持续交付以及持续集成相比,持续部署强调了通过 automated deployment 的手段,对新的软件功能进行集成 目标 持续部署的目标是:代码在任何时刻都是可部署的,可以进入生产阶段 有很多的业务场景里,一种业务需要等待另外的功能特征出现才能上线,这是的持续部署成为不可能。虽然使用功能切换能解决很多这样的情况,但并不是没每次都会这样。所以,持续部署是否适合你的公司是基于你们的业务需求——而不是技术限制 优点 持续部署主要的好处是:可以相对独立地部署新的功能,并能快速地收集真实用户的反馈 敏捷开发 简述 敏捷开发就是一种以人为核心、迭代循环渐进的开发方式。 在敏捷开发中,软件仙姑的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。 简单的说就是把一个大的项目分为多个相互联系,但也可以独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态 注意事项 敏捷开的就是一种面临迅速变化的需求快速开发的能力,要注意一下几点: 敏捷开发不仅仅是一个项目快速完成,而是对整个产品领域需求的高效管理 敏捷开发不仅仅是简单的快,而是短周期的不断改进、提高和调整 敏捷开发不仅仅是一个版本只做几个功能,而是突出重点、果断放弃当前的非重要点 敏捷开发不仅仅是随时增加需求,而是每个迭代周期对需求的重新审核和排序 如何进行敏捷开发 1、组织建设 也就是团队建设,建立以产品经理为主导,包含产品、设计、前后台开发和测试的team,快速进行产品迭代开发;扁平化的团队管理,大家都有共同目标,更有成就感; 2、敏捷制度 要找准适合自身的敏捷开发方式,主要是制定一个完善的效率高的设计、开发、测试、上线流程,制定固定的迭代周期,让用户更有期待; 3、需求收集 这个任何方式下都需要有,需求一定要有交互稿,评审通过后,一定要确定功能需求列表、责任人、工作量、责任人等; 4、工具建设 是指能够快速完成某项事情的辅助工具,比如开发环境的一键安装,各种底层的日志、监控等平台,发布、打包工具等; 5、系统架构 略为超前架构设计:支持良好的扩容性和可维护性;组件化基础功能模块:代码耦合度低,模块间的依赖性小;插件化业务模块:降低营销活动与业务耦合度,自升级、自维护;客户端预埋逻辑;技术预研等等; 6、数据运营与灰度发布 点击率分析、用户路径分析、渠道选择、渠道升级控制等等 原则、特点和优势 敏捷开发技术的12个原则: 1.我们最优先要做的是通过尽早的、持续的交付有价值的软件来使客户满意。 2.即使到了开发的后期,也欢迎改变需求。 3.经常性地交付可以工作的软件,交付的间隔可以从几周到几个月,交付的时间间隔越短越好。 4.在整个项目开发期间,业务人员和开发人员必须天天都在一起工作。 5.围绕被激励起来的个人来构建项目。 6.在团队内部,最具有效果并且富有效率的传递信息的方法,就是面对面的交谈。 7.工作的软件是首要的进度度量标准。 8.敏捷过程提倡可持续的开发速度。 9.不断地关注优秀的技能和好的设计会增强敏捷能力。 10.简单使未完成的工作最大化。 11.最好的构架、需求和设计出自于自组织的团队。 12.每隔一定时间,团队会在如何才能更有效地工作方面进行反省,然后相应地对自己的行为进行调整。 特点: 个体和交互胜过过程和工具 可以工作的软件胜过面面俱到的文档 客户合作胜过合同谈判 响应变化胜过遵循计划 优势总结: 敏捷开发确实是项目进入实质开发迭代阶段,用户很快可以看到一个基线架构班的产品。敏捷注重市场快速反应能力,也即具体应对能力,客户前期满意度高 适用范围: 项目团队的人不能太多 项目经常发生变更 高风险的项目实施 开发人员可以参与决策 劣势总结: 敏捷开发注重人员的沟通 忽略文档的重要性 若项目人员流动太大,维护的时候很难 项目存在新手的比较多的时候,老员工会比较累 需要项目中存在经验较强的人,要不然大项目中容易遇到瓶颈问题 Open-falcon 简述 open-falcon是小米的监控系统,是一款企业级、高可用、可扩展的开源监控解决方案 公司用open-falcon来监控调度系统各种信息,便于监控各个节点的调度信息。在服务器安装了falcon-agent自动采集各项指标,主动上报 特点 强大灵活的数据采集 (自动发现,支持falcon-agent、snmp、支持用户主动push、用户自定义插件支持、opentsdb data model like(timestamp、endpoint、metric、key-value tags) ) 水平扩展能力 (支持每个周期上亿次的数据采集、告警判定、历史数据存储和查询 ) 高效率的告警策略管理 (高效的portal、支持策略模板、模板继承和覆盖、多种告警方式、支持callback调用 ) 人性化的告警设置 (最大告警次数、告警级别、告警恢复通知、告警暂停、不同时段不同阈值、支持维护周期 ) 高效率的graph组件 (单机支撑200万metric的上报、归档、存储(周期为1分钟) ) 高效的历史数据query组件 (采用rrdtool的数据归档策略,秒级返回上百个metric一年的历史数据 ) dashboard(面向用户的查询界面,可以看到push到graph中的所有数据,并查看数据发展趋势 ) (对维度的数据展示,用户自定义Screen) 高可用 (整个系统无核心单点,易运维,易部署,可水平扩展) 开发语言 (整个系统的后端,全部golang编写,portal和dashboard使用python编写。 ) 监控范围 Open-Falcon支持系统基础监控,第三方服务监控,JVM监控,业务应用监控 基础监控指的是Linux系统的指标监控,包括CPU、load、内存、磁盘、IO、网络等, 这些指标由Openfalcon的agent节点直接支持,无需插件 第三方服务监控指的是一些常见的服务监控,包括Mysql、Redis、Nginx等 OpenFalcon官网提供了很多第三方服务的监控插件,也可以自己实现插件,定义采集指标。而采集到的指标,也是通过插件先发送给agent,再由agent发送到OpenFalcon。 JVM监控主要通过插件完成,插件通过JVM开放的JMX通信端口,获取到JVM参数指标,并推送到agent节点,再由agent发送到OpenFalcon。 业务应用监控就是监控企业自主开发的应用服务 主要通过插件完成,插件通过JVM开放的JMX通信端口,获取到JVM参数指标,并推送到agent节点,再由agent发送到OpenFalcon。 数据流向 常见的OpenFalcon包含transfer、hbs、agent、judge、graph、API几个进程 以下是各个节点的数据流向图,主数据流向是agent -> transfer -> judge/graph: SNMP 简述 SNMP:简单网络管理协议,是TCP/IP协议簇 的一个应用层协议,由于SNMP的简单性,在Internet时代得到了蓬勃的发展 ,1992年发布了SNMPv2版本,以增强SNMPv1的安全性和功能。现在,已经有了SNMPv3版本(它对网络管理最大的贡献在于其安全性。增加了对认证和密文传输的支持 )。 一套完整的SNMP系统主要包括:管理信息库(MIB)、管理信息结构(SMI)和 SNMP报文协议 为什么要用SNMP 作为运维人员,我们很大一部分的工作就是为了保证我们的网络能够正常、稳定的运行。因此监控,控制,管理各种网络设备成了我们日常的工作 优点和好处 优点: 简单易懂,部署的开销成本也小 ,正因为它足够简单,所以被广泛的接受,事实上它已经成为了主要的网络管理标准。在一个网络设备上实现SNMP的管理比绝大部分其他管理方式都简单直接。 好处: 标准化的协议:SNMP是TCP/IP网络的标准网络管理协议。 广泛认可:所有主流供应商都支持SNMP。 可移植性:SNMP独立于操作系统和编程语言。 轻量级:SNMP增强对设备的管理能力的同时不会对设备的操作方式或性能产生冲击。 可扩展性:在所有SNMP管理的设备上都会支持相同的一套核心操作集。 广泛部署:SNMP是最流行的管理协议,最为受设备供应商关注,被广泛部署在各种各样的设备上。 MIB、SMI和SNMP报文 MIB 管理信息库MIB:任何一个被管理的资源都表示成一个对象,称为被管理的对象。 MIB是被管理对象的集合。 它定义了被管理对象的一系列属性:对象的名称、对象的访问权限和对象的数据类型等。 每个SNMP设备(Agent)都有自己的MIB。 MIB也可以看作是NMS(网管系统)和Agent之间的沟通桥梁。 MIB文件中的变量使用的名字取自ISO和ITU管理的对象表示符命名空间,他是一个分级数的结构 SMI SMI定义了SNNMP框架多用信息的组织、组成和标识,它还未描述MIB对象和表述协议怎么交换信息奠定了基础 SMI定义的数据类型: 简单类型(simple): Integer:整型是-2,147,483,648~2,147,483,647的有符号整数 octet string: 字符串是0~65535个字节的有序序列 OBJECT IDENTIFIER: 来自按照ASN.1规则分配的对象标识符集 简单结构类型(simple-constructed ): SEQUENCE 用于列表。这一数据类型与大多数程序设计语言中的“structure”类似。一个SEQUENCE包括0个或更多元素,每一个元素又是另一个ASN.1数据类型 SEQUENCE OF type 用于表格。这一数据类型与大多数程序设计语言中的“array”类似。一个表格包括0个或更多元素,每一个元素又是另一个ASN.1数据类型。 应用类型(application-wide): IpAddress: 以网络序表示的IP地址。因为它是一个32位的值,所以定义为4个字节; counter:计数器是一个非负的整数,它递增至最大值,而后回零。在SNMPv1中定义的计数器是32位的,即最大值为4,294,967,295; Gauge :也是一个非负整数,它可以递增或递减,但达到最大值时保持在最大值,最大值为232-1; time ticks:是一个时间单位,表示以0.01秒为单位计算的时间; SNMP报文 SNMP规定了5种协议数据单元PDU(也就是SNMP报文),用来在管理进程和代理之间的交换。 get-request操作:从代理进程处提取一个或多个参数值。 get-next-request操作:从代理进程处提取紧跟当前参数值的下一个参数值。 set-request操作:设置代理进程的一个或多个参数值。 get-response操作:返回的一个或多个参数值。这个操作是由代理进程发出的,它是前面三种操作的响应操作。 trap操作:代理进程主动发出的报文,通知管理进程有某些事情发生。 操作命令 SNMP协议之所以易于使用,这是因为它对外提供了三种用于控制MIB对象的基本操作命令。它们是:Get、Set 和 Trap。 Get:管理站读取代理者处对象的值 Set:管理站设置代理者处对象的值 Trap: 代理者主动向管理站通报重要事件 SLA 简述 SLA(服务等级协议):是关于网络服务供应商和客户之间的一份合同,其中定义了服务类型、服务质量和客户付款等术语 一个完整的SLA同时也是一个合法的文档,包括所涉及的当事人、协定条款(包含应用程序和支持的服务)、违约的处罚、费用和仲裁机构、政策、修改条款、报告形式和双方的义务等。同样服务提供商可以对用户在工作负荷和资源使用方面进行规定。 KPI 简述 KPI(关键绩效指标):是通过对组织内部流程的输入端、输出端的关键参数进行设置、取样、计算、分析,衡量流程绩效的一种目标式量化管理指标,是把企业的战略目标分解为可操作的工作目标的工具,是企业绩效管理的基础。 KPI可以是部门主管明确部门的主要责任,并以此为基础,明确部门人员的业绩衡量指标,建立明确的切实可行的KPI体系,是做好绩效管理的关键。 KPI(关键绩效指标)是用于衡量工作人员工作绩效表现的量化指标,是绩效计划的重要组成部分 转载于:https://www.cnblogs.com/woshinideyugegea/p/11242034.html 本篇文章为转载内容。原文链接:https://blog.csdn.net/anqiongsha8211/article/details/101592137。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-03-19 16:00:05
45
转载
转载文章
...00?0:1, //解析接口状态"msg": res.message, //解析提示文本"count": res.total, //解析数据长度"data": res.data //解析数据列表};},cols: [[ //表头{field: 'id', title: '秒杀商品编号', width:80, sort: true},{field: 'goodsId', title: '商品名字id'},{field: 'seckillPrice', title: '秒杀价格'},{field: 'stockCount', title: '秒杀库存'},{field: 'startDate', title: '活动开始时间'},{field: 'endDate', title: '活动结束时间'},{field: 'goodsName', title: '商品名称'}]]}); 呈现界面: 二、秒杀商品添加 1、后端:接收前端添加秒杀商品的数据 ①、实体类vo:SeckillGoodsVo private List<Map<String,Object>> goods; 修改实体类时间的类型:SeckillGoods @ApiModelProperty("秒杀开始时间")@TableField("start_date")@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Timestamp startDate;@ApiModelProperty("秒杀结束时间")@TableField("end_date")@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Timestamp endDate; ②、mapper层:SeckillGoodsMapper int addGoods(SeckillGoodsVo seckillGoodsVo); ③、mapper.xml层:SeckillGoodsMapper 批量插入秒杀商品的sql语句: <insert id="addGoods">insert into t_seckill_goods(goods_id, seckill_price, stock_count, start_date, end_date)values<foreach collection="goods" item="g" separator=",">({g.gid},{g.goodsPrice},{g.goodsStock},{startDate},{endDate})</foreach></insert> ④、service层 ISeckillGoodsService: ResponseResult<List<SeckillGoodsVo>> addGoods(SeckillGoodsVo seckillGoodsVo); SeckillGoodsServiceImpl: @Overridepublic ResponseResult<List<SeckillGoodsVo>> addGoods(SeckillGoodsVo seckillGoodsVo) {int goods=seckillGoodsMapper.addGoods(seckillGoodsVo);return ResponseResult.success(goods);} ⑤、controller层 @RequestMapping("/add")public ResponseResult<List<SeckillGoodsVo>> add(@RequestBody SeckillGoodsVo seckillGoodsVo){return seckillGoodsService.addGoods(seckillGoodsVo);} 2、前端 ①、定义数据与刷新、添加 goodsList.js: var layer,row,seckill_table// 添加秒杀商品$("seckill_add").click(()=>{layer.open({type:2,content: '/goods/SeckillGoodsOperate',area: ['800px','600px']})})// 秒杀商品刷新var seckill_reload = ()=> {seckill_table.reload({page:{curr:1 //current} });} var layer,row,seckill_tablelayui.define(()=>{let table=layui.tablelayer=layui.layerlet $=layui.jquerylet normal_table=table.render({elem: 'normal_goods',height: 500,url: '/goods/queryAll' //数据接口,page: true //开启分页,parseData(res){ //res 即为原始返回的数据return {"code": res.code===200?0:1, //解析接口状态"msg": res.message, //解析提示文本"count": res.total, //解析数据长度"data": res.data //解析数据列表};},//用于对分页请求的参数:page、limit重新设定名称request: {pageName: 'page' //页码的参数名称,默认:page,limitName: 'rows' //每页数据量的参数名,默认:limit},cols: [[ //表头{field: 'gid', title: '商品编号', width:80, sort: true, fixed: 'left'},{field: 'goodsName', title: '商品名字'},{field: 'goodsTitle', title: '商品标题'},{field: 'goodsImg',title: '商品图片',width:200,templet: (goods) => <b onmouseover='showImg("${goods.goodsImg}",this)'> + goods.goodsImg + </b> },{field: 'goodsDetail', title: '商品详情'},{field: 'goodsPrice', title: '商品价格', sort: true},{field: 'goodsStock', title: '商品库存', sort: true},{field: 'operate', title: '商品操作',toolbar: 'button_1'}]]});// 刷新表格let reloadTable=()=>{let goodsName=$("normal_value").val()// 【JS】自动化渲染的重载,重载表格normal_table.reload({where: {//设定异步数据接口的额外参数,height: 300goodsName},page:{curr:1 //current} });}// 搜索$("normal_search").click(reloadTable)// 增加$("normal_add").click(()=>{row = nullopenDialog()})//工具条事件table.on('tool(normal_goods)', function(obj) { //注:tool 是工具条事件名,test 是 table 原始容器的属性 lay-filter="对应的值"let data = obj.data; //获得当前行数据let layEvent = obj.event; //获得 lay-event 对应的值(也可以是表头的 event 参数对应的值)let tr = obj.tr; //获得当前行 tr 的 DOM 对象(如果有的话)if (layEvent === 'normal_del') { //删除row = data//获得当前行的数据let url="/goods/del/"+data.gidlayer.confirm('确定删除吗?',{title:'删除'}, function(index){//向服务端发送删除指令og$.getJSON(url,{gid:data.gid}, function(ret){layer.close(index);//关闭弹窗reloadTable()});layer.close(index);//关闭弹窗});}if (layEvent === 'normal_edit') { //编辑row = dataopenDialog()} })// 页面弹出let openDialog=()=>{// 如果是iframe层layer.open({type: 2,content: '/goods/goodsOperate', //这里content是一个URL,如果你不想让iframe出现滚动条,你还可以content: ['http://sentsin.com', 'no']area:['800px','600px'],btn: ['确定','取消'],yes(index,layero){let url="/goods/insert"// 拿到表格数据let data=$(layero).find("iframe")[0].contentWindow.getFormData()if(row) {url="/goods/edit"}$.ajax({url,data,datatype: "json",success(res){layer.closeAll()reloadTable()layer.msg(res.message)} })} });}// -------------------------秒杀商品-------------------------------------------seckill_table=table.render({elem: 'seckill_goods',height: 500,url: '/seckillGoods/queryAll' //数据接口,parseData(res){ //res 即为原始返回的数据return {"code": res.code===200?0:1, //解析接口状态"msg": res.message, //解析提示文本"count": res.total, //解析数据长度"data": res.data //解析数据列表};},cols: [[ //表头{field: 'id', title: '秒杀商品编号', width:80, sort: true},{field: 'goodsId', title: '商品名字id'},{field: 'seckillPrice', title: '秒杀价格'},{field: 'stockCount', title: '秒杀库存'},{field: 'startDate', title: '活动开始时间'},{field: 'endDate', title: '活动结束时间'},{field: 'goodsName', title: '商品名称'}]]});// 添加秒杀商品$("seckill_add").click(()=>{layer.open({type:2,content: '/goods/SeckillGoodsOperate',area: ['800px','600px']})})})// 图片显示let showImg = (src,obj)=> {layer.tips(<img src="${src}" width="100px">, obj);}// 秒杀商品刷新var seckill_reload = ()=> {seckill_table.reload({page:{curr:1 //current} });} ②、增加秒杀商品弹出页面样式 <!DOCTYPE html><html lang="en"><head><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><link rel="stylesheet" href="/static/asset/js/layui/css/layui.css" media="all"></head><body><div style="padding:15px 0px;"><div class="layui-condition"><form id="fm" name="fm" action="/" method="post" class="layui-form"><div class="layui-form-item"><div class="layui-inline"><label class="layui-form-label" style="width: 100px;text-align: left;">秒杀活动时间:</label><div class="layui-input-inline" style="width:280px;"><input type="text" class="layui-input" id="dt"></div><div class="layui-input-inline"><button class="layui-btn" id="btn_save" type="button"><i class="fa fa-search fa-right"></i>保 存</button></div></div></div></form></div><div class="layui-fluid" style="margin-top:-18px;"><table id="tb_goods" class="layui-table" lay-filter="tb_goods" style="margin-top:-5px;"></table></div></div><script src="/static/asset/js/layui/layui.js"></script><script src="/static/asset/js/project/seckillGoodsOperate.js"></script></body></html> ③、实现增加秒杀商品 seckillGoodsOperate.js: layui.define(()=>{let table=layui.tablelet laydate = layui.laydatelet $=layui.jquerylet layer=layui.layer// 读取普通商品table.render({elem: 'tb_goods',height: 500,url: '/goods/queryAll' //数据接口,page: true //开启分页,parseData(res){ //res 即为原始返回的数据return {"code": res.code===200?0:1, //解析接口状态"msg": res.message, //解析提示文本"count": res.total, //解析数据长度"data": res.data //解析数据列表};},//用于对分页请求的参数:page、limit重新设定名称request: {pageName: 'page' //页码的参数名称,默认:page,limitName: 'rows' //每页数据量的参数名,默认:limit},cols: [[ //表头// 全选按钮{field: '', type:"checkbox"},{field: 'gid', title: '商品编号', width:80},{field: 'goodsName', title: '商品名字'},{field: 'goodsTitle', title: '商品标题'},{field: 'goodsDetail', title: '商品详情'},{field: 'goodsPrice', title: '商品价格', sort: true},{field: 'goodsStock', title: '商品库存', sort: true}]]});// 构建时间选择器//执行一个laydate实例laydate.render({elem: 'dt', //指定元素type: "datetime",range: "~"});$("btn_save").click(()=>{// 获取时间let val=$("dt").val()if(!val){layer.msg("请选择时间")return}// 解析时间2022-2-2 ~2022-5-2let startDate=new Date(val.split("~")[0]).getTime()let endDate=new Date(val.split("~")[1]).getTime()// 获得选中的普通商品,获取选中行的数据let rows= table.checkStatus('tb_goods').data; //idTest 即为基础参数 id 对应的值if(!rows||rows.length===0){layer.msg("请选择数据")return}layer.prompt(function(value, index, elem){// 修改每个商品的数量rows.forEach(e=>{e.goodsStock=value})let data={startDate,endDate,goods:rows}// 访问后台的秒杀商品的接口$.ajax({url: "/seckillGoods/add",contentType:'application/json',data: JSON.stringify(data),datatype:"json",//返回类型type:"post",success(res){parent.seckill_reload()layer.closeAll()parent.layer.closeAll()layer.msg(res.message)} })});})}) ④、展示结果 增加成功: 三、秒杀商品的操作 1、后端操作秒杀单个商品详情 ①、mapper层 SeckillGoodsMapper: Map<String,Object> querySeckillGoodsById(Long id); mapper.xml文件:SeckillGoodsMapper.xml <select id="querySeckillGoodsById" resultType="map">select sg.id,sg.goods_id,sg.seckill_price,sg.stock_count,sg.start_date,sg.end_date,g.goods_img,g.goods_title,g.goods_detail,g.goods_name,(casewhen current_timestamp < sg.start_date then 0when (current_timestamp between sg.start_date and sg.end_date) then 1when current_timestamp > sg.end_date then 2end) goods_statusfrom t_goods g,t_seckill_goods sgwhere g.gid = sg.goods_idand sg.id = {0}</select> ②、service层 ISeckillGoodsService: Map<String,Object> querySeckillGoodsById(Long id); SeckillGoodsServiceImpl: @Overridepublic Map<String, Object> querySeckillGoodsById(Long id) {return seckillGoodsMapper.querySeckillGoodsById(id);} ③、controller层:SeckillGoodsController package com.example.seckill.controller;import com.example.seckill.service.ISeckillGoodsService;import com.example.seckill.util.response.ResponseResult;import com.example.seckill.vo.SeckillGoodsVo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.;import org.springframework.web.servlet.ModelAndView;import java.util.List;/ <p> 秒杀商品信息表 前端控制器 </p> @author lv @since 2022-03-19/@Controller@RequestMapping("/seckillGoods")public class SeckillGoodsController {@Autowiredprivate ISeckillGoodsService seckillGoodsService;// 返回json@ResponseBody@RequestMapping("/queryAll")public ResponseResult<List<SeckillGoodsVo>> queryAll(){return seckillGoodsService.queryAll();}@ResponseBody@RequestMapping("/add")public ResponseResult<List<SeckillGoodsVo>> add(@RequestBody SeckillGoodsVo seckillGoodsVo){return seckillGoodsService.addGoods(seckillGoodsVo);}// 正常跳转界面@RequestMapping("/query/{id}")public ModelAndView querySeckillGoodsById(@PathVariable("id") Long id) {ModelAndView mv = new ModelAndView("/goods/goodsSeckill");mv.addObject("goods", seckillGoodsService.querySeckillGoodsById(id));return mv;} } 2、前端展示 ①、在goodsList.js增加列的操作 {field: '', title: '操作', width: 140,templet: function (d) {return <div><a class="layui-btn layui-btn-xs layui-btn-danger">删除</a><a href="/seckillGoods/query/${d.id}" class="layui-btn layui-btn-xs layui-btn-normal">秒杀</a></div>;} } ②、添加秒杀详情界面 :goodsSkill.ftl <!DOCTYPE html><html lang="en"><head><include "../common/head.ftl"/></head><body><table style="position: absolute;top:-10px;" class="layui-table" border="1" cellpadding="0" cellspacing="0"><tr><td style="width:120px;">商品图片</td><td><img src="${goods['goods_img']}" alt=""></td></tr><tr><td>商品名称</td><td>${goods['goods_name']}</td></tr><tr><td>商品标题</td><td>${goods['goods_title']}</td></tr><tr><td>商品价格</td><td>${goods['seckill_price']}</td></tr><tr><td>开始时间</td><td><div style="position: relative;${(goods['goods_status']==1)?string('top:10px;','')}">${goods['start_date']?string("yyyy-MM-dd HH:mm:ss")}-${goods['end_date']?string("yyyy-MM-dd HH:mm:ss")}<if goods['goods_status']==0>活动未开始<elseif goods['goods_status']==1>活动热卖中<div style="position:relative;top:-10px;float:right;"><input type="hidden" id="goodsId" value="${goods['goods_id']}" name="goodsId"/><button class="layui-btn" id="buy">立即抢购</button></div><else>活动已结束</if></div></td></tr></table><script src="/static/asset/js/project/goodsSeckill.js"></script></body></html> ③、实现:goodsSkill.js let layer, form, $;layui.define(() => {layer = layui.layerform = layui.form$ = layui.jquery$('buy').click(() => {$.ajax({url: '/seckillOrder/addOrder',data: {goodsId: $('goodsId').val()},dataType: 'json',type: 'post',async: false,success: function (rs) {if (rs.code === 200)layer.msg(rs.message)elselayer.msg(rs.message)} })});}) ④、展示效果 点击秒杀: 3、后端操作秒杀抢购功能 ①、导入雪花id工具包:SnowFlake package com.example.seckill.util;@SuppressWarnings("all")public class SnowFlake {/ 起始的时间戳/private final static long START_STMP = 1480166465631L;/ 每一部分占用的位数/private final static long SEQUENCE_BIT = 12; //序列号占用的位数private final static long MACHINE_BIT = 5; //机器标识占用的位数private final static long DATACENTER_BIT = 5;//数据中心占用的位数/ 每一部分的最大值/private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);/ 每一部分向左的位移/private final static long MACHINE_LEFT = SEQUENCE_BIT;private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;private long datacenterId; //数据中心private long machineId; //机器标识private long sequence = 0L; //序列号private long lastStmp = -1L;//上一次时间戳public SnowFlake(long datacenterId, long machineId) {if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");}if (machineId > MAX_MACHINE_NUM || machineId < 0) {throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");}this.datacenterId = datacenterId;this.machineId = machineId;}public static void main(String[] args) {SnowFlake snowFlake = new SnowFlake(2, 3);long start = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {System.out.println(snowFlake.nextId());}System.out.println(System.currentTimeMillis() - start);}/ 产生下一个ID @return/public synchronized long nextId() {long currStmp = getNewstmp();if (currStmp < lastStmp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id");}if (currStmp == lastStmp) {//相同毫秒内,序列号自增sequence = (sequence + 1) & MAX_SEQUENCE;//同一毫秒的序列数已经达到最大if (sequence == 0L) {currStmp = getNextMill();} } else {//不同毫秒内,序列号置为0sequence = 0L;}lastStmp = currStmp;return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分| datacenterId << DATACENTER_LEFT //数据中心部分| machineId << MACHINE_LEFT //机器标识部分| sequence; //序列号部分}private long getNextMill() {long mill = getNewstmp();while (mill <= lastStmp) {mill = getNewstmp();}return mill;}private long getNewstmp() {return System.currentTimeMillis();} } ②、service层 ISeckillOrderService : package com.example.seckill.service;import com.example.seckill.pojo.SeckillOrder;import com.baomidou.mybatisplus.extension.service.IService;import com.example.seckill.pojo.User;import com.example.seckill.util.response.ResponseResult;/ <p> 秒杀订单信息表 服务类 </p> @author lv @since 2022-03-19/public interface ISeckillOrderService extends IService<SeckillOrder> {ResponseResult<?> addOrder(Long goodsId, User user);} SeckillOrderServiceImpl : package com.example.seckill.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;import com.example.seckill.exception.BusinessException;import com.example.seckill.mapper.GoodsMapper;import com.example.seckill.mapper.OrderMapper;import com.example.seckill.mapper.SeckillGoodsMapper;import com.example.seckill.pojo.;import com.example.seckill.mapper.SeckillOrderMapper;import com.example.seckill.service.ISeckillOrderService;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.example.seckill.util.SnowFlake;import com.example.seckill.util.response.ResponseResult;import com.example.seckill.util.response.ResponseResultCode;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;/ <p> 秒杀订单信息表 服务实现类 </p> @author lv @since 2022-03-19/@Servicepublic class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper, SeckillOrder> implements ISeckillOrderService {@Autowiredprivate SeckillGoodsMapper seckillGoodsMapper;@Autowiredprivate GoodsMapper goodsMapper;@Autowiredprivate OrderMapper orderMapper;@Transactional(rollbackFor = Exception.class)@Overridepublic ResponseResult<?> addOrder(Long goodsId, User user) {// 下单前判断库存数SeckillGoods goods = seckillGoodsMapper.selectOne(new QueryWrapper<SeckillGoods>().eq("goods_id", goodsId));if (goods == null) {throw new BusinessException(ResponseResultCode.SECKILL_ORDER_ERROR);}if (goods.getStockCount() < 1) {throw new BusinessException(ResponseResultCode.SECKILL_ORDER_ERROR);}// 限购SeckillOrder one = this.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));if (one != null) {throw new BusinessException(ResponseResultCode.SECKILL_ORDER_EXISTS_ERROR);}// 库存减一int i = seckillGoodsMapper.update(null, new UpdateWrapper<SeckillGoods>().eq("goods_id", goodsId).setSql("stock_count=stock_count-1"));// 根据商品编号查询对应的商品(拿名字)Goods goodsInfo = goodsMapper.selectOne(new QueryWrapper<Goods>().eq("gid", goodsId));// 生成订单//生成雪花idSnowFlake snowFlake = new SnowFlake(5, 9);long id = snowFlake.nextId();//生成对应的订单Order normalOrder = new Order();normalOrder.setOid(id);normalOrder.setUserId(user.getId());normalOrder.setGoodsId(goodsId);normalOrder.setGoodsName(goodsInfo.getGoodsName());normalOrder.setGoodsCount(1);normalOrder.setGoodsPrice(goods.getSeckillPrice());orderMapper.insert(normalOrder);//生成秒杀订单SeckillOrder seckillOrder = new SeckillOrder();seckillOrder.setUserId(user.getId());seckillOrder.setOrderId(normalOrder.getOid());seckillOrder.setGoodsId(goodsId);this.save(seckillOrder);return ResponseResult.success();} } ③、controller层 SeckillOrderController : package com.example.seckill.controller;import com.example.seckill.pojo.User;import com.example.seckill.service.ISeckillOrderService;import com.example.seckill.util.response.ResponseResult;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/ <p> 秒杀订单信息表 前端控制器 </p> @author lv @since 2022-03-19/@RestController@RequestMapping("/seckillOrder")public class SeckillOrderController {@Autowiredprivate ISeckillOrderService seckillOrderService;@RequestMapping("/addOrder")public ResponseResult<?> addOrder(Long goodsId, User user){return seckillOrderService.addOrder(goodsId,user);} } ④、呈现结果 限购次数: 本期内容结束,下期内容更完善!!!!!!!!!!!!!!!!!!!!!1 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_60389087/article/details/123601288。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-25 23:20:34
121
转载
转载文章
...发送带有恶意脚本代码参数的 URL,当 URL 地址被打开时,特有的恶意代码参数被 HTML 解析、执行。 非持久型 XSS 举一个例子,比如你的 Web 页面中包含有以下代码: Select your language:<select><script>document.write(''+ '<option value=1>'+ location.href.substring(location.href.indexOf('default=') + 8)+ '</option>');document.write('<option value=2>English</option>');</script></select> 攻击者可以直接通过 URL 类似:https://xx.com/xx?default=<script>alert(document.cookie)</script>) 注入可执行的脚本代码。 非持久型 XSS 漏洞攻击有以下几点特征: 即时性,不经过服务器存储,直接通过 HTTP 的 GET 和 POST 请求就能完成一次攻击,拿到用户隐私数据。 攻击者需要诱骗点击 反馈率低,所以较难发现和响应修复 盗取用户敏感保密信息 为了防止出现非持久型 XSS 漏洞,需要确保这么几件事情: Web 页面渲染的所有内容或者渲染的数据都必须来自于服务端。 尽量不要从 URL,document.referrer,document.forms 等这种 DOM API 中获取数据直接渲染。 尽量不要使用 eval, new Function(),document.write(),document.writeln(),window.setInterval(),window.setTimeout(),innerHTML,document.creteElement() 等可执行字符串的方法。 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 escape 转义。 前端渲染的时候对任何的字段都需要做 escape 转义编码。 escape 转义的目的是将一些构成 HTML 标签的元素转义,比如 <,>,空格 等,转义成 <,>, 等显示转义字符。有很多开源的工具可以协助我们做 escape 转义。 持久型 XSS 持久型 XSS 漏洞,也被称为存储型 XSS 漏洞,一般存在于 Form 表单提交等交互功能,如发帖留言,提交文本信息等,黑客利用的 XSS 漏洞,将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。 主要注入页面方式和非持久型 XSS 漏洞类似,只不过持久型的不是来源于 URL,refferer,forms 等,而是来源于后端从数据库中读出来的数据。持久型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可,但是这种 XSS 攻击的成本相对还是很高。攻击成功需要同时满足以下几个条件: POST 请求提交表单后端没做转义直接入库。 后端从数据库中取出数据没做转义直接输出给前端。 前端拿到后端数据没做转义直接渲染成 DOM。 持久型 XSS 有以下几个特点: 持久性,植入在数据库中 危害面广,甚至可以让用户机器变成 DDoS 攻击的肉鸡。 盗取用户敏感私密信息 为了防止持久型 XSS 漏洞,需要前后端共同努力: 后端在入库前应该选择不相信任何前端数据,将所有的字段统一进行转义处理。 后端在输出给前端数据统一进行转义处理。 前端在渲染页面 DOM 的时候应该选择不相信任何后端数据,任何字段都需要做转义处理。 基于字符集的 XSS 其实现在很多的浏览器以及各种开源的库都专门针对了 XSS 进行转义处理,尽量默认抵御绝大多数 XSS 攻击,但是还是有很多方式可以绕过转义规则,让人防不胜防。比如「基于字符集的 XSS 攻击」就是绕过这些转义处理的一种攻击方式,比如有些 Web 页面字符集不固定,用户输入非期望字符集的字符,有时会绕过转义过滤规则。 以基于 utf-7 的 XSS 为例 utf-7 是可以将所有的 unicode 通过 7bit 来表示的一种字符集 (但现在已经从 Unicode 规格中移除)。 这个字符集为了通过 7bit 来表示所有的文字, 除去数字和一部分的符号,其它的部分将都以 base64 编码为基础的方式呈现。 <script>alert("xss")</script>可以被解释为:+ADw-script+AD4-alert(+ACI-xss+ACI-)+ADw-/script+AD4- 可以形成「基于字符集的 XSS 攻击」的原因是由于浏览器在 meta 没有指定 charset 的时候有自动识别编码的机制,所以这类攻击通常就是发生在没有指定或者没来得及指定 meta 标签的 charset 的情况下。 所以我们有什么办法避免这种 XSS 呢? 记住指定 XML 中不仅要指定字符集为 utf-8,而且标签要闭合 牛文推荐:http://drops.wooyun.org/papers/1327 (这个讲的很详细) 基于 Flash 的跨站 XSS 基于 Flash 的跨站 XSS 也是属于反射型 XSS 的一种,虽然现在开发 ActionScript 的产品线几乎没有了,但还是提一句吧,AS 脚本可以接受用户输入并操作 cookie,攻击者可以配合其他 XSS(持久型或者非持久型)方法将恶意 swf 文件嵌入页面中。主要是因为 AS 有时候需要和 JS 传参交互,攻击者会通过恶意的 XSS 注入篡改参数,窃取并操作cookie。 避免方法: 严格管理 cookie 的读写权限 对 Flash 能接受用户输入的参数进行过滤 escape 转义处理 未经验证的跳转 XSS 有一些场景是后端需要对一个传进来的待跳转的 URL 参数进行一个 302 跳转,可能其中会带有一些用户的敏感(cookie)信息。如果服务器端做302 跳转,跳转的地址来自用户的输入,攻击者可以输入一个恶意的跳转地址来执行脚本。 这时候需要通过以下方式来防止这类漏洞: 对待跳转的 URL 参数做白名单或者某种规则过滤 后端注意对敏感信息的保护, 比如 cookie 使用来源验证。 CSRF CSRF(Cross-Site Request Forgery),中文名称:跨站请求伪造攻击 那么 CSRF 到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过 QQ 等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使 Web 应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个 QQ 好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。 所以遇到 CSRF 攻击时,将对终端用户的数据和操作指令构成严重的威胁。当受攻击的终端用户具有管理员帐户的时候,CSRF 攻击将危及整个 Web 应用程序。 CSRF 原理 下图大概描述了 CSRF 攻击的原理,可以理解为有一个小偷在你配钥匙的地方得到了你家的钥匙,然后拿着要是去你家想偷什么偷什么。 csrf原理 完成 CSRF 攻击必须要有三个条件: 用户已经登录了站点 A,并在本地记录了 cookie 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。 站点 A 没有做任何 CSRF 防御 你也许会问:「如果我不满足以上三个条件中的任意一个,就不会受到 CSRF 的攻击」。其实可以这么说的,但你不能保证以下情况不会发生: 你不能保证你登录了一个网站后,不再打开一个 tab 页面并访问另外的网站,特别现在浏览器都是支持多 tab 的。 你不能保证你关闭浏览器了后,你本地的 cookie 立刻过期,你上次的会话已经结束。 上图中所谓的攻击网站 B,可能是一个存在其他漏洞的可信任的经常被人访问的网站。 预防 CSRF CSRF 的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的 CSRF 防御也都在服务端进行。服务端的预防 CSRF 攻击的方式方法有多种,但思路上都是差不多的,主要从以下两个方面入手: 正确使用 GET,POST 请求和 cookie 在非 GET 请求中增加 token 一般而言,普通的 Web 应用都是以 GET、POST 请求为主,还有一种请求是 cookie 方式。我们一般都是按照如下规则设计应用的请求: GET 请求常用在查看,列举,展示等不需要改变资源属性的时候(数据库 query 查询的时候) POST 请求常用在 From 表单提交,改变一个资源的属性或者做其他一些事情的时候(数据库有 insert、update、delete 的时候) 当正确的使用了 GET 和 POST 请求之后,剩下的就是在非 GET 方式的请求中增加随机数,这个大概有三种方式来进行: 为每个用户生成一个唯一的 cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者不能获得第三方的 cookie(理论上),所以表单中的数据也就构造失败,但是由于用户的 cookie 很容易由于网站的 XSS 漏洞而被盗取,所以这个方案必须要在没有 XSS 的情况下才安全。 每个 POST 请求使用验证码,这个方案算是比较完美的,但是需要用户多次输入验证码,用户体验比较差,所以不适合在业务中大量运用。 渲染表单的时候,为每一个表单包含一个 csrfToken,提交表单的时候,带上 csrfToken,然后在后端做 csrfToken 验证。 CSRF 的防御可以根据应用场景的不同自行选择。CSRF 的防御工作确实会在正常业务逻辑的基础上带来很多额外的开发量,但是这种工作量是值得的,毕竟用户隐私以及财产安全是产品最基础的根本。 SQL 注入 SQL 注入漏洞(SQL Injection)是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。 而造成 SQL 注入的原因是因为程序没有有效的转义过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。 很多 Web 开发者没有意识到 SQL 查询是可以被篡改的,从而把 SQL 查询当作可信任的命令。殊不知,SQL 查询是可以绕开访问控制,从而绕过身份验证和权限检查的。更有甚者,有可能通过 SQL 查询去运行主机系统级的命令。 SQL 注入原理 下面将通过一些真实的例子来详细讲解 SQL 注入的方式的原理。 考虑以下简单的管理员登录表单: <form action="/login" method="POST"><p>Username: <input type="text" name="username" /></p><p>Password: <input type="password" name="password" /></p><p><input type="submit" value="登陆" /></p></form> 后端的 SQL 语句可能是如下这样的: let querySQL = SELECT FROM userWHERE username='${username}'AND psw='${password}'; // 接下来就是执行 sql 语句… 目的就是来验证用户名和密码是不是正确,按理说乍一看上面的 SQL 语句也没什么毛病,确实是能够达到我们的目的,可是你只是站在用户会老老实实按照你的设计来输入的角度来看问题,如果有一个恶意攻击者输入的用户名是 zoumiaojiang’ OR 1 = 1 --,密码随意输入,就可以直接登入系统了。WFT! 冷静下来思考一下,我们之前预想的真实 SQL 语句是: SELECT FROM user WHERE username='zoumiaojiang' AND psw='mypassword' 可以恶意攻击者的奇怪用户名将你的 SQL 语句变成了如下形式: SELECT FROM user WHERE username='zoumiaojiang' OR 1 = 1 --' AND psw='xxxx' 在 SQL 中,-- 是注释后面的内容的意思,所以查询语句就变成了: SELECT FROM user WHERE username='zoumiaojiang' OR 1 = 1 这条 SQL 语句的查询条件永远为真,所以意思就是恶意攻击者不用我的密码,就可以登录进我的账号,然后可以在里面为所欲为,然而这还只是最简单的注入,牛逼的 SQL 注入高手甚至可以通过 SQL 查询去运行主机系统级的命令,将你主机里的内容一览无余,这里我也没有这个能力讲解的太深入,毕竟不是专业研究这类攻击的,但是通过以上的例子,已经了解了 SQL 注入的原理,我们基本已经能找到防御 SQL 注入的方案了。 如何预防 SQL 注入 防止 SQL 注入主要是不能允许用户输入的内容影响正常的 SQL 语句的逻辑,当用户的输入的信息将要用来拼接 SQL 语句的话,我们应该永远选择不相信,任何内容都必须进行转义过滤,当然做到这个还是不够的,下面列出防御 SQL 注入的几点注意事项: 严格限制Web应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。 对进入数据库的特殊字符(’,",\,<,>,&,,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。 mysql.query(SELECT FROM user WHERE username = ? AND psw = ?, [username, psw]); 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,以及时修补被发现的 SQL 注入漏洞。网上有很多这方面的开源工具,例如 sqlmap、SQLninja 等。 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。 不要过于细化返回的错误信息,如果目的是方便调试,就去使用后端日志,不要在接口上过多的暴露出错信息,毕竟真正的用户不关心太多的技术细节,只要话术合理就行。 碰到要操作的数据库的代码,一定要慎重,小心使得万年船,多找几个人多来几次 code review,将问题都暴露出来,而且要善于利用工具,操作数据库相关的代码属于机密,没事不要去各种论坛晒自家站点的 SQL 语句,万一被人盯上了呢? 命令行注入 命令行注入漏洞,指的是攻击者能够通过 HTTP 请求直接侵入主机,执行攻击者预设的 shell 命令,听起来好像匪夷所思,这往往是 Web 开发者最容易忽视但是却是最危险的一个漏洞之一,看一个实例: 假如现在需要实现一个需求:用户提交一些内容到服务器,然后在服务器执行一些系统命令去产出一个结果返回给用户,接口的部分实现如下: // 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repoconst exec = require('mz/child_process').exec;let params = {/ 用户输入的参数 /};exec(git clone ${params.repo} /some/path); 这段代码确实能够满足业务需求,正常的用户也确实能从指定的 git repo 上下载到想要的代码,可是和 SQL 注入一样,这段代码在恶意攻击者眼中,简直就是香饽饽。 如果 params.repo 传入的是 https://github.com/zoumiaojiang/zoumiaojiang.github.io.git 当然没问题了。 可是如果 params.repo 传入的是 https://github.com/xx/xx.git && rm -rf / && 恰好你的服务是用 root 权限起的就惨了。 具体恶意攻击者能用命令行注入干什么也像 SQL 注入一样,手法是千变万化的,比如「反弹 shell 注入」等,但原理都是一样的,我们绝对有能力防止命令行注入发生。防止命令行注入需要做到以下几件事情: 后端对前端提交内容需要完全选择不相信,并且对其进行规则限制(比如正则表达式)。 在调用系统命令前对所有传入参数进行命令行参数转义过滤。 不要直接拼接命令语句,借助一些工具做拼接、转义预处理,例如 Node.js 的 shell-escape npm 包。 还是前面的例子,我们可以做到如下: const exec = require('mz/child_process').exec;// 借助 shell-escape npm 包解决参数转义过滤问题const shellescape = require('shell-escape');let params = {/ 用户输入的参数 /};// 先过滤一下参数,让参数符合预期if (!/正确的表达式/.test(params.repo)) {return;}let cmd = shellescape(['git','clone',params.repo,'/some/path']);// cmd 的值: git clone 'https://github.com/xx/xx.git && rm -rf / &&' /some/path// 这样就不会被注入成功了。exec(cmd); DDoS 攻击 DDoS 又叫分布式拒绝服务,全称 Distributed Denial of Service,其原理就是利用大量的请求造成资源过载,导致服务不可用,这个攻击应该不能算是安全问题,这应该算是一个另类的存在,因为这种攻击根本就是耍流氓的存在,「伤敌一千,自损八百」的行为。出于保护 Web App 不受攻击的攻防角度,还是介绍一下 DDoS 攻击吧,毕竟也是挺常见的。 DDoS 攻击可以理解为:「你开了一家店,隔壁家点看不惯,就雇了一大堆黑社会人员进你店里干坐着,也不消费,其他客人也进不来,导致你营业惨淡」。为啥说 DDoS 是个「伤敌一千,自损八百」的行为呢?毕竟隔壁店还是花了不少钱雇黑社会但是啥也没得到不是?DDoS 攻击的目的基本上就以下几个: 深仇大恨,就是要干死你 敲诈你,不给钱就干你 忽悠你,不买我防火墙服务就会有“人”继续干你 也许你的站点遭受过 DDoS 攻击,具体什么原因怎么解读见仁见智。DDos 攻击从层次上可分为网络层攻击与应用层攻击,从攻击手法上可分为快型流量攻击与慢型流量攻击,但其原理都是造成资源过载,导致服务不可用。 网络层 DDoS 网络层 DDos 攻击包括 SYN Flood、ACK Flood、UDP Flood、ICMP Flood 等。 SYN Flood 攻击 SYN flood 攻击主要利用了 TCP 三次握手过程中的 Bug,我们都知道 TCP 三次握手过程是要建立连接的双方发送 SYN,SYN + ACK,ACK 数据包,而当攻击方随意构造源 IP 去发送 SYN 包时,服务器返回的 SYN + ACK 就不能得到应答(因为 IP 是随意构造的),此时服务器就会尝试重新发送,并且会有至少 30s 的等待时间,导致资源饱和服务不可用,此攻击属于慢型 DDoS 攻击。 ACK Flood 攻击 ACK Flood 攻击是在 TCP 连接建立之后,所有的数据传输 TCP 报文都是带有 ACK 标志位的,主机在接收到一个带有 ACK 标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应 RST 包告诉对方此端口不存在。 UDP Flood 攻击 UDP flood 攻击是由于 UDP 是一种无连接的协议,因此攻击者可以伪造大量的源 IP 地址去发送 UDP 包,此种攻击属于大流量攻击。正常应用情况下,UDP 包双向流量会基本相等,因此发起这种攻击的攻击者在消耗对方资源的时候也在消耗自己的资源。 ICMP Flood 攻击 ICMP Flood 攻击属于大流量攻击,其原理就是不断发送不正常的 ICMP 包(所谓不正常就是 ICMP 包内容很大),导致目标带宽被占用,但其本身资源也会被消耗。目前很多服务器都是禁 ping 的(在防火墙在可以屏蔽 ICMP 包),因此这种攻击方式已经落伍。 网络层 DDoS 防御 网络层的 DDoS 攻击究其本质其实是无法防御的,我们能做得就是不断优化服务本身部署的网络架构,以及提升网络带宽。当然,还是做好以下几件事也是有助于缓解网络层 DDoS 攻击的冲击: 网络架构上做好优化,采用负载均衡分流。 确保服务器的系统文件是最新的版本,并及时更新系统补丁。 添加抗 DDos 设备,进行流量清洗。 限制同时打开的 SYN 半连接数目,缩短 SYN 半连接的 Timeout 时间。 限制单 IP 请求频率。 防火墙等防护设置禁止 ICMP 包等。 严格限制对外开放的服务器的向外访问。 运行端口映射程序或端口扫描程序,要认真检查特权端口和非特权端口。 关闭不必要的服务。 认真检查网络设备和主机/服务器系统的日志。只要日志出现漏洞或是时间变更,那这台机器就可能遭到了攻击。 限制在防火墙外与网络文件共享。这样会给黑客截取系统文件的机会,主机的信息暴露给黑客,无疑是给了对方入侵的机会。 加钱堆机器。。 报警。。 应用层 DDoS 应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。应用层攻击千变万化,目的就是在网络应用层耗尽你的带宽,下面列出集中典型的攻击类型。 CC 攻击 当时绿盟为了防御 DDoS 攻击研发了一款叫做 Collapasar 的产品,能够有效的防御 SYN Flood 攻击。黑客为了挑衅,研发了一款 Challenge Collapasar 攻击工具(简称 CC)。 CC 攻击的原理,就是针对消耗资源比较大的页面不断发起不正常的请求,导致资源耗尽。因此在发送 CC 攻击前,我们需要寻找加载比较慢,消耗资源比较多的网页,比如需要查询数据库的页面、读写硬盘文件的等。通过 CC 攻击,使用爬虫对某些加载需要消耗大量资源的页面发起 HTTP 请求。 DNS Flood DNS Flood 攻击采用的方法是向被攻击的服务器发送大量的域名解析请求,通常请求解析的域名是随机生成或者是网络世界上根本不存在的域名,被攻击的DNS 服务器在接收到域名解析请求的时候首先会在服务器上查找是否有对应的缓存,如果查找不到并且该域名无法直接由服务器解析的时候,DNS 服务器会向其上层 DNS 服务器递归查询域名信息。域名解析的过程给服务器带来了很大的负载,每秒钟域名解析请求超过一定的数量就会造成 DNS 服务器解析域名超时。 根据微软的统计数据,一台 DNS 服务器所能承受的动态域名查询的上限是每秒钟 9000 个请求。而我们知道,在一台 P3 的 PC 机上可以轻易地构造出每秒钟几万个域名解析请求,足以使一台硬件配置极高的 DNS 服务器瘫痪,由此可见 DNS 服务器的脆弱性。 HTTP 慢速连接攻击 针对 HTTP 协议,先建立起 HTTP 连接,设置一个较大的 Conetnt-Length,每次只发送很少的字节,让服务器一直以为 HTTP 头部没有传输完成,这样连接一多就很快会出现连接耗尽。 应用层 DDoS 防御 判断 User-Agent 字段(不可靠,因为可以随意构造) 针对 IP + cookie,限制访问频率(由于 cookie 可以更改,IP 可以使用代理,或者肉鸡,也不可靠) 关闭服务器最大连接数等,合理配置中间件,缓解 DDoS 攻击。 请求中添加验证码,比如请求中有数据库操作的时候。 编写代码时,尽量实现优化,并合理使用缓存技术,减少数据库的读取操作。 加钱堆机器。。 报警。。 应用层的防御有时比网络层的更难,因为导致应用层被 DDoS 攻击的因素非常多,有时往往是因为程序员的失误,导致某个页面加载需要消耗大量资源,有时是因为中间件配置不当等等。而应用层 DDoS 防御的核心就是区分人与机器(爬虫),因为大量的请求不可能是人为的,肯定是机器构造的。因此如果能有效的区分人与爬虫行为,则可以很好地防御此攻击。 其他 DDoS 攻击 发起 DDoS 也是需要大量的带宽资源的,但是互联网就像森林,林子大了什么鸟都有,DDoS 攻击者也能找到其他的方式发起廉价并且极具杀伤力的 DDoS 攻击。 利用 XSS 举个例子,如果 12306 页面有一个 XSS 持久型漏洞被恶意攻击者发现,只需在春节抢票期间在这个漏洞中执行脚本使得往某一个小站点随便发点什么请求,然后随着用户访问的增多,感染用户增多,被攻击的站点自然就会迅速瘫痪了。这种 DDoS 简直就是无本万利,不用惊讶,现在大站有 XSS 漏洞的不要太多。 来自 P2P 网络攻击 大家都知道,互联网上的 P2P 用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,成千上万的真实 IP 地址连接过来,没有哪个设备能够支撑住。拿 BT 下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但是这只是基础攻击。 高级的 P2P 攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其它需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个 P2P 网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到 P2P 官方发现问题更新服务器且下载用户重启下载软件为止。 最后总结下,DDoS 不可能防的住,就好比你的店只能容纳 50 人,黑社会有 100 人,你就换一家大店,能容纳 500 人,然后黑社会又找来了 1000 人,这种堆人头的做法就是 DDoS 本质上的攻防之道,「道高一尺,魔高一丈,魔高一尺,道高一丈」,讲真,必要的时候就答应勒索你的人的条件吧,实在不行就报警吧。 流量劫持 流量劫持应该算是黑产行业的一大经济支柱了吧?简直是让人恶心到吐,不吐槽了,还是继续谈干货吧,流量劫持基本分两种:DNS 劫持 和 HTTP 劫持,目的都是一样的,就是当用户访问 zoumiaojiang.com 的时候,给你展示的并不是或者不完全是 zoumiaojiang.com 提供的 “内容”。 DNS 劫持 DNS 劫持,也叫做域名劫持,可以这么理解,「你打了一辆车想去商场吃饭,结果你打的车是小作坊派来的,直接给你拉到小作坊去了」,DNS 的作用是把网络地址域名对应到真实的计算机能够识别的 IP 地址,以便计算机能够进一步通信,传递网址和内容等。如果当用户通过某一个域名访问一个站点的时候,被篡改的 DNS 服务器返回的是一个恶意的钓鱼站点的 IP,用户就被劫持到了恶意钓鱼站点,然后继而会被钓鱼输入各种账号密码信息,泄漏隐私。 dns劫持 这类劫持,要不就是网络运营商搞的鬼,一般小的网络运营商与黑产勾结会劫持 DNS,要不就是电脑中毒,被恶意篡改了路由器的 DNS 配置,基本上做为开发者或站长却是很难察觉的,除非有用户反馈,现在升级版的 DNS 劫持还可以对特定用户、特定区域等使用了用户画像进行筛选用户劫持的办法,另外这类广告显示更加随机更小,一般站长除非用户投诉否则很难觉察到,就算觉察到了取证举报更难。无论如何,如果接到有 DNS 劫持的反馈,一定要做好以下几件事: 取证很重要,时间、地点、IP、拨号账户、截屏、URL 地址等一定要有。 可以跟劫持区域的电信运营商进行投诉反馈。 如果投诉反馈无效,直接去工信部投诉,一般来说会加白你的域名。 HTTP 劫持 HTTP 劫持您可以这么理解,「你打了一辆车想去商场吃饭,结果司机跟你一路给你递小作坊的广告」,HTTP 劫持主要是当用户访问某个站点的时候会经过运营商网络,而不法运营商和黑产勾结能够截获 HTTP 请求返回内容,并且能够篡改内容,然后再返回给用户,从而实现劫持页面,轻则插入小广告,重则直接篡改成钓鱼网站页面骗用户隐私。能够实施流量劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则流量劫持将无法轻易发生。所以防止 HTTP 劫持的方法只有将内容加密,让劫持者无法破解篡改,这样就可以防止 HTTP 劫持了。 HTTPS 协议就是一种基于 SSL 协议的安全加密网络应用层协议,可以很好的防止 HTTP 劫持。这里有篇 文章 讲的不错。HTTPS 在这就不深讲了,后面有机会我会单独好好讲讲 HTTPS。如果不想站点被 HTTP 劫持,赶紧将你的站点全站改造成 HTTPS 吧。 服务器漏洞 服务器除了以上提到的那些大名鼎鼎的漏洞和臭名昭著的攻击以外,其实还有很多其他的漏洞,往往也很容易被忽视,在这个小节也稍微介绍几种。 越权操作漏洞 如果你的系统是有登录控制的,那就要格外小心了,因为很有可能你的系统越权操作漏洞,越权操作漏洞可以简单的总结为 「A 用户能看到或者操作 B 用户的隐私内容」,如果你的系统中还有权限控制就更加需要小心了。所以每一个请求都需要做 userid 的判断 以下是一段有漏洞的后端示意代码: // ctx 为请求的 context 上下文let msgId = ctx.params.msgId;mysql.query('SELECT FROM msg_table WHERE msg_id = ?',[msgId]); 以上代码是任何人都可以查询到任何用户的消息,只要有 msg_id 就可以,这就是比较典型的越权漏洞,需要如下这么改进一下: // ctx 为请求的 context 上下文let msgId = ctx.params.msgId;let userId = ctx.session.userId; // 从会话中取出当前登陆的 userIdmysql.query('SELECT FROM msg_table WHERE msg_id = ? AND user_id = ?',[msgId, userId]); 嗯,大概就是这个意思,如果有更严格的权限控制,那在每个请求中凡是涉及到数据库的操作都需要先进行严格的验证,并且在设计数据库表的时候需要考虑进 userId 的账号关联以及权限关联。 目录遍历漏洞 目录遍历漏洞指通过在 URL 或参数中构造 …/,./ 和类似的跨父目录字符串的 ASCII 编码、unicode 编码等,完成目录跳转,读取操作系统各个目录下的敏感文件,也可以称作「任意文件读取漏洞」。 目录遍历漏洞原理:程序没有充分过滤用户输入的 …/ 之类的目录跳转符,导致用户可以通过提交目录跳转来遍历服务器上的任意文件。使用多个… 符号,不断向上跳转,最终停留在根 /,通过绝对路径去读取任意文件。 目录遍历漏洞几个示例和测试,一般构造 URL 然后使用浏览器直接访问,或者使用 Web 漏洞扫描工具检测,当然也可以自写程序测试。 http://somehost.com/../../../../../../../../../etc/passwdhttp://somehost.com/some/path?file=../../Windows/system.ini 借助 %00 空字符截断是一个比较经典的攻击手法http://somehost.com/some/path?file=../../Windows/system.ini%00.js 使用了 IIS 的脚本目录来移动目录并执行指令http://somehost.com/scripts/..%5c../Windows/System32/cmd.exe?/c+dir+c:\ 防御 方法就是需要对 URL 或者参数进行 …/,./ 等字符的转义过滤。 物理路径泄漏 物理路径泄露属于低风险等级缺陷,它的危害一般被描述为「攻击者可以利用此漏洞得到信息,来对系统进一步地攻击」,通常都是系统报错 500 的错误信息直接返回到页面可见导致的漏洞。得到物理路径有些时候它能给攻击者带来一些有用的信息,比如说:可以大致了解系统的文件目录结构;可以看出系统所使用的第三方软件;也说不定会得到一个合法的用户名(因为很多人把自己的用户名作为网站的目录名)。 防止这种泄漏的方法就是做好后端程序的出错处理,定制特殊的 500 报错页面。 源码暴露漏洞 和物理路径泄露类似,就是攻击者可以通过请求直接获取到你站点的后端源代码,然后就可以对系统进一步研究攻击。那么导致源代码暴露的原因是什么呢?基本上就是发生在服务器配置上了,服务器可以设置哪些路径的文件才可以被直接访问的,这里给一个 koa 服务起的例子,正常的 koa 服务器可以通过 koa-static 中间件去指定静态资源的目录,好让静态资源可以通过路径的路由访问。比如你的系统源代码目录是这样的: |- project|- src|- static|- ...|- server.js 你想要将 static 的文件夹配成静态资源目录,你应该会在 server.js 做如下配置: const Koa = require('koa');const serve = require('koa-static');const app = new Koa();app.use(serve(__dirname + '/project/static')); 但是如果配错了静态资源的目录,可能就出大事了,比如: // ...app.use(serve(__dirname + '/project')); 这样所有的源代码都可以通过路由访问到了,所有的服务器都提供了静态资源机制,所以在通过服务器配置静态资源目录和路径的时候,一定要注意检验,不然很可能产生漏洞。 最后,希望 Web 开发者们能够管理好自己的代码隐私,注意代码安全问题,比如不要将产品的含有敏感信息的代码放到第三方外部站点或者暴露给外部用户,尤其是前端代码,私钥类似的保密性的东西不要直接输出在代码里或者页面中。也许还有很多值得注意的点,但是归根结底还是绷住安全那根弦,对待每一行代码都要多多推敲。 请关注我的订阅号 本篇文章为转载内容。原文链接:https://blog.csdn.net/MrCoderStack/article/details/88547919。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-01-03 14:51:12
493
转载
转载文章
...X 分析一下,这样能解决问题吗? 就算操作A和B都是同步的,但A和B组成的复合操作也未必是同步的,仍然需要自己进行同步 就像这个程序,判断size和进行remove必须是一整个的原子操作 @author 马士兵/import java.util.LinkedList;import java.util.List;import java.util.concurrent.TimeUnit;public class TicketSeller3 {static List<String> tickets = new LinkedList<>();static {for(int i=0; i<1000; i++) tickets.add("票 编号:" + i);}public static void main(String[] args) {for(int i=0; i<10; i++) {new Thread(()->{while(true) {synchronized(tickets) {if(tickets.size() <= 0) break;try {TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("销售了--" + tickets.remove(0));} }}).start();} }} 队列 import java.util.Queue;import java.util.concurrent.ConcurrentLinkedQueue;public class TicketSeller4 {static Queue<String> tickets = new ConcurrentLinkedQueue<>();static {for(int i=0; i<1000; i++) tickets.add("票 编号:" + i);}public static void main(String[] args) {for(int i=0; i<10; i++) {new Thread(()->{while(true) {String s = tickets.poll();if(s == null) break;else System.out.println("销售了--" + s);} }).start();} }} 3、多线程常用容器 1、ConcurrentHashMap(无序)和ConcurrentSkipListMap(有序,链表,使用跳表数据结构,让查询更快) 跳表:http://blog.csdn.net/sunxianghuang/article/details/52221913 import java.util.;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentSkipListMap;import java.util.concurrent.CountDownLatch;public class T01_ConcurrentMap {public static void main(String[] args) {Map<String, String> map = new ConcurrentHashMap<>();//Map<String, String> map = new ConcurrentSkipListMap<>(); //高并发并且排序//Map<String, String> map = new Hashtable<>();//Map<String, String> map = new HashMap<>(); //Collections.synchronizedXXX//TreeMapRandom r = new Random();Thread[] ths = new Thread[100];CountDownLatch latch = new CountDownLatch(ths.length);long start = System.currentTimeMillis();for(int i=0; i<ths.length; i++) {ths[i] = new Thread(()->{for(int j=0; j<10000; j++) map.put("a" + r.nextInt(100000), "a" + r.nextInt(100000));latch.countDown();});}Arrays.asList(ths).forEach(t->t.start());try {latch.await();} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println(end - start);System.out.println(map.size());} } 2、CopyOnWriteList(写时复制)和CopyOnWriteSet 适用于,高并发是,读的多,写的少的情况 当我们写的时候,将容器复制,让写线程去复制的线程写(写的时候加锁) 而读线程依旧去读旧的(读的时候不加锁) 当写完,将对象指向复制后的已经写完的容器,原来容器销毁 大大提高读的效率 / 写时复制容器 copy on write 多线程环境下,写时效率低,读时效率高 适合写少读多的环境 @author 马士兵/import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.Random;import java.util.Vector;import java.util.concurrent.CopyOnWriteArrayList;public class T02_CopyOnWriteList {public static void main(String[] args) {List<String> lists = //new ArrayList<>(); //这个会出并发问题!//new Vector();new CopyOnWriteArrayList<>();Random r = new Random();Thread[] ths = new Thread[100];for(int i=0; i<ths.length; i++) {Runnable task = new Runnable() {@Overridepublic void run() {for(int i=0; i<1000; i++) lists.add("a" + r.nextInt(10000));} };ths[i] = new Thread(task);}runAndComputeTime(ths);System.out.println(lists.size());}static void runAndComputeTime(Thread[] ths) {long s1 = System.currentTimeMillis();Arrays.asList(ths).forEach(t->t.start());Arrays.asList(ths).forEach(t->{try {t.join();} catch (InterruptedException e) {e.printStackTrace();} });long s2 = System.currentTimeMillis();System.out.println(s2 - s1);} } 3、synchronizedList和ConcurrentLinkedQueue package com.mashibing.juc.c_025;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Queue;import java.util.concurrent.ConcurrentLinkedQueue;public class T04_ConcurrentQueue {public static void main(String[] args) {List<String> strsList = new ArrayList<>();List<String> strsSync = Collections.synchronizedList(strsList);//加锁ListQueue<String> strs = new ConcurrentLinkedQueue<>();//Concurrent链表队列,就是读快for(int i=0; i<10; i++) {strs.offer("a" + i); //add添加,但是不同点是,此方法会返回一个布尔值}System.out.println(strs);System.out.println(strs.size());System.out.println(strs.poll());//取出,取完后将元素去除System.out.println(strs.size());System.out.println(strs.peek());//取出,但是不会将元素从队列删除System.out.println(strs.size());//双端队列Deque} } 4、LinkedBlockingQueue 链表阻塞队列(无界链表,可以一直装东西,直到内存满(其实,也不是无限,其长度Integer.MaxValue就是上限,毕竟最大就这么大)) 主要体现在put和take方法,put添加的时候,如果队列满了,就阻塞当前线程,直到队列有空位,继续插入。take方法取的时候,如果没有值,就阻塞,等有值了,立马去取 import java.util.Random;import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.TimeUnit;public class T05_LinkedBlockingQueue {static BlockingQueue<String> strs = new LinkedBlockingQueue<>();static Random r = new Random();public static void main(String[] args) {new Thread(() -> {for (int i = 0; i < 100; i++) {try {strs.put("a" + i); //如果满了,当前线程就会等待(实现阻塞),等多会有空位,将值插入TimeUnit.MILLISECONDS.sleep(r.nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();} }}, "p1").start();for (int i = 0; i < 5; i++) {new Thread(() -> {for (;;) {try {System.out.println(Thread.currentThread().getName() + " take -" + strs.take()); //取内容,如果空了,当前线程就会等待(实现阻塞)} catch (InterruptedException e) {e.printStackTrace();} }}, "c" + i).start();} }} 5、ArrayBlockingQueue 有界阻塞队列(因为Array需要指定长度) import java.util.Random;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;import java.util.concurrent.TimeUnit;public class T06_ArrayBlockingQueue {static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10);static Random r = new Random();public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {strs.put("a" + i);}//strs.put("aaa"); //满了就会等待,程序阻塞//strs.add("aaa");//strs.offer("aaa");strs.offer("aaa", 1, TimeUnit.SECONDS);System.out.println(strs);} } 6、特殊的阻塞队列1:DelayQueue 延时队列(按时间进行调度,就是隔多长时间运行,谁隔的少,谁先) 以下例子中,我们添加线程到队列顺序为t12345,正常情况下,会按照顺序运行,但是这里有了延时时间,也就是时间越短,越先执行 步骤很简单,拿到延时队列 指定构造方法 继承 implements Delayed 重写 compareTo和getDelay import java.util.Calendar;import java.util.Random;import java.util.concurrent.BlockingQueue;import java.util.concurrent.DelayQueue;import java.util.concurrent.Delayed;import java.util.concurrent.TimeUnit;public class T07_DelayQueue {static BlockingQueue<MyTask> tasks = new DelayQueue<>();static Random r = new Random();static class MyTask implements Delayed {String name;long runningTime;MyTask(String name, long rt) {this.name = name;this.runningTime = rt;}@Overridepublic int compareTo(Delayed o) {if(this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))return -1;else if(this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) return 1;else return 0;}@Overridepublic long getDelay(TimeUnit unit) {return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);}@Overridepublic String toString() {return name + " " + runningTime;} }public static void main(String[] args) throws InterruptedException {long now = System.currentTimeMillis();MyTask t1 = new MyTask("t1", now + 1000);MyTask t2 = new MyTask("t2", now + 2000);MyTask t3 = new MyTask("t3", now + 1500);MyTask t4 = new MyTask("t4", now + 2500);MyTask t5 = new MyTask("t5", now + 500);tasks.put(t1);tasks.put(t2);tasks.put(t3);tasks.put(t4);tasks.put(t5);System.out.println(tasks);for(int i=0; i<5; i++) {System.out.println(tasks.take());//获取的是toString方法返回值} }} 7、特殊的阻塞队列2:PriorityQueque 优先队列(二叉树算法,就是排序) import java.util.PriorityQueue;public class T07_01_PriorityQueque {public static void main(String[] args) {PriorityQueue<String> q = new PriorityQueue<>();q.add("c");q.add("e");q.add("a");q.add("d");q.add("z");for (int i = 0; i < 5; i++) {System.out.println(q.poll());} }} 8、特殊的阻塞队列3:SynchronusQueue 同步队列(线程池用处非常大) 此队列容量为0,当插入元素时,必须同时有个线程往外取 就是说,当你往这个队列里面插入一个元素,它就拿着这个元素站着(阻塞),直到有个取元素的线程来,它就把元素交给它 就是用来同步数据的,也就是线程间交互数据用的一个特殊队列 package com.mashibing.juc.c_025;import java.util.concurrent.BlockingQueue;import java.util.concurrent.SynchronousQueue;public class T08_SynchronusQueue { //容量为0public static void main(String[] args) throws InterruptedException {BlockingQueue<String> strs = new SynchronousQueue<>();new Thread(()->{//这个线程就是消费者,来取值try {System.out.println(strs.take());//和同步队列要值} catch (InterruptedException e) {e.printStackTrace();} }).start();strs.put("aaa"); //阻塞等待消费者消费,就拿着aaa站着,等线程来取//strs.put("bbb");//strs.add("aaa");System.out.println(strs.size());} } 9、特殊的阻塞队列4:TransferQueue 传递队列 此队列加入了一个方法transfer()用来向队列添加元素 但是和put()方法不同的是,put添加完元素就走了 而这个方法,添加完自己就阻塞了,直到有人将这个元素取走,它才继续工作(省去我们手动阻塞) import java.util.concurrent.LinkedTransferQueue;public class T09_TransferQueue {public static void main(String[] args) throws InterruptedException {LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();new Thread(() -> {try {System.out.println(strs.take());} catch (InterruptedException e) {e.printStackTrace();} }).start();strs.transfer("aaa");//放东西到队列,同时阻塞等待消费者线程,取走元素//strs.put("aaa");//如果用put就和普通队列一样,放完东西就走了/new Thread(() -> {try {System.out.println(strs.take());} catch (InterruptedException e) {e.printStackTrace();} }).start();/} } 3、线程池 线程池 由于单独创建线程,十分影响效率,而且无法对线程集中管理,一旦疏落,可能线程无限执行,浪费资源 线程池就是一个存储线程的游泳池,而每个线程就是池子里面的赛道 池子里的线程不执行任何任务,只是提供一个资源 而谁提交了任务,比如我想来游泳,那么池子就给你一个赛道,让你游泳 比如它想练憋气,那么给它一个赛道练憋气 当他们用完,走了,那么后面其它人再过来继续用 这就是线程池,始终只有这几个线程,不做实现,而是借用这几个线程的用户,自己掌控用这些线程资源做什么(提交任务给线程,线程空闲就帮他们完成任务) 线程池的两种类型(两类,不是两个) ThreadPoolExecutor(简称TPE) ForkJoinPool(分解汇总任务(将任务细化,最后汇总结果),少量线程执行多个任务(子任务,TPE做不到先执行子任务),CPU密集型) Executors(注意这后面有s) 它可以说是线程池工厂类,我们一般通过它创建线程池,并且它为我们封装了线程 1、常用类 Executor ExecutorService 扩展了execute方法,具有一个返回值 规定了异步执行机制,提供了一些执行器方法,比如shutdown()关闭等 但是它不知道执行器中的线程何时执行完 Callable 对Runnable进行了扩展,实现Callable的调用,可以有返回值,表示线程的状态 但是无法返回线程执行结果 Future 获得未来线程执行结果 由此,我们可以得知线程池基本的一个使用步骤 其中service.submit():为异步提交,也就是说,主线程该干嘛干嘛,我是异步执行的,和同步不一样(当前线程执行完,主线程才能继续执行,叫同步) futuer.get():获取结果集结果,此时因为异步,主线程执行到这里,结果集可能还没封装好,所以此时如果没有值,就阻塞,直到结果集出来 public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<String> c = new Callable() {@Overridepublic String call() throws Exception {return "Hello Callable";} };ExecutorService service = Executors.newCachedThreadPool();Future<String> future = service.submit(c); //异步System.out.println(future.get());//阻塞service.shutdown();} 2、FutureTask 可充当任务的结果集 上面我们介绍Future是用来得到任务的执行结果的 而FutureTask,可以当做一个任务用,并且返回任务的结果,也就是可以跑线程,然后还可以得到线程结果 public static void main(String[] args) throws InterruptedException, ExecutionException {FutureTask<Integer> task = new FutureTask<>(()->{TimeUnit.MILLISECONDS.sleep(500);return 1000;}); //new Callable () { Integer call();}new Thread(task).start();System.out.println(task.get()); //阻塞} 3、CompletableFuture 非常灵活的任务结果集 一个非常灵活的结果集 他可以将很多执行不同任务的线程的结果进行汇总 比如一个网站,它可以启动多个线程去各大电商网站,比如淘宝,京东,收集某些或某一个商品的价格 最后,将获取的数据进行整合封装 最终,客户就可以通过此网站,获取某类商品在各网站的价格信息 / 假设你能够提供一个服务 这个服务查询各大电商网站同一类产品的价格并汇总展示 @author 马士兵 http://mashibing.com/import java.io.IOException;import java.util.Random;import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;public class T06_01_CompletableFuture {public static void main(String[] args) throws ExecutionException, InterruptedException {long start, end;/start = System.currentTimeMillis();priceOfTM();priceOfTB();priceOfJD();end = System.currentTimeMillis();System.out.println("use serial method call! " + (end - start));/start = System.currentTimeMillis();CompletableFuture<Double> futureTM = CompletableFuture.supplyAsync(()->priceOfTM());CompletableFuture<Double> futureTB = CompletableFuture.supplyAsync(()->priceOfTB());CompletableFuture<Double> futureJD = CompletableFuture.supplyAsync(()->priceOfJD());CompletableFuture.allOf(futureTM, futureTB, futureJD).join();//当所有结果集都获取到,才汇总阻塞CompletableFuture.supplyAsync(()->priceOfTM()).thenApply(String::valueOf).thenApply(str-> "price " + str).thenAccept(System.out::println);end = System.currentTimeMillis();System.out.println("use completable future! " + (end - start));try {System.in.read();} catch (IOException e) {e.printStackTrace();} }private static double priceOfTM() {delay();return 1.00;}private static double priceOfTB() {delay();return 2.00;}private static double priceOfJD() {delay();return 3.00;}/private static double priceOfAmazon() {delay();throw new RuntimeException("product not exist!");}/private static void delay() {int time = new Random().nextInt(500);try {TimeUnit.MILLISECONDS.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}System.out.printf("After %s sleep!\n", time);} } 4、TPE型线程池1:ThreadPoolExecutor 原理及其参数 线程池由两个集合组成,一个集合存储线程,一个集合存储任务 存储线程:可以规定大小,最多可以有多少个,以及指定核心线程数量(不会被回收) 任务队列:存储任务 细节:初始线程池没有线程,当有一个任务来,线程池起一个线程,又有一个任务来,再起一个线程,直到达到核心线程数量 核心线程数量达到时,新来的任务将存储到任务队列中等待核心线程处理完成,直到任务队列也满了 当任务队列满了,此时再次启动一个线程(非核心线程,一旦空闲,达到指定时间将会消失),直到达到线程最大数量 当线程容器和任务容器都满了,又来了线程,将会执行拒绝策略 上面的细节涉及的所有步骤内容,均由创建线程池的参数执行 下面是ThreadPoolExecutor构造方法参数的源码注释 / 用给定的初始值,创建一个新的线程池 @param corePoolSize 核心线程数量 @param maximumPoolSize 最大线程数量 @param keepAliveTime 当线程数大于核心线程数量时,空闲的线程可生存的时间 @param unit 时间单位 @param workQueue 任务队列,只能包含由execute提交的Runnable任务 @param threadFactory 工厂,用于创建线程给线程池调度的工厂,可以自定义 @param handler 拒绝策略(可以自定义,JDK默认提供4种),当线程边界和队列容量已经满了,新来线程被阻塞时使用的处理程序/public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) JDK提供的4种拒绝策略,不常用,一般都是自己定义拒绝策略 Abort:抛异常 Discard:扔掉,不抛异常 DiscardOldest:扔掉排队时间最久的(将队列中排队时间最久的扔掉,然后让新来的进来) CallerRuns:调用者处理任务(谁通过execute方法提交任务,谁处理) ThreadPoolExecutor继承关系 继承关系:ThreadPoolExecutor->AbstractExectorService类->ExectorService接口->Exector接口 Executors(注意这后面有s) 它可以说是线程池工厂类,我们一般通过它创建线程池,并且它为我们封装了线程 看看下面创建线程池,哪里用到了它 使用实例 import java.io.IOException;import java.util.concurrent.;public class T05_00_HelloThreadPool {static class Task implements Runnable {private int i;public Task(int i) {this.i = i;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " Task " + i);try {System.in.read();} catch (IOException e) {e.printStackTrace();} }@Overridepublic String toString() {return "Task{" +"i=" + i +'}';} }public static void main(String[] args) {ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4,60, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());//创建线程池,核心2个,最大4个,空闲线程存活时间60s,任务队列容量4,使用默认线程工程,创建线程。拒绝策略是JDK提供的for (int i = 0; i < 8; i++) {tpe.execute(new Task(i));//供提交8次任务}System.out.println(tpe.getQueue());//查看任务队列tpe.execute(new Task(100));//提交新的任务System.out.println(tpe.getQueue());tpe.shutdown();//关闭线程池} } 5、TPE型线程池2:SingleThreadPool 单例线程池(只有一个线程) 为什么有单例线程池 有任务队列,有线程池管理机制 Executors(注意这后面有s) 它可以说是线程池工厂类,我们一般通过它创建线程池,并且它为我们封装了线程 看看下面哪里用到了它 /创建单例线程池,扔5个任务进去,查看输出结果,看看有几个线程执行任务/import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class T07_SingleThreadPool {public static void main(String[] args) {ExecutorService service = Executors.newSingleThreadExecutor();for(int i=0; i<5; i++) {final int j = i;service.execute(()->{System.out.println(j + " " + Thread.currentThread().getName());});} }} 6、TPE型线程池3:CachedPool 缓存,存储线程池 此线程池没有核心线程,来一个任务启动一个线程(最多Integer.MaxValue,不会放在任务队列,因为任务队列容量为0),每个线程空闲后,只能活60s 实例 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class T07_SingleThreadPool {public static void main(String[] args) {ExecutorService service = Executors.newSingleThreadExecutor();//通过Executors获取池子for(int i=0; i<5; i++) {final int j = i;service.execute(()->{//提交任务System.out.println(j + " " + Thread.currentThread().getName());});}service.shutdown();} } 7、TPE型线程池4:FixedThreadPool 固定线程池 此线次池,用于创建一个固定线程数量的线程池,不会回收 实例 import java.util.ArrayList;import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;public class T09_FixedThreadPool {public static void main(String[] args) throws InterruptedException, ExecutionException {//并发执行long start = System.currentTimeMillis();getPrime(1, 200000); long end = System.currentTimeMillis();System.out.println(end - start);//输出并发执行耗费时间final int cpuCoreNum = 4;//并行执行ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);MyTask t1 = new MyTask(1, 80000); //1-5 5-10 10-15 15-20MyTask t2 = new MyTask(80001, 130000);MyTask t3 = new MyTask(130001, 170000);MyTask t4 = new MyTask(170001, 200000);Future<List<Integer>> f1 = service.submit(t1);Future<List<Integer>> f2 = service.submit(t2);Future<List<Integer>> f3 = service.submit(t3);Future<List<Integer>> f4 = service.submit(t4);start = System.currentTimeMillis();f1.get();f2.get();f3.get();f4.get();end = System.currentTimeMillis();System.out.println(end - start);//输出并行耗费时间}static class MyTask implements Callable<List<Integer>> {int startPos, endPos;MyTask(int s, int e) {this.startPos = s;this.endPos = e;}@Overridepublic List<Integer> call() throws Exception {List<Integer> r = getPrime(startPos, endPos);return r;} }static boolean isPrime(int num) {for(int i=2; i<=num/2; i++) {if(num % i == 0) return false;}return true;}static List<Integer> getPrime(int start, int end) {List<Integer> results = new ArrayList<>();for(int i=start; i<=end; i++) {if(isPrime(i)) results.add(i);}return results;} } 8、TPE型线程池5:ScheduledPool 预定,延时线程池 根据延时时间(隔多长时间后运行),排序,哪个线程先执行,用户只需要指定核心线程数量 此线程池返回的池对象,和提交任务方法都不一样,比较涉及到时间 import java.util.Random;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class T10_ScheduledPool {public static void main(String[] args) {ScheduledExecutorService service = Executors.newScheduledThreadPool(4);service.scheduleAtFixedRate(()->{//提交延时任务try {TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}, 0, 500, TimeUnit.MILLISECONDS);//指定延时时间和单位,第一个任务延时0毫秒,之后的任务,延时500毫秒} } 9、手写拒绝策略小例子 import java.util.concurrent.;public class T14_MyRejectedHandler {public static void main(String[] args) {ExecutorService service = new ThreadPoolExecutor(4, 4,0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6),Executors.defaultThreadFactory(),new MyHandler());//将手写拒绝策略传入}static class MyHandler implements RejectedExecutionHandler {//1、继承RejectedExecutionHandler@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {//2、重写方法//log("r rejected")//伪代码,表示通过log4j.log()报一下日志,拒绝的时间,线程名//save r kafka mysql redis//可以尝试保存队列//try 3 times //可以尝试几次,比如3次,重新去抢队列,3次还不行就丢弃if(executor.getQueue().size() < 10000) {//尝试条件,如果size>10000了,就执行拒绝策略//try put again();//如果小于10000,尝试将其放到队列中} }} } 10、ForkJoinPool线程池1:ForkJoinPool 前面我们讲过线程分为两大类,TPE和FJP ForkJoinPool(分解汇总任务(将任务细化,最后汇总结果),少量线程执行多个任务(子任务,TPE做不到先执行子任务),CPU密集型) 适合将大任务切分成多个小任务运行 两个方法,fork():分子任务,将子任务分配到线程池中 join():当前任务的计算结果,如果有子任务,等子任务结果返回后再汇总 下面实例实现,一百万个随机数求和,由两种方法实现,一种ForkJoinPool分任务并行,一种使用单线程做 import java.io.IOException;import java.util.Arrays;import java.util.Random;import java.util.concurrent.ForkJoinPool;import java.util.concurrent.RecursiveAction;import java.util.concurrent.RecursiveTask;public class T12_ForkJoinPool {//1000000个随机数求和static int[] nums = new int[1000000];//一堆数static final int MAX_NUM = 50000;//分任务时,每个任务的操作量不能多于50000个,否则就继续细分static Random r = new Random();//使用随机数将数组初始化static {for(int i=0; i<nums.length; i++) {nums[i] = r.nextInt(100);}System.out.println("---" + Arrays.stream(nums).sum()); //stream api 单线程就这么做,一个一个加}//分任务,需要继承,可以继承RecursiveAction(不需要返回值,一般用在不需要返回值的场景)或//RecursiveTask(需要返回值,我们用这个,因为我们需要最后获取求和结果)两个更好实现的类,//他俩继承与ForkJoinTaskstatic class AddTaskRet extends RecursiveTask<Long> {private static final long serialVersionUID = 1L;int start, end;AddTaskRet(int s, int e) {start = s;end = e;}@Overrideprotected Long compute() {if(end-start <= MAX_NUM) {//如果任务操作数小于规定的最大操作数,就进行运算,long sum = 0L;for(int i=start; i<end; i++) sum += nums[i];return sum;//返回结果} //如果分配的操作数大于规定,就继续细分(简单的重中点分,两半)int middle = start + (end-start)/2;//获取中间值AddTaskRet subTask1 = new AddTaskRet(start, middle);//传入起始值和中间值,表示一个子任务AddTaskRet subTask2 = new AddTaskRet(middle, end);//中间值和结尾值,表示一个子任务subTask1.fork();//分任务subTask2.fork();//分任务return subTask1.join() + subTask2.join();//最后返回结果汇总} }public static void main(String[] args) throws IOException {/ForkJoinPool fjp = new ForkJoinPool();AddTask task = new AddTask(0, nums.length);fjp.execute(task);/ForkJoinPool fjp = new ForkJoinPool();//创建线程池AddTaskRet task = new AddTaskRet(0, nums.length);//创建任务fjp.execute(task);//传入任务long result = task.join();//返回汇总结果System.out.println(result);//System.in.read();} } 11、ForkJoinPool线程池2:WorkStealingPool 任务偷取线程池 原来的线程池,都是有一个任务队列,而这个不同,它给每个线程都分配了一个任务队列 当某一个线程的任务队列没有任务,并且自己空闲,它就去其它线程的任务队列中偷任务,所以叫任务偷取线程池 细节:当线程自己从自己的任务队列拿任务时,不需要加锁,但是偷任务时,因为有两个线程,可能发生同步问题,需要加锁 此线程继承FJP 实例 import java.io.IOException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;public class T11_WorkStealingPool {public static void main(String[] args) throws IOException {ExecutorService service = Executors.newWorkStealingPool();System.out.println(Runtime.getRuntime().availableProcessors());service.execute(new R(1000));service.execute(new R(2000));service.execute(new R(2000));service.execute(new R(2000)); //daemonservice.execute(new R(2000));//由于产生的是精灵线程(守护线程、后台线程),主线程不阻塞的话,看不到输出System.in.read(); }static class R implements Runnable {int time;R(int t) {this.time = t;}@Overridepublic void run() {try {TimeUnit.MILLISECONDS.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(time + " " + Thread.currentThread().getName());} }} 12、流式API:ParallelStreamAPI 不懂的请参考:https://blog.csdn.net/grd_java/article/details/110265219 实例 import java.util.ArrayList;import java.util.List;import java.util.Random;public class T13_ParallelStreamAPI {public static void main(String[] args) {List<Integer> nums = new ArrayList<>();Random r = new Random();for(int i=0; i<10000; i++) nums.add(1000000 + r.nextInt(1000000));//System.out.println(nums);long start = System.currentTimeMillis();nums.forEach(v->isPrime(v));long end = System.currentTimeMillis();System.out.println(end - start);//使用parallel stream apistart = System.currentTimeMillis();nums.parallelStream().forEach(T13_ParallelStreamAPI::isPrime);//并行流,将任务切分成子任务执行end = System.currentTimeMillis();System.out.println(end - start);}static boolean isPrime(int num) {for(int i=2; i<=num/2; i++) {if(num % i == 0) return false;}return true;} } 13、总结 总结 Callable相当于一Runnable但是它有返回值 Future:存储执行完产生的结果 FutureTask 相当于Future+Runnable,既可以执行任务,又能获取任务执行的Future结果 CompletableFuture 可以多任务异步,并对多任务控制,整合任务结果,细化完美,比如可以一个任务完成就可以整合结果,也可以所有任务完成才整合结果 4、ThreadPoolExecutor源码解析 依然只讲重点,实际还需要大家按照上篇博客中看源码的方式来看 1、常用变量的解释 // 1. ctl,可以看做一个int类型的数字,高3位表示线程池状态,低29位表示worker数量private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));// 2. COUNT_BITS,Integer.SIZE为32,所以COUNT_BITS为29private static final int COUNT_BITS = Integer.SIZE - 3;// 3. CAPACITY,线程池允许的最大线程数。1左移29位,然后减1,即为 2^29 - 1private static final int CAPACITY = (1 << COUNT_BITS) - 1;// runState is stored in the high-order bits// 4. 线程池有5种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATEDprivate static final int RUNNING = -1 << COUNT_BITS;private static final int SHUTDOWN = 0 << COUNT_BITS;private static final int STOP = 1 << COUNT_BITS;private static final int TIDYING = 2 << COUNT_BITS;private static final int TERMINATED = 3 << COUNT_BITS;// Packing and unpacking ctl// 5. runStateOf(),获取线程池状态,通过按位与操作,低29位将全部变成0private static int runStateOf(int c) { return c & ~CAPACITY; }// 6. workerCountOf(),获取线程池worker数量,通过按位与操作,高3位将全部变成0private static int workerCountOf(int c) { return c & CAPACITY; }// 7. ctlOf(),根据线程池状态和线程池worker数量,生成ctl值private static int ctlOf(int rs, int wc) { return rs | wc; }/ Bit field accessors that don't require unpacking ctl. These depend on the bit layout and on workerCount being never negative./// 8. runStateLessThan(),线程池状态小于xxprivate static boolean runStateLessThan(int c, int s) {return c < s;}// 9. runStateAtLeast(),线程池状态大于等于xxprivate static boolean runStateAtLeast(int c, int s) {return c >= s;} 2、构造方法 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {// 基本类型参数校验if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();// 空指针校验if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;// 根据传入参数unit和keepAliveTime,将存活时间转换为纳秒存到变量keepAliveTime 中this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;} 3、提交执行task的过程 public void execute(Runnable command) {if (command == null)throw new NullPointerException();/ Proceed in 3 steps: 1. If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task. The call to addWorker atomically checks runState and workerCount, and so prevents false alarms that would add threads when it shouldn't, by returning false. 2. If a task can be successfully queued, then we still need to double-check whether we should have added a thread (because existing ones died since last checking) or that the pool shut down since entry into this method. So we recheck state and if necessary roll back the enqueuing if stopped, or start a new thread if there are none. 3. If we cannot queue task, then we try to add a new thread. If it fails, we know we are shut down or saturated and so reject the task./int c = ctl.get();// worker数量比核心线程数小,直接创建worker执行任务if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}// worker数量超过核心线程数,任务直接进入队列if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();// 线程池状态不是RUNNING状态,说明执行过shutdown命令,需要对新加入的任务执行reject()操作。// 这儿为什么需要recheck,是因为任务入队列前后,线程池的状态可能会发生变化。if (! isRunning(recheck) && remove(command))reject(command);// 这儿为什么需要判断0值,主要是在线程池构造方法中,核心线程数允许为0else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。// 这儿有3点需要注意:// 1. 线程池不是运行状态时,addWorker内部会判断线程池状态// 2. addWorker第2个参数表示是否创建核心线程// 3. addWorker返回false,则说明任务执行失败,需要执行reject操作else if (!addWorker(command, false))reject(command);} 4、addworker源码解析 private boolean addWorker(Runnable firstTask, boolean core) {retry:// 外层自旋for (;;) {int c = ctl.get();int rs = runStateOf(c);// 这个条件写得比较难懂,我对其进行了调整,和下面的条件等价// (rs > SHUTDOWN) || // (rs == SHUTDOWN && firstTask != null) || // (rs == SHUTDOWN && workQueue.isEmpty())// 1. 线程池状态大于SHUTDOWN时,直接返回false// 2. 线程池状态等于SHUTDOWN,且firstTask不为null,直接返回false// 3. 线程池状态等于SHUTDOWN,且队列为空,直接返回false// Check if queue empty only if necessary.if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;// 内层自旋for (;;) {int wc = workerCountOf(c);// worker数量超过容量,直接返回falseif (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;// 使用CAS的方式增加worker数量。// 若增加成功,则直接跳出外层循环进入到第二部分if (compareAndIncrementWorkerCount(c))break retry;c = ctl.get(); // Re-read ctl// 线程池状态发生变化,对外层循环进行自旋if (runStateOf(c) != rs)continue retry;// 其他情况,直接内层循环进行自旋即可// else CAS failed due to workerCount change; retry inner loop} }boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {w = new Worker(firstTask);final Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;// worker的添加必须是串行的,因此需要加锁mainLock.lock();try {// Recheck while holding lock.// Back out on ThreadFactory failure or if// shut down before lock acquired.// 这儿需要重新检查线程池状态int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {// worker已经调用过了start()方法,则不再创建workerif (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();// worker创建并添加到workers成功workers.add(w);// 更新largestPoolSize变量int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;} } finally {mainLock.unlock();}// 启动worker线程if (workerAdded) {t.start();workerStarted = true;} }} finally {// worker线程启动失败,说明线程池状态发生了变化(关闭操作被执行),需要进行shutdown相关操作if (! workerStarted)addWorkerFailed(w);}return workerStarted;} 5、线程池worker任务单元 private final class Workerextends AbstractQueuedSynchronizerimplements Runnable{/ This class will never be serialized, but we provide a serialVersionUID to suppress a javac warning./private static final long serialVersionUID = 6138294804551838833L;/ Thread this worker is running in. Null if factory fails. /final Thread thread;/ Initial task to run. Possibly null. /Runnable firstTask;/ Per-thread task counter /volatile long completedTasks;/ Creates with given first task and thread from ThreadFactory. @param firstTask the first task (null if none)/Worker(Runnable firstTask) {setState(-1); // inhibit interrupts until runWorkerthis.firstTask = firstTask;// 这儿是Worker的关键所在,使用了线程工厂创建了一个线程。传入的参数为当前workerthis.thread = getThreadFactory().newThread(this);}/ Delegates main run loop to outer runWorker /public void run() {runWorker(this);}// 省略代码...} 6、核心线程执行逻辑-runworker final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;// 调用unlock()是为了让外部可以中断w.unlock(); // allow interrupts// 这个变量用于判断是否进入过自旋(while循环)boolean completedAbruptly = true;try {// 这儿是自旋// 1. 如果firstTask不为null,则执行firstTask;// 2. 如果firstTask为null,则调用getTask()从队列获取任务。// 3. 阻塞队列的特性就是:当队列为空时,当前线程会被阻塞等待while (task != null || (task = getTask()) != null) {// 这儿对worker进行加锁,是为了达到下面的目的// 1. 降低锁范围,提升性能// 2. 保证每个worker执行的任务是串行的w.lock();// If pool is stopping, ensure thread is interrupted;// if not, ensure thread is not interrupted. This// requires a recheck in second case to deal with// shutdownNow race while clearing interrupt// 如果线程池正在停止,则对当前线程进行中断操作if ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();// 执行任务,且在执行前后通过beforeExecute()和afterExecute()来扩展其功能。// 这两个方法在当前类里面为空实现。try {beforeExecute(wt, task);Throwable thrown = null;try {task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);} } finally {// 帮助gctask = null;// 已完成任务数加一 w.completedTasks++;w.unlock();} }completedAbruptly = false;} finally {// 自旋操作被退出,说明线程池正在结束processWorkerExit(w, completedAbruptly);} } 本篇文章为转载内容。原文链接:https://blog.csdn.net/grd_java/article/details/113116244。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-07-21 16:19:45
328
转载
转载文章
...代码中,对第三方代码功能的复用依赖,往往通过直接进行库的引入实现,第三方代码独立而完整,也较容易进行整体更新;这是最简单的情况,只需要所有下游使用者保证仅使用官方版本,跟进官方更新即可;但在实践中很难如此贯彻,这是下节讨论的问题。 ·有些第三方发布的代码,模式就是需要被源码形式包含到其他项目中进行统一编译使用(例如腾讯的开源Json解析库RapidJSON,就是纯C++头文件形式)。在开源领域有如GPL等规约对此进行规范,下游开发者遵循协议,引用代码,强制或可选地显式保留其GPL声明,可以进行使用和更改。这样的源码依赖关系,结合规范化的changelog声明代码改动,侧面也是为开发过程中跟进考虑。但是一个成型的产品,比如企业自有的服务端底层产品、中间件,新版本的发版更新是复杂的过程,开发者在旧版本仍然“功能正常”的情况下往往倾向于不跟进新版本;而上游代码如果进行安全漏洞修复,通常也都只在其最新版本代码中改动,安全修复与功能迭代并存,如果没有类似Linux发行版社区的努力,旧版本代码完全没有干净的安全更新patch可用。 ·在特定场景下,有些开发实践可能不严格遵循开源代码协议限定,引入了GPL等协议保护的代码而不做声明(以规避相关责任),丢失了引入和版本的信息跟踪;在另一些场景下,可能存在对开源代码进行大刀阔斧的修改、剪裁、定制,以符合自身业务的极端需求,但是过多的修改、人员的迭代造成与官方代码严重的失同步,丧失可维护性。 ·更一般的情况是,在开发中,开发者个体往往心照不宣的存在对网上代码文件、代码片段的复制-粘贴操作。被参考的代码,可能有上述的开源代码,也可能有各种Github作者练手项目、技术博客分享的代码片段、正式开源项目仅用来说明用法的不完备示例代码。这些代码的引入完全无迹可寻,即便是作者自己也很难解释用了什么。这种情况下,上面两条认定的那些与官方安全更新失同步的问题同样存在,且引入了独特的风险:被借鉴的代码可能只是原作者随手写的、仅仅是功能成立的片段,甚至可能是恶意作者随意散布的有安全问题的代码。由此,问题进入了最大的发散空间。 在Synopsys下BLACKDUCK软件之前发布的《2018 Open Source Security and Risk Analysis Report》中分析,96%的应用中包含有开源组件和代码,开源代码在应用全部代码中的占比约为57%,78%的应用中在引用的三方开源代码中存在历史漏洞。也就是说,现在互联网上所有厂商开发的软件、应用,其开发人员自己写的代码都是一少部分,多数都是借鉴来的。而这还只是可统计、可追溯的;至于上面提到的非规范的代码引用,如果也纳入进来考虑,三方代码占应用中的比例会上升到多少?曾经有分析认为至少占80%,我们只期望不会更高。 Ⅱ. 从碎片到乱刃:OpenSSH在野后门一览 在进行基础软件梳理时,回忆到反病毒安全软件提供商ESET在2018年十月发布的一份白皮书《THE DARK SIDE OF THE FORSSHE: A landscape of OpenSSH backdoors》。其站在一个具有广泛用户基础的软件提供商角度,给出了一份分析报告,数据和结论超出我们对于当前基础软件使用全景的估量。以下以我的角度对其中一方面进行解读。 一些必要背景 SSH的作用和重要性无需赘言;虽然我们站在传统互联网公司角度,可以认为SSH是通往生产服务器的生命通道,但当前多样化的产业环境已经不止于此(如之前libssh事件中,不幸被我言中的,SSH在网络设备、IoT设备上(如f5)的广泛使用)。 OpenSSH是目前绝大多数SSH服务端的基础软件,有完备的开发团队、发布规范、维护机制,本身是靠谱的。如同绝大多数基础软件开源项目的做法,OpenSSH对漏洞有及时的响应,针对最新版本代码发出安全补丁,但是各大Linux发行版使用的有各种版本的OpenSSH,这些社区自行负责将官方开发者的安全补丁移植到自己系统搭载的低版本代码上。天空彩 白皮书披露的现状 如果你是一个企业的运维管理人员,需要向企业生产服务器安装OpenSSH或者其它基础软件,最简单的方式当然是使用系统的软件管理安装即可。但是有时候,出于迁移成本考虑,可能企业需要在一个旧版本系统上,使用较新版本的OpenSSL、OpenSSH等基础软件,这些系统不提供,需要自行安装;或者需要一个某有种特殊特性的定制版本。这时,可能会选择从某些rpm包集中站下载某些不具名第三方提供的现成的安装包,或者下载非官方的定制化源码本地编译后安装,总之从这里引入了不确定性。 这种不确定性有多大?我们粗估一下,似乎不应成为问题。但这份白皮书给我们看到了鲜活的数据。 ESET研究人员从OpenSSH的一次历史大规模Linux服务端恶意软件Windigo中获得启示,采用某种巧妙的方式,面向在野的服务器进行数据采集,主要是系统与版本、安装的OpenSSH版本信息以及服务端程序文件的一个特殊签名。整理一个签名白名单,包含有所有能搜索到的官方发布二进制版本、各大Linux发行版本各个版本所带的程序文件版本,将这些标定为正常样本进行去除。最终结论是: ·共发现了几百个非白名单版本的OpenSSH服务端程序文件ssh和sshd; ·分析这些样本,将代码部分完全相同,仅仅是数据和配置不同的合并为一类,且分析判定确认有恶意代码的,共归纳为 21个各异的恶意OpenSSH家族; ·在21个恶意家族中,有12个家族在10月份时完全没有被公开发现分析过;而剩余的有一部分使用了历史上披露的恶意代码样本,甚至有源代码; ·所有恶意样本的实现,从实现复杂度、代码混淆和自我保护程度到代码特征有很大跨度的不同,但整体看,目的以偷取用户凭证等敏感信息、回连外传到攻击者为主,其中有的攻击者回连地址已经存在并活跃数年之久; ·这些后门的操控者,既有传统恶意软件黑产人员,也有APT组织; ·所有恶意软件或多或少都在被害主机上有未抹除的痕迹。ESET研究者尝试使用蜜罐引诱出攻击者,但仍有许多未解之谜。这场对抗,仍未取胜。 白皮书用了大篇幅做技术分析报告,此处供细节分析,不展开分析,以下为根据恶意程序复杂度描绘的21个家族图谱: 问题思考 问题引入的可能渠道,我在开头进行了一点推测,主要是由人的原因切入的,除此以外,最可能的是恶意攻击者在利用各种方法入侵目标主机后,主动替换了目标OpenSSH为恶意版本,从而达成攻击持久化操作。但是这些都是止血的安全运维人员该考虑的事情;关键问题是,透过表象,这显露了什么威胁形式? 这个问题很好回答,之前也曾经反复说过:基础软件碎片化。 如上一章节简单提到,在开发过程中有各种可能的渠道引入开发者不完全了解和信任的代码;在运维过程中也是如此。二者互相作用,造成了软件碎片化的庞杂现状。在企业内部,同一份基础软件库,可能不同的业务线各自定制一份,放到企业私有软件仓库源中,有些会有人持续更新供自己产品使用,有些由系统软件基础设施维护人员单独维护,有些则可能是开发人员临时想起来上传的,他们自己都不记得;后续用到的这个基础软件的开发和团队,在这个源上搜索到已有的库,很大概率会倾向于直接使用,不管来源、是否有质量背书等。长此以往问题会持续发酵。而我们开最坏的脑洞,是否可能有黑产人员入职到内部,提交个恶意基础库之后就走人的可能?现行企业安全开发流程中审核机制的普遍缺失给这留下了空位。 将源码来源碎片化与二进制使用碎片化并起来考虑,我们不难看到一个远远超过OpenSSH事件威胁程度的图景。但这个问题不是仅仅靠开发阶段规约、运维阶段规范、企业内部管控、行业自查、政府监管就可以根除的,最大的问题归根结底两句话: 不可能用一场战役对抗持续威胁;不可能用有限分析对抗无限未知。 Ⅲ. 从自信到自省:RHEL、CentOS backport版本BIND漏洞 2018年12月20日凌晨,在备战冬至的软件供应链安全大赛决赛时,我注意到漏洞预警平台捕获的一封邮件。但这不是一个漏洞初始披露邮件,而是对一个稍早已披露的BIND在RedHat、CentOS发行版上特定版本的1day漏洞CVE-2018-5742,由BIND的官方开发者进行额外信息澄(shuǎi)清(guō)的邮件。 一些必要背景 关于BIND 互联网的一个古老而基础的设施是DNS,这个概念在读者不应陌生。而BIND“是现今互联网上最常使用的DNS软件,使用BIND作为服务器软件的DNS服务器约占所有DNS服务器的九成。BIND现在由互联网系统协会负责开发与维护参考。”所以BIND的基础地位即是如此,因此也一向被大量白帽黑帽反复测试、挖掘漏洞,其开发者大概也一直处在紧绷着应对的处境。 关于ISC和RedHat 说到开发者,上面提到BIND的官方开发者是互联网系统协会(ISC)。ISC是一个老牌非营利组织,目前主要就是BIND和DHCP基础设施的维护者。而BIND本身如同大多数历史悠久的互联网基础开源软件,是4个UCB在校生在DARPA资助下于1984年的实验室产物,直到2012年由ISC接管。 那么RedHat在此中是什么角色呢?这又要提到我之前提到的Linux发行版和自带软件维护策略。Red Hat Enterprise Linux(RHEL)及其社区版CentOS秉持着稳健的软件策略,每个大的发行版本的软件仓库,都只选用最必要且质量久经时间考验的软件版本,哪怕那些版本实在是老掉牙。这不是一种过分的保守,事实证明这种策略往往给RedHat用户在最新漏洞面前提供了保障——代码总是跑得越少,潜在漏洞越多。 但是这有两个关键问题。一方面,如果开源基础软件被发现一例有历史沿革的代码漏洞,那么官方开发者基本都只为其最新代码负责,在当前代码上推出修复补丁。另一方面,互联网基础设施虽然不像其上的应用那样爆发性迭代,但依然持续有一些新特性涌现,其中一些是必不可少的,但同样只在最新代码中提供。两个刚需推动下,各Linux发行版对长期支持版本系统的软件都采用一致的策略,即保持其基础软件在一个固定的版本,但对于这些版本软件的最新漏洞、必要的最新软件特性,由发行版维护者将官方开发者最新代码改动“向后移植”到旧版本代码中,即backport。这就是基础软件的“官宣”碎片化的源头。 讲道理,Linux发行版维护者与社区具有比较靠谱的开发能力和监督机制,backport又基本就是一些复制粘贴工作,应当是很稳当的……但真是如此吗? CVE-2018-5742漏洞概况 CVE-2018-5742是一个简单的缓冲区溢出类型漏洞,官方评定其漏洞等级moderate,认为危害不大,漏洞修复不积极,披露信息不多,也没有积极给出代码修复patch和新版本rpm包。因为该漏洞仅在设置DEBUG_LEVEL为10以上才会触发,由远程攻击者构造畸形请求造成BIND服务崩溃,在正常的生产环境几乎不可能具有危害,RedHat官方也只是给出了用户自查建议。 这个漏洞只出现在RHEL和CentOS版本7中搭载的BIND 9.9.4-65及之后版本。RedHat同ISC的声明中都证实,这个漏洞的引入原因,是RedHat在尝试将BIND 9.11版本2016年新增的NTA机制向后移植到RedHat 7系中固定搭载的BIND 9.9版本代码时,偶然的代码错误。NTA是DNS安全扩展(DNSSEC)中,用于在特定域关闭DNSSEC校验以避免不必要的校验失败的机制;但这个漏洞不需要对NTA本身有进一步了解。 漏洞具体分析 官方没有给出具体分析,但根据CentOS社区里先前有用户反馈的bug,我得以很容易还原漏洞链路并定位到根本原因。 若干用户共同反馈,其使用的BIND 9.9.4-RedHat-9.9.4-72.el7发生崩溃(coredump),并给出如下的崩溃时调用栈backtrace: 这个调用过程的逻辑为,在9 dns_message_logfmtpacket函数判断当前软件设置是否DEBUG_LEVEL大于10,若是,对用户请求数据包做日志记录,先后调用8 dns_message_totext、7 dns_message_sectiontotext、6 dns_master_rdatasettotext、5 rdataset_totext将请求进行按协议分解分段后写出。 由以上关键调用环节,联动RedHat在9.9.4版本BIND源码包中关于引入NTA特性的源码patch,进行代码分析,很快定位到问题产生的位置,在上述backtrace中的5,masterdump.c文件rdataset_totext函数。漏洞相关代码片段中,RedHat进行backport后,这里引入的代码为: 这里判断对于请求中的注释类型数据,直接通过isc_buffer_putstr宏对缓存进行操作,在BIND工程中自定义维护的缓冲区结构对象target上,附加一字节字符串(一个分号)。而漏洞就是由此产生:isc_buffer_putstr中不做缓冲区边界检查保证,这里在缓冲区已满情况下将造成off-by-one溢出,并触发了缓冲区实现代码中的assertion。 而ISC上游官方版本的代码在这里是怎么写的呢?找到ISC版本BIND 9.11代码,这里是这样的: 这里可以看到,官方代码在做同样的“附加一个分号”这个操作时,审慎的使用了做缓冲区剩余空间校验的str_totext函数,并额外做返回值成功校验。而上述提到的str_totext函数与RETERR宏,在移植版本的masterdump.c中,RedHat开发者也都做了保留。但是,查看代码上下文发现,在RedHat开发者进行代码移植过程中,对官方代码进行了功能上的若干剪裁,包括一些细分数据类型记录的支持;而这里对缓冲区写入一字节,也许开发者完全没想到溢出的可能,所以自作主张地简化了代码调用过程。 问题思考 这个漏洞本身几乎没什么危害,但是背后足以引起思考。 没有人在“借”别人代码时能不出错 不同于之前章节提到的那种场景——将代码文件或片段复制到自己类似的代码上下文借用——backport作为一种官方且成熟的做法,借用的代码来源、粘贴到的代码上下文,是具有同源属性的,而且开发者一般是追求稳定性优先的社区开发人员,似乎质量应该有足够保障。但是这里的关键问题是:代码总要有一手、充分的语义理解,才能有可信的使用保障;因此,只要是处理他人的代码,因为不够理解而错误使用的风险,只可能减小,没办法消除。 如上分析,本次漏洞的产生看似只是做代码移植的开发者“自作主张”之下“改错了”。但是更广泛且可能的情况是,原始开发者在版本迭代中引入或更新大量基础数据结构、API的定义,并用在新的特性实现代码中;而后向移植开发人员仅需要最小规模的功能代码,所以会对增量代码进行一定规模的修改、剪裁、还原,以此适应旧版本基本代码。这些过程同样伴随着第三方开发人员不可避免的“望文生义”,以及随之而来的风险。后向移植操作也同样助长了软件碎片化过程,其中每一个碎片都存在这样的问题;每一个碎片在自身生命周期也将有持续性影响。 多级复制粘贴无异于雪上加霜 这里简单探讨的是企业通行的系统和基础软件建设实践。一些国内外厂商和社区发布的定制化Linux发行版,本身是有其它发行版,如CentOS特定版本渊源的,在基础软件上即便同其上游发行版最新版本间也存在断层滞后。RedHat相对于基础软件开发者之间已经隔了一层backport,而我们则人为制造了二级风险。 在很多基础而关键的软件上,企业系统基础设施的维护者出于与RedHat类似的初衷,往往会决定自行backport一份拷贝;通过早年心脏滴血事件的洗礼,即暴露出来OpenSSL一个例子。无论是需要RHEL还没来得及移植的新版本功能特性,还是出于对特殊使用上下文场景中更高执行效率的追求,企业都可能自行对RHEL上基础软件源码包进行修改定制重打包。这个过程除了将风险幂次放大外,也进一步加深了代码的不可解释性(包括基础软件开发人员流动性带来的不可解释)。 Ⅳ. 从武功到死穴:从systemd-journald信息泄露一窥API误用 1月10日凌晨两点,漏洞预警平台爬收取一封漏洞披露邮件。披露者是Qualys,那就铁定是重型发布了。最后看披露漏洞的目标,systemd?这就非常有意思了。 一些必要背景 systemd是什么,不好简单回答。Linux上面软件命名,习惯以某软件名后带个‘d’表示后台守护管理程序;所以systemd就可以说是整个系统的看守吧。而即便现在描述了systemd是什么,可能也很快会落伍,因为其初始及核心开发者Lennart Poettering(供职于Red Hat)描述它是“永无开发完结完整、始终跟进技术进展的、统一所有发行版无止境的差异”的一种底层软件。笼统讲有三个作用:中央化系统及设置管理;其它软件开发的基础框架;应用程序和系统内核之间的胶水。如今几乎所有Linux发行版已经默认提供systemd,包括RHEL/CentOS 7及后续版本。总之很基础、很底层、很重要就对了。systemd本体是个主要实现init系统的框架,但还有若干关键组件完成其它工作;这次被爆漏洞的是其journald组件,是负责系统事件日志记录的看守程序。 额外地还想简单提一句Qualys这个公司。该公司创立于1999年,官方介绍为信息安全与云安全解决方案企业,to B的安全业务非常全面,有些也是国内企业很少有布局的方面;例如上面提到的涉及碎片化和代码移植过程的历史漏洞移动,也在其漏洞管理解决方案中有所体现。但是我们对这家公司粗浅的了解来源于其安全研究团队近几年的发声,这两年间发布过的,包括有『stack clash』、『sudo get_tty_name提权』、『OpenSSH信息泄露与堆溢出』、『GHOST:glibc gethostbyname缓冲区溢出』等大新闻(仅截至2017年年中)。从中可见,这个研究团队专门啃硬骨头,而且还总能开拓出来新的啃食方式,往往爆出来一些别人没想到的新漏洞类型。从这个角度,再联想之前刷爆朋友圈的《安全研究者的自我修养》所倡导的“通过看历史漏洞、看别人的最新成果去举一反三”的理念,可见差距。 CVE-2018-16866漏洞详情 这次漏洞披露,打包了三个漏洞: ·16864和16865是内存破坏类型 ·16866是信息泄露 ·而16865和16866两个漏洞组和利用可以拿到root shell。 漏洞分析已经在披露中写的很详细了,这里不复述;而针对16866的漏洞成因来龙去脉,Qualys跟踪的结果留下了一点想象和反思空间,我们来看一下。 漏洞相关代码片段是这样的(漏洞修复前): 读者可以先肉眼过一遍这段代码有什么问题。实际上我一开始也没看出来,向下读才恍然大悟。 这段代码中,外部信息输入通过buf传入做记录处理。输入数据一般包含有空白字符间隔,需要分隔开逐个记录,有效的分隔符包括空格、制表符、回车、换行,代码中将其写入常量字符串;在逐字符扫描输入数据字符串时,将当前字符使用strchr在上述间隔符字符串中检索是否匹配,以此判断是否为间隔符;在240行,通过这样的判断,跳过记录单元字符串的头部连续空白字符。 但是问题在于,strchr这个极其基础的字符串处理函数,对于C字符串终止字符'\0'的处理上有个坑:'\0'也被认为是被检索字符串当中的一个有效字符。所以在240行,当当前扫描到的字符为字符串末尾的NULL时,strchr返回的是WHITESPACE常量字符串的终止位置而非NULL,这导致了越界。 看起来,这是一个典型的问题:API误用(API mis-use),只不过这个被误用的库函数有点太基础,让我忍不住想是不是还会有大量的类似漏洞……当然也反思我自己写的代码是不是也有同样情况,然而略一思考就释然了——我那么笨的代码都用for循环加if判断了:) 漏洞引入和消除历史 有意思的是,Qualys研究人员很贴心地替我做了一步漏洞成因溯源,这才是单独提这个漏洞的原因。漏洞的引入是在2015年的一个commit中: 在GitHub中,定位到上述2015年的commit信息,这里commit的备注信息为: journald: do not strip leading whitespace from messages. Keep leading whitespace for compatibility with older syslog implementations. Also useful when piping formatted output to the logger command. Keep removing trailing whitespace. OK,看起来是一个兼容性调整,对记录信息不再跳过开头所有连续空白字符,只不过用strchr的简洁写法比较突出开发者精炼的开发风格(并不),说得过去。 之后在2018年八月的一个当时尚未推正式版的另一次commit中被修复了,先是还原成了ec5ff4那次commit之前的写法,然后改成了加校验的方式: 虽然Qualys研究者认为上述的修改是“无心插柳”的改动,但是在GitHub可以看到,a6aadf这次commit是因为有外部用户反馈了输入数据为单个冒号情况下journald堆溢出崩溃的issue,才由开发者有目的性地修复的;而之后在859510这个commit再次改动回来,理由是待记录的消息都是使用单个空格作为间隔符的,而上一个commit粗暴地去掉了这种协议兼容性特性。 如果没有以上纠结的修改和改回历史,也许我会倾向于怀疑,在最开始漏洞引入的那个commit,既然改动代码没有新增功能特性、没有解决什么问题(毕竟其后三年,这个改动的代码也没有被反映issue),也并非出于代码规范等考虑,那么这么轻描淡写的一次提交,难免有人为蓄意引入漏洞的嫌疑。当然,看到几次修复的原因,这种可能性就不大了,虽然大家仍可以保留意见。但是抛开是否人为这个因素,单纯从代码的漏洞成因看,一个传统但躲不开的问题仍值得探讨:API误用。 API误用:程序员何苦为难程序员 如果之前的章节给读者留下了我反对代码模块化和复用的印象,那么这里需要正名一下,我们认可这是当下开发实践不可避免的趋势,也增进了社会开发速度。而API的设计决定了写代码和用代码的双方“舒适度”的问题,由此而来的API误用问题,也是一直被当做单纯的软件工程课题讨论。在此方面个人并没有什么研究,自然也没办法系统地给出分类和学术方案,只是谈一下自己的经验和想法。 一篇比较新的学术文章总结了API误用的研究,其中一个独立章节专门分析Java密码学组件API误用的实际,当中引述之前论文认为,密码学API是非常容易被误用的,比如对期望输入数据(数据类型,数据来源,编码形式)要求的混淆,API的必需调用次序和依赖缺失(比如缺少或冗余多次调用了初始化函数、主动资源回收函数)等。凑巧在此方面我有一点体会:曾经因为业务方需要,需要使用C++对一个Java的密码基础中间件做移植。Java对密码学组件支持,有原生的JDK模块和权威的BouncyCastle包可用;而C/C++只能使用第三方库,考虑到系统平台最大兼容和最小代码量,使用Linux平台默认自带的OpenSSL的密码套件。但在开发过程中感受到了OpenSSL满满的恶意:其中的API设计不可谓不反人类,很多参数没有明确的说明(比如同样是表示长度的函数参数,可能在不同地方分别以字节/比特/分组数为计数单位);函数的线程安全没有任何解释标注,需要自行试验;不清楚函数执行之后,是其自行做了资源释放还是需要有另外API做gc,不知道资源释放操作时是否规规矩矩地先擦除后释放……此类问题不一而足,导致经过了漫长的测试之后,这份中间件才提供出来供使用。而在业务场景中,还会存在比如其它语言调用的情形,这些又暴露出来OpenSSL API误用的一些完全无从参考的问题。这一切都成为了噩梦;当然这无法为我自己开解是个不称职开发的指责,但仅就OpenSSL而言其API设计之恶劣也是始终被人诟病的问题,也是之后其他替代者宣称改进的地方。 当然,问题是上下游都脱不了干系的。我们自己作为高速迭代中的开发人员,对于二方、三方提供的中间件、API,又有多少人能自信地说自己仔细、认真地阅读过开发指南和API、规范说明呢?做过通用产品技术运营的朋友可能很容易理解,自己产品的直接用户日常抛出不看文档的愚蠢问题带来的困扰。对于密码学套件,这个问题还好办一些,毕竟如果在没有背景知识的情况下对API望文生义地一通调用,绝大多数情况下都会以抛异常形式告终;但还是有很多情况,API误用埋下的是长期隐患。 不是所有API误用情形最终都有机会发展成为可利用的安全漏洞,但作为一个由人的因素引入的风险,这将长期存在并困扰软件供应链(虽然对安全研究者、黑客与白帽子是很欣慰的事情)。可惜,传统的白盒代码扫描能力,基于对代码语义的理解和构建,但是涉及到API则需要预先的抽象,这一点目前似乎仍然是需要人工干预的事情;或者轻量级一点的方案,可以case by case地分析,为所有可能被误用的API建模并单独扫描,这自然也有很强局限性。在一个很底层可信的开发者还对C标准库API存在误用的现实内,我们需要更多的思考才能说接下来的解法。 Ⅴ. 从规则到陷阱:NASA JIRA误配置致信息泄露血案 软件的定义包括了代码组成的程序,以及相关的配置、文档等。当我们说软件的漏洞、风险时,往往只聚焦在其中的代码中;关于软件供应链安全风险,我们的比赛、前面分析的例子也都聚焦在了代码的问题;但是真正的威胁都来源于不可思议之处,那么代码之外有没有可能存在来源于上游的威胁呢?这里就借助实例来探讨一下,在“配置”当中可能栽倒的坑。 引子:发不到500英里以外的邮件? 让我们先从一个轻松愉快的小例子引入。这个例子初见于Linux中国的一篇译文。 简单说,作者描述了这么一个让人啼笑皆非的问题:单位的邮件服务器发送邮件,发送目标距离本地500英里范围之外的一律失败,邮件就像悠悠球一样只能飞出一定距离。这个问题本身让描述者感到尴尬,就像一个技术人员被老板问到“为什么从家里笔记本上Ctrl-C后不能在公司台式机上Ctrl-V”一样。 经过令人窒息的分析操作后,笔者定位到了问题原因:笔者作为负责的系统管理员,把SunOS默认安装的Senmail从老旧的版本5升级到了成熟的版本8,且对应于新版本诸多的新特性进行了对应配置,写入配置文件sendmail.cf;但第三方服务顾问在对单位系统进行打补丁升级维护时,将系统软件“升级”到了系统提供的最新版本,因此将Sendmail实际回退到了版本5,却为了软件行为一致性,原样保留了高版本使用的配置文件。但Sendmail并没有在大版本间保证配置文件兼容性,这导致很多版本5所需的配置项不存在于保留下来的sendmail.cf文件中,程序按默认值0处理;最终引起问题的就是,邮件服务器与接收端通信的超时时间配置项,当取默认配置值0时,邮件服务器在1个单位时间(约3毫秒)内没有收到网络回包即认为超时,而这3毫秒仅够电信号打来回飞出500英里。 这个“故事”可能会给技术人员一点警醒,错误的配置会导致预期之外的软件行为,但是配置如何会引入软件供应链方向的安全风险呢?这就引出了下一个重磅实例。 JIRA配置错误致NASA敏感信息泄露案例 我们都听过一个事情,马云在带队考察美国公司期间问Google CEO Larry Page自视谁为竞争对手,Larry的回答是NASA,因为最优秀的工程师都被NASA的梦想吸引过去了。由此我们显然能窥见NASA的技术水位之高,这样的人才团队大概至少是不会犯什么低级错误的。 但也许需要重新定义“低级错误”……1月11日一篇技术文章披露,NASA某官网部署使用的缺陷跟踪管理系统JIRA存在错误的配置,可分别泄漏内部员工(JIRA系统用户)的全部用户名和邮件地址,以及内部项目和团队名称到公众,如下: 问题的原因解释起来也非常简单:JIRA系统的过滤器和配置面板中,对于数据可见性的配置选项分别选定为All users和Everyone时,系统管理人员想当然地认为这意味着将数据对所有“系统用户”开放查看,但是JIRA的这两个选项的真实效果逆天,是面向“任意人”开放,即不限于系统登录用户,而是任何查看页面的人员。看到这里,我不厚道地笑了……“All users”并不意味着“All ‘users’”,意不意外,惊不惊喜? 但是这种字面上把戏,为什么没有引起NASA工程师的注意呢,难道这样逆天的配置项没有在产品手册文档中加粗标红提示吗?本着为JIRA产品设计找回尊严的态度,我深入挖掘了一下官方说明,果然在Atlassian官方的一份confluence文档(看起来更像是一份增补的FAQ)中找到了相关说明: 所有未登录访客访问时,系统默认认定他们是匿名anonymous用户,所以各种权限配置中的all users或anyone显然应该将匿名用户包括在内。在7.2及之后版本中,则提供了“所有登录用户”的选项。 可以说是非常严谨且贴心了。比较讽刺的是,在我们的软件供应链安全大赛·C源代码赛季期间,我们设计圈定的恶意代码攻击目标还包括JIRA相关的敏感信息的窃取,但是却想不到有这么简单方便的方式,不动一行代码就可以从JIRA中偷走数据。 软件的使用,你“配”吗? 无论是开放的代码还是成型的产品,我们在使用外部软件的时候,都是处于软件供应链下游的消费者角色,为了要充分理解上游开发和产品的真实细节意图,需要我们付出多大的努力才够“资格”? 上一章节我们讨论过源码使用中必要细节信息缺失造成的“API误用”问题,而软件配置上的“误用”问题则复杂多样得多。从可控程度上讨论,至少有这几种因素定义了这个问题: ·软件用户对必要配置的现有文档缺少了解。这是最简单的场景,但又是完全不可避免的,这一点上我们所有有开发、产品或运营角色经验的应该都曾经体会过向不管不顾用户答疑的痛苦,而所有软件使用者也可以反省一下对所有软件的使用是否都以完整细致的文档阅读作为上手的准备工作,所以不必多说。 ·软件拥有者对配置条目缺少必要明确说明文档。就JIRA的例子而言,将NASA工程师归为上一条错误有些冤枉,而将JIRA归为这条更加合适。在边角但重要问题上的说明通过社区而非官方文档形式发布是一种不负责任的做法,但未引发安全事件的情况下还有多少这样的问题被默默隐藏呢?我们没办法要求在使用软件之前所有用户将软件相关所有文档、社区问答实现全部覆盖。这个问题范围内一个代表性例子是对配置项的默认值以及对应效果的说明缺失。 ·配置文件版本兼容性带来的误配置和安全问题。实际上,上面的SunOS Sendmail案例足以点出这个问题的存在性,但是在真实场景下,很可能不会以这么戏剧性形式出现。在企业的系统运维中,系统的版本迭代常见,但为软件行为一致性,配置的跨版本迁移是不可避免的操作;而且软件的更新迭代也不只会由系统更新推动,还有大量出于业务性能要求而主动进行的定制化升级,对于中小企业基础设施建设似乎是一个没怎么被提及过的问题。 ·配置项组合冲突问题。尽管对于单个配置项可能明确行为与影响,但是特定的配置项搭配可能造成不可预知的效果。这完全有可能是由于开发者与用户在信息不对等的情况下产生:开发者认为用户应该具有必需的背景知识,做了用户应当具备规避配置冲突能力的假设。一个例子是,对称密码算法在使用ECB、CBC分组工作模式时,从密码算法上要求输入数据长度必须是分组大小的整倍数,但如果用户搭配配置了秘钥对数据不做补齐(nopadding),则引入了非确定性行为:如果密码算法库对这种组合配置按某种默认补齐方式操作数据则会引起歧义,但如果在算法库代码层面对这种组合抛出错误则直接影响业务。 ·程序对配置项处理过程的潜在暗箱操作。这区别于简单的未文档化配置项行为,仅特指可能存在的蓄意、恶意行为。从某种意义上,上述“All users”也可以认为是这样的一种陷阱,通过浅层次暗示,引导用户做出错误且可能引起问题的配置。另一种情况是特定配置组合情况下触发恶意代码的行为,这种触发条件将使恶意代码具有规避检测的能力,且在用户基数上具有一定概率的用户命中率。当然这种情况由官方开发者直接引入的可能性很低,但是在众包开发的情况下如果存在,那么扫描方案是很难检测的。 Ⅵ. 从逆流到暗流:恶意代码溯源后的挑战 如果说前面所说的种种威胁都是面向关键目标和核心系统应该思考的问题,那么最后要抛出一个会把所有人拉进赛场的理由。除了前面所有那些在软件供应链下游被动污染受害的情况,还有一种情形:你有迹可循的代码,也许在不经意间会“反哺”到黑色产业链甚至特殊武器中;而现在研究用于对程序进行分析和溯源的技术,则会让你陷入百口莫辩的境地。 案例:黑产代码模块溯源疑云 1月29日,猎豹安全团队发布技术分析通报文章《电信、百度客户端源码疑遭泄漏,驱魔家族窃取隐私再起波澜》,矛头直指黑产上游的恶意信息窃取代码模块,认定其代码与两方产品存在微妙的关联:中国电信旗下“桌面3D动态天气”等多款软件,以及百度旗下“百度杀毒”等软件(已不可访问)。 文章中举证有三个关键点。 首先最直观的,是三者使用了相同的特征字符串、私有文件路径、自定义内部数据字段格式; 其次,在关键代码位置,三者在二进制程序汇编代码层面具有高度相似性; 最终,在一定范围的非通用程序逻辑上,三者在经过反汇编后的代码语义上显示出明显的雷同,并提供了如下两图佐证(图片来源): 文章指出的涉事相关软件已经下线,对于上述样本文件的相似度试验暂不做复现,且无法求证存在相似、疑似同源的代码在三者中占比数据。对于上述指出的代码雷同现象,猎豹安全团队认为: 我们怀疑该病毒模块的作者通过某种渠道(比如“曾经就职”),掌握有中国电信旗下部分客户端/服务端源码,并加以改造用于制作窃取用户隐私的病毒,另外在该病毒模块的代码中,我们还发现“百度”旗下部分客户端的基础调试日志函数库代码痕迹,整个“驱魔”病毒家族疑点重重,其制作传播背景愈发扑朔迷离。 这样的推断,固然有过于直接的依据(例如三款代码中均使用含有“baidu”字样的特征注册表项);但更进一步地,需要注意到,三个样本在所指出的代码位置,具有直观可见的二进制汇编代码结构的相同,考虑到如果仅仅是恶意代码开发者先逆向另外两份代码后借鉴了代码逻辑,那么在面临反编译、代码上下文适配重构、跨编译器和选项的编译结果差异等诸多不确定环节,仍能保持二进制代码的雷同,似乎确实是只有从根本上的源代码泄漏(抄袭)且保持相同的开发编译环境才能成立。 但是我们却又无法做出更明确的推断。这一方面当然是出于严谨避免过度解读;而从另一方面考虑,黑产代码的一个关键出发点就是“隐藏自己”,而这里居然如此堂而皇之地照搬了代码,不但没有进行任何代码混淆、变形,甚至没有抹除疑似来源的关键字符串,如果将黑产视为智商在线的对手,那这里背后是否有其它考量,就值得琢磨了。 代码的比对、分析、溯源技术水准 上文中的安全团队基于大量样本和粗粒度比对方法,给出了一个初步的判断和疑点。那么是否有可能获得更确凿的分析结果,来证实或证伪同源猜想呢? 无论是源代码还是二进制,代码比对技术作为一种基础手段,在软件供应链安全分析上都注定仍然有效。在我们的软件供应链安全大赛期间,针对PE二进制程序类型的题目,参赛队伍就纷纷采用了相关技术手段用于目标分析,包括:同源性分析,用于判定与目标软件相似度最高的同软件官方版本;细粒度的差异分析,用于尝试在忽略编译差异和特意引入的混淆之外,定位特意引入的恶意代码位置。当然,作为比赛中针对性的应对方案,受目标和环境引导约束,这些方法证明了可行性,却难以保证集成有最新技术方案。那么做一下预言,在不计入情报辅助条件下,下一代的代码比对将能够到达什么水准? 这里结合近一年和今年内,已发表和未发表的学术领域顶级会议的相关文章来简单展望: ·针对海量甚至全量已知源码,将可以实现准确精细化的“作者归属”判定。在ACM CCS‘18会议上曾发表的一篇文章《Large-Scale and Language-Oblivious Code Authorship Identification》,描述了使用RNN进行大规模代码识别的方案,在圈定目标开发者,并预先提供每个开发者的5-7份已知的代码文件后,该技术方案可以很有效地识别大规模匿名代码仓库中隶属于每个开发者的代码:针对1600个Google Code Jam开发者8年间的所有代码可以实现96%的成功识别率,而针对745个C代码开发者于1987年之后在GitHub上面的全部公开代码仓库,识别率也高达94.38%。这样的结果在当下的场景中,已经足以实现对特定人的代码识别和跟踪(例如,考虑到特定开发人员可能由于编码习惯和规范意识,在时间和项目跨度上犯同样的错误);可以预见,在该技术方向上,完全可以期望摆脱特定已知目标人的现有数据集学习的过程,并实现更细粒度的归属分析,例如代码段、代码行、提交历史。 ·针对二进制代码,更准确、更大规模、更快速的代码主程序分析和同源性匹配。近年来作为一项程序分析基础技术研究,二进制代码相似性分析又重新获得了学术界和工业界的关注。在2018年和2019(已录用)的安全领域四大顶级会议上,每次都会有该方向最新成果的展示,如S&P‘2019上录用的《Asm2Vec: Boosting Static Representation Robustness for Binary Clone Search against Code Obfuscation and Compiler Optimization》,实现无先验知识的条件下的最优汇编代码级别克隆检测,针对漏洞库的漏洞代码检测可实现0误报、100%召回。而2018年北京HITB会议上,Google Project Zero成员、二进制比对工具BinDiff原始作者Thomas Dullien,探讨了他借用改造Google自家SimHash算法思想,用于针对二进制代码控制流图做相似性检测的尝试和阶段结果;这种引入规模数据处理的思路,也可期望能够在目前其他技术方案大多精细化而低效的情况下,为高效、快速、大规模甚至全量代码克隆检测勾出未来方案。 ·代码比对方案对编辑、优化、变形、混淆的对抗。近年所有技术方案都以对代码“变种”的检测有效性作为关键衡量标准,并一定程度上予以保证。上文CCS‘18论文工作,针对典型源代码混淆(如Tigress)处理后的代码,大规模数据集上可有93.42%的准确识别率;S&P‘19论文针对跨编译器和编译选项、业界常用的OLLVM编译时混淆方案进行试验,在全部可用的混淆方案保护之下的代码仍然可以完成81%以上的克隆检测。值得注意的是以上方案都并非针对特定混淆方案单独优化的,方法具有通用价值;而除此以外还有很多针对性的的反混淆研究成果可用;因此,可以认为在采用常规商用代码混淆方案下,即便存在隐藏内部业务逻辑不被逆向的能力,但仍然可以被有效定位代码复用和开发者自然人。 代码溯源技术面前的“挑战” 作为软件供应链安全的独立分析方,健壮的代码比对技术是决定性的基石;而当脑洞大开,考虑到行业的发展,也许以下两种假设的情景,将把每一个“正当”的产品、开发者置于尴尬的境地。 代码仿制 在本章节引述的“驱魔家族”代码疑云案例中,黑产方面通过某种方式获得了正常代码中,功能逻辑可以被自身复用的片段,并以某种方法将其在保持原样的情况下拼接形成了恶意程序。即便在此例中并非如此,但这却暴露了隐忧:将来是不是有这种可能,我的正常代码被泄漏或逆向后出现在恶意软件中,被溯源后扣上黑锅? 这种担忧可能以多种渠道和形式成为现实。 从上游看,内部源码被人为泄漏是最简单的形式(实际上,考虑到代码的完整生命周期似乎并没有作为企业核心数据资产得到保护,目前实质上有没有这样的代码在野泄漏还是个未知数),而通过程序逆向还原代码逻辑也在一定程度上可获取原始代码关键特征。 从下游看,则可能有多种方式将恶意代码伪造得像正常代码并实现“碰瓷”。最简单地,可以大量复用关键代码特征(如字符串,自定义数据结构,关键分支条件,数据记录和交换私有格式等)。考虑到在进行溯源时,分析者实际上不需要100%的匹配度才会怀疑,因此仅仅是仿造原始程序对于第三方公开库代码的特殊定制改动,也足以将公众的疑点转移。而近年来类似自动补丁代码搜索生成的方案也可能被用来在一份最终代码中包含有二方甚至多方原始代码的特征和片段。 基于开发者溯源的定点渗透 既然在未来可能存在准确将代码与自然人对应的技术,那么这种技术也完全可能被黑色产业利用。可能的忧患包括强针对性的社会工程,结合特定开发者历史代码缺陷的漏洞挖掘利用,联动第三方泄漏人员信息的深层渗透,等等。这方面暂不做联想展开。 〇. 没有总结 作为一场旨在定义“软件供应链安全”威胁的宣言,阿里安全“功守道”大赛将在后续给出详细的分解和总结,其意义价值也许会在一段时间之后才能被挖掘。 但是威胁的现状不容乐观,威胁的发展不会静待;这一篇随笔仅仅挑选六个侧面做摘录分析,可即将到来的趋势一定只会进入更加发散的境地,因此这里,没有总结。 本篇文章为转载内容。原文链接:https://blog.csdn.net/systemino/article/details/90114743。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-05 13:33:43
300
转载
Element-UI
...ue.js的数据绑定功能,实现了currentPage和total等变量与elpagination分页组件的实时同步。 Web Worker或Service Worker , Web Worker是浏览器提供的多线程技术,允许JavaScript在后台线程上运行脚本,独立于主线程执行计算密集型任务,以避免阻塞用户界面。在海量数据加载场景下,开发者可以利用Web Worker预加载或异步处理数据,提升用户体验。而Service Worker则是一种特殊的Web Worker,它可以拦截网络请求,离线缓存资源,并支持推送消息等功能,常用于实现离线应用、增强网页性能和提高数据加载速度。虽然文章中未直接提到Web Worker或Service Worker在elpagination分页组件的具体应用,但在实际项目中,它们可以为实现类似无缝翻页体验提供技术支持。
2023-07-21 09:36:26
537
幽谷听泉-t
转载文章
...avaServer Pages)虽然曾作为构建动态网页的一种强大工具被广泛使用,但随着Spring Boot、Vue.js、React等前后端分离框架的崛起,其使用场景逐渐发生了变化。然而,对于学习Web开发基础和理解MVC模式的学生及开发者来说,深入理解和掌握JSP的工作原理仍然具有重要意义。 近期,有开发者回顾了使用JSP实现用户注册功能的经典案例,并指出其中存在的局限性,如单一用户注册处理与日期计算逻辑的不足。实际上,在实际项目中,为解决这些问题,可以引入数据库存储多用户信息,并利用Java 8的LocalDate类进行精准的日期处理,以适应闰年和平年的变化。 此外,为了提升用户体验和系统性能,现今推荐采用RESTful API设计原则,通过Ajax异步提交表单数据,后端用Spring MVC或Spring Boot框架接收并处理请求,前端则采用现代化的JavaScript库(例如Vue.js或React)实现实时验证和数据显示。 同时,为了确保数据安全,除了基本的字符编码设置避免乱码问题外,还需要对密码进行加密处理,并考虑XSS跨站脚本攻击和CSRF跨站请求伪造等安全风险,这在传统的JSP开发中往往需要借助额外的安全库来完成,而在现代框架中已内置了丰富的安全机制。 总之,尽管文中提到的基于JSP的用户注册页面在当下已不作为主流开发实践,但它为我们提供了理解Web开发流程的基础模型,而文中提及的问题改进方案恰好体现了现代Web开发技术的发展趋势——注重用户体验、数据安全以及前后端分离架构的设计理念。因此,无论是重温经典技术还是紧跟时代步伐,这篇文章都为我们提供了一个有价值的思考视角。
2023-08-15 09:02:21
113
转载
转载文章
...、登录验证机制、数据解析异常等。这些问题的解决不仅有助于提升个人编程能力,更对了解反爬机制与合法合规的数据抓取有重要启示作用。 近期,关于网络爬虫技术的法律边界和道德规范引起了广泛关注。2022年,中国最高人民法院发布了《关于审理使用人脸识别技术处理个人信息相关民事案件适用法律若干问题的规定》,其中强调了在数据抓取过程中应尊重用户隐私权和个人信息安全。这意味着,在开发爬虫项目时,除了关注技术实现外,开发者还需严格遵守相关法律法规,确保数据来源的合法性。 另外,各大电商平台针对爬虫行为不断升级反爬策略,例如采用动态加载、加密参数、验证码等方式防止非授权抓取。在这种情况下,学习和研究如何通过模拟登录、设置合适的请求头(如User-Agent)、以及运用更高级的网络代理、IP池等手段绕过反爬机制,成为爬虫开发者必须掌握的技术要点。 与此同时,对于页面数据解析环节,诸如Jsoup这样的HTML解析库虽然强大易用,但在面对复杂多变的网页结构时,可能需要结合XPath或CSS选择器等更多工具进行精细化处理。此外,随着JavaScript渲染技术在现代网页中的广泛应用,传统的HTTP请求方式已无法满足部分动态加载内容的抓取需求,因此引入Selenium、Puppeteer等无头浏览器工具进行交互式爬虫开发已成为一种趋势。 总之,在深入学习和应用Java爬虫技术的同时,我们应当紧跟技术发展潮流,并时刻保持对法律、伦理及技术挑战的关注,以确保我们的爬虫项目既高效又合规。
2023-03-13 10:48:12
104
转载
JQuery
...动画和Ajax交互等功能。在本文中,开发者使用jQuery来为页面中的按钮元素绑定click事件,以便在用户点击按钮时执行相应的功能(例如弹出提示框)。 DOM元素加载完成 , DOM (Document Object Model) 是浏览器对HTML或XML文档的一种内部表示方式,允许通过JavaScript操作网页内容和结构。\ DOM元素加载完成\ 指的是网页中的HTML元素已被浏览器解析并构建成为可被JavaScript代码访问和操作的对象模型。在$(document).ready()函数中,当DOM元素加载完成后,jQuery会立即执行传递给它的回调函数,确保在此阶段设置的事件处理器可以正确绑定到指定的DOM元素上,如按钮myButton。 事件委托 , 在JavaScript和jQuery中,事件委托是一种将事件处理器绑定到父级元素而非直接绑定到子元素的技术。这样,即使子元素是动态生成的,也能触发该事件处理器。在本文的情境下,虽然未明确提到事件委托,但它是解决动态生成DOM元素点击事件无效问题的有效策略之一。事件委托利用了事件冒泡机制,当子元素触发事件时,事件会逐级向上冒泡至父级元素,在父级元素上设置的事件处理器能够捕获并响应这些事件,从而避免了为每个动态生成的子元素单独绑定事件的低效做法。
2023-03-10 18:35:11
148
码农
HTML
分页功能 , 在网站开发中,分页功能是一种用户界面设计技术,用于将大量连续或相关的内容划分为多个独立的页面展示给用户。当数据量超过单个页面可以承载时,通过设置分页,用户可以点击不同的页码或者导航按钮来查看不同部分的数据,避免了加载时间过长和滚动浏览的不便,提高了用户体验与内容定位效率。 前端HTML分页组件 , 前端HTML分页组件是网页开发中的一种UI元素,它由HTML、CSS和JavaScript等前端技术构建而成,负责实现用户在网页上切换不同数据页面的功能。该组件通常包括一系列可点击的页码或导航按钮,以及可能的状态指示(如当前页数、总页数),在用户触发分页操作后,会通过AJAX请求后台服务器获取对应页面的数据,并在前端进行动态更新。 后台数据分页逻辑处理 , 在Web应用开发中,后台数据分页逻辑处理是指服务器端根据客户端(前端)传来的页码及每页显示记录数量等参数,从数据库中筛选并返回相应数据的过程。例如,当用户点击第3页的分页链接时,前端会发送一个包含页码信息的请求到后台,后台接收到请求后执行SQL查询语句,只取出第3页需要展示的数据,然后将这些数据以JSON或其他格式返回给前端,从而实现用户对海量数据的逐页浏览。这一过程涉及到了前后端数据交互、数据库查询优化等方面的技术细节。
2023-07-10 13:52:04
610
数据库专家
站内搜索
用于搜索本网站内部文章,支持栏目切换。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
sort file.txt
- 对文本文件进行排序,默认按行排序。
推荐内容
推荐本栏目内的其它文章,看看还有哪些文章让你感兴趣。
2023-04-28
2023-08-09
2023-06-18
2023-04-14
2023-02-18
2023-04-17
2024-01-11
2023-10-03
2023-09-09
2023-06-13
2023-08-07
2023-03-11
历史内容
快速导航到对应月份的历史文章列表。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"