Servlet - 會話跟蹤

Servlet

標籤 : Java與Web


會話跟蹤

HTTP本身是“無狀態”協議,它不保存連接交互信息,一次響應完成之後即連接斷開,下一次請求需要重新建立連接,服務器不記錄上次連接的內容.因此如果判斷兩次連接是否是同一用戶, 就需要使用會話跟蹤技術來解決.常見的會話跟蹤技術有如下幾種:

  • URL重寫: 在URL結尾附加會話ID標識,服務器通過會話ID識別不同用戶.
  • 隱藏表單域: 將會話ID埋入HTML表單隱藏域提交到服務端(會話ID不在瀏覽器頁面顯示).
  • Cookie: 第一次請求時服務器主動發一小段信息給瀏覽器(即Cookie),下次請求時瀏覽器自動附帶該段信息發送給服務器,服務器讀取Cookie識別用戶.
  • Session: 服務器爲每個用戶創建一個Session對象保存到內存,並生成一個sessionID放入Cookie發送給瀏覽器,下次訪問時sessionID會隨Cookie傳回來,服務器再根據sessionID找到對應Session對象(Java領域特有).

Session機制依賴於Cookie,如果Cookie被禁用Session也將失效.


Cookie是識別當前用戶,實現持久會話的最好方式.最初由網景公司開發,但現在所有主流瀏覽器都支持.以至於HTTP協議爲他定義了一些新的HTTP首部.

URL重寫與隱藏表單域兩種技術都有一定的侷限,細節可參考博客四種會話跟蹤技術

  • Cookie規範
    • Cookie通過請求頭/響應頭在服務器與客戶端之間傳輸, 大小限制爲4KB;
    • 一臺服務器在一個客戶端最多保存20個Cookie;
    • 一個瀏覽器最多保存300個Cookie;

Cookie的key/value均不能保存中文,如果需要,可以在保存前對中文進行編碼, 取出時再對其解碼.


在Java中使用Cookie, 必須熟悉javax.servlet.http.Cookie類, 以及HttpServletRequest/HttpServletResponse接口提供的幾個方法:

Cookie 描述
Cookie(String name, String value) Constructs a cookie with the specified name and value.
String getName() Returns the name of the cookie.
String getValue() Gets the current value of this Cookie.
void setValue(String newValue) Assigns a new value to this Cookie.
void setMaxAge(int expiry) Sets the maximum age in seconds for this Cookie.
int getMaxAge() Gets the maximum age in seconds of this Cookie.
void setPath(String uri) Specifies a path for the cookie to which the client should return the cookie.
void setDomain(String domain) Specifies the domain within which this cookie should be presented.
Request 描述
Cookie[] getCookies() Returns an array containing all of the Cookie objects the client sent with this request.
Response 描述
void addCookie(Cookie cookie) Adds the specified cookie to the response.
  • 示例: 獲取上次訪問時間
    Request中獲取Cookie: last_access_time, 如果沒有則新建,否則顯示last_access_time內容, 並更新爲當前系統時間, 最後放入Response:
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie[] cookies = request.getCookies();
    Cookie latCookie = null;
    if (cookies != null){
        for (Cookie cookie : cookies){
            if (cookie.getName().equals(L_A_T)){
                latCookie = cookie;
                break;
            }
        }
    }

    // 已經訪問過了
    if (latCookie != null){
        printResponse("您上次訪問的時間是" + latCookie.getValue(), response);
        latCookie.setValue(new Date().toString());
    } else{
        printResponse("您還是第一次訪問", response);
        latCookie = new Cookie(L_A_T, new Date().toString());
    }

    response.addCookie(latCookie);
}

private void printResponse(String data, HttpServletResponse response) throws IOException {
    response.setContentType("text/html; charset=utf-8");
    response.getWriter().print("<H1>" + data + "</H1>");
}

有效期

Cookie的Max-Age決定了Cookie的有效期,單位爲秒.Cookie類通過getMaxAge()setMaxAge(int maxAge)方法來讀寫Max-Age屬性:

Max-Age 描述
0 Cookie立即作廢(如果原先瀏覽器已經保存了該Cookie,那麼可以通過設置Max-Age爲0使其失效)
< 0 默認,表示只在瀏覽器內存中存活,一旦瀏覽器關閉則Cookie銷燬
> 0 將Cookie持久化到硬盤上,有效期由Max-Age決定

域屬性

服務器可向Set-Cookie響應首部添加一個Domain屬性來控制哪些站點可以看到該Cookie, 如

Set-Cookie: last_access_time="xxx"; Domain=.fq.com

該響應首部就是在告訴瀏覽器將Cookie last_access_time="xxx"發送給域”.fq.com”中的所有站點(如www.fq.com, mail.fq.com).

Cookie類通過setDomain()方法設置域屬性.

如果沒有指定域, 則Domain默認爲產生Set-Cookie響應的服務器主機名.


路徑屬性

Cookie規範允許用戶將Cookie與部分Web站點關聯起來.該功能可通過向Set-Cookie響應首部添加Path屬性來實現:

Set-Cookie:last_access_time="Tue Apr 26 19:35:16 CST 2016"; Path=/servlet/

這樣如果訪問http://www.example.com/hello_http_servlet.do就不會獲得last_access_time,但如果訪問http://www.example.com/servlet/index.html, 就會帶上這個Cookie.

Cookie類中通過setPath()方法設置路徑屬性.

如果沒有指定路徑, Path默認爲產生Set-Cookie響應的URL的路徑.


Session

在所有的會話跟蹤技術中, Session是功能最強大,最多的. 每個用戶可以沒有或者有一個HttpSession對象, 並且只能訪問他自己的Session對象.

與URL重寫, 隱藏表單域和Cookie不同, Session是保存在服務器內存中的數據,在達到一定的閾值後, Servlet容器會將Session持久化到輔助存儲器中, 因此最好將使保存到Session內的對象實現java.io.Serializable接口.

使用Session, 必須熟悉javax.servlet.http.HttpSession接口, 以及HttpServletRequest接口中提供的幾個方法:

HttpSession 描述
void setAttribute(String name, Object value) Binds an object to this session, using the name specified.
Object getAttribute(String name) Returns the object bound with the specified name in this session, or null if no object is bound under the name.
void invalidate() Invalidates this session then unbinds any objects bound to it.
Enumeration<String> getAttributeNames() Returns an Enumeration of String objects containing the names of all the objects bound to this session.
void removeAttribute(String name) Removes the object bound with the specified name from this session.
String getId() Returns a string containing the unique identifier assigned to this session.
boolean isNew() Returns true if the client does not yet know about the session or if the client chooses not to join the session.
Request 描述
HttpSession getSession() Returns the current session associated with this request, or if the request does not have a session, creates one.
HttpSession getSession(boolean create) Returns the current HttpSession associated with this request or, if there is no current session and create is true, returns a new session.
String getRequestedSessionId() Returns the session ID specified by the client.

示例-購物車

  • domain
/**
 * @author jifang.
 * @since 2016/5/1 20:14.
 */
public class Product implements Serializable {

    private int id;
    private String name;
    private String description;
    private double price;

    public Product(int id, String name, String description, double price) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
    }

    // ...
}
public class ShoppingItem implements Serializable {
    private Product product;
    private int quantity;

    public ShoppingItem(Product product, int quantity) {
        this.product = product;
        this.quantity = quantity;
    }

    // ...
}
  • 商品列表頁面(/jsp/products.jsp)
<%@ page import="com.fq.web.domain.Product" %>
<%@ page import="com.fq.web.util.ProductContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Products</title>
</head>
<body>
<h2>Products</h2>
<ul>
    <%
        for (Product product : ProductContainer.products) {
    %>
    <li><%=product.getName()%>
        ($<%=product.getPrice()%>)
        (<a href="${pageContext.request.contextPath}/jsp/product_details.jsp?id=<%=product.getId()%>">Details</a>)
    </li>
    <%
        }
    %>
</ul>
<a href="${pageContext.request.contextPath}/jsp/shopping_cart.jsp">Shopping Cart</a>
</body>
</html>
  • 商品詳情(/jsp/product_details.jsp)
<%@ page import="com.fq.web.domain.Product" %>
<%@ page import="com.fq.web.util.ProductContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Product Details</title>
</head>
<body>
<h2>Product Details</h2>
<%
    int id = Integer.parseInt(request.getParameter("id"));
    Product product = ProductContainer.getProduct(id);
    assert product != null;
%>
<form action="${pageContext.request.contextPath}/session/add_to_card.do" method="post">
    <input type="hidden" name="id" value="<%=id%>"/>
    <table>
        <tr>
            <td>Name:</td>
            <td><%=product.getName()%>
            </td>
        </tr>
        <tr>
            <td>Price:</td>
            <td><%=product.getPrice()%>
            </td>
        </tr>
        <tr>
            <td>Description:</td>
            <td><%=product.getDescription()%>
            </td>
        </tr>
        <tr>
            <td><input type="text" name="quantity"></td>
            <td><input type="submit" value="Buy"></td>
        </tr>
        <tr>
            <td><a href="${pageContext.request.contextPath}/jsp/products.jsp">Products</a></td>
            <td><a href="${pageContext.request.contextPath}/jsp/shopping_cart.jsp">Shopping Cart</a></td>
        </tr>
    </table>
</form>
</body>
</html>
  • 加入購物車(AddCardServlet)
@WebServlet(name = "AddCardServlet", urlPatterns = "/session/add_to_card.do")
public class AddCardServlet extends HttpServlet {

    @SuppressWarnings("All")
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int id = Integer.parseInt(request.getParameter("id"));
        Product product = ProductContainer.getProduct(id);
        int quantity = Integer.parseInt(request.getParameter("quantity"));

        HttpSession session = request.getSession();
        List<ShoppingItem> items = (List<ShoppingItem>) session.getAttribute(SessionConstant.CART_ATTRIBUTE);
        if (items == null) {
            items = new ArrayList<ShoppingItem>();
            session.setAttribute(SessionConstant.CART_ATTRIBUTE, items);
        }
        items.add(new ShoppingItem(product, quantity));

        request.getRequestDispatcher("/jsp/products.jsp").forward(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}
  • 購物車(/jsp/shopping_card.jsp)
<%@ page import="com.fq.web.constant.SessionConstant" %>
<%@ page import="com.fq.web.domain.ShoppingItem" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Shopping Cart</title>
</head>
<body>
<h2>Shopping Cart</h2>
<a href="${pageContext.request.contextPath}/jsp/products.jsp">Products</a>
<table>
    <tr>
        <td style="width: 150px">Quantity</td>
        <td style="width: 150px">Product</td>
        <td style="width: 150px">Price</td>
        <td>Amount</td>
    </tr>
    <%
        List<ShoppingItem> items = (List<ShoppingItem>) session.getAttribute(SessionConstant.CART_ATTRIBUTE);
        if (items != null) {
            double total = 0.0;
            for (ShoppingItem item : items) {
                double subtotal = item.getQuantity() * item.getProduct().getPrice();
    %>
    <tr>
        <td><%=item.getQuantity()%>
        </td>
        <td><%=item.getProduct().getName()%>
        </td>
        <td><%=item.getProduct().getPrice()%>
        </td>
        <td><%=subtotal%>
        </td>
    </tr>
    <%
            total += subtotal;
        }%>
    <tr>
        <td>Total: <%=total%>
        </td>
    </tr>
    <%
        }
    %>

</table>
</body>
</html>

有效期

Session有一定的過期時間: 當用戶長時間不去訪問該Session,就會超時失效,雖然此時sessionID可能還在Cookie中, 只是服務器根據該sessionID已經找不到Session對象了.
Session的超時時間可以在web.xml中配置, 單位爲分鐘:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

另外一種情況: 由於sessionID保存在Cookie中且Max-Age-1,因此當用戶重新打開瀏覽器時已經沒有sessionID了, 此時服務器會再創建一個Session,此時新的會話又開始了.而原先的Session會因爲超時時間到達而被銷燬.


字符編碼

字符編碼就是以二進制的數字來對應字符集的字符,常見字符編碼方式有:ISO-8859-1(不支持中文),GB2312,GBK,UTF-8等.在JavaWeb中, 經常遇到的需要編碼/解碼的場景有響應編碼/請求編碼/URL編碼:


響應編碼

服務器發送數據給客戶端由Response對象完成,如果響應數據是二進制流,就無需考慮編碼問題.如果響應數據爲字符流,那麼就一定要考慮編碼問題:

response.getWriter()默認使用ISO-889-1發送數據,而該字符集不支持中文,因此遇到中文就一定會亂碼.

在需要發送中文時, 需要使用:

response.setCharacterEncoding("UTF-8");
// getWriter() ...

設置編碼方式,由於在getWriter()輸出前已經設置了UTF-8編碼,因此輸出字符均爲UTF-8編碼,但我們並未告訴客戶端使用什麼編碼來讀取響應數據,因此我們需要在響應頭中設置編碼信息(使用Content-Type):

response.setContentType("text/html;charset=UTF-8");
// getWriter() ...

注意: 這句代碼不只在響應頭中添加了編碼信息,還相當於調用了一次response.setCharacterEncoding("UTF-8");


請求編碼

1. 瀏覽器地址欄編碼

在瀏覽器地址欄書寫字符數據,由瀏覽器編碼後發送給服務器,因此如果在地址欄輸入中文,則其編碼方式由瀏覽器決定:

瀏覽器 編碼
IE/FireFox GB2312
Chrome UTF-8

2. 頁面請求

如果通過頁面的超鏈接/表單向服務器發送數據,那麼其編碼方式由當前頁面的編碼方式確定:

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

3. GET

當客戶端發送GET請求時,無論客戶端發送的數據編碼方式爲何,服務端均已ISO-8859-1解碼(Tomcat8.x之後改用UTF-8),這就需要我們在request.getParameter()獲取數據後再轉換成正確的編碼:

private Map<String, String> convertToParameterMap(HttpServletRequest request) throws UnsupportedEncodingException {
    Enumeration<String> names = request.getParameterNames();
    Map<String, String> parameters = new HashMap<String, String>();
    if (names != null) {
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            String value = request.getParameter(name);
            parameters.put(name, new String(value.getBytes("ISO-8859-1"), "UTF-8"));
        }
    }
    return parameters;
}

4. POST

當客戶端發送POST請求時,服務端也是默認使用IOS-8859-1解碼,但POST的數據是通過請求體傳送過來,因此POST請求可以通過request.setCharacterEncoding()來指定請求體編碼方式:

private Map<String, String> convertToParameterMap(HttpServletRequest request) throws IOException {
    Map<String, String> parameters = new HashMap<String, String>();
    if (request.getMethod().equals("POST")) {
        request.setCharacterEncoding("UTF-8");
        Enumeration<String> names = request.getParameterNames();
        while (names.hasMoreElements()) {
            String key = names.nextElement();
            parameters.put(key, request.getParameter(key));
        }
    } else {
        Enumeration<String> names = request.getParameterNames();
        while (names.hasMoreElements()) {
            String key = names.nextElement();
            String value = request.getParameter(key);
            parameters.put(key, new String(value.getBytes("ISO-8859-1"), "UTF-8"));
        }
    }

    return parameters;
}

URL編碼

網絡標準RFC 1738規定:

“…Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL.”
“只有字母和數字[0-9a-zA-Z]、一些特殊符號"$-_.+!*'(),"[不包括雙引號]、以及某些保留字,纔可以不經過編碼直接用於URL。”

如果URL中有漢字,就必須編碼後使用, 而URL編碼過程其實很簡單:

首先需要指定一種字符編碼,把字符串解碼後得到byte[],然後把小於0的字節+256,再將其轉換成16進制,最後前面再添加一個%.

這個編碼過程在Java中已經封裝成了現成的庫, 可直接使用:

URLEncoder 描述
static String encode(String s, String enc) Translates a string into application/x-www-form-urlencoded format using a specific encoding scheme.
URLDecoder 描述
static String decode(String s, String enc) Decodes a application/x-www-form-urlencoded string using a specific encoding scheme.

注: 在Web中Tomcat容器會自動識別URL是否已經編碼並自動解碼.


參考

更多有關編碼知識, 可以參考:
1. 阮一峯: 關於URL編碼
2. Web開發者應知的URL編碼知識
3. 字符集和字符編碼(Charset & Encoding)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章