Web開發高級技術

1 http長鏈接與短鏈接

HTTP的長鏈接和短鏈接本質上是TCP長鏈接和短鏈接。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網絡層使用IP協議。IP協議主要解決網絡路由和尋址問題,TCP協議主要解決如何在IP層之上可靠的傳遞數據包,使在網絡上的另外一端收到發端發出的全部包,而且順序與發出順序一致。TCP有可靠,面向鏈接的特色。javascript

1.1 如何理解HTTP協議是無狀態的

HTTP協議是無狀態的,指的是協議對於事務處理沒有記憶能力,服務器不知道客戶端是什麼狀態。也就是說,打開一個服務器上的網頁和你以前打開這個服務器上的網頁之間沒有任何聯繫。HTTP是一個無狀態的面向鏈接的協議,無狀態不表明HTTP不能保持TCP鏈接,更不能表明HTTP使用的是UDP協議(無鏈接)。html

1.2 什麼是長鏈接、短鏈接?

在HTTP/1.0中,默認使用的是短鏈接。也就是說,瀏覽器和服務器每進行一次HTTP操做,就創建一次鏈接,但任務結束就中斷鏈接。若是客戶端瀏覽器訪問的某個HTML或其餘類型的 Web頁中包含有其餘的Web資源,如JavaScript文件、圖像文件、CSS文件等;當瀏覽器每遇到這樣一個Web資源,就會創建一個HTTP會話。前端

但從 HTTP/1.1起,默認使用長鏈接,用以保持鏈接特性。使用長鏈接的HTTP協議,會在響應頭有加入這行代碼:Connection:keep-alive。在使用長鏈接的狀況下,當一個網頁打開完成後,客戶端和服務器之間用於傳輸HTTP數據的 TCP鏈接不會關閉,若是客戶端再次訪問這個服務器上的網頁,會繼續使用這一條已經創建的鏈接。Keep-Alive不會永久保持鏈接,它有一個保持時間,能夠在不一樣的服務器軟件(如Apache)中設定這個時間。實現長鏈接要客戶端和服務端都支持長鏈接。HTTP協議的長鏈接和短鏈接,實質上是TCP協議的長鏈接和短鏈接。java

1.3 TCP短鏈接

模擬一下TCP短鏈接的狀況,client向server發起鏈接請求,server接到請求,而後雙方創建鏈接。client向server 發送消息,server迴應client,而後一次讀寫就完成了,這時候雙方任何一個均可以發起close操做,不過通常都是client先發起 close操做。爲何呢,通常的server不會回覆完client後當即關閉鏈接的,固然不排除有特殊的狀況。從上面的描述看,短鏈接通常只會在 client/server間傳遞一次讀寫操做jquery

短鏈接的優勢是:管理起來比較簡單,存在的鏈接都是有用的鏈接,不須要額外的控制手段web

 1.4 TCP長鏈接

長鏈接的狀況,client向server發起鏈接,server接受client鏈接,雙方創建鏈接。Client與server完成一次讀寫以後,它們之間的鏈接並不會主動關閉,後續的讀寫操做會繼續使用這個鏈接。ajax

首先說一下TCP/IP詳解上講到的TCP保活功能,保活功能主要爲服務器應用提供,服務器應用但願知道客戶主機是否崩潰,從而能夠表明客戶使用資源。若是客戶已經消失,使得服務器上保留一個半開放的鏈接,而服務器又在等待來自客戶端的數據,則服務器將永遠等待客戶端的數據,保活功能就是試圖在服務器端檢測到這種半開放的鏈接。數據庫

若是一個給定的鏈接在兩小時內沒有任何的動做,則服務器就向客戶發一個探測報文段,客戶主機必須處於如下4個狀態之一:apache

客戶主機依然正常運行,並從服務器可達。客戶的TCP響應正常,而服務器也知道對方是正常的,服務器在兩小時後將保活定時器復位。json

客戶主機已經崩潰,而且關閉或者正在從新啓動。在任何一種狀況下,客戶的TCP都沒有響應。服務端將不能收到對探測的響應,並在75秒後超時。服務器總共發送10個這樣的探測 ,每一個間隔75秒。若是服務器沒有收到一個響應,它就認爲客戶主機已經關閉並終止鏈接。

客戶主機崩潰並已經從新啓動。服務器將收到一個對其保活探測的響應,這個響應是一個復位,使得服務器終止這個鏈接。

客戶機正常運行,可是服務器不可達,這種狀況與2相似,TCP能發現的就是沒有收到探查的響應。

1.5 長鏈接短鏈接操做過程

短鏈接的操做步驟是:

創建鏈接——數據傳輸——關閉鏈接…創建鏈接——數據傳輸——關閉鏈接

長鏈接的操做步驟是:

創建鏈接——數據傳輸…(保持鏈接)…數據傳輸——關閉鏈接

1.6 長鏈接是何時關閉

  1. 響應頭Keep-Alive: timeout。這個值可以讓一些瀏覽器主動關閉鏈接,這樣服務器就沒必要要去關閉鏈接了。
  2. tcp自動探測一次,發現對方關閉,則斷開鏈接

1.7 長鏈接和短鏈接的優勢和缺點

由上能夠看出,長鏈接能夠省去較多的TCP創建和關閉的操做,減小浪費,節約時間。對於頻繁請求資源的客戶來講,較適用長鏈接。不過這裏存在一個問題,存活功能的探測週期太長,還有就是它只是探測TCP鏈接的存活,屬於比較斯文的作法,遇到惡意的鏈接時,保活功能就不夠使了。在長鏈接的應用場景下,client端通常不會主動關閉它們之間的鏈接,Client與server之間的鏈接若是一直不關閉的話,會存在一個問題,隨着客戶端鏈接愈來愈多,server遲早有扛不住的時候,這時候server端須要採起一些策略,如關閉一些長時間沒有讀寫事件發生的鏈接,這樣可 以免一些惡意鏈接致使server端服務受損;若是條件再容許就能夠以客戶端機器爲顆粒度,限制每一個客戶端的最大長鏈接數,這樣能夠徹底避免某個蛋疼的客戶端連累後端服務。

短鏈接對於服務器來講管理較爲簡單,存在的鏈接都是有用的鏈接,不須要額外的控制手段。但若是客戶請求頻繁,將在TCP的創建和關閉操做上浪費時間和帶寬

1.8 何時用長鏈接?何時用短鏈接?

長鏈接多用於操做頻繁,點對點的通信,並且鏈接數不能太多狀況。每一個TCP鏈接都須要三次握手,這須要時間,若是每一個操做都是先鏈接,再操做的話那麼處理速度會下降不少,因此每一個操做完後都不斷開,次處理時直接發送數據包就OK了,不用創建TCP鏈接。例如:數據庫的鏈接用長鏈接, 若是用短鏈接頻繁的通訊會形成socket錯誤,並且頻繁的socket 建立也是對資源的浪費。

而像WEB網站的http服務通常都用短連接,由於長鏈接對於服務端來講會耗費必定的資源,而像WEB網站這麼頻繁的成千上萬甚至上億客戶端的鏈接用短鏈接會更省一些資源,若是用長鏈接,並且同時有成千上萬的用戶,若是每一個用戶都佔用一個鏈接的話,那可想而知吧。因此併發量大,但每一個用戶無需頻繁操做狀況下需用短連好。

2 瀏覽器跨域訪問資源

在當前域名請求網站中,默認不容許經過ajax請求發送其餘域名。

好比我在b網站的www.b.com/b/index.jsp中訪問www.a.com/a/FromServlet資源就會產生這種跨域問題。

依賴:

<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.31</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.1.43</version>
        </dependency>

a網站後端代碼:

@WebServlet("/FromServlet")
public class FromServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username=req.getParameter("userName");
        JSONObject jsonObject=new JSONObject();
        jsonObject.put("userName",username);
        resp.getWriter().write(jsonObject.toJSONString());
    }
}

b網站前端代碼:

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>B網站訪問</title>
</head>
<script type="text/javascript">
    $(document).ready(function() {
        $.ajax({
            type : "GET",
            async : false,
            url : "http://www.a.com/a/FromServlet?userName=644064",
            dataType : "json",
            success : function(data) {
                alert(data["userName"]);
            },
            error : function() {
                alert('fail');
            }
        });

    });
</script>
<body>
<img alt="" src="http://www.a.com/a/img/timg.jpg">
</body>
</html>

訪問http://www.a.com/a/FromServlet?userName=ddsdasd後獲得以下響應:

而經過www.b.com域名訪問好比http://www.b.com/b/index

顯示失敗。以下圖能夠看到請求地址與訪問地址是不同的,說明ajax不能跨域訪問。

 

2.1 解決方法一

從這裏能夠看出只要在響應頭部添加response.setHeader("Access-Control-Allow-Origin", "*"); 支持全部網站便可

 

2.2 解決方法二jsonp

在同源策略下,在某個服務器下的頁面是沒法獲取到該服務器之外的數據的,即通常的ajax是不能進行跨域請求的。但 img、iframe 、script等標籤是個例外,這些標籤能夠經過src屬性請求到其餘服務器上的數據。利用 script標籤的開放策略,咱們能夠實現跨域請求數據,固然這須要服務器端的配合。 Jquery中ajax 的核心是經過 XmlHttpRequest獲取非本頁內容,而jsonp的核心則是動態添加 <script>標籤來調用服務器提供的 js腳本。

  當咱們正常地請求一個JSON數據的時候,服務端返回的是一串 JSON類型的數據,而咱們使用 JSONP模式來請求數據的時候服務端返回的是一段可執行的 JavaScript代碼。由於jsonp 跨域的原理就是用的動態加載 script的src ,因此咱們只能把參數經過 url的方式傳遞,因此jsonp的 type類型只能是get 

示例:

$.ajax({

    url: 'http://www.a.com/a/FromServlet?userName=ddsdasd', //不一樣的域

    type: 'GET', // jsonp模式只有GET 是合法的

    data: {

        'action': 'aaron'

    },

    dataType: 'jsonp', // 數據類型

    jsonp: 'backfunc', // 指定回調函數名,與服務器端接收的一致,並回傳回來

})

其實jquery 內部會轉化成

http://'http://www.a.com/a/FromServlet?userName=ddsdasd?backfunc=jQuery2030038573939353227615_1402643146875&action=aaron

而後動態加載

<script type="text/javascript"src="'http://www.a.com/a/FromServlet?userName=ddsdasd?backfunc= jQuery2030038573939353227615_1402643146875&action=aaron"></script>

而後後端就會執行backfunc(傳遞參數 ),把數據經過實參的形式發送出去。

  使用JSONP 模式來請求數據的整個流程:客戶端發送一個請求,規定一個可執行的函數名(這裏就是 jQuery作了封裝的處理,自動幫你生成回調函數並把數據取出來供success屬性方法來調用,而不是傳遞的一個回調句柄),服務器端接受了這個 backfunc函數名,而後把數據經過實參的形式發送出去

(在jquery 源碼中, jsonp的實現方式是動態添加<script>標籤來調用服務器提供的 js腳本。jquery 會在window對象中加載一個全局的函數,當 <script>代碼插入時函數執行,執行完畢後就 <script>會被移除。同時jquery還對非跨域的請求進行了優化,若是這個請求是在同一個域名下那麼他就會像正常的 Ajax請求同樣工做。)

$.ajax({
			type : "GET",
			async : false,
			url : "http://www.a.com/a/FromServlet?userName=張三",
			dataType : "jsonp",//數據類型爲jsonp  
			jsonp : "jsonpCallback",//服務端用於接收callback調用的function名的參數  
			success : function(data) {
				alert(data.result);
			},
			error : function() {
				alert('fail');
			}
		});

後端:

@WebServlet("/FromServlet")
public class FromServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username=req.getParameter("userName");
        JSONObject jsonObject=new JSONObject();
        jsonObject.put("userName",username);
        String jsonpCallback = req.getParameter("jsonpCallback");// 客戶端請求參數
        resp.getWriter().write(jsonpCallback+"("+jsonObject.toJSONString()+")");
    }
}

3 表單重複提交解決方案

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <title>Form表單</title>
  </head>
  
  <body>
      <form action="${pageContext.request.contextPath}/DoFormServlet" method="post">
        用戶名:<input type="text" name="userName">
        <input type="submit" value="提交" id="submit">
    </form>
  </body>
</html>
@WebServlet("/DoFormServlet")
public class DoFormServlet extends HttpServlet {

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		req.setCharacterEncoding("UTF-8");
		String userName = req.getParameter("userName");
		try {
			Thread.sleep(300);
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println("往數據庫插入數據...."+userName);
		resp.getWriter().write("success");
	}

}

網絡延時

 若是網速比較慢的狀況下,用戶提交表單後,發現服務器半天都沒有響應,那麼用戶可能會覺得是本身沒有提交表單,就會再點擊提交按鈕重複提交表單,開發中必須防止表單重複提交。

從新刷新

表單提交後用戶點擊【刷新】按鈕致使表單重複提交或者從新加載

點擊瀏覽器的【後退】按鈕回退到表單頁面後進行再次提交

用戶提交表單後,點擊瀏覽器的【後退】按鈕回退到表單頁面後進行再次提交

3.1 解決方案一(前端js)

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Form表單</title>
<script type="text/javascript">
	var isFlag = false; //表單是否已經提交標識,默認爲false

	function submitFlag() {

		if (!isFlag) {  //沒有提交
			isFlag = true;
			return true;
		} else {
			return false;
		}

	}
</script>
</head>

<body>
	<form action="${pageContext.request.contextPath}/DoFormServlet"
		method="post" onsubmit="return submitFlag()">
		用戶名:<input type="text" name="userName"> <input type="submit"
			value="提交" id="submit">
	</form>
</body>
</html>

 除了用這種方式以外,常常見的另外一種方式就是表單提交以後,將提交按鈕設置爲不可用,讓用戶沒有機會點擊第二次提交按鈕,代碼以下:

function dosubmit(){
    //獲取表單提交按鈕
    var btnSubmit = document.getElementById("submit");
    //將表單提交按鈕設置爲不可用,這樣就能夠避免用戶再次點擊提交按鈕
    btnSubmit.disabled= "disabled";
    //返回true讓表單能夠正常提交
    return true;
}

3.2 後端解決

能夠看到其實3.1並沒真正解決從新刷新回退以後的問題。既然客戶端沒法解決,那麼就在服務器端解決,在服務器端解決就須要用到session了。

作法:在服務器端生成一個惟一的隨機標識號,專業術語稱爲Token(令牌),同時在當前用戶的Session域中保存這個Token而後將Token發送到客戶端的Form表單中,在Form表單中使用隱藏域來存儲這個Token,表單提交的時候連同這個Token一塊兒提交到服務器端,而後在服務器端判斷客戶端提交上來的Token與服務器端生成的Token是否一致,若是不一致,那就是重複提交了,此時服務器端就能夠不處理重複提交的表單。若是相同則處理表單提交,處理完後清除當前用戶的Session域中存儲的標識號。
  在下列狀況下,服務器程序將拒絕處理用戶提交的表單請求:

  1. 存儲Session域中的Token(令牌)與表單提交的Token(令牌)不一樣。
  2. 當前用戶的Session中不存在Token(令牌)。
  3. 用戶提交的表單數據中沒有Token(令牌)。

轉發代碼:

@WebServlet("/ForwardServlet")
public class ForwardServlet extends HttpServlet {
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		req.getSession().setAttribute("sesionToken", TokenUtils.getToken());
		req.getRequestDispatcher("form.jsp").forward(req, resp);
	}
}

jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Form表單</title>

</head>

<body>
	<form action="${pageContext.request.contextPath}/DoFormServlet"
		method="post" onsubmit="return dosubmit()">
		<input type="hidden" name="token" value="${sesionToken}"> 用戶名:<input type="text"
			name="userName"> <input type="submit" value="提交" id="submit">
	</form>
</body>
</html>
@WebServlet("/DoFormServlet")
public class DoFormServlet extends HttpServlet {
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		req.setCharacterEncoding("UTF-8");
		boolean flag = isFlag(req);
		if (!flag) {
			resp.getWriter().write("已經提交...");
			System.out.println("數據已經提交了..");
			return;
		}
		String userName = req.getParameter("userName");
		try {
			Thread.sleep(300);
		} catch (Exception e) {
			// TODO: handle exception
		}
		resp.getWriter().write("success");
	}

	public boolean isFlag(HttpServletRequest request) {
		HttpSession session = request.getSession();
		String sesionToken = (String) session.getAttribute("sesionToken");
		String token = request.getParameter("token");
		if (!(token.equals(sesionToken))) {
			return false;
		}
		session.removeAttribute("sesionToken");
		return true;
	}
}

3.3 防止接口被模擬

能夠看到使用3.2的方式後,通常人模擬這個表單的請求的,由於這個token的生成方式你是不知道的,固然若是這個token的生成方式被知道了的話,就只能使用驗證碼了。。

4 XSS攻擊

XSS攻擊使用Javascript腳本注入進行攻擊

例如在表單中輸入: <script>location.href='http://www.baidu.com'</script>或者<script>alter("zz網站")</script>而後再另外一個jsp文件中這樣${name}。想一想看這是什麼效果????

fromToXss.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/XssDemo" method="post">
    <input type="text" name="userName"> <input type="submit">
</form>
</body>
</html>

user.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>

</head>
<body>userName:${userName}

</body>
</html>

XssDemo.java

@WebServlet("/XssDemo")
public class XssDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username=req.getParameter("userName");
        req.setAttribute("userName",username);
        req.getRequestDispatcher("user.jsp").forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req,resp);
    }
}

此時若是在表單中輸入<script>location.href='http://www.baidu.com'</script>將跳轉到百度首頁而輸入<script>alter("123")</script>將彈出警告框。。(這裏須要火狐瀏覽器才能顯示效果)

4.1 解決方式

歸根究底是瀏覽器沒有將這些特殊字符進行HTML字符轉換,所以只要書寫一個過濾器攔截對應的請求以後,轉義特殊字符以後就OK。

filter:

public class XssFilter implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        XssHttpServlteRequest xssHttpServlteRequest=new XssHttpServlteRequest(request);
        filterChain.doFilter(xssHttpServlteRequest,servletResponse);
    }

    public void destroy() {

    }
}

xss請求的參數獲取

public class XssHttpServlteRequest extends HttpServletRequestWrapper {
    private HttpServletRequest request;
    public XssHttpServlteRequest(HttpServletRequest request) {
        super(request);
        this.request=request;
    }

    @Override
    public String getParameter(String name) {
        String value=request.getParameter(name);
        System.out.println("name:"+name+","+value);
        if(!StringUtils.isEmpty(value)){
            //轉換HTML
            value=StringEscapeUtils.escapeHtml4(value);
        }
        return value;
    }
}

web.xml添加如下內容

<filter>
        <filter-name>XssFilter</filter-name>
        <filter-class>com.itboy.filter.XssFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>XssFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>