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的原理
如果客戶端向服務(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工作原理
瀏覽器第一次訪問服務(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ù)端不需要存儲用戶的登錄記錄。大概的流程是這樣的:
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;
}
}