HTML 协议与应用

HTML 协议介绍与常见应用。服务端交互使用HTTP(s)协议

协议介绍

发送请求与spring集成

@RequestBody

该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上。再把HttpMessageConverter返回的对象数据绑定到 controller中方法的参数上。
Spring Boot目前没有注解同时兼容form(application/x-www-form-urlencoded)与json两种请求格式。json格式用@RequestBody,form用的是@RequestParam。要解决这个问题有两种方法:

  1. 通过自己实现一个Converter去处理请求。
  2. javax.servlet.http.HttpServletRequest,通过原生的HttpServletRequest去捕捉参数,自行进行解析

当前台界面使用GET或POST方式提交数据时,数据编码格式由请求头的Content-Type指定。分为以下几种情况:

  1. application/x-www-form-urlencoded,这种情况的数据@RequestParam、@ModelAttribute可以处理。(参数通过url或者body传输,非json格式数据)
  2. multipart/form-data,@RequestBody不能处理这种格式的数据。(form表单里面有文件上传时,必须要指定enctype属性值为multipart/form-data,意思是以二进制流的形式传输文件。)
  3. application/json、application/xml等格式的数据, 必须 使用@RequestBody来处理。

@ResponseBody

该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

返回的数据(如json、xml等)。

常用必备知识储备

Content-Type

application/x-www-form-urlencoded

最常见的 POST 提交数据的方式,浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。key 和 val 进行 URL 转码。表单数据编码为键值对,&分隔。如果是GET请求参数拼接在URL后面,如果是POST请求参数将会放在BODY中传递。Ajax提交数据,也可以使用这种方式。

multipart/form-data

我们使用表单上传文件时,必须让 form 的 enctyped 等于这个值

application/json

除了低版本IE外的浏览器基本都原生支持 JSON.stringify 。对于复杂嵌套对象可以自行进行转换成字符串传输

text/xml

客户端告诉服务器,我是用XML方式提交数据。还有个远程调用方案XML-RPC(XML Remote Procedure Call)

  • 用于告知服务端两个请求是否来自同一浏览器。Cookie 曾一度用于客户端数据的存储。

  • 服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中

  • Domain 标识指定了哪些主机可以接受 Cookie

  • 标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用,避免跨站脚本攻击XSS攻击

  • 为了保证安全浏览器可以禁用 Cookie

分块传输

Chunked Transfer Coding

chunked

chunk编码格式如下:

1
[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]

如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。消息最后以CRLF结尾。常用与返回消息比如文件下载列表查询等。

例子:
编码的应答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

25
This is the data in the first chunk

1C
and this is the second one

3
con

8
sequence

0

编码应答的解释
前两个块的数据中包含有显式的\r\n字符。

1
2
3
4
5
"This is the data in the first chunk\r\n"      (37 字符(包括\r\n) → 十六进制: 0x25)
"and this is the second one\r\n"            (28 字符(包括\r\n) → 十六进制: 0x1C)
"con"                              ( 3 字符 → 十六进制: 0x03)
"sequence"                          ( 8 字符 → 十六进制: 0x08)
应答需要以0长度的块( "0\r\n\r\n".)结束。

解码的数据

1
2
3
This is the data in the first chunk
and this is the second one
consequence

boundary

一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。常用于表单文件上传。

例如,表单上传文件时使用如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
Content-Type: multipart/form-data; boundary=WebKitFormBoundaryrSDFR23f

--WebKitFormBoundaryrSDFR23f
Content-Disposition: form-data; name="test"

Hello
--WebKitFormBoundaryrSDFR23f
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain

... contents of a.txt ...
--WebKitFormBoundaryrSDFR23f--
  • boundary 用于分割不同的字段
  • 每部分都是以 --boundary 开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)
  • 消息主体最后以 --boundary-- 标示结束

多线程分块搬运,断点续传

HTTP断点续传

Range:客户端 发请求的范围, Range:(unit=first byte pos)-[last byte pos],例子:Range: bytes=0-299 表示第 0-299 字节范围的内容
Content-Range:服务端 返回当前请求范围和文件总大小,Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
返回的响应头:HTTP/1.1 200 Ok(不使用断点续传方式) HTTP/1.1 206 Partial Content(使用断点续传方式)

HTTP1.1 协议(RFC2616)定义了断点续传相关的HTTP头 Range和Content-Range字段

每个线程获取数据时指定获取的数据库的起始位置与结束位置。要考虑是服务端网络瓶颈还是客户端网络瓶颈,如果是服务端可以考虑一个客户端去多个服务端获取数据块进行合并。主要看客户端的实现。

HTTPS

HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率

认证

校验对方身份,CA

HTTP相关基础概念

HTTP一般不是长连接的,需要长连接可以使用websocket,或者CS架构的tcp socket。

HTTP的状态

Keep-Alive 是指TCP连接不断开,不会永久保持连接,在最后一个交互结束后还保持一段时间。不用每次请求都重新建立TCP连接。实现长连接需要客户端和服务端都支持长连接。如果HTTP1.1版本的HTTP请求报文不希望使用长连接,则要在HTTP请求报文首部加上Connection: close。
http协议无状态中的【状态】到底指的是什么?!
所以【在服务器端开辟一块缓存区】才是真正的条件,也就是说,它确实等价于【有状态】。通过在服务器端开辟一块缓存区,存储、记忆、共享一些临时数据,你就可以:
协议对于事务处理有记忆能力【事物处理】【记忆能力】
对同一个url请求有上下文关系【上下文关系】
每次的请求都是不独立的,它的执行情况和结果与前面的请求和之后的请求是直接关系的【不独立】【直接关系】
服务器中保存客户端的状态【状态】
cookie和session应该是完全实现了有状态这个功能
TCP一直有状态,HTTP一直无状态,但是应用为了有状态,就给HTTP加了cookie和session机制,让使用http的应用也能有状态,但http还是无状态

HTTP头部有了Keep-Alive这个值并不代表一定会使用长连接,客户端和服务器端都可以无视这个值,也就是不按标准来,譬如我自己写的HTTP客户端多线程去下载文件,就可以不遵循这个标准,并发的或者连续的多次GET请求,都分开在多个TCP通道中,每一条TCP通道,只有一次GET,GET完之后,立即有TCP关闭的四次握手,这样写代码更简单,这时候虽然HTTP头有Connection: Keep-alive,但不能说是长连接。

TCP的keep alive是检查当前TCP连接是否活着;HTTP的Keep-alive是要让一个TCP连接活久点。它们是不同层次的概念。

HTTP长连接(HTTP persistent connection )之后的好处,包括可以使用HTTP 流水线技术(HTTP pipelining,也有翻译为管道化连接),它是指,在一个TCP连接内,多个HTTP请求可以并行,下一个HTTP请求在上一个HTTP请求的应答完成之前就发起。

其他实现HTTP长连接方案:
客户端方案:Flash XMLSocket、Java Applet 套接口
Comet : AJAX长轮询与http流(长轮询是短轮询的翻版,短轮询的方式是:页面定时向服务器发送请求,看有没有更新的数据,长轮询的方式是,页面向服务器发起一个请求,服务器一直保持tcp连接打开,知道有数据可发送。发送完数据后,页面关闭该连接,随即又发起一个新的服务器请求,在这一过程中循环,而长轮询中服务器等待新的数据到来才响应,因此实现了服务器向页面推送实时,并减少了页面的请求次数。http流不同于上述两种轮询,因为它在页面整个生命周期内只使用一个HTTP连接,具体使用方法即页面向浏览器发送一个请求,而服务器保持tcp连接打开,然后不断向浏览器发送数据)
DWR : Java 的成熟的服务器推送框架。是通过动态把 Java 类生成为 Javascript。它的代码就像 Ajax 一样,你感觉调用就像发生在浏览器端,但是实际上代码调用发生在服务器端,DWR 负责数据的传递和转换。DWR用起来有种非常像RMI或者SOAP的常规RPC机制,而且DWR的优点在于不需要任何的网页浏览器插件就能运行在网页上
大多数浏览器实现了SSE(Server-Sent Events,服务器发送事件) API,SSE支持短轮询、长轮询和HTTP流

URI

URI:Uniform Resource Identifier,即统一资源标志符,用来唯一的标识一个资源。

URL:Uniform Resource Locator,统一资源定位符。即URL可以用来标识一个资源,而且还指明了如何locate这个资源。

URN:Uniform Resource Name,统一资源命名。通过名字来表示资源。

请求与响应报文

GET请求报文

1
2
3
4
5
6
7
8
9
10
GET /admin/device?_search=false&nd=15532&pageSize=10&pageNo=1 HTTP/1.1
Host: 47.101.188.88:8888
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36
Referer: http://47.101.188.88:8888/admin/modules/analyse/device.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=9945ffba-927e-4c29-9fd2-3886805dabfe

GET请求不建议在请求体Body中传递参数,比如Apache Http Client库也没提供GET请求设置body的接口,AsyncHttpClient库有Get有设置body的接口,PostmanGET请求也不支持设置Body。有可能有些服务端的实现也会忽略GET请求的Body。具体可以用curl结合抓包工具与springmvc进行测试。

HTTP请求组成:

  • 第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本
  • 第二部分:请求头部,紧接着请求行(第一行)之后的部分,用来说明服务器要使用的附加信息
  • 第三部分:空行,请求头部后面必须有空行
  • 第四部分:请求数据也叫主体,可以添加任意数据

GET响应报文

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Fri, 22 Mar 2019 07:12:26 GMT
Transfer-Encoding: chunked

{"msg":"success","code":0,data:{"totalCount":0}}

状态码200代表成功

POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /hems/device.do?reqCode=insertItem HTTP/1.1
Host: 192.168.33.11:88
Connection: keep-alive
Content-Length: 19
Origin: https://192.168.33.11:88
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Referer: https://192.168.33.226:7443/hems/monitor.do?reqCode=init
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: account=dev; JSESSIONID=ACC630D4ADF84514D8A640F45EF7988C

id=10000001&name=12

Content-Length 标识请求主体内容长度

响应报文

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2019 07:27:29 GMT

{"msg":"添加成功!","success":true}

HTTP之状态码

  • 1xx:指示信息 表示请求已接收,继续处理
  • 2xx:成功 表示请求已被成功接收、理解、接受
  • 3xx:重定向 要完成请求必须进行更进一步的操作
  • 4xx:客户端错误 请求有语法错误或请求无法实现
  • 5xx:服务器端错误 服务器未能实现合法的请求

扩展

status canceled 可能是浏览器客户端代码设置了超时时间,超过时间了自己取消了。(网络上出现跨域或者协议不一致或者被其他脚本打断请求等原因引起的这个待确认)。超时、刷新页面、页面跳转之前的请求就需要取消。
资料:
We fought a similar problem where Chrome was canceling requests to load things within frames or iframes, but only intermittently and it seemed dependent on the computer and/or the speed of the internet connection.

This information is a few months out of date, but I built Chromium from scratch, dug through the source to find all the places where requests could get cancelled, and slapped breakpoints on all of them to debug. From memory, the only places where Chrome will cancel a request:

The DOM element that caused the request to be made got deleted (i.e. an IMG is being loaded, but before the load happened, you deleted the IMG node)
You did something that made loading the data unnecessary. (i.e. you started loading a iframe, then changed the src or overwrite the contents)
There are lots of requests going to the same server, and a network problem on earlier requests showed that subsequent requests weren’t going to work (DNS lookup error, earlier (same) request resulted e.g. HTTP 400 error code, etc)

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82


// ApplicationCacheResourceLoader.cpp
void ApplicationCacheResourceLoader::cancel(Error error)
{
auto protectedThis = makeRef(*this);

if (auto callback = WTFMove(m_callback))
callback(makeUnexpected(error));

if (m_resource) {
m_resource->removeClient(*this);
m_resource = nullptr;
}
}

// SubresourceLoader.cpp
void SubresourceLoader::cancelIfNotFinishing()
{
if (m_state != Initialized)
return;

ResourceLoader::cancel();
}
void ApplicationCacheResourceLoader::notifyFinished(CachedResource& resource, const NetworkLoadMetrics&)
{
auto protectedThis = makeRef(*this);

ASSERT_UNUSED(resource, &resource == m_resource);

if (m_resource->errorOccurred()) {
cancel(Error::NetworkError);
return;
}
if (auto callback = WTFMove(m_callback))
callback(WTFMove(m_applicationCacheResource));

CachedResourceHandle<CachedRawResource> resourceHandle;
std::swap(resourceHandle, m_resource);
if (resourceHandle)
resourceHandle->removeClient(*this);
}
// BlockingResponseMap.h
public:
BlockingResponseMap() : m_canceled(false) { }
~BlockingResponseMap() { ASSERT(m_responses.isEmpty()); }

std::unique_ptr<T> waitForResponse(uint64_t requestID)
{
while (true) {
std::unique_lock<Lock> lock(m_mutex);

if (m_canceled)
return nullptr;

if (std::unique_ptr<T> response = m_responses.take(requestID))
return response;

m_condition.wait(lock);
}

return nullptr;
}

void didReceiveResponse(uint64_t requestID, std::unique_ptr<T> response)
{
auto locker = holdLock(m_mutex);
ASSERT(!m_responses.contains(requestID));

m_responses.set(requestID, WTFMove(response));

// FIXME: Could get a slight speed-up from using notifyOne().
m_condition.notifyAll();
}

void cancel()
{
m_canceled = true;

// FIXME: Could get a slight speed-up from using notifyOne().
m_condition.notifyAll();
}

Webkit源码学习:handleEvent等

HTTP请求方法

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法

  • GET 请求指定的页面信息,并返回实体主体。
  • HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头,主要用于确认 URL 的有效性以及资源更新的日期时间等
  • POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中
  • PATCH 从客户端向服务器传送的数据取代指定的文档的内容。PATCH 允许部分修改
  • PUT 由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法
  • DELETE 请求服务器删除指定的资源。
  • CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输
  • OPTIONS 查询指定的 URL 能够支持的方法
  • TRACE 服务器会将通信路径返回给客户端。

HTTP 请求头参数

通用首部字段

首部字段名 说明
Cache-Control 控制缓存
Connection 控制不再转发给代理的首部字段、管理持久连接
Date 创建报文的日期时间
Transfer-Encoding 指定报文主体的传输编码方式
Via 代理服务器的相关信息

请求首部字段

首部字段名 说明
Accept 用户可处理的媒体类型
Accept-Charset 优先的字符集
Accept-Encoding 优先的内容编码
Accept-Language 优先的语言(自然语言)
Host 请求资源所在服务器包括域名和端口号
Origin 用来说明请求从哪里发起的,包括,且仅仅包括协议和域名。CORS跨域请求中会用到,response有对应的header:Access-Control-Allow-Origin
If-Match 比较实体标记(ETag)
Max-Forwards 最大传输逐跳数
Proxy-Authorization 代理服务器要求客户端的认证信息
Range 实体的字节范围请求
Referer 原始请求URI 包括:协议+域名+查询参数
User-Agent HTTP 客户端程序的信息
Content-Length 标识请求主体内容长度

响应首部字段

首部字段名 说明
Accept-Ranges 是否接受字节范围请求
Age 推算资源创建经过时间
ETag 资源的匹配信息
Server HTTP 服务器的安装信息
Vary 代理服务器缓存的管理信息
Access-Control-Allow-Credentials 是否允许后续请求携带认证信息
Access-Control-Allow-Origin 指定允许其他域名访问,设置*是最简单粗暴的

编码

一般使用js框架进行post发送数据的时候都会对参数进行encodeURIComponent。如果不想进行编码可以修改框架源码,或者使用原生的Ajax请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var params1 = {
username: username,
passwrod: password
};
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
var data = xhr.responseText;
data = JSON.prase(data);
console.log(data);
}
}
xhr.open("POST","/url",true);
xhr.setRequestHeader('Content-Type', 'multipart/x-www-form-urlencoded; charset=UTF-8');
xhr.send(params1);

// ------------------
// 进行encodeURL的方法
function $params(obj) {
var str = [];
for (var p in obj) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
}
xhr.send($params(params1));

最常用的encodeURI和encodeURIComponent

  • encodeURI方法不会对下列字符编码 ASCII字母、数字、~!@#$&*()=:/,;?+'
  • encodeURIComponent方法不会对下列字符编码 ASCII字母、数字、~!*()'
  • encodeURIComponent比encodeURI编码的范围更大。

过程:

UTF-8编码→UTF-8(iso-8859-1)编码→iso-8859-1解码→UTF-8解码

encodeURL函数主要是来对URI来做转码,它默认是采用的UTF-8的编码
UTF-8编码的格式:一个汉字来三个字节构成,每一个字节会转换成16进制的编码,同时添加上%号

模拟请求

设置超时时间

HttpClient 4.5

1
2
3
4
5
6
7
8
9
CloseableHttpClient httpclient = HttpClients.createDefault();  
HttpGet httpGet = new HttpGet("http://www.baidu.com/");
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000).setConnectionRequestTimeout(1000)
.setSocketTimeout(5000).build();
httpGet.setConfig(requestConfig);
CloseableHttpResponse response = httpclient.execute(httpGet);
System.out.println("Result:" + response.getStatusLine()); //得到请求结果
HttpEntity entity = response.getEntity(); //得到请求回来的数据

setConnectTimeout:设置连接超时时间,单位毫秒。
setConnectionRequestTimeout:设置从connect Manager(连接池)获取Connection 超时时间,单位毫秒。这个属性是新加的属性,因为目前版本是可以共享连接池的。
setSocketTimeout:请求获取数据的超时时间(即响应时间),单位毫秒。 如果访问一个接口,多少时间内无法返回数据,就直接放弃此次调用

特殊问题解决

JS多次触发事件,在一定延迟内只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
var div = document.querySelectorAll(".div")[0];
var num = 0; // 计数
var t = null;// 外部定义一个timeout对象
// 事件累加的功能
// JS 多次触发点击事件,在一定延迟内只执行一次
div.onclick = function(){
if(t != null) {
clearTimeout(t);// clearTimeout() 方法可取消由 setTimeout() 方法设置的 timeout。
}
t = setTimeout(function(){
num ++;
console.log(num);
}, 500)
}
</script>

还可以自己实现,通过时间控制。执行完操作,记录一个执行时间,下次执行的时候判断是否时间间隔是超过5秒,这样可以保证5秒不重复执行,还可以解决heartbeat重复调用脚本的问题。不过linux可以对文件加锁,避免多次访问。

应用

树型结构

实现方式:一次性全部价值,异步加载

ztree json设计:

标准:需要嵌套关系,返回json数据可以没有pId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
id: "1",
pId: "-1",
treeType: "0",
name: "dept1",
open: false,
isParent: true,
children:[
{
id: "2",
pId: "1",
treeType: "1",
name: "user1",
open: false,
isParent: false
}
]
}
type 0:dept,1:person

非标准:不需要嵌套关系

1
2
3
4
5
[
{id:1, pId:0, name: "父节点1"},
{id:11, pId:1, name: "子节点1"},
{id:12, pId:1, name: "子节点2"}
]

如果异步加载每次都只返回单层的节点数据,那么可以不设置简单 JSON 数据模式
客户端自己特殊处理服务端给的数据比如 部门→设备、人员树:

1
2
3
4
5
[
{id:1, pId:0, name: "父节点部门1", memberList:[id:1, username: "用户1"], deviceList:[id:1, deviceName: "设备1"]},
{id:11, pId:1, name: "子节点部门1", memberList:[], deviceList:[]},
{id:12, pId:1, name: "子节点部门2", memberList:[], deviceList:[]}
]

模糊查询过滤,很多客户的树插件自带模糊搜索过滤功能根据情况选择后端或者前端实现,数据量不大可以考虑使用前端插件自带过滤功能

css

图片居中,在img标签父级标签增加text-align:center

安全

CSRF

CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解。攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
CSRF攻击攻击原理及过程如下:

  1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A
  2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A
  3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B
  4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A
  5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行

直白说:csrf 场景是,坏人不能进入银行系统拿到cookie也不能截获http包,此时他只能发来一个真实的取款链接,而防控手段就是把每个取款链接都放个token,所以此时坏人发来的取款链接,好人怎么点都没有用,不起作用了。csrfcheck 不通过(前提是坏人无法截获http包,否则csrf的token一点用没有)。

CSRF(通常)发生在第三方域名。
CSRF攻击者不能获取到Cookie等信息,只是使用。
针对这两点,我们可以专门制定防护策略,如下:
阻止不明外域的访问:
同源检测
Samesite Cookie
提交时要求附加本域才能获取的信息:
CSRF Token
双重Cookie验证

解决方法就是验证请求头的来源Referer+请求内容url或者body加上随机码,后端进行验证。避免仅仅Cookies就通过验证

1
2
3
登录时后端加生成一个uuid返回给前端并且存储在session中,前端在ajax请求统一加一个uuid参数或者请求头加uuid参数,登录后的请求统一拦截判断是ajax请求进行校验。对于其他请求比如get对于旧框架目前还没有比较好的方法。对于refer还得改造保证客户端都带上。校验不通过返回403错误码。主要还是新增、修改、删除接口需要校验。
每次请求都变换标识符,类似每个请求都加上验证码,使用tomcat7 CsrfPreventionFilter,客户端统一在请求中加上标识符。
每次打开页面获取随机码,发送Ajax请求就更新返回随机码。

如果要做到避免被截取报文修改报文使用https协议。还有就是每次请求后都生成一个新的随机码返回给前端,前端调用统一处理返回信息下次请求带上新的随机码,这样还可以解决表单重复提交的问题(方案很多还可以后台缓存提交内容进行对比,前端也进行页面按钮控制等),(还可以在提交内容做处理比如加上MD5(时间戳+提交内容+token),后端进行校验,这样可以避免报文被中途随意修改,不过如果对方知道加密模式的话还是很容易破解的,而且无法避免数据包重放)还可以加上黑白名单。(还有个更简单的方法防止重复提交或者发包被拦截修改重放,就是每个请求后生成随机码都设置一下cookies,后端后续请求校验cookies是否跟后端一致,只能特定请求进行处理、如果所有请求都做的话并发访问可能会有问题,每个请求都需要一个新的随机码)

如果查询也每次查询都需要变一个随机码的话还得保证同一个页面多个Ajax查询请求使用同步查询,不然并发请求会用同一个随机码(或者再设计一个随机码策略),一般查询请求没有危害可以不加。如果后端有做严格限制的话比如只用POST进行增删改的话可以通过POST进行生产新随机码,否则就是根据路径匹配增删改操作变换随机码,也就是登录给客户端一个随机码有做增删改也给客户端一个新的随机码可以使用cookies也可以response内容中传输。cookies.setPath("/");防止新增多个相同cookies。

XSS

跨站脚本攻击是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
主要是要过滤< > <script></script>等风险标签。仅仅过滤< >还是不够,可能还有其他编码格式传输到后端或者ascii吗%XX
直接使用的spring自带的HtmlUtils类的htmlEscape方法转义的,方便很多。转义操作可以统一加在返回前端过滤器中。部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String convertToReference(char character, String encoding) {
if (encoding.startsWith("UTF-")) {
switch(character) {
case '"':
return "&quot;";
case '&':
return "&amp;";
case '\'':
return "&#39;";
case '<':
return "&lt;";
case '>':
return "&gt;";
}
} else if (character < 1000 || character >= 8000 && character < 10000) {
int index = character < 1000 ? character : character - 7000;
String entityReference = this.characterToEntityReferenceMap[index]; // 包含各种字符的转义
if (entityReference != null) {
return entityReference;
}
}

return null;
}

ESAPI.encoder().encodeForHTMLAttribute() ESAPI也很强大

前端测试例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转义测试</title>
</head>
<body>
test:<div color="red" id="test">abc</div>
test2:<a id="test2" href="//www.infotech.vip">infotech</a>
</br>
test3:<a id="test3" href="//www.infotech.vip">infotech</a>
</br>
test4:<a id="test4" href="//www.infotech.vip">infotech</a>
</br>
test5:<a id="test5" href="//www.infotech.vip">infotech</a>
</br>
test6:<input id="test6" type="text">
</body>
<script>
//HTML标签转义(< -> &lt;) 这种方式可能有风险,可以自己写转换方法
function html2Escape(sHtml) {
var temp = document.createElement("div");
(temp.textContent != null) ? (temp.textContent = sHtml) : (temp.innerText = sHtml);
var output = temp.innerHTML;
temp = null;
return output;
}
//HTML标签反转义(&lt; -> <)
function escape2Html(str) {
var temp = document.createElement("div");
temp.innerHTML = str;
var output = temp.innerText || temp.textContent;
temp = null;
return output;
}
document.getElementById("test").innerText="<>12345<>";
document.getElementById('test2').innerHTML="abc";
document.getElementById('test3').innerHTML="<font color=red>ffff</font>";
document.getElementById('test4').innerHTML=html2Escape("<font color=red>ffff</font>");
console.log(html2Escape("<font color=red>ffff</font>")); //&lt;font color=red&gt;ffff&lt;/font&gt;
document.getElementById('test5').innerHTML=escape2Html("&lt;font color=red&gt;ffff&lt;/font&gt;");
console.log(escape2Html("&lt;font color=red&gt;ffff&lt;/font&gt;")); //<font color=red>ffff</font>
document.getElementById('test6').value=escape2Html("&lt;font color=red&gt;ffff&lt;/font&gt;");
</script>
</html>

展示结果

1
2
3
4
5
6
7
test:
<>12345<>
test2:abc
test3:ffff
test4:<font color=red>ffff</font>
test5:ffff
test6:<font color=red>ffff</font>

input输入框的内容需要原始形式可以不需要转义转义成&lt;之类就不好编辑了,其他标签文本内容基本需要转义,需要分开处理,查下前端框架的处理?

参考