一文看懂cookie和session

給大家分享我收藏的幾個(gè)不錯(cuò)的 github 項(xiàng)目,內(nèi)容都還是不錯(cuò)的,如果覺(jué)得有幫助,可以順便給個(gè) star。

  • 計(jì)算機(jī)專業(yè)學(xué)生必須要啃的書籍推薦: https://github.com/hello-go-maker/cs-books
  • Java 實(shí)戰(zhàn)項(xiàng)目推薦: https://github.com/hello-go-maker/Java-project
  • 推薦一些很不錯(cuò)的計(jì)算機(jī)學(xué)習(xí)教程,包括:數(shù)據(jù)結(jié)構(gòu)、算法、計(jì)算機(jī)網(wǎng)絡(luò)、操作系統(tǒng)、Java(spring、springmvc、springboot、springcloud),也包括多個(gè)企業(yè)級(jí)實(shí)戰(zhàn)項(xiàng)目: https://github.com/hello-go-maker/cs-learn-source
  • 【Java面試+Java后端技術(shù)學(xué)習(xí)指南】:一份通向理想互聯(lián)網(wǎng)公司的面試指南,包括 Java,技術(shù)面試必備基礎(chǔ)知識(shí)、Leetcode、計(jì)算機(jī)操作系統(tǒng)、計(jì)算機(jī)網(wǎng)絡(luò)、系統(tǒng)設(shè)計(jì)、分布式、數(shù)據(jù)庫(kù)(MySQL、Redis)、Java 項(xiàng)目實(shí)戰(zhàn)等: https://github.com/hello-java-maker/JavaInterview

一、會(huì)話的概念

會(huì)話可簡(jiǎn)單理解為:用戶開(kāi)一個(gè)瀏覽器,點(diǎn)擊多個(gè)超鏈接,訪問(wèn)服務(wù)器多個(gè)web資源,然后關(guān)閉瀏覽器,整個(gè)過(guò)程稱之為一個(gè)會(huì)話。

二、會(huì)話過(guò)程中要解決的一些問(wèn)題

每個(gè)用戶在使用瀏覽器與服務(wù)器進(jìn)行會(huì)話的過(guò)程中,不可避免各自會(huì)產(chǎn)生一些數(shù)據(jù),程序要想辦法為每個(gè)用戶保存這些數(shù)據(jù)。

三、保存會(huì)話數(shù)據(jù)的兩種技術(shù)

1、Cookie

Cookie意為"甜餅",是由W3C組織提出,最早由Netscape社區(qū)發(fā)展的一種機(jī)制。目前Cookie已經(jīng)成為標(biāo)準(zhǔn),所有的主流瀏覽器如IE、Netscape、Firefox、Opera等都支持Cookie。

由于HTTP是一種無(wú)狀態(tài)的協(xié)議,服務(wù)器單從網(wǎng)絡(luò)連接上無(wú)從知道客戶身份。怎么辦呢?就給客戶端們頒發(fā)一個(gè)通行證吧,每人一個(gè),無(wú)論誰(shuí)訪問(wèn)都必須攜帶自己通行證。這樣服務(wù)器就能從通行證上確認(rèn)客戶身份了。這就是Cookie的工作原理。

Cookie實(shí)際上是一小段的文本信息??蛻舳苏?qǐng)求服務(wù)器,如果服務(wù)器需要記錄該用戶狀態(tài),就使用response向客戶端瀏覽器頒發(fā)一個(gè)Cookie??蛻舳藶g覽器會(huì)把Cookie保存起來(lái)。當(dāng)瀏覽器再請(qǐng)求該網(wǎng)站時(shí),瀏覽器把請(qǐng)求的網(wǎng)址連同該Cookie一同提交給服務(wù)器。服務(wù)器檢查該Cookie,以此來(lái)辨認(rèn)用戶狀態(tài)。服務(wù)器還可以根據(jù)需要修改Cookie的內(nèi)容。

2、Session

Session是服務(wù)器端技術(shù),利用這個(gè)技術(shù),服務(wù)器在運(yùn)行時(shí)可以為每一個(gè)用戶的瀏覽器創(chuàng)建一個(gè)其獨(dú)享的session對(duì)象,由于session為用戶瀏覽器獨(dú)享,所以用戶在訪問(wèn)服務(wù)器的web資源時(shí),可以把各自的數(shù)據(jù)放在各自的session中,當(dāng)用戶再去訪問(wèn)服務(wù)器中的其它web資源時(shí),其它web資源再?gòu)挠脩舾髯缘膕ession中取出數(shù)據(jù)為用戶服務(wù)。

四、Cookie類的主要方法

int getMaxAge() 返回Cookie過(guò)期之前的最大時(shí)間,以秒計(jì)算。
void setMaxAge(intexpiry) 以秒計(jì)算,設(shè)置Cookie過(guò)期時(shí)間。
String getName() 返回Cookie的名字。名字和值是我們始終關(guān)心的兩個(gè)部分,筆者會(huì)在后面詳細(xì)介紹 getName/setName。
void setValue(String newValue) Cookie創(chuàng)建后設(shè)置一個(gè)新的值。
String getValue() 返回Cookie的值。筆者也將在后面詳細(xì)介紹getValue/setValue。
void setDomain(String pattern) 設(shè)置cookie中Cookie適用的域名
String getDomain() 返回Cookie中Cookie適用的域名. 使用getDomain() 方法可以指示瀏覽器把Cookie返回給同 一域內(nèi)的其他服務(wù)器,而通常Cookie只返回給與發(fā)送它的服務(wù)器名字完全相同的服務(wù)器。注意域名必須以點(diǎn)開(kāi)始(例如.yesky.com)
void setPath(String uri) 指定Cookie適用的路徑。
String getPath() 返回Cookie適用的路徑。如果不指定路徑,Cookie將返回給當(dāng)前頁(yè)面所在目錄及其子目錄下 的所有頁(yè)面。
void setSecure(boolean flag) 指出瀏覽器使用的安全協(xié)議,例如HTTPS或SSL。
boolean getSecure() 如果瀏覽器通過(guò)安全協(xié)議發(fā)送cookies將返回true值,如果瀏覽器使用標(biāo)準(zhǔn)協(xié)議則返回false值。
void setVersion(int v) Cookie所遵從的協(xié)議版本。
int getVersion() 返回Cookie所遵從的協(xié)議版本。
void setComment(String purpose) 設(shè)置cookie中注釋。
String getComment() 返回Cookie中注釋,如果沒(méi)有注釋的話將返回空值。
Cookie(String name, String value) 實(shí)例化Cookie對(duì)象,傳入cooke名稱和cookie的值。

response接口也中定義了一個(gè)addCookie方法,它用于在其響應(yīng)頭中增加一個(gè)相應(yīng)的Set-Cookie頭字段。 同樣,request接口中也定義了一個(gè)getCookies方法,它用于獲取客戶端提交的Cookie。

五、Cookie使用

1、使用cookie記錄用戶上一次訪問(wèn)的時(shí)間

public class CookieDemo extends HttpServlet{
 
    private static final long serialVersionUID = 5757885987685925915L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //設(shè)置服務(wù)器以UTF-8編碼輸出
        resp.setCharacterEncoding("UTF-8");
        //設(shè)置瀏覽器以UTF-8編碼進(jìn)行接收
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        
        //獲取Cookie數(shù)組
        Cookie[] cookie = req.getCookies();
        if(cookie == null){
            out.write("這是你的第一次訪問(wèn)!");
        } else {
            for (Cookie ck : cookie) {
                if(ck.getName().equals("cookieName")){
                    //獲取Cookie里面保存的數(shù)據(jù)
                    Long time = Long.parseLong(ck.getValue());
                    Date date = new Date(time);
                    out.write("上次訪問(wèn)時(shí)間:" + date.toLocaleString());
                }
            }
        }
        
        //創(chuàng)建一個(gè)cookie,cookie的名字是cookieName
        Cookie cookies = new Cookie("cookieName", System.currentTimeMillis()+"");
        //設(shè)置Cookie的有效期為1天,這樣即使關(guān)閉了瀏覽器,下次再訪問(wèn)時(shí),也依然可以通過(guò)cookie獲取用戶上一次訪問(wèn)的時(shí)間。
        cookies.setMaxAge(24*60*60);
        //將cookie對(duì)象添加到response對(duì)象中
        resp.addCookie(cookies);
    }
}

第一次訪問(wèn)時(shí),如下所示:

image

再次訪問(wèn):

image

2、刪除Cookie

public class CookieDemo extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
          //創(chuàng)建一個(gè)名字為lastAccessTime的cookie
        Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");
        //將cookie的有效期設(shè)置為0,命令瀏覽器刪除該cookie
        cookie.setMaxAge(0);
        req.addCookie(cookie);
    }
}

3、cookie中存/取中文

public class CookieDemo2 extends HttpServlet{
 
    private static final long serialVersionUID = 5757885987685925915L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //設(shè)置服務(wù)器以UTF-8編碼輸出
        resp.setCharacterEncoding("UTF-8");
        //設(shè)置瀏覽器以UTF-8編碼進(jìn)行接收
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        
        //創(chuàng)建一個(gè)cookie,cookie的名字是cookieName
        //存儲(chǔ)中文時(shí),使用URLEncoder類里面的encode(String s, String enc)方法進(jìn)行中文轉(zhuǎn)碼
        Cookie cookies = new Cookie("cookieName", URLEncoder.encode("哎喲!不錯(cuò)喲", "UTF-8"));
        //將cookie對(duì)象添加到response對(duì)象中
        resp.addCookie(cookies);
        
        //獲取Cookie數(shù)組
        Cookie[] cookie = req.getCookies();
        if(cookie != null){
            for (Cookie ck : cookie) {
                if(ck.getName().equals("cookieName")){
                    //獲取Cookie里面保存的數(shù)據(jù)
                    String text = ck.getValue();
                    //使用URLDecoder類里面的decode(String s, String enc)進(jìn)行解碼
                    out.write("存儲(chǔ)的中文數(shù)據(jù):" + URLDecoder.decode(text, "UTF-8"));
                }
            }
        }
    }
}

結(jié)果如下:

image

Cookie注意細(xì)節(jié)

1,一個(gè)Cookie只能標(biāo)識(shí)一種信息,它至少含有一個(gè)標(biāo)識(shí)該信息的名稱(NAME)和設(shè)置值(VALUE)。

2,一個(gè)WEB站點(diǎn)可以給一個(gè)WEB瀏覽器發(fā)送多個(gè)Cookie,一個(gè)WEB瀏覽器也可以存儲(chǔ)多個(gè)WEB站點(diǎn)提供的Cookie。

3,瀏覽器一般只允許存放300個(gè)Cookie,每個(gè)站點(diǎn)最多存放20個(gè)Cookie,每個(gè)Cookie的大小限制為4KB。

4,如果創(chuàng)建了一個(gè)cookie,并將他發(fā)送到瀏覽器,默認(rèn)情況下它是一個(gè)會(huì)話級(jí)別的cookie(即存儲(chǔ)在瀏覽器的內(nèi)存中),用戶退出瀏覽器之后即被刪除。若希望瀏覽器將該cookie存儲(chǔ)在磁盤上,則需要使用maxAge,并給出一個(gè)以秒為單位的時(shí)間。將最大時(shí)效設(shè)為0則是命令瀏覽器刪除該cookie。

六、Session簡(jiǎn)單介紹

在WEB開(kāi)發(fā)中,服務(wù)器可以為每個(gè)用戶瀏覽器創(chuàng)建一個(gè)會(huì)話對(duì)象(session對(duì)象),注意:一個(gè)瀏覽器獨(dú)占一個(gè)session對(duì)象(默認(rèn)情況下)。因此,在需要保存用戶數(shù)據(jù)時(shí),服務(wù)器程序可以把用戶數(shù)據(jù)寫到用戶瀏覽器獨(dú)占的session中,當(dāng)用戶使用瀏覽器訪問(wèn)其它程序時(shí),其它程序可以從用戶的session中取出該用戶的數(shù)據(jù),為用戶服務(wù)。

Session和Cookie的主要區(qū)別

1,Cookie是把用戶的數(shù)據(jù)寫給用戶的瀏覽器。

2,Session技術(shù)把用戶的數(shù)據(jù)寫到用戶獨(dú)占的session中。

3,Session對(duì)象由服務(wù)器創(chuàng)建,開(kāi)發(fā)人員可以調(diào)用request對(duì)象的getSession方法得到session對(duì)象。

七、Session基礎(chǔ)知識(shí)

Session是服務(wù)器端技術(shù),利用這個(gè)技術(shù),服務(wù)器在運(yùn)行時(shí)可以為每一個(gè)用戶的瀏覽器創(chuàng)建一個(gè)其獨(dú)享的session對(duì)象,由于session為用戶瀏覽器獨(dú)享,所以用戶在訪問(wèn)服務(wù)器的web資源時(shí),可以把各自的數(shù)據(jù)放在各自的session中,當(dāng)用戶再去訪問(wèn)服務(wù)器中的其它web資源時(shí),其它web資源再?gòu)挠脩舾髯缘膕ession中取出數(shù)據(jù)為用戶服務(wù)。

當(dāng)用戶打開(kāi)瀏覽器,訪問(wèn)某個(gè)網(wǎng)站操作session時(shí),服務(wù)器就會(huì)在服務(wù)器的內(nèi)存為該瀏覽器分配一個(gè)session對(duì)象,該session對(duì)象被這個(gè)瀏覽器獨(dú)占。

這個(gè)session對(duì)象也可以看做是一個(gè)容器,session默認(rèn)存在時(shí)間為30min,你可以修改。

1、Session可以用來(lái)做什么

1、網(wǎng)上商城中的購(gòu)物車

2、保存登錄用戶的信息

3、將某些數(shù)據(jù)放入到Session中,供同一用戶的各個(gè)頁(yè)面使用

4、防止用戶非法登錄到某個(gè)頁(yè)面。

2、Session基本使用

request.getSession() 返回這個(gè)request綁定的session對(duì)象,如果沒(méi)有,則創(chuàng)建一個(gè)
request.getSession(boolean create) 返回這個(gè)request綁定的session對(duì)象,如果沒(méi)有,則根據(jù)create的值決定是否創(chuàng)建一個(gè)
session.setAttribute(String name,Object val) 向session中添加屬性
session.getAttribute(String name) 從session中得到某個(gè)屬性
session.removeAttribute(String name) 從session中刪除某個(gè)屬性
session.setMaxInactiveInterval() 設(shè)置Session的生命周期(單位秒),Session的生命周期默認(rèn)是30min
session.invalidate() 清除所有session
session.removeAttribute(String name) 刪除指定名稱的session

Servlet1:

public class Servlet1 extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取session
        HttpSession session = req.getSession();
        //將數(shù)據(jù)存儲(chǔ)到session中
        session.setAttribute("name", "Zender");
    }
}

Servlet2:

public class Servlet2 extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取session
        HttpSession session = req.getSession();
        //獲取name
        String name = (String) session.getAttribute("name");
        PrintWriter out = resp.getWriter();
        out.print("name:" + name);
    }
}

同一瀏覽器訪問(wèn)Servlet1,再訪問(wèn)Servlet2,結(jié)果如下:

image

不同瀏覽器訪問(wèn)Servlet1,再訪問(wèn)Servlet2,結(jié)果如下:

image

可以看到這時(shí)候name是null,也就是沒(méi)有從session對(duì)象中取出值,因?yàn)?60瀏覽器并沒(méi)有運(yùn)行Servlet1來(lái)創(chuàng)建Session對(duì)象,上面的session對(duì)象是Chrome瀏覽器獨(dú)占的。

3、Session生命周期

Session中的屬性的默認(rèn)生命周期是30min,這個(gè)默認(rèn)時(shí)間可以通過(guò)修改web.xml文件來(lái)修改

1,在Tomcat根目錄\conf\web.xml文件中修改

<session-config>

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

</session-config>

2,如果只需要對(duì)某一個(gè)web應(yīng)用設(shè)置,則只需要修改對(duì)應(yīng)web應(yīng)用的web.xml文件。在這個(gè)web.xml文件中添加如上的代碼:

<session-config>

  <session-timeout>10</session-timeout>

</session-config>

除了設(shè)置默認(rèn)生命周期之外,最重要的是在程序中設(shè)置,調(diào)用setMaxInacttiveInterval(int interval),這里的interval是以秒為單位的,而且這個(gè)方法設(shè)置的是發(fā)呆時(shí)間,比如你設(shè)置的是60秒,那么在這60秒之內(nèi)如果你沒(méi)有操作過(guò)session,它就會(huì)自動(dòng)刪除,如果你操作過(guò),不管是設(shè)置屬性還是讀取屬性,它都會(huì)從頭開(kāi)始計(jì)時(shí)。

session.setMaxInactiveInterval(60);

八、Session實(shí)現(xiàn)原理

服務(wù)器是如何實(shí)現(xiàn)一個(gè)session為一個(gè)用戶瀏覽器服務(wù)的?

image

1,瀏覽器A先訪問(wèn)Servlet1,這時(shí)候它創(chuàng)建了一個(gè)Session,ID號(hào)為110,然后Servlet1將這個(gè)ID號(hào)以Cookie的方式返回給瀏覽器A。

2,瀏覽器A繼續(xù)訪問(wèn)Servlet2,那么這個(gè)請(qǐng)求會(huì)帶上Cookie值: JSESSIONID=110,然后服務(wù)器根據(jù)瀏覽器A傳遞過(guò)來(lái)的ID號(hào)找到內(nèi)存中的這個(gè)Session。

3,瀏覽器B來(lái)訪問(wèn)Servlet1了,它的請(qǐng)求并沒(méi)有帶上 JSESSIONID這個(gè)Cookie值,由于它也要使用Session,所以服務(wù)器會(huì)新創(chuàng)建一個(gè)Session,ID號(hào)為111。

4,瀏覽器B繼續(xù)訪問(wèn)Servlet2,那么這個(gè)請(qǐng)求會(huì)帶上Cookie值: JSESSIONID=111,然后服務(wù)器根據(jù)瀏覽器B傳遞過(guò)來(lái)的ID號(hào)找到內(nèi)存中的這個(gè)Session。

例如:

Servlet1:

public class Servlet1 extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取session
        HttpSession session = req.getSession();
        //將數(shù)據(jù)存儲(chǔ)到session中
        session.setAttribute("name", "Zender");
        PrintWriter out = resp.getWriter();
        out.print("create Session OK");
    }
}

Servlet2:

public class Servlet2 extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取session
        HttpSession session = req.getSession();
        //獲取name
        String name = (String) session.getAttribute("name");
        PrintWriter out = resp.getWriter();
        out.println("Session ID:" + session.getId());
        out.print("name:" + name);
    }
}

第一次訪問(wèn)Servlet1時(shí),服務(wù)器會(huì)創(chuàng)建一個(gè)新的sesion,并且把session的Id以cookie的形式發(fā)送給客戶端瀏覽器,如下圖所示:

image

可以看到,Request Headers中并沒(méi)有Cookie的信息,而Response Headers中有這么一句話:

Set-Cookie:

JSESSIONID=05A94199DDC64311563740CC2C78D656; Path=/CookieAndSession/; HttpOnly

說(shuō)明這個(gè)時(shí)候服務(wù)器向客戶端通過(guò)Cookie傳遞回了 JSESSIONID這個(gè)屬性。

然后訪問(wèn)Servlet2,如下圖所示:

image

可以看到Response Headers中沒(méi)有出現(xiàn)Set-Cookie這個(gè)頭,而Request Headers中帶上了Cookie這個(gè)頭:

Cookie:

JSESSIONID=05A94199DDC64311563740CC2C78D656

而這個(gè)頭中正包含 JSESSIONID,并且它的值也就是我們之前Set-Cookie中 JSESSIONID的值。

這就證明了我們之前圖解的Session的原理,也就是服務(wù)器能夠?yàn)椴煌臑g覽器區(qū)分不同的Session的機(jī)制。

九、Session的簡(jiǎn)單應(yīng)用

1,用戶登錄時(shí)候驗(yàn)證驗(yàn)證碼

Index.jsp:

<head>
<base href="<%=basePath%>" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>表單提交</title>
<link href="css/bootstrap.css" rel="stylesheet">
<script src="js/jquery-3.2.1.js"></script>
<script src="js/bootstrap.js"></script>
</head>
<body>
    <form class="form-horizontal" action="<%=request.getContextPath()%>/CodeServlet.html" role="form" method="post">
        <div class="form-group">
            <label for="firstname" class="col-sm-1 control-label">驗(yàn)證碼</label>
            <div class="col-sm-3">
                <input type="text" class="form-control" name="idCodeNum"
                    placeholder="請(qǐng)輸入驗(yàn)證碼">
            </div>
            <img src="idCode" onclick="this.src+=''" style="cursor:pointer;" width="115" height="30" title="看不清?換一個(gè)">
        </div>
        <div class="form-group">
            <div class="col-sm-offset-1 col-sm-3">
                <button type="submit" class="btn btn-default">提交</button>
                <button type="reset" class="btn btn-default">重置</button>
            </div>
        </div>
    </form>
</body>
</html>

CodeServlet:

public class CodeServlet extends HttpServlet {
    private static final long serialVersionUID = -8236507185410764108L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取session
        HttpSession session = req.getSession();
        //獲取存儲(chǔ)的驗(yàn)證碼
        String idCodeText = (String) session.getAttribute("idCode");
        System.out.println("服務(wù)端驗(yàn)證碼:" + idCodeText);
        
        String idCodeNum = req.getParameter("idCodeNum");
        System.out.println("輸入的驗(yàn)證碼:" + idCodeNum);
        //設(shè)置瀏覽器以UTF-8編碼進(jìn)行接收
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        if(idCodeText.equalsIgnoreCase(idCodeNum)){
            out.println("驗(yàn)證成功!");
        } else {
            out.println("驗(yàn)證碼錯(cuò)誤!");
        }
    }
}

Web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    id="WebApp_ID" version="3.0">
    <display-name>CookieAndSession</display-name>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    <servlet>
        <servlet-name>CodeServlet</servlet-name>
        <servlet-class>com.zender.session.CodeServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>CodeServlet</servlet-name>
        <url-pattern>/CodeServlet.html</url-pattern>
    </servlet-mapping>
    <!-- 生成驗(yàn)證碼的Servlet -->
    <servlet>
        <servlet-name>ValidateCode</servlet-name>
        <servlet-class>org.jelly.image.ValidateCode</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ValidateCode</servlet-name>
        <url-pattern>/idCode</url-pattern>
    </servlet-mapping>
</web-app>

這里使用了jelly-core-1.7.0.GA.jar來(lái)生成了驗(yàn)證碼,具體使用方式請(qǐng)點(diǎn)擊:jelly-core-1.7.0.GA.jar(http://www.blogjava.net/fancydeepin/archive/2014/08/03/jelly_image.html)

訪問(wèn)http://localhost:8081/CookieAndSession/index.jsp輸入驗(yàn)證碼,點(diǎn)擊提交:

image

輸入正確驗(yàn)證碼,頁(yè)面響應(yīng)結(jié)果:

image

輸入錯(cuò)誤驗(yàn)證碼,頁(yè)面響應(yīng)結(jié)果:

image

輸入正確驗(yàn)證碼,后臺(tái)結(jié)果:

image

輸入錯(cuò)誤驗(yàn)證碼,后臺(tái)結(jié)果:

image

2,實(shí)現(xiàn)簡(jiǎn)易購(gòu)物車

模擬一個(gè)數(shù)據(jù)庫(kù):

public class DB {
    private static HashMap<String, Book> hm = null;
    private DB(){
    }
 
    static{
        hm = new LinkedHashMap<String, Book>();
 
        Book book1 = new Book(1, "Java基礎(chǔ)");
        Book book2 = new Book(2, "Oracle數(shù)據(jù)庫(kù)");
        Book book3 = new Book(3, "C語(yǔ)言");
        Book book4 = new Book(4, "Python核心教程");
        Book book5 = new Book(5, "Web技術(shù)");
 
        hm.put(book1.getId() + "" , book1);
        hm.put(book2.getId() + "" , book2);
        hm.put(book3.getId() + "" , book3);
        hm.put(book4.getId() + "" , book4);
        hm.put(book5.getId() + "" , book5);
    }
 
    /**
     * 得到數(shù)據(jù)庫(kù)中所有的書
     * @return
     */
    public static HashMap<String, Book> getBooks(){
        return hm;
    }
 
    /**
     * 根據(jù)ID得到書
     * @param id
     * @return
     */
    public static Book getBookById(String id){
        if(hm.containsKey(id)){
            return hm.get(id);
        }
        return null;
    }
}

BuyBookServlet這個(gè)Servlet用于購(gòu)買圖書:

//顯示購(gòu)買的圖書
public class ShowMyCartServlet extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        HttpSession session = req.getSession();
        List<Book> list = (ArrayList<Book>) session.getAttribute("books");
        if(list==null || list.size()==0){
            out.write("對(duì)不起,您還沒(méi)有購(gòu)買任何商品!!");
            return;
        }
        
        //顯示用戶買過(guò)的商品
        out.write("您買過(guò)如下商品:<br/>");
        for(Book book : list){
            out.write(book.getName() + "<br/>");
        }
    }
}

運(yùn)行結(jié)果:

image

3,防止用戶非法登錄到某個(gè)頁(yè)面

比如我們的用戶管理系統(tǒng),必須要登錄成功后才能跳轉(zhuǎn)到主頁(yè)面,而不能直接繞過(guò)登錄頁(yè)面直接到主頁(yè)面,這個(gè)應(yīng)用是一個(gè)非常常見(jiàn)的應(yīng)用。

當(dāng)在驗(yàn)證用戶的控制器LoginClServlet.java驗(yàn)證用戶成功后,將當(dāng)前的用戶信息保存在Session對(duì)象中:

// 把user對(duì)象保存在session

HttpSession session = req.getSession();

session.setAttribute("loginUser", user);

然后在主頁(yè)面Main.java最開(kāi)始的地方,取出Session中的登錄用戶信息,如果信息為空,則為非法訪問(wèn),直接跳轉(zhuǎn)到登錄頁(yè)面,并提示相關(guān)信息:

// 取出login-user這個(gè)session
User loginUser = (User)req.getSession().getAttribute("loginUser");
if(loginUser == null){
    // 說(shuō)明用戶沒(méi)有登錄,讓他跳轉(zhuǎn)到登錄頁(yè)面
    req.setAttribute("error", "請(qǐng)登錄!");
    req.getRequestDispatcher("/LoginServlet").forward(req,res);
    return;
}

那么這里就存在一個(gè)問(wèn)題,一個(gè)網(wǎng)站會(huì)有很多個(gè)需要防止非法訪問(wèn)的頁(yè)面,如果都是用這種方法豈不是很麻煩?

兩種解決辦法:

第一種:將這段驗(yàn)證用戶的代碼封裝成函數(shù),每次調(diào)用

第二種:使用過(guò)濾器

4,利用Session防止表單重復(fù)提交

具體的做法:

在服務(wù)器端生成一個(gè)唯一的隨機(jī)標(biāo)識(shí)號(hào),專業(yè)術(shù)語(yǔ)稱為Token(令牌),同時(shí)在當(dāng)前用戶的Session域中保存這個(gè)Token。然后將Token發(fā)送到客戶端的Form表單中,在Form表單中使用隱藏域來(lái)存儲(chǔ)這個(gè)Token,表單提交的時(shí)候連同這個(gè)Token一起提交到服務(wù)器端,然后在服務(wù)器端判斷客戶端提交上來(lái)的Token與服務(wù)器端生成的Token是否一致,如果不一致,那就是重復(fù)提交了,此時(shí)服務(wù)器端就可以不處理重復(fù)提交的表單。如果相同則處理表單提交,處理完后清除當(dāng)前用戶的Session域中存儲(chǔ)的標(biāo)識(shí)號(hào)。

在下列情況下,服務(wù)器程序?qū)⒕芙^處理用戶提交的表單請(qǐng)求:

1,存儲(chǔ)Session域中的Token(令牌)與表單提交的Token(令牌)不同。

2,當(dāng)前用戶的Session中不存在Token(令牌)。

3,用戶提交的表單數(shù)據(jù)中沒(méi)有Token(令牌)。

例如:

創(chuàng)建FormTokenServlet,用于生成Token和跳轉(zhuǎn)到token.jsp頁(yè)面:

public class FormTokenServlet extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
 
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 創(chuàng)建令牌
        String token = TokenProccessor.getInstance().makeToken();
        System.out.println("在FormServlet中生成的token:" + token);
        // 在服務(wù)器使用session保存token(令牌)
        req.getSession().setAttribute("token", token);
        // 跳轉(zhuǎn)到token.jsp頁(yè)面
        req.getRequestDispatcher("/token.jsp").forward(req, resp);
    }
}

在token.jsp中使用隱藏域來(lái)存儲(chǔ)Token(令牌),提交Token(令牌)到服務(wù)器:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
   String path = request.getContextPath();
   String basePath = request.getScheme() + "://"
           + request.getServerName() + ":" + request.getServerPort()
           + path + "/";
%>
<html>
<head>
<base href="<%=basePath%>" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>表單提交</title>
<link href="css/bootstrap.css" rel="stylesheet">
<script src="js/jquery-3.2.1.js"></script>
<script src="js/bootstrap.js"></script>
</head>
<body>
    <form class="form-horizontal" action="<%=request.getContextPath()%>/TokenServlet.html" role="form" method="post">
        <input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
        <div class="form-group">
            <label for="firstname" class="col-sm-1 control-label">用戶名</label>
            <div class="col-sm-3">
                <input type="text" class="form-control" name="idCodeNum"
                    placeholder="請(qǐng)輸入用戶名">
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-offset-1 col-sm-3">
                <button type="submit" class="btn btn-default">提交</button>
                <button type="reset" class="btn btn-default">重置</button>
            </div>
        </div>
    </form>
</body>
</html>

TokenServlet處理表單提交:

public class TokenServlet extends HttpServlet {
 
    private static final long serialVersionUID = -8236507185410764108L;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
 
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        boolean b = false;
        String client_token = req.getParameter("token");
        // 如果用戶提交的表單數(shù)據(jù)中沒(méi)有token,則用戶是重復(fù)提交了表單
        if (client_token == null) {
            b = true;
        }
        // 取出存儲(chǔ)在Session中的token
        String serverToken = (String) req.getSession().getAttribute("token");
        // 如果當(dāng)前用戶的Session中不存在Token,則用戶是重復(fù)提交了表單
        if (serverToken == null) {
            b = true;
        }
 
        // 存儲(chǔ)在Session中的Token與表單提交的Token不同,則用戶是重復(fù)提交了表單
        if (!client_token.equals(serverToken)) {
            b = true;
        }
 
        if (b == true) {
            System.out.println("請(qǐng)不要重復(fù)提交!");
            return;
        }
        // 移除session中的token
        req.getSession().removeAttribute("token");
        System.out.println("正在處理用戶提交請(qǐng)求!!");
    }
}

運(yùn)行結(jié)果如下:

image

十、用戶禁用Cookie后的Session處理

這里存在一種情況,假如用戶瀏覽器禁用了Cookie怎么辦?比如我把Chrome的Cookie禁用,如下:

image

解決方法:URL重寫

Servlet中的response提供了對(duì)URL重寫的方法:

response.encodeRedirectURL(String url) 用于對(duì)sendRedirect方法后的url地址進(jìn)行重寫
response.encodeURL(String url) 用于對(duì)表單action和超鏈接的url地址進(jìn)行重寫

那么URL重寫是什么意思呢?其實(shí)就是人為地把JSESSIONID附在了url的后面,比如我們修改之前寫的簡(jiǎn)易購(gòu)物車,ShowBook中所有的點(diǎn)擊購(gòu)買超鏈接都要重寫。

之前我們是這么寫的:

out.println("<tr><td>"+book.getName()+"</td><td><a href='"+ req.getContextPath() +"/BuyBookServlet.html?id="+book.getId()+"'>點(diǎn)擊購(gòu)買</a></td></tr>");

現(xiàn)在進(jìn)行URL重寫:

req.getSession();
String url = "/MyCart/BuyBookCl?id="+book.getId();
url = resp.encodeURL(url);
out.println("<tr><td>"+book.getName()+"</td><td><a href='"+url+"'>點(diǎn)擊購(gòu)買</a></td></tr>");

需要注意的是,重寫之前一定要調(diào)用或者確保使用過(guò)request.getSession()這個(gè)方法。

重寫之前,訪問(wèn)ShowBookServlet的源代碼是這樣的:

image

重寫之后呢:

image

可以看到URL重寫之后,jsessionid這個(gè)參數(shù)自動(dòng)附在了url后面,由此,得以確保我們的Session在Cookie被禁用的情況下繼續(xù)正常使用。這時(shí)候我們查看購(gòu)物車的地址欄如下,可以明顯看到j(luò)sessionid這個(gè)參數(shù):

image

最后,給大家分享我收藏的幾個(gè)不錯(cuò)的 github 項(xiàng)目,內(nèi)容都還是不錯(cuò)的,如果覺(jué)得有幫助,可以順便給個(gè) star。

  • 計(jì)算機(jī)專業(yè)學(xué)生必須要啃的書籍推薦: https://github.com/hello-go-maker/cs-books
  • Java 實(shí)戰(zhàn)項(xiàng)目推薦: https://github.com/hello-go-maker/Java-project
  • 推薦一些很不錯(cuò)的計(jì)算機(jī)學(xué)習(xí)教程,包括:數(shù)據(jù)結(jié)構(gòu)、算法、計(jì)算機(jī)網(wǎng)絡(luò)、操作系統(tǒng)、Java(spring、springmvc、springboot、springcloud),也包括多個(gè)企業(yè)級(jí)實(shí)戰(zhàn)項(xiàng)目: https://github.com/hello-go-maker/cs-learn-source
  • Java面試+Java后端技術(shù)學(xué)習(xí)指南】:一份通向理想互聯(lián)網(wǎng)公司的面試指南,包括 Java,技術(shù)面試必備基礎(chǔ)知識(shí)、Leetcode、計(jì)算機(jī)操作系統(tǒng)、計(jì)算機(jī)網(wǎng)絡(luò)、系統(tǒng)設(shè)計(jì)、分布式、數(shù)據(jù)庫(kù)(MySQL、Redis)、Java 項(xiàng)目實(shí)戰(zhàn)等: https://github.com/hello-java-maker/JavaInterview
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容