Java基礎(chǔ)——Cookie和Session以及Token

1.1. 狀態(tài)管理

1.1.1. 為什么需要狀態(tài)管理?
Web應(yīng)用程序使用HTTP協(xié)議作為傳輸數(shù)據(jù)的標(biāo)準(zhǔn)協(xié)議,而HTTP協(xié)議是無狀態(tài)協(xié)議,即一次請求對應(yīng)一次響應(yīng),響應(yīng)結(jié)束后連接即斷開,同一個用戶的不同請求對于服務(wù)器端來講并不會認(rèn)為這兩個請求有什么關(guān)聯(lián)性,并不會以此區(qū)分不同的客戶端。但實(shí)際情況中還是需要服務(wù)器端能夠區(qū)分不同的客戶端以及記錄與客戶端相關(guān)的一些數(shù)據(jù),所以狀態(tài)管理能夠做到不同客戶端的身份識別。

1.1.2. 什么是狀態(tài)管理?
將客戶端與服務(wù)器之間多次交互當(dāng)做一個整體來看待,并且將多次交互中涉及的數(shù)據(jù)保存下來,提供給后續(xù)的交互進(jìn)行數(shù)據(jù)的管理即狀態(tài)管理。

這里的狀態(tài)指的是當(dāng)前的數(shù)據(jù),管理指的是在這個多次交互的過程中對數(shù)據(jù)的存儲、修改、刪除。比如:車主每次攜帶卡片洗車后由商家修改次數(shù),車主即可帶走這張記錄數(shù)據(jù)的卡片,商家不會保存任何數(shù)據(jù),客戶自己負(fù)責(zé)攜帶需要維護(hù)的數(shù)據(jù)。

1.1.3. 狀態(tài)管理兩種常見模式
狀態(tài)管理的過程中重要的是數(shù)據(jù)的保存,只有存下來的數(shù)據(jù)才能在多次交互中起到記錄的作用,所以可以按照管理的數(shù)據(jù)的存儲方式和位置的不同來區(qū)分狀態(tài)管理的模式。

如果將數(shù)據(jù)存儲在客戶端,每次向服務(wù)器端發(fā)請求時都將存在客戶端的數(shù)據(jù)隨著請求發(fā)送到服務(wù)器端,修改后再發(fā)回到客戶端保存的這種模式叫做Cookie。

如果將數(shù)據(jù)存儲在服務(wù)器端,并且為這組數(shù)據(jù)標(biāo)示一個編號,只將編號發(fā)回給客戶端。當(dāng)客戶端向服務(wù)器發(fā)送請求時只需要將這個編號發(fā)過來,服務(wù)器端按照這個編號找到對應(yīng)的數(shù)據(jù)進(jìn)行管理的這種模式叫做Session——會話。

1.2. Cookie

1.2.1. 什么是Cookie?
一小段文本信息隨著請求和響應(yīng),在客戶端和服務(wù)器端之間來回傳遞。根據(jù)設(shè)定的時間來決定該段文本在客戶端保存時長的這種工作模式叫做Cookie。最初服務(wù)器將信息發(fā)給客戶端時是通過響應(yīng)數(shù)據(jù)的Set-Cookie頭信息來完成的。

1.2.2. Cookie的原理


cookie生成及原理.png

如果客戶端向服務(wù)器端AddServlet發(fā)送請求,遇到創(chuàng)建Cookie的代碼時,那么一小段文本信息就會隨著response響應(yīng)中的頭信息被傳遞回客戶端。如圖中Set-Cookie:uname=xxx就是從服務(wù)器端傳遞回客戶端的文本信息。當(dāng)文本信息到達(dá)客戶端以后,會被保存在客戶端的內(nèi)存或硬盤上,存在內(nèi)存中會隨著內(nèi)存的釋放而消失,存在硬盤上則會保存更長的時間。

一旦客戶端存有服務(wù)器發(fā)回的文本信息,那么當(dāng)瀏覽器再次向服務(wù)器發(fā)起請求時,如請求FindServlet這個組件,那么存儲的文本信息會隨著請求數(shù)據(jù)包的消息頭以Cookie:uname=xxx這樣的形式將文本信息發(fā)送到服務(wù)器端。只要Cookie的生命周期沒有結(jié)束,那么不管是存在內(nèi)存還是硬盤上的信息都會在客戶端向服務(wù)器端發(fā)出請求時自動的隨著消息頭發(fā)送過去。

1.2.3. 如何創(chuàng)建Cookie
Servlet API提供了javax.servlet.http.Cookie這種類型來解釋Cookie。其中存儲的文本以name-value對的形式進(jìn)行區(qū)分,所以創(chuàng)建Cookie時指定name-value對即可。這個name-value最終是以Set-Cookie這種消息頭的形式跟隨相應(yīng)數(shù)據(jù)包到達(dá)客戶端,所以要想將數(shù)據(jù)添加到消息頭中需要使用response對象提供的方法。創(chuàng)建Cookie的代碼如下所示:

Cookie  c = new Cookie(String name,String value);  //文本的創(chuàng)建
response.addCookie( c );    /**在響應(yīng)數(shù)據(jù)包中追加一個Set-Cookie的消息頭,如果發(fā)送了相同name的Cookie數(shù)據(jù),那么之前的數(shù)據(jù)會被覆蓋。能夠創(chuàng)建多少個Cookie存放在客戶端與當(dāng)前瀏覽器的種類相關(guān)。**/
//實(shí)現(xiàn)創(chuàng)建兩個Cookie代碼示例:
package Paint;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.*;
public class AddCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        // 創(chuàng)建cookie
        Cookie c = new Cookie("username", "Lisa");
        Cookie c2 = new Cookie("city", "NewYork");
        response.addCookie(c);
        response.addCookie(c2);
        out.close();
    }
}

1.2.4. 如何查詢Cookie
當(dāng)客戶端向服務(wù)器發(fā)出請求時,服務(wù)器端可以嘗試著從請求數(shù)據(jù)包的消息頭中獲取是否攜帶了Cookie信息。實(shí)現(xiàn)這一功能的代碼如下:

Cookie[] request.getCookies();

由于客戶端是可以存放多個Cookie的,所以request提供的獲取Cookie的方法的返回值是Cookie數(shù)組,如果想進(jìn)一步獲取某一個Cookie信息可以通過遍歷數(shù)組,分別獲取每一個Cookie的name和value。代碼如下:

Cookie[] cookies =  request.getCookies();
    if(cookies!=null){
        for (Cookie c : cookies) {
            String cookieName = c.getName();
            String cookieValue = c.getValue();
        }
    }

查詢Cookie的完整代碼如下:

package Paint;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import javax.servlet.*;
import javax.servlet.http.*;

public class FindCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response)
                          throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for(int i=0;i<cookies.length;i++){
                Cookie c = cookies[i];
                String name = c.getName();
                String value = c.getValue();
                out.println(name + ":"  value + "<br/>");
            }
        }else{
            out.println("沒有找到cookie");
        }
        out.close();
    }
}

1.2.5. 如何修改Cookie
所謂Cookie的修改,本質(zhì)是獲取到要變更值的Cookie,通過setValue方法將新的數(shù)據(jù)存入到cookie中,然后由response響應(yīng)對象發(fā)回到客戶端,對原有舊值覆蓋后即實(shí)現(xiàn)了修改。主要實(shí)現(xiàn)代碼:

/**
其中response.addCookie(c)是非常重要的語句,如果沒有這一行代碼,那么就算是使用setValue方法修改了Cookie的值,但是不發(fā)回到客戶端的話,也不會實(shí)現(xiàn)數(shù)值的改變。所以只要見到response.addCookie這行代碼,即服務(wù)器端發(fā)回了帶有Set-Cookie消息頭的信息.
***/
Cookie[] cookies =  request.getCookies();
if(cookies!=null){
        for(Cookie c : cookies){
            String cookieName = c.getName();
            if(name.equals(“uname”)){
                c.setValue(“Mark”);
                response.addCookie( c );
        }
}

1.2.6. Cookie的生存時間
默認(rèn)情況下,Cookie會被瀏覽器保存在內(nèi)存中,此時Cookie的生命周期由瀏覽器決定,只要不關(guān)閉瀏覽器Cookie就會一直存在。

如果希望關(guān)閉瀏覽器后Cookie仍存在,則可以通過設(shè)置過期時間使得Cookie存在硬盤上得以保存更長的時間。設(shè)置Cookie的過期時間使用如下代碼:

/**
該方法是Cookie提供的實(shí)例方法。參數(shù)seconds的單位為秒,但精度不是很高。
seconds > 0 :代表Cookie保存在硬盤上的時長
seconds = 0 : 代表Cookie的生命時長為現(xiàn)在,而這一刻稍縱即逝,所以馬上Cookie就等同于過了生存時間,所以會被立即刪除。這也是刪除Cookie的實(shí)現(xiàn)方式。
seconds < 0 :缺省值,瀏覽器會將Cookie保存在內(nèi)存中。
**/
void setMaxAge(int seconds);

//以下代碼實(shí)現(xiàn)了Cookie保存在硬盤上40秒:
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.*;

public class AddCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        // 創(chuàng)建cookie
        Cookie c = new Cookie("username", "Lisa");
        c.setMagAge(40);
        Cookie c2 = new Cookie("city", "NewYork");
        response.addCookie(c);
        response.addCookie(c2);
        out.close();
    }
}

1.2.7. Cookie編碼
Cookie作為在網(wǎng)絡(luò)傳輸?shù)囊欢巫址谋荆荒鼙4婧戏ǖ腁SCII字符,如果要保存中文需要將中文變成合法的ASCII字符,即編碼。使用如下代碼可以實(shí)現(xiàn)將中文保存到Cookie中。

Cookie c = new Cookie("city",URLEncoder.encode("北京","utf-8"));

//完整實(shí)現(xiàn)保存用戶名和城市信息的代碼如下
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.*;

public class AddCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response)
                            throws ServletException, IOException {
        response.setContentType(
                "text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        //創(chuàng)建cookie
        Cookie c = new Cookie("username",URLEncoder.encode("女神",”utf-8”));             
        Cookie c2 = new Cookie("city",URLEncoder.encode(“北京”,"utf-8"));
        response.addCookie(c);
        response.addCookie(c2);
        out.close();
    }
}

1.2.8. Cookie解碼
服務(wù)器讀取客戶端經(jīng)過編碼之后的信息時,要想能夠正確顯示需要將信息解碼后才能輸出。使用URLDecoder的decode()方法即可。實(shí)現(xiàn)解碼的完整代碼如下:

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import javax.servlet.*;
import javax.servlet.*;

public class FindCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response)
                           throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for(int i=0;i<cookies.length;i++){
                Cookie c = cookies[i];
                String name = c.getName();
                String value = c.getValue();
                out.println(name + “:” + URLDecoder.decode(value,"utf-8"));
            }
        }else{
            out.println("沒有找到cookie");
        }
        out.close();
    }
}

1.3. Cookie的路徑問題

1.3.1. 什么是Cookie的路徑問題?
客戶端存儲Cookie之后,并不是針對同一個應(yīng)用訪問任何資源時都自動發(fā)送Cookie到服務(wù)器端,而是會進(jìn)行路徑的判斷。只有符合路徑規(guī)范的請求才會發(fā)送Cookie到服務(wù)器端。

客戶端在接受Cookie時會為該Cookie記錄一個默認(rèn)路徑,這個路徑記錄的是添加這個Cookie的Web組件的路徑。如,當(dāng)客戶端向 http://localhost:8080/test/file/addCookie.jsp發(fā)送請求時創(chuàng)建了cookie,那么該cookie的路徑就是 /test/file.

1.3.2. 什么時候發(fā)送Cookie?
只有當(dāng)訪問的地址是Cookie的路徑或者其子路徑時,瀏覽器才發(fā)送Cookie到服務(wù)器端。如Cookie的路徑是 /test/file,那么如果訪問的是 /test/file/a.jsp 或者 /test/file/b/c.jsp時,都會發(fā)送Cookie。
如果訪問的是 /test/d.jsp,則瀏覽器不會發(fā)送Cookie。

1.3.3. 如何設(shè)置Cookie的路徑?
設(shè)置Cookie的路徑可以使用Cookie的API方法,setPath(String uri);
如以下代碼就實(shí)現(xiàn)了設(shè)置Cookie的路徑為應(yīng)用的頂級目錄,這樣所有資源路徑要么與此路徑相等,要么是子路徑,從而實(shí)現(xiàn)了客戶端發(fā)送任何請求時都會發(fā)送Cookie。

Cookie c  = new Cookie(“uname”,“jack”);
c.setPath(“/test”);
response.addCookie(c);

1.3.4. Cookie的限制
Cookie由于存放的位置在客戶端,所以可以通過修改設(shè)置被用戶禁止。Cookie本質(zhì)就是一小段文本,只能保存少量數(shù)據(jù),長度是有限制的,一般為4kb左右。文本說的是只能保存字符串,不能保留復(fù)雜的對象類型數(shù)據(jù)。

作為網(wǎng)絡(luò)中傳輸?shù)膬?nèi)容,Cookie安全性很低,非常容易通過截取數(shù)據(jù)包來獲取,在沒有加密的情況下不要用于存放敏感數(shù)據(jù)。就算是能夠存放的長度很短,但作為網(wǎng)絡(luò)中傳輸?shù)膬?nèi)容也會增加網(wǎng)絡(luò)的傳輸量影響帶寬。在服務(wù)器處理大量請求的時候,Cookie的傳遞無疑會增加網(wǎng)絡(luò)的負(fù)載量。

2.1. Session

2.1.1. 什么是Session?
服務(wù)器為不同的客戶端在內(nèi)存中創(chuàng)建了用于保存數(shù)據(jù)的Session對象,并將用于標(biāo)識該對象的唯一Id發(fā)回給與該對象對應(yīng)的客戶端。當(dāng)瀏覽器再次發(fā)送請求時,SessionId也會被發(fā)送過來,服務(wù)器憑借這個唯一Id找到與之對應(yīng)的Session對象。在服務(wù)器端維護(hù)的這些用于保存與不同客戶端交互時的數(shù)據(jù)的對象叫做Session。

2.1.2. Session工作原理


Session工作原理.png

瀏覽器第一次訪問服務(wù)器時,服務(wù)器會為該客戶端分配一塊對象空間,并且使用不同的SID來進(jìn)行標(biāo)識,該標(biāo)識SID會隨著響應(yīng)發(fā)回到客戶端,且被保存在內(nèi)存中。當(dāng)同一個客戶端再次發(fā)送請求時,標(biāo)識也會被同時發(fā)送到服務(wù)器端,而服務(wù)器判斷要使用哪一個Session對象內(nèi)的數(shù)據(jù)時,就會根據(jù)客戶端發(fā)來的這個SID來進(jìn)行查找。

2.1.3. 如何獲得Session
獲得session有兩種情況
(1)請求中沒有SID,則需要創(chuàng)建;
(2)請求中包含一個SID,根據(jù)SID去找對應(yīng)的對象,但也存在找到找不到的可能。
不管哪種情況都依賴于請求中的這個唯一標(biāo)識,雖然對于編程人員來講不需要去查看這個基本不會重復(fù)、編號很長的標(biāo)識,但要想獲取到與客戶端關(guān)聯(lián)的這個session對象一定要基于請求,所以在Request類型的API中包含獲取到session對象的方法,代碼如下所示:

/**
使用第一種獲取session對象的方法時——

flag = true:先從請求中找找看是否有SID,沒有會創(chuàng)建新Session對象,有SID會查找與編號對應(yīng)的對象,找到匹配的對象則返回,找不到SID對應(yīng)的對象時則會創(chuàng)建新Session對象。所以,填寫true就一定會得到一個Session對象。

flag= false:不存在SID以及按照SID找不到Session對象時都會返回null,只有根據(jù)SID找到對應(yīng)的對象時會返回具體的Session對象。所以,填寫false只會返回已經(jīng)存在并且與SID匹配上了的Session對象。

使用第二種獲取session對象的方法——request.getSession()方法不填寫參數(shù)時等同于填寫true,提供該方法主要是為了書寫代碼時更方便,大多數(shù)情況下還是希望能夠返回一個Session對象的。
**/
HttpSession s = request.getSession(boolean flag);
HttpSession s = request.getSession( );

2.1.4. 如何使用Session綁定對象
Session作為服務(wù)器端為各客戶端保存交互數(shù)據(jù)的一種方式,采用name-value對的形式來區(qū)分每一組數(shù)據(jù)。向Session添加數(shù)據(jù)綁定的代碼如下:

void session.setAttribute(String name,Object obj);
//獲取綁定數(shù)據(jù)或移除綁定數(shù)據(jù)的代碼如下:
void session.getAttribute(String name);
void session.removeAttribute(String name);

Session對象可以保存更復(fù)雜的對象類型數(shù)據(jù)了,不像Cookie只能保存字符串。

2.1.5. 如何刪除Session對象
如果客戶端想刪除SID對應(yīng)的Session對象時,可以使用Session對象的如下方法:
void invalidate()
該方法會使得服務(wù)器端與該客戶端對應(yīng)的Session對象不再被Session容器管理,進(jìn)入到垃圾回收的狀態(tài)。對于這種立即刪除Session對象的操作主要應(yīng)用于不再需要身份識別的情況下,如登出操作。

2.2. Session超時

2.2.1. 什么是Session超時?
Session會以對象的形式占用服務(wù)器端的內(nèi)存,過多的以及長期的消耗內(nèi)存會降低服務(wù)器端的運(yùn)行效率,所以Session對象存在于內(nèi)存中時會有默認(rèn)的時間限制,一旦Session對象存在的時間超過了這個缺省的時間限制則認(rèn)為是Session超時,Session會失效,不能再繼續(xù)訪問。

Web服務(wù)器缺省的超時時間設(shè)置一般是30分鐘。

2.2.2. 如何修改Session的缺省時間限制
有兩種方式可以修改Session的缺省時間限制,編程式和聲明式。

void  setMaxInactiveInterval(int seconds)  //編程式
//聲明式:
<session-config>
        <session-timeout>30</session-timeout>
</session-config>

使用聲明式來修改缺省時間,那么該應(yīng)用創(chuàng)建的所有Session對象的生命周期都會應(yīng)用這個規(guī)定的時間,單位為分鐘。

使用編程式來修改缺省時間只會針對調(diào)用該方法的Session對象應(yīng)用這一原則,不會影響到其他對象,所以更靈活。通常在需要特殊設(shè)置時使用這種方式。時間單位是秒,與聲明式的時間單位不同。

2.2.3. Session驗(yàn)證
Session既然區(qū)分不同的客戶端,所以可以利用Session來實(shí)現(xiàn)對訪問資源的保護(hù)。如,可以將資源劃分為登錄后才能訪問。Session多用于記錄身份信息,在保護(hù)資源被訪問前可以通過判斷Session內(nèi)的信息來決定是否允許。使用Session實(shí)現(xiàn)驗(yàn)證的步驟如下:

//步驟一、為Session對象綁定數(shù)據(jù),代碼如下:
HttpSession s = request.getSession();
s.setAttribute(“uname”,“Rose”);

//步驟二、讀取Session對象中的綁定值,讀取成功代表驗(yàn)證成功,讀取失敗則跳轉(zhuǎn)回登錄頁面,代碼如:
HttpSession s = request.getSession();
if(s.getAttribute(“uname”)==null){
        response.sendRedirect(“l(fā)ogIn.jsp”);
}else{
        //… … 
}

2.2.4. Session優(yōu)缺點(diǎn)
優(yōu):Session對象的數(shù)據(jù)由于保存在服務(wù)器端,并不在網(wǎng)絡(luò)中進(jìn)行傳輸,所以安全一些,并且能夠保存的數(shù)據(jù)類型更豐富,同時Session也能夠保存更多的數(shù)據(jù),Cookie只能保存大約4kb的字符串。

缺:Session的安全性是以犧牲服務(wù)器資源為代價的,如果用戶量過大,會嚴(yán)重影響服務(wù)器的性能。

2.2.5. 瀏覽器禁用Cookie的后果
Session對象的查找依靠的是SID,而這個ID保存在客戶端時是以Cookie的形式保存的。一旦瀏覽器禁用Cookie,那么SID無法保存,Session對象將不能再使用。

為了在禁用Cookie后依然能使用Session,那么將使用其他的存儲方法來完成SID的保存。URL地址在網(wǎng)絡(luò)傳輸過程中不僅僅能夠起到標(biāo)示地址的作用,還可以在其后攜帶一些較短的數(shù)據(jù),SID就可以通過URL來實(shí)現(xiàn)保存,及URL重寫。

2.2.6. 什么是URL重寫?
瀏覽器在訪問服務(wù)器的某個地址時,會使用一個改寫過的地址,即在原有地址后追加SessionID,這種重新定義URL內(nèi)容的方式叫做URL重寫。

如原有地址的寫法為http://localhost:8080/test/some
而重寫后的地址寫法為http://localhost:8080/test/some;jsessionid=4E113CB3

2.2.7. 如何實(shí)現(xiàn)URL重寫?
生成鏈接地址和表單提交時,使用如下代碼:

<a href=”<%=response.encodeURL(String url)>”>鏈接地址</a>

//如果是重定向,使用如下代碼代替response.sendRedirect()
response.encodeRedirectURL(String url);

3.1 Cookie和Session對比

3.1.1 兩者區(qū)別
(1)session 能夠存儲任意的 java 對象,cookie 只能存儲 String 類型的對象
(2)cookie在客戶端而session在服務(wù)端,因Cookie在客戶端所以可以編輯偽造,不是十分安全。
(3)Session過多時會消耗服務(wù)器資源,大型網(wǎng)站會有專門Session服務(wù)器,Cookie存在客戶端不存在過多的問題。
(4)域的支持范圍不一樣,比方說a.com的Cookie在a.com下都能用,而www.a.com的Session在api.a.com下都不能用,解決這個問題的辦法是JSONP或者跨域資源共享。
(5)單個cookie保存的數(shù)據(jù)不能超過4K,很多瀏覽器都限制一個站點(diǎn)最多保存20個cookie。

3.1.2 所以一般情況:
將登陸信息等重要信息存放為SESSION
其他信息如果需要保留,可以放在COOKIE中

補(bǔ)充

什么是Token?和Cookie和Session有什么區(qū)別?
傳統(tǒng)身份驗(yàn)證一般采用的方法是,當(dāng)用戶請求登錄的時候,會在服務(wù)端生成一條記錄,這個記錄里可以說明一下登錄的用戶是誰,然后把這條記錄的 ID 號發(fā)送給客戶端,客戶端收到以后把這個 ID 號存儲在 Cookie 里,下次這個用戶再向服務(wù)端發(fā)送請求的時候,可以帶著這個 Cookie ,這樣服務(wù)端會驗(yàn)證一個這個 Cookie 里的信息,看看能不能在服務(wù)端這里找到對應(yīng)的記錄,如果可以,說明用戶已經(jīng)通過了身份驗(yàn)證,就把用戶請求的數(shù)據(jù)返回給客戶端。
上面說的就是 Session,我們需要在服務(wù)端存儲為登錄的用戶生成的 Session ,這些 Session 可能會存儲在內(nèi)存,磁盤,或者數(shù)據(jù)庫里。我們可能需要在服務(wù)端定期的去清理過期的 Session 。

基于 Token 的身份驗(yàn)證方法:
使用基于 Token 的身份驗(yàn)證方法,在服務(wù)端不需要存儲用戶的登錄記錄。大概的流程是這樣的:


Token登陸驗(yàn)證機(jī)制圖解.jpg

1)客戶端使用用戶名跟密碼請求登錄
2)服務(wù)端收到請求,去驗(yàn)證用戶名與密碼
3)驗(yàn)證成功后,服務(wù)端會簽發(fā)一個 Token,再把這個 Token 發(fā)送給客戶端
4)客戶端收到 Token 以后可以把它存儲起來,比如放在 Cookie 里或者 Local Storage 里
5)客戶端每次向服務(wù)端請求資源的時候需要帶著服務(wù)端簽發(fā)的 Token
6)服務(wù)端收到請求,然后去驗(yàn)證客戶端請求里面帶著的 Token,如果驗(yàn)證成功,就向客戶端返回請求的數(shù)據(jù)

看圖示,很容易理解Token登錄機(jī)制,我們可以把Token理解為令牌。一般生成token用的是一個spring控制器[基于項(xiàng)目和項(xiàng)目之間的調(diào)用秘鑰生成之后放redis,兩小時后失效],我們看代碼:

import java.security.MessageDigest;
 
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
 
import com.csair.openapi.basic.annotation.WEBApi;
import com.csair.openapi.qo.sub.TokenCredential;
import com.csair.openapi.vo.sub.TokenSuccess;
 
@RestController
@RequestMapping("/credential")
public class TokenCredentialController {
 
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
 
    private Map<String, String> key = new HashMap<String, String>();
 
    @PostConstruct
    public void init() {
        key.put("lps", "lrKvmMg3h9c8UQsvzDn0S4X");
        
    }
 
     @RequestMapping(value = "/getToken")
     @ResponseBody
     @WEBApi
     public Object export(HttpServletRequest request,HttpServletResponse response,@RequestBody TokenCredential limitsAuthority) throws Exception {
         TokenSuccess tokenSuccess   =  new TokenSuccess();
         if (limitsAuthority!=null&&limitsAuthority.getAppid()!=null&&limitsAuthority.getSecret()!=null) {//校驗(yàn)用戶是否有權(quán)限
             String appid= limitsAuthority.getAppid();
             String secretPass =(String) key.get(appid);
             String secret = limitsAuthority.getSecret();
             if (secret.equals(secretPass)) {
                 String Timestamp= System.currentTimeMillis()+"";
                 String token = md5Password(appid+secretPass+System.currentTimeMillis()+Timestamp);
                 redisTemplate.opsForValue().set(token, Timestamp,7200, TimeUnit.SECONDS);//token和驗(yàn)證碼對應(yīng)的放到redis里面 ,2小時秒過期
                 tokenSuccess.setAccess_token(token);
                 tokenSuccess.setExpires_in("7200");
                 return tokenSuccess;
             }else{
                 throw new RuntimeException("invalid secret");          
            }
         }
         throw new RuntimeException("invalid appid");
     }

    /**
     * 生成32位md5碼
     */
    public static String md5Password(String password) {
 
        try {
            // 得到一個信息摘要器
            MessageDigest digest = MessageDigest.getInstance("md5");
            byte[] result = digest.digest(password.getBytes());
            StringBuffer buffer = new StringBuffer();
            // 把每一個byte 做一個與運(yùn)算 0xff;
            for (byte b : result) {
                // 與運(yùn)算
                int number = b & 0xff;// 加鹽
                String str = Integer.toHexString(number);
                if (str.length() == 1) {
                    buffer.append("0");
                }
                buffer.append(str);
            }
            // 標(biāo)準(zhǔn)的md5加密后的結(jié)果
            return buffer.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return "";
        }
    }
    
}

用java自定義注解引入Aop來鑒權(quán)

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthToken {
    
 
}

import javax.servlet.http.HttpServletRequest;
 
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
 
import com.csair.cocc.basic.constant.EnvironmentEnum;
import com.csair.openapi.basic.annotation.AuthToken;
 
@Component
@Aspect
public class AuthTokenDecorator implements Ordered {
    
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Value("${environment}")
    private String environment;
 
    @Around("within(com.csair.**.controller.**.*) && @annotation(authToken)")
    public Object decorate(ProceedingJoinPoint pjp, AuthToken authToken) throws Throwable {
         
      if (EnvironmentEnum.DEV.getValue().equals(environment)) {//如果是開發(fā)環(huán)境
            return pjp.proceed();//這個是可以繼續(xù)傳輸對象到Controller的邏輯
      }
        
      Object[] obj = pjp.getArgs();
      HttpServletRequest request = (HttpServletRequest) obj[0];
      String accessToken = request.getParameter("accessToken");
      logger.info("accessToken值為:"+accessToken);
      
      if (StringUtils.isEmpty(accessToken)) {
             throw new RuntimeException("token is null");       
        }else {
            String timestamp = redisTemplate.opsForValue().get(accessToken); 
            if (StringUtils.isEmpty(timestamp)) {
             throw new RuntimeException("Invalid token");       
            }
        }
        return pjp.proceed();
    }
 
    public int getOrder() {
        return 9;
    }
    
}

引用Redis配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
 
    <context:property-placeholder location="classpath:redis.properties" />
 
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="5000" />
        <property name="maxIdle" value="2000" />
        <property name="maxWaitMillis" value="4000" />
        <property name="testOnBorrow" value="true" />
        <property name="testOnReturn" value="true" />
        <property name="testWhileIdle" value="true" />
    </bean>
 
    <bean id="redisSentinelConfiguration"
        class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
 
        <property name="master">
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <property name="name" value="${redis.master.name}"></property>
            </bean>
        </property>
 
        <property name="sentinels">
            <set>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel1.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel1.port}"></constructor-arg>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel2.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel2.port}"></constructor-arg>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel3.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel3.port}"></constructor-arg>
                </bean>
            </set>
        </property>
 
    </bean>
 
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="password" value="${redis.password}" />
        <property name="poolConfig" ref="jedisPoolConfig" />
        <constructor-arg ref="redisSentinelConfiguration" />
    </bean>
 
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashValueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
    </bean>
 
</beans>

最重要的是Controller的入?yún)⒁由螲ttpServletRequest request
@RequestMapping(value = "/saveCargoPlaneUploadLpsInfo", method = RequestMethod.POST)
@ResponseBody
@WEBApi
@AuthToken
public Object saveCargoPlaneUploadLpsInfo(HttpServletRequest request,@RequestBody CargoPlaneUploadLpsInfoDto param)

我們再看一個簡單的例子:

import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;


public class TokenUtil {

    private static final String[] codeBase= {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"};

    private static Random rand= new Random();

    /** XXTEA加密解密的密鑰 */
    private static String secKey = "captcha";

    /** token超時門限(天) */
    private static long expire = 30;


    /** 驗(yàn)證碼字符數(shù) */
    private static int charCount = 4;

    public static final  String  genToken() {
        StringBuffer sb= new StringBuffer();
        for(int i=0; i<charCount; i++){
            int randInt= Math.abs(rand.nextInt());
            sb.append(codeBase[randInt % codeBase.length]);
        }
        long timestamp= System.currentTimeMillis();
        String token= null;
        token= String.format("%s_%d", sb.toString(), timestamp);
        System.out.println("未加密的token:"+token);
        token= XXTEAUtil.encrypt(token, secKey);
        return token;
    }

    public static final boolean verificationToken(String token) throws StatusInfoException{
        String plainText= XXTEAUtil.decrypt(token, secKey);
        if (StringUtils.isBlank(plainText)){
                throw new IllegalStateException("解密失敗,token可能遭到篡改");
            }
            String[] plainTextArr= plainText.split("_");
            if (plainTextArr.length!=2){
                throw new IllegalStateException("token數(shù)據(jù)格式錯誤");
            }
            long timestamp= 0;
            try{
                timestamp= Long.parseLong(plainTextArr[1]);
            }catch(NumberFormatException e){
                throw new IllegalStateException("時間戳無效");
            }
            if ((System.currentTimeMillis() - timestamp)>TimeUnit.MILLISECONDS.convert(expire+5, TimeUnit.DAYS)){
                throw new IllegalStateException("token已過期");
            }
        return true;
    }
}

引入加密解密Util工具類,代碼如下:

import org.apache.commons.codec.binary.Base64;


public class XXTEAUtil {

    /**
     * 使用密鑰加密數(shù)據(jù)
     * @param plain
     * @param key
     * @return
     */
    public static byte[] encrypt(byte[] plain, byte[] key) {
        if (plain.length == 0) {
            return plain;
        }
        return toByteArray(encrypt(toIntArray(plain, true), toIntArray(key, false)), false);
    }

    /**
     * 使用密鑰解密
     * @param cipher
     * @param key
     * @return
     */
    public static byte[] decrypt(byte[] cipher, byte[] key) {
        if (cipher.length == 0) {
            return cipher;
        }
        return toByteArray(decrypt(toIntArray(cipher, false), toIntArray(key, false)), true);
    }

    /**
     * 使用密鑰加密數(shù)據(jù)
     * @param v
     * @param k
     * @return
     */
    public static int[] encrypt(int[] v, int[] k) {
        int n = v.length - 1;

        if (n < 1) {
            return v;
        }
        if (k.length < 4) {
            int[] key = new int[4];

            System.arraycopy(k, 0, key, 0, k.length);
            k = key;
        }
        int z = v[n], y = v[0], delta = 0x9E3779B9, sum = 0, e;
        int p, q = 6 + 52 / (n + 1);

        while (q-- > 0) {
            sum = sum + delta;
            e = sum >>> 2 & 3;
            for (p = 0; p < n; p++) {
                y = v[p + 1];
                z = v[p] += (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
            }
            y = v[0];
            z = v[n] += (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
        }
        return v;
    }

    /**
     * 使用密鑰解密數(shù)據(jù)
     * @param v
     * @param k
     * @return
     */
    public static int[] decrypt(int[] v, int[] k) {
        int n = v.length - 1;

        if (n < 1) {
            return v;
        }
        if (k.length < 4) {
            int[] key = new int[4];

            System.arraycopy(k, 0, key, 0, k.length);
            k = key;
        }
        int z = v[n], y = v[0], delta = 0x9E3779B9, sum, e;
        int p, q = 6 + 52 / (n + 1);

        sum = q * delta;
        while (sum != 0) {
            e = sum >>> 2 & 3;
        for (p = n; p > 0; p--) {
            z = v[p - 1];
            y = v[p] -= (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
        }
        z = v[n];
        y = v[0] -= (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
        sum = sum - delta;
        }
        return v;
    }

    /**
     * 字節(jié)數(shù)組轉(zhuǎn)換為整型數(shù)組
     * @param data
     * @param includeLength
     * @return
     */
    private static int[] toIntArray(byte[] data, boolean includeLength) {
        int n = (((data.length & 3) == 0) ? (data.length >>> 2) : ((data.length >>> 2) + 1));
        int[] result;

        if (includeLength) {
            result = new int[n + 1];
            result[n] = data.length;
        } else {
            result = new int[n];
        }
        n = data.length;
        for (int i = 0; i < n; i++) {
            result[i >>> 2] |= (0x000000ff & data[i]) << ((i & 3) << 3);
        }
        return result;
    }

    /**
     * 整型數(shù)組轉(zhuǎn)換為字節(jié)數(shù)組
     * @param data
     * @param includeLength
     * @return
     */
    private static byte[] toByteArray(int[] data, boolean includeLength) {
        int n = data.length << 2;
        if (includeLength) {
            int m = data[data.length - 1];

            if (m > n) {
                return null;
            } else {
                n = m;
            }
        }
        byte[] result = new byte[n];

        for (int i = 0; i < n; i++) {
            result[i] = (byte) ((data[i >>> 2] >>> ((i & 3) << 3)) & 0xff);
        }
        return result;
    }

    /**
     * 先XXXTEA加密,后Base64加密
     * @param plain
     * @param key
     * @return
     */
    public static String encrypt(String plain, String key) {
        String cipher = "";
        byte[] k = key.getBytes();
        byte[] v = plain.getBytes();
        cipher = new String(Base64.encodeBase64(XXTEAUtil.encrypt(v, k)));
        cipher = cipher.replace('+', '-');
        cipher = cipher.replace('/', '_');
        cipher = cipher.replace('=', '.');
        return cipher;
    }

    /**
     * 先Base64解密,后XXXTEA解密
     * @param cipher
     * @param key
     * @return
     */
    public static String decrypt(String cipher, String key) {
        String plain = "";
        cipher = cipher.replace('-', '+');
        cipher = cipher.replace('_', '/');
        cipher = cipher.replace('.', '=');
        byte[] k = key.getBytes();
        byte[] v = Base64.decodeBase64(cipher);
        plain = new String(XXTEAUtil.decrypt(v, k));
        return plain;
    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內(nèi)容