首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
116 阅读
2
nvim番外之将配置的插件管理器更新为lazy
85 阅读
3
2018总结与2019规划
65 阅读
4
PDF标准详解(五)——图形状态
43 阅读
5
为 MariaDB 配置远程访问权限
34 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
elisp
linux
文本编辑器
Java
反汇编
读书笔记
OLEDB
数据库编程
数据结构
Masimaro
累计撰写
319
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
319
篇与
的结果
2020-06-13
记一次失败的项目经历
最近因为疫情原因一直在家,已经有快半年没有更新博客了,最近返回公司上班之后,去年做的项目已经完结,虽然已成功交付用户使用,但是在我看来这仍然是失败项目,在这里我想回顾这些经历,算是给后面的自己一个警醒吧为何说这是一个失败的项目我一直认为这是一个失败的项目,原因有如下几点:项目为能如期交付,原定计划是在2月份交付并发行1.0稳定版,但是由于种种原因推迟到了6月1号,延期了快半年项目到后期难以维护,在后期代码复杂度上升了不止一个层次,给维护与扩展带来了不小的麻烦项目质量无法达到预期效果,在发布之时仍有部分隐藏bug,没有经过细致的测试,为了按时交付,很多测试工作都省略了,目前只进行了两轮测试。功能有些无法达到预期效果,当初为了赶进度很多不重要的功能也是能省则省,有些需求并没有很好的实现项目中存在的一些问题前期需求设计不合理:早期立项的时候,我参考了许多类似的产品、与相关同事进行过探讨,但是由于经验不足,没有相关产品的开发与使用经验,导致许多需求设计不太合理,后续变更需求频繁,甚至出现过回炉重造的情况,这个主要责任在我这边,当时很多需求是我自己一拍脑门想起来觉得这样做可能符合客户需要,但是没有跟其他人进行商量,导致在后续实施时要么是在此基础上无法实施,或者成为鸡肋功能影响后续工作。后续需求变更频繁:后续需求变更频繁,很多时候老板看过项目之后觉得哪块不好会直接提出来,比如某些功能不合理,某些地方配色,页面布局不合理之类的。测试人员会在测试实际使用中告知他们想要某个功能,以便更方便的使用与测试。但是这些变更往往是在后期开发已经完成,正式进入测试流程中时提出,给后续开发与维护造成很多不必要的麻烦。这种情况的主要问题在于老板与测试人员在早期需求制定时参与过少,以及我本身对这方面不够专业。导致出现后期疯狂修改需求的情况需求记录不及时:在前期需求设计时我会详细记录需求的相关内容与演示效果,但是后续开发任务紧张,需求变更频繁,无法有充足的时间进行需求变更的记录,很多时候都是老板或者测试人员口述,然后由开发人员进行修改,没有合理的需求评审,没有详细的记录,有时候时间一长自己都给忘了当初是如何制定的。后续无法复盘架构设计问题:还是由于自己当初经验不足,当时考虑到用户可能需要二次开发,所以规定当时所有的功能都采用restful-API的形式,并且前端采用纯粹的ajax请求直接调用后台接口,但是有些功能确实不太适合做成接口,而且对于前端大家都不了解的情况下没有使用常用的前端界面框架,而纯粹采用自己编写的方式造成大量的时间浪费与遗留无法解决的bug。这些都给项目造成了不小的麻烦;没有详细设计:当初项目留给前期设计的时间并不充足,按照一般软件功能的流程来说,重要的时间应该留给前期设计,编码与测试只占极少数部分,而在这个项目进行过程中,完全颠倒过来了,不到一个月完成前期的需求与详细设计以及开发测试的分工,和接近3个月的开发与测试。导致的结果就是代码臃肿,很多公共功能没有抽象为具体的函数,重复代码过多,代码结构混乱无法进行后续维护。也就是说我们项目一开始就早就出了一个屎山。分工不合理:在前期设计与开发环境框架制定完成之后,进入到分工环节,在这个环节中出现分工不够合理的问题。主要体现在我自己不清楚员工的能力与擅长的部分,在制定计划时采用平均分配的方式没有考虑需求难度与员工自身的能力相结合。导致后续能力强的员工快速完成任务而存在空闲时间,而能力一般的往往会拖慢进度,或者对于困难需求实现的也是差强人意。或者出现能力强的员工去帮助能力一般的员工完成剩余需求,出现能者多劳但是无法多得的情况。测试与验收问题:在早期设计阶段并没有完整的测试计划,测试一直推迟到开发完成之后,在那个时候发现想要详细测试时间不够、测试出来的bug由于设计等原因难以修改、甚至出现某些地方使用不太合理又要新加需求的情况,这些都导致无限期的加班与代码修改,代码越改越乱,人心烦躁,开发与测试怨声载道。后续该如何改进培养自己的产品思维:早期需求制定的不合理,我自身有很大的原因,我由于本职工作是做开发的,很多时候在设计需求时采用的是开发者的思维方式,而没有站在用户角度,设计出来的系统在后续测试中会发现很难用,没有合理的引导,功能过于分散,常用功能操作不够简单,操作步骤过多等情况时有发生,为了用户必须得改需求。我想自己得加强这方面的素养,设计完成之后少改需求制定规范的需求管理制度:在需求制定、变更、实现、验收这些过程,在项目中没有与很好的得到解决和管理,造成很多需求后续无法查证,不合理的需求无法定位到具体的责任人,甚至谁都可以提需求。为了解决需求相关的问题,需要引入规范的需求管理制度,加入需求评审等操作,让需求更合理,更有迹可循。延长早期设计时间:在大学中学习软件工程相关课程时,我的老师告诉我,在项目开发中,编码只占很少一部分,而现在似乎反过来了,编码占据了时间的大头,而前期设计只是为了给开发人员安排工作而已。我想一个成功的项目应该是会在前期设计过程中下了很大的功夫的。可以在前期多进行相关会议,比如需求评审、针对需求进行测试用例的评审、开发框架与相关方案的评审、以及工作计划的合理安排等等。规范开发中的代码审查机制:员工的能力,与代码编写风格对项目的可维护性有巨大的影响,过去我一直觉得开发人员应该保留自己的个性,编写能体现能力的牛X的代码,但是经过几次与他人合作、带领团队开发项目之后,我改变了这个看法,作为底层的码农,为了项目的统一于可维护性,最好还是安心做一颗螺丝钉,多人合作不需要个性,一切为了项目才是正道。所以在后续如果还有项目需要我带队开发,我会统一编码格式与注释格式,像大厂那样制定编码规范,甚至细微到如何给变量、函数、类命名等等。最好每天下班前一次 code review,及时消除冗余代码、不规范的代码、不合理的功能,特别是同一个功能,多个人编写函数,函数的参数列表与实现完全不同的情况。测试与开发并行的机制:之前测试永远是等到所有开发任务完成之后进行,一旦项目完结,进入维护节点,要修改bug是相当困难,而且是牵一发而动全身的,测试应该与开发并行,在需求评审时应该做到给出需求验收标准与对应的测试用例,而且需要配合代码审查,提醒开发人员针对每一个功能函数编写单元测试。需求完成之后立即对照测试用例进行测试,保证每个需求都准确无误。更加合理的工作安排:合理安排工作,需要做到针对员工的能力和需求评审时得出的需求的难度来合理安排,合理安排包括合理的时间、合理的人员与合理的需求安排等等。这个可能没有相应的参考标准,只能根据经验来判断了。自己应该更加投入到这个项目中:由于我在公司待的时间较长,对公司业务比较熟悉,所以很多时候总有人会来问问题、商量某些事,我一贯又是一个老好人的态度,甚至有时候做到事无巨细都亲自动手。甚至公司主要产品也需要我来进行维护,而且由于项目人手不够,我也参与到项目的具体开发与测试工作之中,导致长时间都消耗在无意义的事情之上,无法专注于项目管理工作上。在后面项目需要做需求变更、更改开发计划时无法及时记录与评审;看到不合理的代码没有时间做code review、提取公共部分,重构部分代码,这些工作都由于没有时间而暂时搁置了。在后面项目越发的超出我的掌控。以上就是之前带队开发时出现的问题以及一些反思,如果后面还有机会作为项目的leader,我想尽量避免这些情况。更加专注于项目。制定相关制度,保证项目质量。
2020年06月13日
5 阅读
0 评论
0 点赞
2020-01-12
使用Python调用Nessus 接口实现自动化扫描
之前在项目中需要接入nessus扫描器,研究了一下nessus的api,现在将自己的成果分享出来。Nessus提供了丰富的二次开发接口,无论是接入其他系统还是自己实现自动化扫描,都十分方便。同时Nessus也提供了完备的API文档,可以在 Settings->My Account->API Keys->API documentation认证nessus提供两种认证方式,第一种采用常规的登录后获取token的方式,在[https://localhost:8834/api#/resources/session]()条目中可以找到这种方式,它的接口定义如下:POST /session { "username":{string}, "password":{string} }输入正确的用户名和密码,登录成功后会返回一个token{ "token": {string} }在后续请求中,将token放入请求头信息中请求头的key为X-Cookie,值为 token=xxxx,例如 :X-Cookie: token=5fa3d3fd97edcf40a41bb4dbdfd0b470ba45dde04ebc37f8;,下面是获取任务列表的例子import requests import json def get_token(ip, port, username, password): url = "https://{0}:{1}/session".format(ip, port) post_data = { 'username': username, 'password': password } respon = requests.post(url, data=post_data, verify=False) if response.status_code == 200: data = json.loads(response.text) return data["token"] def get_scan_list() # 这里ip和port可以从配置文件中读取或者从数据库读取,这里我省略了获取这些配置值得操作 url = "https://{ip}:{port}/scans".format(ip, port) token = get_token(ip, port, username, password) if token: header = { "X-Cookie":"token={0}".format(token), "Content-Type":"application/json" } response = requests.get(url, headers=header, verify=False) if response.status_code == 200: result = json.loads(respon.text) return result第二种方式是使用Nessus生成的API Key,这里我们可以依次点击 Settings->My Account->API Keys-->Generate按钮,生成一个key,后续使用时填入头信息中,还是以获取扫描任务列表作为例子def get_scan_list() accessKey = "XXXXXX" #此处填入真实的内容 secretKey = "XXXXXX" #此处填入真实内容 url = "https://{ip}:{port}/scans".format(ip, port) token = get_token(ip, port, username, password) if token: header = { 'X-ApiKeys': 'accessKey={accesskey};secretKey={secretkey}'.format(accesskey=accessKey, secretkey=secretKey) "Content-Type":"application/json" } response = requests.get(url, headers=header, verify=False) if response.status_code == 200: result = json.loads(respon.text) return result对比来看使用第二种明显方便一些,因此后续例子都采用第二种方式来呈现策略模板配置策略模板的接口文档在 [https://localhost:8834/api#/resources/policies]() 中。创建策略模板创建策略模板使用的是 策略模板的create接口,它里面有一个必须填写的参数 uuid 这个参数是一个uuid值,表示以哪种现有模板进行创建。在创建之前需要先获取系统中可用的模板。获取的接口是 /editor/{type}/templates,type 可以选填policy或者scan。这里我们填policy一般我们都是使用模板中的 Advanced 来创建,如下图下面是获取该模板uuid的方法,主要思路是获取系统中所有模板,然后根据模板名称返回对应的uuid值def get_nessus_template_uuid(ip, port, template_name = "advanced"): header = { 'X-ApiKeys': 'accessKey={accesskey};secretKey={secretkey}'.format(accesskey=accesskey, secretkey=secretkey), 'Content-type': 'application/json', 'Accept': 'text/plain'} api = "https://{ip}:{port}/editor/scan/templates".format(ip=ip, port=port) response = requests.get(api, headers=header, verify=False) templates = json.loads(response.text)['templates'] for template in templates: if template['name'] == template_name: return template['uuid'] return None有了这个id之后,下面来创建策略模板,这个接口的参数较多,但是很多参数都是选填项。这个部分文档写的很简陋,很多参数不知道是干嘛用的,当时我为了搞清楚每个参数的作用,一个个的尝试,然后去界面上看它的效果,最后终于把我感兴趣的给弄明白了。 它的主体部分如下:{ "uuid": {template_uuid}, "audits": { "feed": { "add": [ { "id": {audit_id}, "variables": { "1": {audit_variable_value}, "2": {audit_variable_value}, "3": {audit_variable_value} } } ] } }, "credentials": { "add": { {credential_category}: { {credential_name}: [ { {credential_input_name}: {string} } ] } } }, "plugins": { {plugin_family_name}: { "status": {string}, "individual": { {plugin_id}: {string} } } }, "scap": { "add": { {scap_category}: [ { {scap_input_name}: {string} } ] } }, "settings": { "acls": [ permission Resource ], //其他的减值对,这里我将他们都省略了 }他们与界面上配置的几个大项有对应关系,能对应的上的我给做了标记,但是有的部分对应不上。settings 是给策略模板做基础配置的,包括配置扫描的端口范围,服务检测范围等等。credentials 是配置登录扫描的,主要包括 windows、ssh、telnet等等plugins 配置扫描使用的插件,例如服务扫描版本漏洞等等在settings中,对应关系如下图所示下面是创建扫描策略模板的实际例子:def create_template(ip, port, **kwargs): # kwargs 作为可选参数,用来配置settings和其他项 header = { "X-ApiKeys": "accessKey={accesskey};secretKey={secretkey}".format(accesskey=accesskey, secretkey=secretkey), "Content-Type": "application/json", "Accept": "text/plain" } policys = {} # 这里 grouppolicy_set 存储的是策略模板中各个脚本名称以及脚本是否启用的信息 for policy in grouppolicy_set: enabled = "enabled" if policy.enable else "disabled" policys[policy.name] = { "status": enabled } # settings里面的各小项必须得带上,否则会创建不成功 "settings": { "name": template.name, "watchguard_offline_configs": "", "unixfileanalysis_disable_xdev": "no", "unixfileanalysis_include_paths": "", "unixfileanalysis_exclude_paths": "", "unixfileanalysis_file_extensions": "", "unixfileanalysis_max_size": "", "unixfileanalysis_max_cumulative_size": "", "unixfileanalysis_max_depth": "", "unix_docker_scan_scope": "host", "sonicos_offline_configs": "", "netapp_offline_configs": "", "junos_offline_configs": "", "huawei_offline_configs": "", "procurve_offline_configs": "", "procurve_config_to_audit": "Saved/(show config)", "fortios_offline_configs": "", "fireeye_offline_configs": "", "extremeos_offline_configs": "", "dell_f10_offline_configs": "", "cisco_offline_configs": "", "cisco_config_to_audit": "Saved/(show config)", "checkpoint_gaia_offline_configs": "", "brocade_offline_configs": "", "bluecoat_proxysg_offline_configs": "", "arista_offline_configs": "", "alcatel_timos_offline_configs": "", "adtran_aos_offline_configs": "", "patch_audit_over_telnet": "no", "patch_audit_over_rsh": "no", "patch_audit_over_rexec": "no", "snmp_port": "161", "additional_snmp_port1": "161", "additional_snmp_port2": "161", "additional_snmp_port3": "161", "http_login_method": "POST", "http_reauth_delay": "", "http_login_max_redir": "0", "http_login_invert_auth_regex": "no", "http_login_auth_regex_on_headers": "no", "http_login_auth_regex_nocase": "no", "never_send_win_creds_in_the_clear": "yes" if kwargs["never_send_win_creds_in_the_clear"] else "no", "dont_use_ntlmv1": "yes" if kwargs["dont_use_ntlmv1"] else "no", "start_remote_registry": "yes" if kwargs["start_remote_registry"] else "no", "enable_admin_shares": "yes" if kwargs["enable_admin_shares"] else "no", "ssh_known_hosts": "", "ssh_port": kwargs["ssh_port"], "ssh_client_banner": "OpenSSH_5.0", "attempt_least_privilege": "no", "region_dfw_pref_name": "yes", "region_ord_pref_name": "yes", "region_iad_pref_name": "yes", "region_lon_pref_name": "yes", "region_syd_pref_name": "yes", "region_hkg_pref_name": "yes", "microsoft_azure_subscriptions_ids": "", "aws_ui_region_type": "Rest of the World", "aws_us_east_1": "", "aws_us_east_2": "", "aws_us_west_1": "", "aws_us_west_2": "", "aws_ca_central_1": "", "aws_eu_west_1": "", "aws_eu_west_2": "", "aws_eu_west_3": "", "aws_eu_central_1": "", "aws_eu_north_1": "", "aws_ap_east_1": "", "aws_ap_northeast_1": "", "aws_ap_northeast_2": "", "aws_ap_northeast_3": "", "aws_ap_southeast_1": "", "aws_ap_southeast_2": "", "aws_ap_south_1": "", "aws_me_south_1": "", "aws_sa_east_1": "", "aws_use_https": "yes", "aws_verify_ssl": "yes", "log_whole_attack": "no", "enable_plugin_debugging": "no", "audit_trail": "use_scanner_default", "include_kb": "use_scanner_default", "enable_plugin_list": "no", "custom_find_filepath_exclusions": "", "custom_find_filesystem_exclusions": "", "reduce_connections_on_congestion": "no", "network_receive_timeout": "5", "max_checks_per_host": "5", "max_hosts_per_scan": "100", "max_simult_tcp_sessions_per_host": "", "max_simult_tcp_sessions_per_scan": "", "safe_checks": "yes", "stop_scan_on_disconnect": "no", "slice_network_addresses": "no", "allow_post_scan_editing": "yes", "reverse_lookup": "no", "log_live_hosts": "no", "display_unreachable_hosts": "no", "report_verbosity": "Normal", "report_superseded_patches": "yes", "silent_dependencies": "yes", "scan_malware": "no", "samr_enumeration": "yes", "adsi_query": "yes", "wmi_query": "yes", "rid_brute_forcing": "no", "request_windows_domain_info": "no", "scan_webapps": "no", "start_cotp_tsap": "8", "stop_cotp_tsap": "8", "modbus_start_reg": "0", "modbus_end_reg": "16", "hydra_always_enable": "yes" if kwargs["hydra_always_enable"] else "no", "hydra_logins_file": "" if kwargs["hydra_logins_file"] else kwargs["hydra_logins_file"], # 弱口令文件需要事先上传,后面会提到上传文件接口 "hydra_passwords_file": "" if kwargs["hydra_passwords_file"] else kwargs["hydra_passwords_file"], "hydra_parallel_tasks": "16", "hydra_timeout": "30", "hydra_empty_passwords": "yes", "hydra_login_as_pw": "yes", "hydra_exit_on_success": "no", "hydra_add_other_accounts": "yes", "hydra_postgresql_db_name": "", "hydra_client_id": "", "hydra_win_account_type": "Local accounts", "hydra_win_pw_as_hash": "no", "hydra_cisco_logon_pw": "", "hydra_web_page": "", "hydra_proxy_test_site": "", "hydra_ldap_dn": "", "test_default_oracle_accounts": "no", "provided_creds_only": "yes", "smtp_domain": "example.com", "smtp_from": "
[email protected]
", "smtp_to": "postmaster@[AUTO_REPLACED_IP]", "av_grace_period": "0", "report_paranoia": "Normal", "thorough_tests": "no", "detect_ssl": "yes", "tcp_scanner": "no", "tcp_firewall_detection": "Automatic (normal)", "syn_scanner": "yes", "syn_firewall_detection": "Automatic (normal)", "wol_mac_addresses": "", "wol_wait_time": "5", "scan_network_printers": "no", "scan_netware_hosts": "no", "scan_ot_devices": "no", "ping_the_remote_host": "yes", "tcp_ping": "yes", "icmp_unreach_means_host_down": "no", "test_local_nessus_host": "yes", "fast_network_discovery": "no", "arp_ping": "yes" if kwargs["arp_ping"] else "no", "tcp_ping_dest_ports": kwargs["tcp_ping_dest_ports"], "icmp_ping": "yes" if kwargs["icmp_ping"] else "no", "icmp_ping_retries": kwargs["icmp_ping_retries"], "udp_ping": "yes" if kwargs["udp_ping"] else "no", "unscanned_closed": "yes" if kwargs["unscanned_closed"] else "no", "portscan_range": kwargs["portscan_range"], "ssh_netstat_scanner": "yes" if kwargs["ssh_netstat_scanner"] else "no", "wmi_netstat_scanner": "yes" if kwargs["wmi_netstat_scanner"] else "no", "snmp_scanner": "yes" if kwargs["snmp_scanner"] else "no", "only_portscan_if_enum_failed": "yes" if kwargs["only_portscan_if_enum_failed"] else "no", "verify_open_ports": "yes" if kwargs["verify_open_ports"] else "no", "udp_scanner": "yes" if kwargs["udp_scanner"] else "no", "svc_detection_on_all_ports": "yes" if kwargs["svc_detection_on_all_ports"] else "no", "ssl_prob_ports": "Known SSL ports" if kwargs["ssl_prob_ports"] else "All ports", "cert_expiry_warning_days": kwargs["cert_expiry_warning_days"], "enumerate_all_ciphers": "yes" if kwargs["enumerate_all_ciphers"] else "no", "check_crl": "yes" if kwargs["check_crl"] else "no", } credentials = { "add": { "Host": { "SSH": [], "SNMPv3": [], "Windows": [], }, "Plaintext Authentication": { "telnet/rsh/rexec": [] } } } try: if kwargs["snmpv3_username"] and kwargs["snmpv3_port"] and kwargs["snmpv3_level"]: level = kwargs["snmpv3_level"] if level == NessusSettings.LOW: credentials["add"]["Host"]["SNMPv3"].append({ "security_level": "No authentication and no privacy", "username": kwargs["snmpv3_username"], "port": kwargs["snmpv3_port"] }) elif level == NessusSettings.MID: credentials["add"]["Host"]["SNMPv3"].append({ "security_level": "Authentication without privacy", "username": kwargs["snmpv3_username"], "port": kwargs["snmpv3_port"], "auth_algorithm": NessusSettings.AUTH_ALG[kwargs["snmpv3_auth"][1]], "auth_password": kwargs["snmpv3_auth_psd"] }) elif level == NessusSettings.HIGH: credentials["add"]["Host"]["SNMPv3"].append({ "security_level": "Authentication and privacy", "username": kwargs["snmpv3_username"], "port": kwargs["snmpv3_port"], "auth_algorithm": NessusSettings.AUTH_ALG[kwargs["snmpv3_auth"]][1], "auth_password": kwargs["snmpv3_auth_psd"], "privacy_algorithm": NessusSettings.PPIVACY_ALG[kwargs["snmpv3_hide"]][1], "privacy_password": kwargs["snmpv3_hide_psd"] }) if kwargs["ssh_username"] and kwargs["ssh_psd"]: credentials["add"]["Host"]["SSH"].append( { "auth_method": "password", "username": kwargs["ssh_username"], "password": kwargs["ssh_psd"], "elevate_privileges_with": "Nothing", "custom_password_prompt": "", }) if kwargs["windows_username"] and kwargs["windows_psd"]: credentials["add"]["Host"]["Windows"].append({ "auth_method": "Password", "username": kwargs["windows_username"], "password": kwargs["windows_psd"], "domain": kwargs["ssh_host"] }) if kwargs["telnet_username"] and kwargs["telnet_password"]: credentials["add"]["Plaintext Authentication"]["telnet/rsh/rexec"].append({ "username": kwargs["telnet_username"], "password": kwargs["telnet_password"] }) data = { "uuid": get_nessus_template_uuid(terminal, "advanced"), "settings": settings, "plugins": policys, "credentials": credentials } api = "https://{0}:{1}/policies".format(ip, port) response = requests.post(api, headers=header, data=json.dumps(data, ensure_ascii=False).encode("utf-8"), # 这里做一个转码防止在nessus端发生中文乱码 verify=False) if response.status_code == 200: data = json.loads(response.text) return data["policy_id"] # 返回策略模板的id,后续可以在创建任务时使用 else: return None策略还有copy、delete、config等操作,这里就不再介绍了,这个部分主要弄清楚各参数的作用,后面的这些接口使用的参数都是一样的任务任务部分的API 在[https://localhost:8834/api#/resources/scans]() 中创建任务创建任务重要的参数如下说明如下:uuid: 创建任务时使用的模板id,这个id同样是我们上面说的系统自带的模板idname:任务名称policy_id:策略模板ID,这个是可选的,如果要使用上面我们自己定义的扫描模板,需要使用这个参数来指定,并且设置上面的uuid为 custom 的uuid,这个值表示使用用户自定义模板;当然如果就想使用系统提供的,这个字段可以不填text_targets:扫描目标地址,这个参数是一个数组,可以填入多个目标地址,用来一次扫描多个主机创建任务的例子如下:def create_task(task_name, policy_id, hosts): # host 是一个列表,存放的是需要扫描的多台主机 uuid = get_nessus_template_uuid(terminal, "custom") # 获取自定义策略的uuid if uuid is None: return False data = {"uuid": uuid, "settings": { "name": name, "policy_id": policy_id, "enabled": True, "text_targets": hosts, "agent_group_id": [] }} header = { 'X-ApiKeys': 'accessKey={accesskey};secretKey={secretkey}'.format(accesskey=accesskey, secretkey=secretkey), 'Content-type': 'application/json', 'Accept': 'text/plain'} api = "https://{ip}:{port}/scans".format(ip=terminal.ip, port=terminal.port) response = requests.post(api, headers=header, data=json.dumps(data, ensure_ascii=False).encode("utf-8"), verify=False) if response.status_code == 200: data = json.loads(response.text) if data["scan"] is not None: scan = data["scan"] # 新增任务扩展信息记录 return scan["id"] # 返回任务id启动/停止任务启动任务的接口为 POST /scans/{scan_id}/launch scan_id 是上面创建任务返回的任务ID, 它有个可选参数 alt_targets,如果这个参数被指定,那么该任务可以扫描这个参数中指定的主机,而之前创建任务时指定的主机将被替代停止任务的接口为: POST /scans/{scan_id}/stop下面给出启动和停止任务的方法def start_task(task_id, hosts): header = { 'X-ApiKeys': 'accessKey={accesskey};secretKey={secretkey}'.format(accesskey=accesskey, secretkey=secretkey), 'Content-type': 'application/json', 'Accept': 'text/plain'} data = { "alt_targets": [hosts] # 重新指定扫描地址 } api = "https://{ip}:{port}/scans/{scan_id}/launch".format(ip=ip, port=port, scan_id=scan_id) response = requests.post(api, data=data, verify=False, headers=header) if response.status_code != 200: return False else: return True def stop_task(task_id): header = { 'X-ApiKeys': 'accessKey={accesskey};secretKey={secretkey}'.format(accesskey=terminal.reserved1, secretkey=terminal.reserved2), 'Content-type': 'application/json', 'Accept': 'text/plain'} api = "https://{ip}:{port}/scans/{scan_id}/stop".format(ip=ip, port=port, task_id) response = requests.post(api, headers=header, verify=False) if response.status_code == 200 or response.status_code == 409: # 根据nessus api文档可以知道409 表示任务已结束 return True return False获取扫描结果使用接口 GET /scans/{scan_id} 可以获取最近一次扫描的任务信息,从接口文档上看,它还可以获取某次历史扫描记录的信息,如果不填这个参数,接口中会返回所有历史记录的id。如果不填历史记录id,那么会返回最近一次扫描到的漏洞信息,也就是说新扫描到的信息会把之前的信息给覆盖下面是返回信息的部分说明{ "info": { "edit_allowed": {boolean}, "status": {string}, //当前状态 completed 字符串表示结束,cancel表示停止 "policy": {string}, "pci-can-upload": {boolean}, "hasaudittrail": {boolean}, "scan_start": {string}, "folder_id": {integer}, "targets": {string}, "timestamp": {integer}, "object_id": {integer}, "scanner_name": {string}, "haskb": {boolean}, "uuid": {string}, "hostcount": {integer}, "scan_end": {string}, "name": {string}, "user_permissions": {integer}, "control": {boolean} }, "hosts": [ //按主机区分的漏洞信息 host Resource ], "comphosts": [ host Resource ], "notes": [ note Resource ], "remediations": { "remediations": [ remediation Resource ], "num_hosts": {integer}, "num_cves": {integer}, "num_impacted_hosts": {integer}, "num_remediated_cves": {integer} }, "vulnerabilities": [ vulnerability Resource //本次任务扫描到的漏洞信息 ], "compliance": [ vulnerability Resource ], "history": [ history Resource //历史扫描信息,可以从这个信息中获取历史记录的id ], "filters": [ filter Resource ] }这个信息里面vulnerabilities和host里面都可以拿到漏洞信息,但是 vulnerabilities中是扫描到的所有漏洞信息,而host则需要根据id再次提交请求,也就是需要额外一次请求,但它是按照主机对扫描到的漏洞进行了分类。而使用vulnerabilities则需要根据漏洞信息中的host_id 手工进行分类下面是获取任务状态的示例:def get_task_status(task_id): header = { "X-ApiKeys": "accessKey={accesskey};secretKey={secretkey}".format(accesskey=accesskey, secretkey=secretkey), "Content-Type": "application/json", "Accept": "text/plain" } api = "https://{ip}:{port}/scans/{task_id}".format(ip=ip, port=port, task_id=task_id) response = requests.get(api, headers=header, verify=False) if response.status_code != 200: return 2, "Data Error" data = json.loads(response.text) hosts = data["hosts"] for host in hosts: get_host_vulnerabilities(scan_id, host["host_id"]) # 按主机获取漏洞信息 if data["info"]["status"] == "completed" or data["info"]["status"] =='canceled': # 已完成,此时更新本地任务状态 return 1, "OK"获取漏洞信息在获取任务信息中,已经得到了本次扫描中发现的弱点信息了,只需要我们解析这个json。它具体的内容如下:"host_id": {integer}, //主机id "host_index": {string}, "hostname": {integer},//主机名称 "progress": {string}, //扫描进度 "critical": {integer}, //危急漏洞数 "high": {integer}, //高危漏洞数 "medium": {integer}, //中危漏洞数 "low": {integer}, //低危漏洞数 "info": {integer}, //相关信息数目 "totalchecksconsidered": {integer}, "numchecksconsidered": {integer}, "scanprogresstotal": {integer}, "scanprogresscurrent": {integer}, "score": {integer}根据主机ID可以使用 GET /scans/{scan_id}/hosts/{host_id} 接口获取主机信息,它需要两个参数,一个是扫描任务id,另一个是主机id。下面列举出来的是返回值得部分内容,只列举了我们感兴趣的部分:{ "info": { "host_start": {string}, "mac-address": {string}, "host-fqdn": {string}, "host_end": {string}, "operating-system": {string}, "host-ip": {string} }, "vulnerabilities": [ { "host_id": {integer}, //主机id "hostname": {string}, //主机名称 "plugin_id": {integer}, //策略id "plugin_name": {string}, //策略名称 "plugin_family": {string}, //所属策略组 "count": {integer}, //该种漏洞数 "vuln_index": {integer}, "severity_index": {integer}, "severity": {integer} } ], }根据上面获取任务信息中得到的主机id和任务id,我们可以实现这个功能def get_host_vulnerabilities(scan_id, host_id): header = { "X-ApiKeys": "accessKey={accesskey};secretKey={secretkey}".format(accesskey=accesskey, secretkey=secretkey), "Content-Type": "application/json", "Accept": "text/plain" } scan_history = ScanHistory.objects.get(id=scan_id) api = "https://{ip}:{port}/scans/{task_id}/hosts/{host_id}".format(ip=ip, port=port, task_id=scan_id, host_id=host_id) response = requests.get(api, headers=header, verify=False) if response.status_code != 200: return 2, "Data Error" data = json.loads(response.text) vulns = data["vulnerabilities"] for vuln in vulns: vuln_name = vuln["plugin_name"] plugin_id = vuln["plugin_id"] #插件id,可以获取更详细信息,包括插件自身信息和扫描到漏洞的解决方案等信息 #保存漏洞信息获取漏洞输出信息与漏洞知识库信息我们在nessus web页面中可以看到每条被检测到的漏洞在展示时会有输出信息和知识库信息,这些信息也可以根据接口来获取获取漏洞的知识库可以通过接口 GET /scans/{scan_id}/hosts/{host_id}/plugins/{plugin_id} , 它的路径为: [https://localhost:8834/api#/resources/scans/plugin-output]()它返回的值如下:{ "info": { "plugindescription": { "severity": {integer}, //危险等级,从info到最后的critical依次为1,2,3,4,5 "pluginname": {string}, "pluginattributes": { "risk_information": { "risk_factor": {string} }, "plugin_name": {string}, //插件名称 "plugin_information": { "plugin_id": {integer}, "plugin_type": {string}, "plugin_family": {string}, "plugin_modification_date": {string} }, "solution": {string}, //漏洞解决方案 "fname": {string}, "synopsis": {string}, "description": {string} //漏洞描述 }, "pluginfamily": {string}, "pluginid": {integer} } }, "output": [ plugin_output:{ "plugin_output": {string}, //输出信息 "hosts": {string}, //主机信息 "severity": {integer}, "ports": {} //端口信息 } ] }有了这些信息,我们可以通过下面的代码获取这些信息:def get_vuln_detail(scan_id, host_id, plugin_id) header = { "X-ApiKeys": "accessKey={accesskey};secretKey={secretkey}".format(accesskey=accesskey, secretkey=secretkey), "Content-Type": "application/json", "Accept": "text/plain" } api = "https://{ip}:{port}/scans/{scan_id}/hosts/{host_id}/plugins/{plugin_id}".format(ip=ip, port=port, scan_id=scan_id, host_id=host_id, plugin_id=plugin_id) response = requests.get(api, headers=header, verify=False) data = json.loads(response.text) outputs = data["outputs"] return outputs最后总结这篇文章我们主要介绍了nessus API从扫描设置到扫描任务创建、启动、停止、以及结果的获取的内容,当然nessus的api不止这些,但是最重要的应该是这些,如果能帮助各位解决手头上的问题自然是极好的,如果不能或者说各位朋友需要更细致的控制,可以使用浏览器抓包的方式来分析它的请求和响应包。在摸索时最好的两个帮手是浏览器 F12工具栏中的 network和nessus api文档页面上的test工具了。我们可以先按 f12 打开工具并切换到network,然后在页面上执行相关操作,观察发包即可发现该如何使用这些API,因为Nessus Web端在操作时也是使用API。如下图:或者可以使用文档中的test工具,例如下面是测试 获取插件输出信息的接口
2020年01月12日
4 阅读
0 评论
0 点赞
2020-01-01
2019年终总结与2020年展望
时光荏苒,岁月如梭。转眼间2019已经过去,来到了新的一年,回顾这一年很多目标没有达成,有些遗憾,成长似乎比原来少了很多。下面来仔细回顾一下过去一年的得失2019 年回顾在2019 年年初立下的flag似乎没有一个能很好的执行的,博客,读书计划,学习方面,似乎总体来说仍然在原地踏步。博客方面,最开始想的是一周4篇,希望通过输出博客的方式来总结经验,提高自己。但是后续在执行时似乎变了味道,为了产出而产出。特别是在学习总结Java相关内容的时候。关于Java的文章我感觉是自己写的最烂的,从时间上说,过去为了产出一篇博客,会先列提纲、考虑文章的结构,会考虑如何组织语言让自己写的内容更好懂,甚至会精心准备实例代码,画图等等,但是在Java部分我省略了这些过程,博客书写时间从2个小时以上下降到半个小时,虽然说数量上去了,但是质量堪忧,与我之前想的总结提高完全背道而驰。这个问题今年得改。学习方面今年并没有什么大的成就,从4月份开始学习Java,到现在仍然有许多内容没有学完,基础部分进度很快,但是在框架中我会体会一下具体的设计模式,Web编程方面我学会了Web中编程中的基础操作,cookie、session一般如何使用、jsp模板思想,反射、工厂模式。我觉的学习它最大的用处不是学会了一门语言,而是让我开始思考如何从C语言的面向过程到面向对象的转化,让我接触到之前一直懵懵懂懂的web开发方面。如果有机会我可能会单独写写从面向过程到面向对象的转化思想关于读书方面,19年似乎读书量较去年又减少了许多。具体多少我没有统计可能不到10本,很多书都只能读一个开头,然后长时间不读,前面的忘记了再看后面的就无法串联起来,结果就放弃了。最后是工作方面的回顾,在这方面我感觉现在每年都是一个循环,年前一般是去年招的员工离职,自己工作的重心放在了维护扫描器产品上,年中开启一个新项目,然后招人,我带项目,项目一般持续两个月,没完没了的加班,然后修改,交付,过年,年后似乎又重复这个循环。每年招新人都需要我花时间来讲解公司流程,公司产品,甚至教一些编程语言与开发方面的东西,每年都是这样;自己感觉已经有点厌烦了。工作方面似乎没有任何进步,今年做漏洞管理平台方面的内容,针对漏洞信息做增删改查、明年又做一个什么平台,然后再针对不同数据做增删改查,一个项目下来似乎我只会了增删改查。做出来的平台都是一两个用户,当作单机软件使用,完全对不起项目名称中的平台。现在自己的处境虽然不在外包公司,但是我自己的感觉跟在外包公司类似。永远是针对不同信息的增删改查,似乎总有写不完的业务层代码。但是如果说自己所做的工作一无是处也不是这样的。今年的项目相比去年的项目来说,我参与的更多,对新人的要求也严格了许多,之前搭的gitlab服务终于启用起来了,并且尝试了单元测试。这些对我来说都是成长。在这次带领项目中,我学到了下面几点:有框架,有模板的,一定要用框架用模板。哪怕前期不动工。技术选型选的好会节约大量的时间。这次jQuery纯手写各种特效真的是给我热了很大的麻烦,如果早期选择一个好的模板,将节省大量调试前段的时间。敲定需求时要严谨,而且一旦定下来就不容更改。很多需求不是一拍脑袋想到就定下来的,必须得经过评审,是否可行,该如何去做,这些问题都得考虑,不然就得想我一样频繁改需求,导致所有人都得加班赶进度。我相信好的设计、好的产品应该做到让所有人节省工作时间,在单位时间内的产出更高2020年展望不管过的怎么样,总得面对现实,面对新来临的2020年,现在针对新的一年的展望如下:读书:这个是必须得坚持的,但是现在我觉得不应该硬性规定该读完多少本,我想的是现在应该坚持每天读半个小时书不再熬夜:这些年睡觉时间越来越晚,普遍超过12点才放下手机。现在在这里立下一个flag,11点以后放下手机,读半个小时书然后睡觉学习一门新的编程语言:在上面的部分也说过,学习Java最大的收获是完成了一个编程思维的转化,而之前读《黑客与画家》这本书时提到一个观点:编程语言的高度能决定一个人看问题的高度,之前一直不明白,现在似乎有点理解这个意思,而作者一直推崇lisp,所以明年的目标就是学习lisp,看看函数式编程语言能带给我怎样的收获开启一个新项目,之前吐槽了公司能学到的东西少,既然这样要么离开公司,要么自己想办法通过项目学习新内容。说实话我也很久没有自己独立做项目了,这里先立下一个flag,明年开启一个新项目,具体写什么东西还没有定目前能想到的就是这些,祝各位朋友心想事成,新年大吉吧
2020年01月01日
4 阅读
0 评论
0 点赞
2019-12-16
Mybatis框架
在之前的内容中,我写了Java的基础知识、Java Web的相关知识。有这些内容就可以编写各种各样丰富的程序。但是如果纯粹手写所有代码,工作量仍然很大。为了简化开发,隐藏一些不必要的细节,专心处理业务相关内容 ,Java提供了许多现成的框架可以使用Mybatis介绍在程序开发中讲究 MVC 的分层架构,其中M表示的是存储层,也就是与数据库交互的内容。一般来说使用jdbc时,需要经历:导入驱动、创建连接、创建statement对象,执行sql、获取结果集、封装对象、关闭连接这样几个过程。里面很多过程的代码都是固定的,唯一有变化的是执行sql并封装对象的操作。而封装对象时可以利用反射的机制,将返回字段的名称映射到Java实体类的各个属性上。这样我们很自然的就想到了,可以编写一个框架或者类库,实现仅配置sql语句和对应的映射关系,来实现查询到封装的一系列操作,从而简化后续的开发。Mybatis帮助我们实现了这个功能。Mybatis实例假设现在有一个用户表,存储用户的相关信息,我们现在需要使用mybatis来进行查询操作,可能要经历如下步骤:定义对应的实体类public class User { private Integer id; private String username; private String birthday; private char sex; private String address; //后面省略对应的getter和setter方法 //为了方便后面的实体类都会省略这些内容 } 编辑主配置文件,主要用来配置mybati的数据库连接信息以及指定对应dao的配置文件<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTDConfig3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--mybatis主配置文件--> <configuration> <!--配置环境--> <environments default="mybatis_demo"> <environment id="mybatis_demo"> <!--配置事务的类型--> <transactionManager type="JDBC"></transactionManager> <!--配置连接池--> <dataSource type="POOLED"> <!--配置数据库连接的4个基本信息--> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/> <property name="username" value="root"/> <property name="password" value="masimaro_root"/> </dataSource> </environment> </environments> <!--指定配置文件的位置,配置文件是每个dao独立的配置文件--> <mappers> <mapper resource="com/MybatisDemo/Dao/IUserDao.xml"></mapper> </mappers> </configuration>编写dao接口public interface IUserDao { public List<User> findAll(); }并提供dao的xml配置文件<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTDMapper3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!--每个函数配置一条,标签名是要进行的数据库操作,resultType是需要返回的数据类型--> <mapper namespace="com.MyBatisDemo.Dao.IUserDao"> <!--标签里面的文本是sql语句--> <select id="findAll" resultType="com.MyBatisDemo.domain.User"> select * from user; </select> </mapper>写完了对应的配置代码,接下来就是通过简单的几行代码来驱动mybatis,完成查询并封装的操作InputStream is = null; SqlSession = null; try { //加载配置文件 is = Resources.getResourceAsStream("dbconfig.xml"); //创建工厂对象 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); //创建sqlsession对象 sqlSession = factory.openSession(); //使用sqlsession对象创建dao接口的代理对象 IUserDao userDao = sqlSession.getMapper(IUserDao.class); //使用对象执行方法 List<User> users = this.userDao.findAll(); System.out.println(users); } catch (IOException e) { e.printStackTrace(); }finally{ // 清理资源 if (null != this.is){ try { this.sqlSession.commit(); this.is.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != this.sqlSession){ this.sqlSession.close(); } }mybatis大致的执行过程根据我们传入的InputStream对象来获取配置xml中对应对象的值接着根据配置信息创建连接并生成数据库的连接池对象根据配置文件中的mapper项获取到对应的Dao接口的配置文件,在读取该文件时会准备一个Map结构,其中key是mapper中的namespace + id,value是对应的sql语句,例如上述例子中得到的map结构为{"com.MyBatisDemo.Dao.IUserDao.findAll", "select * from user"}在创建sqlsession时从连接中获取到一个Statement对象在我们调用dao接口时,首先根据dao接口得到详细的类名,然后获取到当前调用的接口名称,由这两项得到一个key,比如在上述例子中,dao接口的名称为com.MyBatisDemo.Dao.IUserDao, 而调用的方法是 findAll,将这两个字符串进行拼接,得到一个key,根据这个key去map中查找对应的sql语句。并执行执行sql语句获取查询的结果集根据resultType中指定的对象进行封装并返回对应的实体类使用mybatis实现增删改查操作在之前的代码上可以看出,使用mybatis来实现功能时,只需要提供dao接口中的方法,并且将方法与对应的sql语句绑定。在提供增删改查的dao方法时如果涉及到需要传入参数的情况下该怎么办呢?下面以根据id查询内容为例:我们先在dao中提供这样一个方法:public User findById(int id);然后在dao的配置文件中编写sql语句<!--parameterType 表示传入的参数的类型--> <select id="findById" resultType="com.MyBatisDemo.domain.User" parameterType="int"> select * from user where id = #{id} </select>从上面的配置可以看到,mybatis中, 使用#{} 来表示输入参数,使用属性parameterType属性来表示输入参数的类型。一般如果使用Java内置对象是不需要使用全限定类名,也不区分大小写。当我们使用内置类型的时候,这里的id 仅仅起到占位符的作用,取任何名字都可以看完了使用内置对象的实例,再来看看使用使用自定义类类型的情况,这里我们使用update的例子来说明,首先与之前的操作一样,先定义一个upate的方法:void updateUser(User user);然后使用如下配置<update id="updateUser" parameterType="User"> update user set username=#{username}, birthday=#{birthday}, sex=#{sex}, address=#{address} where id = #{id} </update>与使用id查询的配置类似,当我们使用的是自定义类类型时,在对应的字段位置需要使用类的属性表示,在具体执行的时候,mybatis会根据传入的类对象来依据配置取出对应的属性作为sql语句的参数。上面在使用内置对象时我们说它可以取任何的名称,但是这里请注意 名称只能是自定义对象的属性名,而且区分大小写这里使用的都是确定的值,如果要使用模糊查询时该如何操作呢,这里我们按照名称来模糊查询,首先在dao中提供一个对应的方法User findByName(String name);接着再来进行配置<select resultType="com.MyBatisDemo.domain.User" parameterType="String"> select * from User where username like #{username} </select>从sql语句来看我们并没有实现模糊的方式,这时候在传入参数的时候就需要使用模糊的方式,调用时应该在参数中添加 %%, 就像这样 userDao.findByName("%" + username + "%")当然我们可以使用另外一种配置<select resultType="com.MyBatisDemo.domain.User" parameterType="String"> select * from User where username like %${username}% </select>这样我们在调用时就不需要额外添加 % 了。既然他们都可以作为参数,那么这两个符号有什么区别呢?区别在于他们进行查询的方式,$ 使用的是字符串拼接的方式来组成一个完成的sql语句进行查询,而#使用的是参数话查询的方式。一般来说拼接字符串容易造成sql注入的漏洞,为了安全一定要使用参数话查询的方式mybatis的相关标签resultMap标签在之前的配置中,其实一直保持着数据库表的字段名与对应的类属性名同名,但是有些时候我们不能保证二者同名,为了解决这问题也为了以后进行一对多和多对多的配置,可以使用resultMap来定义数据库表字段名和类属性名的映射关系下面是一个使用它的例子。我们简单修改一下User类的属性定义public class User { private Integer uid; private String name; private String userBirthday; private char userSex; private String userAddress; //后面省略对应的getter和setter方法 }这样直接使用之前的配置执行会报错,报找不到对应属性的错误,这个时候就可以使用resultMap属性来解决这个问题<resultMap id="UserMapper" type="User"> <id column="id" property="uid"></id> <result column="username" property="username"></result> <result column="sex" property="sex"></result> <result column="birthday" property="birthday"></result> <result column="address" property="address"></result> </resultMap> <select id="findAll" resultMap="UserMapper"> select * from user; </select>其中 id属性来唯一标示这个映射关系,在需要使用到这个映射关系的地方,使用resultMap这个属性来指定type属性表示要将这些值封装到哪个自定义的类类型中resultMap中有许多子标签用来表示这个映射关系id用来表明表结构中主键的映射关系result表示其他字段的映射关系每个标签中的column属性表示的是对应的表字段名标签中的property对应的是类属性的名称properties 标签properties标签可以用来定义数据库的连接属性,主要用于引入外部数据库连接属性的文件,这样我们可以通过直接修改连接属性文件而不用修改具体的xml配置文件。假设现在在工程中还有一个database.properties文件jdbc.driver ="com.mysql.jdbc.Driver" jdbc.url = "jdbc:mysql://localhost:3306/mybatis_demo" jdbc.username ="root" jdbc.password" ="masimaro_root"然后修改对应的主配置文件<!--引入properties文件--> <properties resource="database.properties"> </properties> <!--修改对应的dataSource标签--> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource>typeAliases 标签之前我们说过,使用内置类型时不需要写全限定类名,而且它不区分大小写。而使用自定义类型时需要写很长一串,如何使自定义类型与内置类型一样呢?这里可以使用typeAliases标签。它用来定义类名的别名<typeAliases> <!--typeAlias中来定义具体类的别名,type表示真实类名,alias表示别名--> <typeAlias type="com.MyBatisDemo.domain.User" alias="user"></typeAlias> </typeAliases>使用typeAlias标签时,每个类都需要提供一条对应的配置,当实体类多了,写起来就很麻烦了,这个时候可以使用package子标签来代替typeAlias<typeAliases> <package name="com.MyBatisDemo.domain"/> </typeAliases>它表示这里包中的所有类都使用别名,别名就是它的类名package标签在定义对应的mapper xml文件时,一个dao接口就需要一条配置。dao接口多了,一条条的写很麻烦,为了减轻编写的工作量可以使用package标签<mappers> <!--它表示这个包中的所有xml都是mapper配置文件--> <package name="com/MyBatis/Dao"/> </mappers>连接池在配置数据库连接 dataSource 标签中有一个type属性,它用来定义使用的连接池,该属性有三个取值:POOLE:使用连接池,采用javax.sql.DataSource 规范中的连接池,mybatis中有针对它的数据库连接池的实现UNPOOLED:与POOLED相同,使用的都是javax.sql.DataSource 规范,但是它使用的是常规的连接方式,没有采用池的思想JNDI:根据服务器提供的jndi基础来获取数据库的连接 ,具体获取到的连接对象又服务器提供动态sql当我们自己拼接sql的时候可以根据传入的参数的不同来动态生成不同的sql语句执行,而在之前的配置中,我们事先已经写好了使用的sql语句,但是如果碰上使用需要按照条件搜索,而且又不确定用户会输入哪些查询条件,在这样的情况下,没办法预先知道该怎么写sql语句。这种情况下可以使用mybatis中提供的动态sql假设我们提供一个findByValue的方法,根据值来进行查询。public List<User> findByValue(User user);事先并不知道user的哪些属性会被赋值,我们需要做的就是判断user的哪些属性不为空,根据这些不为空的属性来进行and的联合查询。这种情况下我们可以使用if标签<select id="findByValue" resultType="User" parameterType="User"> select * from user where <if test="id != null"> id = #{id} and </if> <if test="username != null"> username=#{username} and </if> ..... 1=1 </select>if标签中使用test来进行条件判断,而判断条件可以完全使用Java的语法来进行。这里在最后用了一个1=1的条件来结束判断,因为事先并不知道用户会传入哪些值,不知道哪条语句是最后一个条件,因此我们加一个恒成立的条件来确保sql语句的完整当然mybatis中也有办法可以省略最后的1=1,我们可以使用 where标签来包裹这些if,表明if中的所有内容都是作为查询条件的,这样mybatis在最后会在生成查询条件后自动帮助我们进行格式的整理使用if标签我们搞定了不确定用户会使用哪些查询条件的问题,如果有这样一个场景:用户只知道某个字段的名字有几种可能,我们在用户输入的几种可能值中进行查找,也就是说,用户可以针对同一个查询条件输入多个可能的值,根据这些个可能的值进行匹配,只要有一个值匹配上即可返回;针对这种情况没办法使用if标签了,我们可以使用循环标签,将用户输入的多个值依次迭代,最终组成一个in的查询条件我们在这里提供一个根据多个id查找用户的方法public List<User> findByIds(List<Integer> ids);这里我们为了方便操作,额外提供一个类用来存储查询条件public class QueryVo { List<Integer> ids; }<select id="findUserByIds" resultType="User" parameterType="QueryVo"> select * from user <where> <if test="ids != null and ids.size() != 0"> <foreach collection="ids" open="and id in (" close=")" item= "id" separator=","> ${id} </foreach> </if> </where> </select>在上面的例子中使用foreach来迭代容器其中使用collection表示容器,这里取的是parameterType中指定类的属性,open表示在迭代开始时需要加入查询条件的sql语句,close表示在迭代结束后需要添加到查询语句中的sql,item表示每个元素的变量名,separator表示每次迭代结束后要添加到查询语句中的字符串。当我们迭代完成后,整个sql语句就变成了这样: select * from user where 1=1 and id in (id1, id2, ...)多表查询一对多查询在现实中存在着这么一些一对多的对应关系,像什么学生和班级的对应关系,用户和账户的对应关系等等。关系型数据库在处理这种一对多的情况下,使用的是在多对应的那张表中添加一个外键,这个外键就是对应的一那张表的主键,比如说在处理用户和账户关系时,假设一个用户可以创建多个账户,那么在账户表中会有一个外键,指向的是用户表的ID在上面例子的基础之上,来实现一个一对多的关系。首先添加一个账户的实体类,并且根据关系账户中应该有一个唯一的用户类对象,用来表示它所属的用户public class Account { private int id; private int uid; private double money; private User user; }同时需要在User这个实体类上添加一个Account的列表对象,表示一个User下的多个Accountpublic class User { private Integer id; private String username; private String birthday; private char sex; private String address; private List<Account> accounts; }首先根据user来查询多个account,我们可以写出这样的sql语句来查询select u.*, a.id as aid, a.money, a.uid from user as u left join account as a on a.uid = u.id;那么它查询出来的结果字段名称应该是id, username, sex, birthday, address, aid, money, uid 这些,前面的部分可以封装为一个User对象,但是后面的部分怎么封装到Accounts中去呢,这里可以在resultMap中使用collection标签,该标签中对应的对象会被封装为一个容器。因此这里的配置可以写为:<resultMap id="UserAccountMap" type="user"> <id property="id" column="id"></id> <result property="username" column="username"></result> <result property="birthday" column="birthday"></result> <result property="sex" column="sex"></result> <result property="address" column="address"></result> <collection property="accounts" ofType="account"> <id property="id" column="aid"></id> <result property="money" column="money"></result> <result property="uid" column="uid"></result> </collection> </resultMap> <select id="findAll" resultMap="UserAccountMap"> select u.*, a.ID as aid, a.MONEY, a.UID from user as u left join acc ount as a on u.id = a.uid </select>我们需要一个resultMap来告诉Mybatis,这些多余的字段该怎么进行封装,为了表示一个容器,我们使用了一个coolection标签,标签中的property属性表示这个容器被封装到resultType对应类的哪个属性中,ofType表示的是,容器中每一个对象都是何种类型,而它里面的子标签的含义与resultMap子标签的含义完全相同从User到Account是一个多对多的关心,而从Account到User则是一个一对一的关系,当我们反过来进行查询时,需要使用的配置是 association 标签,它的配置与使用与collection相同<resultMap id="AccountUserMap" type="Account"> <id property="id" column="aid"></id> <result property="uid" column="uid"></result> <result property="money" column="money"></result> <association property="user" column="uid" javaType="user"> <id property="id" column="uid"></id> <result property="username" column="username"></result> <result property="birthday" column="birthday"></result> <result property="sex" column="sex"></result> <result property="address" column="address"></result> </association> </resultMap> <select id="findUserAccounts" resultType="Account" parameterType="User"> select * from account where uid = ${id} </select>多对多查询说完了一对多,再来说说多对多查询。多对多在关系型数据库中使用第三张表来体现,第三张表中记录另外两个表的主键作为它的外键。这里使用用户和角色的关系来演示多对多查询与之前一样,在两个实体类中新增对方的一个list对象,表示多对多的关系public class Role implements Serializable { private int id; private String roleName; private String roleDesc; private List<User> users; }利用之前一对多的配置,我们只需要修改一下ResultMap和sql语句就可以完成多对多的查询<mapper namespace="com.liuhao.Dao.IUserDao"> <resultMap id="UserRoleMapper" type="User"> <id property="id" column="id"></id> <result column="username" property="username"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> <result column="birthday" property="birthday"></result> <collection property="roles" ofType="role"> <id property="id" column="rid"></id> <result column="role_desc" property="roleDesc"></result> <result column="role_name" property="roleName"></result> </collection> </resultMap> <select id="findAll" resultMap="UserRoleMapper"> select user.*, role.ID as rid, role.ROLE_DESC, role.ROLE_NAME from u ser left outer join user_role on user_role.uid = user.id left OUTER join role on user_role.RID = role.ID </select> </mapper>另一个多对多的关系与这个类似,这里就不再单独说明了延迟加载之前说了该如何做基本的单表和多表查询。这里有一个问题,在多表查询中,我们是否有必要一次查询出它所关联的所有数据,就像之前的一对多的关系中,在查询用户时是否需要查询对应的账户,以及查询账户时是否需要查询它所对应的用户。如果不需要的话,我么采用上面的写法会造成多执行一次查询,而且当它关联的数据过多,而这些数据我们用不到,这个时候就会造成内存资源的浪费。这个时候我们需要考虑使用延迟加载,只有需要才进行查询。之前的sql语句一次会同时查询两张表,当然不满足延迟加载的要求,延迟加载应该将两张表的查询分开,先只查询需要的一张表数据,另一张表数据只在需要的时候查询。根据这点我们进行拆分,假设我们要针对User做延迟加载,我们先不管accounts的数据,只查询user表,可以使用sql语句select * from user, 在需要的时候执行select * from account where uid = id在xml配置中可以在collection标签中使用select属性,该属性指向一个方法,该方法的功能是根据id获取所有对象的列表。也就说我们需要在AccountDao接口中提供这么一个方法,并且编写它的xml配置public List<Account> findByUid(int uid);接着我们对之前的xml进行改写<resultMap id="UserMapper" type="User"> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="sex" property="sex"></result> <result column="birthday" property="birthday"></result> <result column="address" property="address"></result> <collection property="accounts" ofType="Account" select="com.liuhao.Dao.IAccountDao.findByUid" column="id"> </collection> </resultMap> <select id="findAll" resultMap="UserMapper"> select * from user; </select>完成了接口的编写与配置,还需要对主配置文件做一些配置,我们在主配置文件中添加settings节点,开启延迟加载<settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>缓存缓存用来存储一些不经常变化的内容,使用缓存可以减少查询数据库的次数,提高效率。mybatis有两种缓存,一种是在每个sqlsession中的缓存,一种是在每个SqlSessionFactory中的缓存在SqlSession中的缓存又被叫做是Mybatis的一级缓存。每当完成一次查询操作时,会在SqlSession中形成一个map结构,用来保存调用了哪个方法,以及方法返回的结果,下一次调用同样的方法时会优先从缓存中取当我们执行insert、update、delete等sql操作,或者执行SqlSession的close或者clearCache等方法时缓存会被清理在SqlSessionFactory中的缓存被称做二级缓存,所有由同一个SqlSessionFactory创建出来的SqlSessin共享同一个二级缓存。二级缓存是一个结果的二进制值,每当我们使用它时,它会取出这个二进制值,并将这个值封装为一个新的对象。在我们多次使用同一片二级缓存中的数据,得到的对象也不是同一个使用二级缓存需要进行一些额外的配置:在主配置文件中添加配置 在settings的子标签setting 中添加属性 enableCache=True开启二级缓存在对应的dao xml配置中添加 cache标签(标签中不需要任何属性或者文本内容),使接口支持缓存在对应的select、update等标签上添加属性 useCache=true,为方法开启二级缓存
2019年12月16日
3 阅读
0 评论
0 点赞
2019-12-07
EL表达式与JSTL
JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。JSTL支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签。 除了这些,它还提供了一个框架来使用集成JSTL的自定义标签。JSTL安装要使用jstl需要导入对应的库,可以去官方站点下载, 点击这里下载然后解压文件将得到的jar包放入到WEB-INF的lib中导入之后,在要使用它的jsp文件中使用taglib 导入库<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>prefix 是标签的前缀,类似于命名空间,在使用库中的标签时需要加上这个前缀常用标签if 标签if标签用来做判断,当条件成立时,执行标签体的内容,条件写在test属性中,注意:只有if标签而没有对应的else标签。下面是一个例子:<c:if test="${not empty requestScope.error}"> <div style="color:red;width:100%;" align = "center">${requestScope.error}</div> </c:if>上述这个例子表示,当服务器返回错误信息时,将错误信息显示到页面上choose 标签choose 标签相当于switch 语句,该标签中可以包含 when 和 otherwise 作为字标签,相当于switch语句中的case和default,例如下面的例子<p>当前薪水为 : <c:out value="${salary}"/></p> <c:choose> <c:when test="${salary <= 2000}"> 老板我是你爹, 这个工作谁爱干谁干 </c:when> <c:when test="${salary > 50000}"> 公司是我家,工作就是我的价值,我热爱工作 </c:when> <c:otherwise> 心中无半点波澜,甚至想提前下班 </c:otherwise> </c:choose>foreach 标签foreach 用来迭代容器中的元素,或者完成一些重复的操作。当使用foreach标签来进行重复性的操作时可以使用begin、end、var来控制循环,begin表示循环变量开始的值,end表示循环变量结束的值,与正常的for循环不同,循环变量的值可以等于end的值;使用var标签来定义循环变量的名称,使用step表示步进。例如:<c:foreach begin = "1" end = "10" var = "i step = "1"> ${i} <br /> </c:foreach>等价于for(int i = 1; i <= 10; i++){ System.out.println(i); }当使用 foreach来迭代容器时使用item和 var来迭代,其中item为需要迭代的容器,var表示获取到的容器中的元素。例如<c:foreach items = "list" var = "l"> ${l} </c:foreach>等价于for(String l:list){ System.out.println(l); }ELEL 表达式:Expression Language 表达式语言,用于替换和简化jsp页面中java代码的编写。EL 表达式使用 ${} 来表示jsp 默认支持el表达式,在page指令中可以使用 isELIgnored 来指定是否忽略jsp页面中的el表达式;当然也可以使用 \ 来作为转义符,表示 这个el表达式原样输出,例如 \${cookie}EL表达式中可以支持算数运算符、比较运算符、逻辑运算符合empty 空运算符;empty用于判断字符串、集合、数组对象是否为null或者长度为0。在使用el表达式时需要注意以下几点:el表达式只能从域对象中获取值el表达式中如果是类对象,可以根据Java Bean规范来获取属性值针对list这种有序集合可以使用 ${域对象.键名[索引].属性}针对Map集合,使用 ${域对象.键名.key名}或者 ${域对象.键名["key名"]}el 表达式中对域对象都做了重命名,pageScope 对应于 pageContext、requestScope对应于request、sessionScope对应于session、applicationScope对应于applicate(ServletContext)表达式${键名} 依次从最小的域中去查找对应的键值,直到找到为止
2019年12月07日
5 阅读
0 评论
0 点赞
2019-11-17
jsp
之前聊过用java处理web请求,处理cookie和session等等,但是唯独没有提及如何返回信息。作为一个web程序,肯定需要使用HTML作为用户界面,这个界面需要由服务端返回。返回信息可以使用HttpResponse中的OutputStream对象来写入数据。但是既要在里面写入HTML,又要写入相应的值,造成程序很难编写,同时HTML代码长了也不容易维护。我们需要一种机制来专门处理返回给浏览器的信息。JSP就是用来专门处理这种需求的。JSP概述JSP (Java Server Page):Java 服务端页面。是由 Sun Microsystems 公司倡导和许多公司参与共同创建的一种使软件开发者可以响应客户端请求,而动态生成 HTML、XML 或其他格式文档的Web网页的技术标准。jsp可以很方便的在页面中通过java代码嵌入动态页面JSP原理分析下面是一个简单的hello world程序<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>index</title> </head> <body> <% out.println("hello world"); %> </body> </html>我们将其部署到tomcat服务器上,启动并访问它之后会在tomcat所在目录的 work\Catalina\localhost\JSPDemo\org\apache\jsp (其中JSPDemo是项目名称), 在这个目录下面可以看到生成了一个index_jsp.java、index_jsp.class下面是这个jsp生成的部分源码package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { static { _jspx_imports_packages = new java.util.HashSet<>(); _jspx_imports_packages.add("javax.servlet"); _jspx_imports_packages.add("javax.servlet.http"); _jspx_imports_packages.add("javax.servlet.jsp"); _jspx_imports_classes = null; } public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); out.write("\n"); out.write("\n"); out.write("<html>\n"); out.write(" <head>\n"); out.write(" <title>index</title>\n"); out.write(" </head>\n"); out.write(" <body>\n"); out.write(" "); //这里是java代码的开始 out.println("hello world"); //这里是java代码的结尾 out.write("\n"); out.write(" </body>\n"); out.write("</html>\n"); } catch (java.lang.Throwable t) { //todo someting } finally { //todo someting } } }我们查询一下HttpJspBase这个类可以得到如下继承关系java.lang.Object | +--javax.servlet.GenericServlet | +--javax.servlet.http.HttpServlet | +--org.apache.jasper.runtime.HttpJspBase也就是说jsp本质上还是一个Servlet类,当我们第一次访问这个jsp页面时,服务器会根据jsp代码生成一个Servlet类的.java源码文件然后编译。对比jsp代码可以看得出来,在翻译的时候它逐行翻译,将html代码采用out.write进行输出,对应的java代码则原封不动的放在对应的位置。既然它是一个servlet,那么他的生命周期与相关注意事项就与Servlet相同了。jsp语法jsp确实简化了用户界面的编写,但是如果只知道原理,而不知道如何使用它仍然是白瞎,这部分来简单聊聊如何使用它jsp的代码主要放在3种标签中<% code %>: 这种格式中的代码,主要放的是要执行的java代码,它们最后会被解析到类的service方法中<%! code %>: 这种格式中的代码,主要包含的是成员变量的定义,它们最后会被解析到类的成员变量定义中<%= code %>: 这种格式中的代码,最终会被输出到页面上,会被解析到 out.print中进行输出下面我们对index.jsp进行改造,做一个简单的统计页面访问量的功能:<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>index</title> </head> <body> <%! private int totalVisited = 0; %> <% totalVisited++; %> <%= "客户请求次数:" + totalVisited %> </body> </html>然后再看看生成的java代码public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { private int totalVisited = 0; public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final java.lang.String _jspx_method = request.getMethod(); final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); out.write("\n"); out.write("\n"); out.write("<html>\n"); out.write(" <head>\n"); out.write(" <title>index</title>\n"); out.write(" </head>\n"); out.write(" <body>\n"); out.write(" "); out.write("\n"); out.write("\n"); out.write(" "); totalVisited++; out.write("\n"); out.write("\n"); out.write(" "); out.print( "客户请求次数:" + totalVisited ); out.write("\n"); out.write(" </body>\n"); out.write("</html>\n"); } }jsp内置对象我们在写jsp页面时关注的其实是Servlet的service 方法,谈及jsp内置对象的时候主要关注的是service中定义的相关变量,从生成的代码上来看,我们可以使用的是service方法中的输入参数request和response 再加它事先定义好的9个局部变量。它们的含义如下:HttpServletRequest request: 请求对象,之前在HttpServlet中已经了解了它该如何使用javax.servlet.jsp.PageContext pageContext: 页面的上下文,提供对JSP页面所有对象以及命名空间的访问。可以用它拿到request、cookie、session等一系列页面中可以访问到的对象HttpSession session: 当前会话ServletContext application: servlet上下文,我们可以拿到servlet的相关对象。比如获取当前servlet对象的名称,然后拼接一个路径。这样就不用考虑如何部署的问题JspWriter out: 输出对象HttpServletResponse response: HTTP响应对象Object page: 从定义和初始化值来看,它代表的是当前Servlet对象ServletConfig config: ServletConfig类的实例,获取当前servlet的配置信息Except: 当前异常,只有当jsp页面是错误页面是才能使用这个对象。其他的东西基本上用不上,这里也就不再介绍了。指令通过上面的相关知识点,现在已经能写相关的jsp代码了,但是既然本质上是servlet类,那么java其他的操作,比如导入相关库文件怎么办呢?这就需要用到对应的jsp指令。jsp指令放在 <%@ code %>中,jsp指令主要有3大类:page: 定义网页依赖属性,比如脚本语言、error页面、缓存需求等等include: 包含其他文件,可以利用这个属性事先抽取出页面的公共部分(比如页面的头部导航栏和页脚部分),最后再用include做拼接。taglib: 引入标签库的定义, 这个在使用jstl 和es表达式等第三方jsp扩展库的时候使用每条指令可以有多个属性,page 指令的相关属性如下:属性含义contentType等同于 response.setContentType方法,用于设置响应头的Content-Type属性pageEncoding设置jsp页面自身的编码方式language定义jsp脚本所使用的语言,目前只支持java 语言import导入java包errorPage当前页面发生异常后会自动跳转到指定错误页面isErrorPage标识当前页面是否是错误页面,错误页面中可以使用exception 对象,用来捕获异常include 指令的相关属性如下:属性含义file包含的文件路径taglib 的属性如下:属性含义prefix前缀,它们是自定义的,将来要用lib中的标签时用它作为前缀uri第三方库所在路径
2019年11月17日
5 阅读
0 评论
0 点赞
2019-11-03
Servlet 会话
在网络的七层模型中,会话层位于传输层之上,它定义如何开始、控制和结束一个会话。七层模式目前仅仅处于理论阶段,但是Web中借鉴了其中的一些思路。在Web中浏览器第一次发送请求到服务器开始直到一方断开为止算作一个会话。HTTP协议本身没有状态,那么Web服务如何知道这次请求是否在一个会话中呢?Web提供了Cookie和Session两种技术。服务器在第一次收到请求之后,会在HTTP响应头的Set-Cookie中,设置Cookie值,浏览器收到响应后,保存这个Cookie在本地。后续再进行请求的时候在HTTP的请求头中设置Cookie值,服务器根据此Cookie来识别请求的状态。Cookie值本身是一个键值对,例如 Cookie: name=value;Servlet 使用Cookie在Servlet中,使用Cookie的步骤如下:创建Cookie对象 new Cookie(String name, String value)发送cookie到浏览器 response.addCookie(Cookie)获取浏览器中发送过来的cookie request.getCookies() 返回所有Cookie遍历Cookies 获取所有cookie对象调用Cookie.getName(), Cookie.getValue()获取Cookie中的键和值使用的注意事项如下:一次可以返回多个Cookie,多次调用response.addCookie即可默认情况下浏览器关闭页面后cookie失效,但是可以设置cookie失效时间Cookie虽然可以用来识别一次会话,但是也不能滥用,第一Cookie是存储在浏览器端的,可以被伪造,一般做过爬虫自动登录的都这样干过,第二浏览器对于单个cookie大小有限制,一般是4kb。同时浏览器对于单个域名的cookie也有限制,默认是20个。由于cookie本身是类似于小饼干的小料,一般来说不会把小料作为主菜。SessionCookie一般作为小料,作为会话标识来说,用Session更为常见。与 Cookie相比Session存储在服务器端,Session没有cookie的那些限制。实现原理Session的实现是基于Cookie的。第一次调用request.getSession获取Session,没有Cookie 会在内存中创建一个新的Cookie对象,名称为JSESSION值是一个唯一的ID,作为session的唯一标识在给客户端响应时会包含一个cookie值,Set-Cookie: JSESSION=ID浏览器在下一次访问web中的其他资源时会将cookie作为请求头发送到服务器。服务器会从cookie中取出ID值,并根据ID从内存中查找对应的Session对象使用 HttpSession session = request.getSession(); 来获取一个Session对象函数列表Session 对象常用函数如下:public Object getAttribute(String name); //该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null。 public Enumeration getAttributeNames(); //该方法返回 String 对象的枚举,String 对象包含所有绑定到该 session 会话的对象的名称。 public long getCreationTime(); //该方法返回该 session 会话被创建的时间,自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。 public String getId(); //该方法返回一个包含分配给该 session 会话的唯一标识符的字符串。 public long getLastAccessedTime(); //该方法返回客户端最后一次发送与该 session 会话相关的请求的时间自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。 public int getMaxInactiveInterval(); //该方法返回 Servlet 容器在客户端访问时保持 session 会话打开的最大时间间隔,以秒为单位。 public void invalidate(); //该方法指示该 session 会话无效,并解除绑定到它上面的任何对象。 public boolean isNew(); //如果客户端还不知道该 session 会话,或者如果客户选择不参入该 session 会话,则该方法返回 true。 public void removeAttribute(String name); //该方法将从该 session 会话移除指定名称的对象。 public void setAttribute(String name, Object value); //该方法使用指定的名称绑定一个对象到该 session 会话。 public void setMaxInactiveInterval(int interval); //该方法在 Servlet 容器指示该 session 会话无效之前,指定客户端请求之间的时间,以秒为单位。
2019年11月03日
6 阅读
0 评论
0 点赞
2019-10-27
Servlet 常用类
Servlet 是一套标准的接口规范,当用户通过web请求来访问服务器时,由web容器根据配置调用我们实现的对应的servlet对象来提供服务。同时为了方便开发,servlet标准中也提供了许多常用的工具类,比如基本的Request 和Response对象以及其他要说到的常用的类。ServletRequest 对象Servlet接口中的service方法的定义如下:public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;从接口上来看,我们只能在service方法中使用这个对象。当用户请求到达服务器时,服务器会将标准的HTTP请求包封装为一个 ServletRequest对象,并调用service将该对象作为参数传入。也就是说这个对象是与HTTP协议息息相关的下面是它的一些常用方法//请求行相关方法 public String getMethod(); //返回请求的方法 public String getContextPath(); //获取请求的虚拟目录(或者项目所在目录) public String getServletPath(); //获取servlet路径 public String getQueryString();// 获取查询字符串,也就是提交请求url中参数部分 public String getRequestURI(); //获取请求的URI public StringBuffer getRequestURL(); //获取请求的URL public String getProtocol(); //获取协议版本 public String getRemoteAddr(); //获取客户端的ip //请求头相关方法 public String getHeader(String name); //以字符串的形式返回请求头 public java.util.Enumeration<E> getHeaderNames(); //返回所有请求头名称的枚举 public java.util.Enumeration<E> getHeaders(String name); //返回指定请求头中的所有值的枚举 //请求体相关方法 public java.io.BufferedReader getReader() throws java.io.IOException; //获取请求体的字符流 public ServletInputStream getInputStream() throws java.io.IOException; //获取请求体中的字节流一般来说get方法会将请求参数放入到url中,而post则是将其放入到请求体中,根据上面的方法要获得请求参数,需要判断请求是get还是post,如果是get则采用 getQueryString, 如果是post则采用 getReader等方法。为了解决这个问题,Servlet提供了一些通用的方法。public String getParameter(String name); //获取请求参数,根据传入的键名获得请求的键值 public java.util.Map<K, V> getParameterMap(); //获取所有请求参数,以map方式返回 public String[] getParameterValues(String name); //如果请求中一个键对应多个值,可以使用这个方法获取所有的值请求中文乱码的问题一般HTML前端都使用utf-8编码,如果后台使用系统默认的编码则会可能造成乱码。我们可以先指定编码方式为utf-8,代码如下:request.setCharacterEncoding("UTF-8");请求转发为了保证程序模块的合理性,每个servlet 提供指定的功能,而有时候用户需要多个servlet来联合提供服务。这个时候需要使用请求转发。通过 request.getRequestDispatcher 方法来转发到其他servlet。它的特点如下:浏览器地址栏不变只能访问当前服务器的内部资源,也就是同一套web程序中的其他servlet转发是一次HTTP请求域对象每个servlet都有一个service。不同的servlet通过不同的serivce来提供服务。由于是不同的类对象和方法,不同的servlet之间不能进行信息的传递。但是有的时候需要不同的servlet之间进行数据的共享。这个时候可以使用域对象域对象是一个有作用范围的对象,域对象中数据随着对象的消亡而消亡。servlet中有下列几种作用域:一次请求, resquest一次会话, cookie, session使用域对象的 setAttribute 来保存数据, 使用getAttribute 来获取数据。使用removeAtribute 来移除数据response 对象对HTTP 响应的封装,常用的方法如下:public void setStatus(int sc);//设置响应的状态码 public void setHeader(String name, String value); //设置响应头 public java.io.PrintWriter getWriter() throws java.io.IOException; //获取输出的字符流流 public ServletOutputStream getOutputStream() throws java.io.IOException; //获取输出的字节流重定向重定向的原理是利用HTTP协议中的301和302 消息。在HTTP响应头中指定状态码为302,并指定Location字段,浏览器会根据响应再重新发送一次请求。servlet中,提供了专门的方法来实现重定向。public void sendRedirect(String location) throws java.io.IOException;重定向的特点如下:重定向时,浏览器地址栏发生了变化重定向可以访问互联网任意节点的资源重定向是多次请求
2019年10月27日
7 阅读
0 评论
0 点赞
2019-10-13
Servlet
通过前面一系列的博客的梳理,学习了一下Java基础的编程知识,从我自己的感觉上来说,Java与c++的差距并不是很大,Java将c++做了更进一步的抽象,同时丢弃了c++中一些容易出错和难懂的部分。Java的基础语法比起c++来说要简单很多。但是Java与c++一样,不能光学基础语法,还得从应用角度来学习它,使用它来做一些真真的项目。从现在开始博客内容从Java的基础过渡到了Java2E的学习Servlet 简介Servlet 英文全称是Server Applet,也叫做Java Servlet;是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。———— 来自维基百科Servlet 容器从上面的定义来看 Servlet 就是一套Java的接口标准,在编写Java Web程序的时候需要遵循这套标准的接口,实现自己的Servlet 接口,并且由服务器端程序来调用,并向其他用户提供服务。目前市面上常见的支持Servlet 标准的Web容器有:Tomcat: 由Apache 基金会的一个项目,由 Apache、Sun和其他公司及个人合作开发而成。Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。WebLogic: WebLogic是美国Oracle公司出品的一个application server;WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器,量级比Tomcat 要大上许多,同时也能承载更多用户的使用JBoss:是一个基于J2EE的开放源代码的应用服务器。 JBoss代码遵循LGPL许可,可以在任何商业应用中免费使用;但JBoss核心服务不包括支持servlet/JSP的WEB容器,一般与Tomcat或Jetty绑定使用。WebSphere: 是由IBM遵照开放标准,例如Java EE、XML及Web Services,开发并发行的一种应用服务器Servlet架构一般一个web项目中,主要分为WEB-INFO目录和一些jsp/html页面,其中WEB-INFO中主要包含 classes 目录(java 字节码文件)、web.xml(项目配置文件)、lib目录(项目依赖文件)在web.xml 中会指定哪些url由哪些Servlet 接口的实现类来处理,当url到达web服务器后,由服务器处理并调用对应的Servlet 类;Web容器要实现这个功能,必然会用到反射机制当用户在浏览器中输入对应的url并点击回车后:浏览器会向对应的地址发送 HTTP请求包对应的服务器收到这个请求包,并获取到请求的路径根据请求路径找到对应的项目在项目中查找对应的web.xml 文件,找到这个路径对应的Servlet 类调用Servlet 类并拿到返回值将返回封装到Http 的响应中,响应到浏览器上Servlet使用Servlet普通Servlet 接口定义如下:public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); }要回答这些接口函数都是干什么的,首先需要知道 一个Servlet对象的生命周期。当项目所在的Web容器启动之后,容器中的所有项目也随之启动,这个时候项目中所有的Servlet 被创建,当容器正常关闭之前,会停止所有项目并析构回收所有Servlet 资源。init方法:每当Servlet 创建的时候调用一次,并且仅调用这一次。,所有我们可以将一些在项目启动之时需要提前做的操作放到这个方法中service方法:用于提供服务,每当用户通过浏览器或者其他方式访问该servlet时,服务器会产生一个新的线程并调用该方法一次,该方法用户为用户提供服务并返回处理结果。destroy方法:当servlet正常被Web容器关闭的时候会首先调用该方法,用于清理某些资源并做最后的收尾工作。实例为了方便进行Web 开发,JavaEE中提供了一个HttpServlet 类,它封装了基本的Servlet 接口,并提供了一些列的doXXX方法来处理HTTP 协议中几种不同的请求方法,例如doGet和doPost等等,下面的例子就使用这个类来进行编写import java.io.*; import javax.servlet.*; import javax.servlet.http.*; // 扩展 HttpServlet 类 public class HelloWorldServlet extends HttpServlet { private String message; public void init() throws ServletException { // 执行必需的初始化 System.out.println("init........"); message = "Hello World"; } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应内容类型 response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<h1>" + message + "</h1>"); } public void destroy() { // 什么也不做 } }代码写完后,可以配置对应的web.xml 文件,其中重要的部分如下:<web-app><!--web项目置于次标签内--> <servlet> <servlet-name>HelloWorld</servlet-name> <!--目录名称--> <servlet-class>HelloWorldServlet</servlet-class> <!--对应类名称--> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/HelloWorld</url-pattern> </servlet-mapping> </web-app> 最终访问 http://localhost:8080/HelloWorld 时会调用HelloWorldServlet 类,并返回helloworld字符串到浏览器中
2019年10月13日
7 阅读
0 评论
0 点赞
2019-09-08
Java 网络编程
Java 中网络编程接口在java.net 包中在使用C/C++进行网络编程时,针对TCP Server端需要这些操作创建SOCKET绑定监听接受连接收取数据包发送数据包TCP Client端需要这些操作创建SOCKET连接Server端发送数据包读取响应包Java中针对Server 端和Client端分别提供了两个类 ServerSocket 和 SocketServerScoket 使用步骤如下:创建ServerSocket 对象并传入一个端口号到构造函数中。在构造的时候会自动创建Socket对象并执行绑定端口、监听端口的操作调用对象的 accept 方法等待连接调用对象的 getInputStream 和 getOutputStream 获取输入输出流,并通过输入输出流来进行收发数据在不用时调用 close 方法关闭套接字Socket 类使用步骤如下:创建 Socket 对象调用 connet 方法连接到指定服务器端口(或者在构造时传入服务器和端口进行连接)调用对象的 getInputStream 和 getOutputStream 获取输入输出流,并通过输入输出流来进行收发数据在不用时调用 close 方法关闭套接字一个普通的TCP通信的实例如下:import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; public class TCPServer{ public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(6666); Socket socket = server.accept() ; InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); byte[] bytes = new byte[1024]; int len = is.read(bytes); os.write(("echo:" + new String(bytes, 0, len)).getBytes()); server.close(); socket.close(); } }import java.net.Socket; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; public class TCPClient{ public static void main(String[] args)throws IOException{ Socket socket = new Socket("127.0.0.1", 6666); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); os.write("hello world!".getBytes()); byte[] bytes = new byte[1024]; int len = is.read(bytes); System.out.println(new String(bytes, 0, len)); socket.close(); } }下面是一个使用TCP协议进行文件传输的例子:import java.net.Socket; import java.io.IOException; import java.io.FileOutputStream; import java.io.InputStream; import java.net.ServerSocket; public class FileUploadServer{ public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(6666); FileOutputStream fos = new FileOutputStream("1.avi"); Socket socket = server.accept(); InputStream is = socket.getInputStream(); byte[] buff = new byte[1024]; int len = 0; while((len = is.read(buff)) != -1){ fos.write(buff, 0, len); } System.out.println("upload success!"); fos.close(); server.close(); socket.close(); } }import java.net.Socket; import java.io.IOException; import java.io.FileInputStream; import java.io.OutputStream; public class FileUploadClient{ public static void main(String[] args) throws IOException{ Socket socket = new Socket("127.0.0.1", 6666); FileInputStream fis = new FileInputStream("1.avi"); OutputStream os = socket.getOutputStream(); int len = 0; byte[] buff = new byte[1024]; while((len = fis.read(buff)) != -1){ os.write(buff, 0, len); } fis.close(); socket.close(); } }最后提供一个简易的http server 的例子import java.net.Socket; import java.net.ServerSocket; import java.io.InputStreamReader; import java.io.InputStream; import java.io.FileInputStream; import java.io.OutputStream; import java.io.BufferedReader; import java.io.IOException; public class HttpServer{ public static void main(String[] args)throws IOException{ ServerSocket server = new ServerSocket(8080); while(true){ Socket socket = server.accept(); new Thread(()->{ try{ BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); OutputStream os = socket.getOutputStream(); String head = bf.readLine(); String path = head.split(" ")[1].substring(1); System.out.println(path); FileInputStream fis = new FileInputStream(path); int len = 0; byte[] buff = new byte[1024]; os.write("HTTP/1.1 200 OK\r\n".getBytes()); os.write("Content-Type:text/html\r\n".getBytes()); os.write("\r\n".getBytes()); while((len = fis.read(buff)) != -1){ os.write(buff, 0, len); } bf.close(); fis.close(); socket.close(); }catch(IOException e){ e.printStackTrace(); } }).start(); } //server.close(); } }
2019年09月08日
5 阅读
0 评论
0 点赞
1
...
14
15
16
...
32