前言
作為一個(gè)JAVA開(kāi)發(fā)排惨,之前有好幾次出去面試,面試官都問(wèn)我碰凶,JAVAWeb掌握的怎么樣暮芭,我當(dāng)時(shí)就不知道怎么回答,Web欲低,日常開(kāi)發(fā)中用的是什么辕宏?今天我們來(lái)說(shuō)說(shuō)JAVAWeb最應(yīng)該掌握的三個(gè)內(nèi)容。
發(fā)展歷程
1砾莱、很久很久以前瑞筐,Web 基本上就是文檔的瀏覽而已, 既然是瀏覽腊瑟,作為服務(wù)器聚假, 不需要記錄誰(shuí)在某一段時(shí)間里都瀏覽了什么文檔,每次請(qǐng)求都是一個(gè)新的HTTP協(xié)議闰非, 就是請(qǐng)求加響應(yīng)膘格, 尤其是我不用記住是誰(shuí)剛剛發(fā)了HTTP請(qǐng)求, 每個(gè)請(qǐng)求對(duì)我來(lái)說(shuō)都是全新的河胎。
2闯袒、但是隨著交互式Web應(yīng)用的興起,像在線購(gòu)物網(wǎng)站游岳,需要登錄的網(wǎng)站等等政敢,馬上就面臨一個(gè)問(wèn)題,那就是要管理會(huì)話胚迫,必須記住哪些人登錄系統(tǒng)喷户, 哪些人往自己的購(gòu)物車中放商品, 也就是說(shuō)我必須把每個(gè)人區(qū)分開(kāi)访锻,這就是一個(gè)不小的挑戰(zhàn)褪尝,因?yàn)镠TTP請(qǐng)求是無(wú)狀態(tài)的闹获,所以想出的辦法就是給大家發(fā)一個(gè)會(huì)話標(biāo)識(shí)(session id), 說(shuō)白了就是一個(gè)隨機(jī)的字串,每個(gè)人收到的都不一樣河哑, 每次大家向我發(fā)起HTTP請(qǐng)求的時(shí)候避诽,把這個(gè)字符串給一并捎過(guò)來(lái), 這樣我就能區(qū)分開(kāi)誰(shuí)是誰(shuí)了璃谨。
3沙庐、這樣大家很嗨皮了,可是服務(wù)器就不嗨皮了佳吞,每個(gè)人只需要保存自己的session id拱雏,而服務(wù)器要保存所有人的session id ! 如果訪問(wèn)服務(wù)器多了底扳, 就得由成千上萬(wàn)铸抑,甚至幾十萬(wàn)個(gè)。
這對(duì)服務(wù)器說(shuō)是一個(gè)巨大的開(kāi)銷 衷模, 嚴(yán)重的限制了服務(wù)器擴(kuò)展能力鹊汛, 比如說(shuō)我用兩個(gè)機(jī)器組成了一個(gè)集群, 小F通過(guò)機(jī)器A登錄了系統(tǒng)算芯, 那session id會(huì)保存在機(jī)器A上柒昏, 假設(shè)小F的下一次請(qǐng)求被轉(zhuǎn)發(fā)到機(jī)器B怎么辦? 機(jī)器B可沒(méi)有小F的 session id啊熙揍。
有時(shí)候會(huì)采用一點(diǎn)小伎倆: session sticky , 就是讓小F的請(qǐng)求一直粘連在機(jī)器A上氏涩, 但是這也不管用届囚, 要是機(jī)器A掛掉了, 還得轉(zhuǎn)到機(jī)器B去是尖。
那只好做session 的復(fù)制了意系, 把session id 在兩個(gè)機(jī)器之間搬來(lái)搬去, 快累死了饺汹。
后來(lái)有個(gè)叫Memcached的支了招: 把session id 集中存儲(chǔ)到一個(gè)地方蛔添, 所有的機(jī)器都來(lái)訪問(wèn)這個(gè)地方的數(shù)據(jù), 這樣一來(lái)兜辞,就不用復(fù)制了迎瞧, 但是增加了單點(diǎn)失敗的可能性, 要是那個(gè)負(fù)責(zé)session 的機(jī)器掛了逸吵, 所有人都得重新登錄一遍凶硅, 估計(jì)得被人罵死。
也嘗試把這個(gè)單點(diǎn)的機(jī)器也搞出集群扫皱,增加可靠性足绅, 但不管如何捷绑, 這小小的session 對(duì)我來(lái)說(shuō)是一個(gè)沉重的負(fù)擔(dān)
4 于是有人就一直在思考, 我為什么要保存這可惡的session呢氢妈, 只讓每個(gè)客戶端去保存該多好粹污?
可是如果不保存這些session id , 怎么驗(yàn)證客戶端發(fā)給我的session id 的確是我生成的呢? 如果不去驗(yàn)證首量,我們都不知道他們是不是合法登錄的用戶厕怜, 那些不懷好意的家伙們就可以偽造session id , 為所欲為了。
嗯蕾总,對(duì)了粥航,關(guān)鍵點(diǎn)就是驗(yàn)證 !
比如說(shuō)生百, 小F已經(jīng)登錄了系統(tǒng)递雀, 我給他發(fā)一個(gè)令牌(token), 里邊包含了小F的 user id蚀浆, 下一次小F 再次通過(guò)Http 請(qǐng)求訪問(wèn)我的時(shí)候缀程, 把這個(gè)token 通過(guò)Http header 帶過(guò)來(lái)不就可以了。
不過(guò)這和session id沒(méi)有本質(zhì)區(qū)別啊市俊, 任何人都可以可以偽造杨凑, 所以我得想點(diǎn)兒辦法, 讓別人偽造不了摆昧。
那就對(duì)數(shù)據(jù)做一個(gè)簽名吧撩满, 比如說(shuō)我用HMAC-SHA256 算法,加上一個(gè)只有我才知道的密鑰绅你, 對(duì)數(shù)據(jù)做一個(gè)簽名伺帘, 把這個(gè)簽名和數(shù)據(jù)一起作為token , 由于密鑰別人不知道忌锯, 就無(wú)法偽造token了伪嫁。
這個(gè)token 我不保存, 當(dāng)小F把這個(gè)token 給我發(fā)過(guò)來(lái)的時(shí)候偶垮,我再用同樣的HMAC-SHA256 算法和同樣的密鑰张咳,對(duì)數(shù)據(jù)再計(jì)算一次簽名, 和token 中的簽名做個(gè)比較似舵, 如果相同调限, 我就知道小F已經(jīng)登錄過(guò)了卿叽,并且可以直接取到小F的user id , 如果不相同, 數(shù)據(jù)部分肯定被人篡改過(guò), 我就告訴發(fā)送者: 對(duì)不起溪胶,沒(méi)有認(rèn)證元镀。
Token 中的數(shù)據(jù)是明文保存的(雖然我會(huì)用Base64做下編碼, 但那不是加密), 還是可以被別人看到的脆淹, 所以我不能在其中保存像密碼這樣的敏感信息。
當(dāng)然沽一, 如果一個(gè)人的token 被別人偷走了盖溺, 那我也沒(méi)辦法, 我也會(huì)認(rèn)為小偷就是合法用戶铣缠, 這其實(shí)和一個(gè)人的session id 被別人偷走是一樣的烘嘱。
這樣一來(lái), 我就不保存session id 了蝗蛙, 我只是生成token , 然后驗(yàn)證token 蝇庭, 我用我的CPU計(jì)算時(shí)間獲取了我的session 存儲(chǔ)空間 !
解除了session id這個(gè)負(fù)擔(dān)捡硅, 可以說(shuō)是無(wú)事一身輕哮内, 我的機(jī)器集群現(xiàn)在可以輕松地做水平擴(kuò)展, 用戶訪問(wèn)量增大壮韭, 直接加機(jī)器就行北发。 這種無(wú)狀態(tài)的感覺(jué)實(shí)在是太好了!
Cookie
1.什么是Cookie
Cookie翻譯成中文的意思是‘小甜餅’喷屋,是由W3C組織提出琳拨,最早由Netscape社區(qū)發(fā)展的一種機(jī)制。目前Cookie已經(jīng)成為標(biāo)準(zhǔn)屯曹,所有的主流瀏覽器如IE狱庇、Netscape、Firefox是牢、Opera等都支持Cookie僵井。
服務(wù)器單從網(wǎng)絡(luò)連接上無(wú)從知道客戶身份。怎么辦呢驳棱?就給客戶端們頒發(fā)一個(gè)通行證吧,每人一個(gè)农曲,無(wú)論誰(shuí)訪問(wèn)都必須攜帶自己通行證社搅。這樣服務(wù)器就能從通行證上確認(rèn)客戶身份了。這就是Cookie的工作原理乳规。
Cookie是客戶端保存用戶信息的一種機(jī)制形葬,用來(lái)記錄用戶的一些信息,也是實(shí)現(xiàn)Session的一種方式暮的。Cookie存儲(chǔ)的數(shù)據(jù)量有限笙以,且都是保存在客戶端瀏覽器中。不同的瀏覽器有不同的存儲(chǔ)大小冻辩,但一般不超過(guò)4KB猖腕。因此使用Cookie實(shí)際上只能存儲(chǔ)一小段的文本信息(key-value格式)拆祈。
2.Cookie的機(jī)制
當(dāng)用戶第一次訪問(wèn)并登陸一個(gè)網(wǎng)站的時(shí)候,cookie的設(shè)置以及發(fā)送會(huì)經(jīng)歷以下4個(gè)步驟:
客戶端發(fā)送一個(gè)請(qǐng)求到服務(wù)器倘感;
服務(wù)器發(fā)送一個(gè)HttpResponse響應(yīng)到客戶端放坏,其中包含Set-Cookie的頭部;
客戶端保存cookie老玛,之后向服務(wù)器發(fā)送請(qǐng)求時(shí)淤年,HttpRequest請(qǐng)求中會(huì)包含一個(gè)Cookie的頭部;
服務(wù)器返回響應(yīng)數(shù)據(jù)蜡豹。
為了探究這個(gè)過(guò)程麸粮,寫了代碼進(jìn)行測(cè)試,如下:
我在doGet方法中镜廉,new了一個(gè)Cookie對(duì)象并將其加入到了HttpResponse對(duì)象中
@RestController
public class TestController {
@GetMapping(value = "/doGet")
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 設(shè)置生命周期為MAX_VALUE
cookie.setMaxAge(Integer.MAX_VALUE);
resp.addCookie(cookie);
}
}
瀏覽器輸入地址進(jìn)行訪問(wèn)弄诲,結(jié)果如圖所示:
可見(jiàn)Response Headers中包含Set-Cookie頭部,而Request Headers中包含了Cookie頭部桨吊。name和value正是上述設(shè)置的威根。
3.Cookie的屬性
Expires
該屬性用來(lái)設(shè)置Cookie的有效期。Cookie中的maxAge用來(lái)表示該屬性视乐,單位為秒洛搀。Cookie中通過(guò)getMaxAge()和setMaxAge(int maxAge)來(lái)讀寫該屬性。maxAge有3種值佑淀,分別為正數(shù)留美,負(fù)數(shù)和0。
如果maxAge屬性為正數(shù)伸刃,則表示該Cookie會(huì)在maxAge秒之后自動(dòng)失效谎砾。瀏覽器會(huì)將maxAge為正數(shù)的Cookie持久化,即寫到對(duì)應(yīng)的Cookie文件中(每個(gè)瀏覽器存儲(chǔ)的位置不一致)捧颅。無(wú)論客戶關(guān)閉了瀏覽器還是電腦景图,只要還在maxAge秒之前,登錄網(wǎng)站時(shí)該Cookie仍然有效碉哑。下面代碼中的Cookie信息將永遠(yuǎn)有效挚币。
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 設(shè)置生命周期為MAX_VALUE,永久有效
cookie.setMaxAge(Integer.MAX_VALUE);
resp.addCookie(cookie);
當(dāng)maxAge屬性為負(fù)數(shù),則表示該Cookie只是一個(gè)臨時(shí)Cookie扣典,不會(huì)被持久化妆毕,僅在本瀏覽器窗口或者本窗口打開(kāi)的子窗口中有效,關(guān)閉瀏覽器后該Cookie立即失效贮尖。
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 設(shè)置生命周期為MAX_VALUE,永久有效
cookie.setMaxAge(-1);
resp.addCookie(cookie);
當(dāng)maxAge為0時(shí)笛粘,表示立即刪除Cookie。
Cookie[] cookies = req.getCookies();
Cookie cookie = null;
// get Cookie
for (Cookie ck : cookies) {
if ("jiangwang".equals(ck.getName())) {
cookie = ck;
break;
}
}
if (null != cookie) {
// 刪除一個(gè)cookie
cookie.setMaxAge(0);
resp.addCookie(cookie);
}
修改或者刪除Cookie
HttpServletResponse提供的Cookie操作只有一個(gè)addCookie(Cookie cookie),所以想要修改Cookie只能使用一個(gè)同名的Cookie來(lái)覆蓋原先的Cookie薪前。如果要?jiǎng)h除某個(gè)Cookie润努,則只需要新建一個(gè)同名的Cookie,并將maxAge設(shè)置為0序六,并覆蓋原來(lái)的Cookie即可任连。
新建的Cookie,除了value例诀、maxAge之外的屬性随抠,比如name、path繁涂、domain都必須與原來(lái)的一致才能達(dá)到修改或者刪除的效果拱她。否則,瀏覽器將視為兩個(gè)不同的Cookie不予覆蓋扔罪。
Cookie的域名
Cookie是不可以跨域名的秉沼,隱私安全機(jī)制禁止網(wǎng)站非法獲取其他網(wǎng)站的Cookie。
正常情況下矿酵,同一個(gè)一級(jí)域名下的兩個(gè)二級(jí)域名也不能交互使用Cookie唬复,比如a1.jiangwang.com
和a2.jiangwang.com
,因?yàn)槎叩挠蛎煌耆嗤埂H绻胍?code>jiangwnag.com名下的二級(jí)域名都可以使用該Cookie敞咧,需要設(shè)置Cookie的domain參數(shù)為.jiangwang.com
,這樣使用a1.jiangwang.com
和a2.jiangwang.com
就能訪問(wèn)同一個(gè)cookie
一級(jí)域名又稱為頂級(jí)域名辜腺,一般由字符串+后綴組成休建。熟悉的一級(jí)域名有baidu.com,qq.com评疗。com测砂,cn,net等均是常見(jiàn)的后綴百匆。
二級(jí)域名是在一級(jí)域名下衍生的砌些,比如有個(gè)一級(jí)域名為abc.com
,則blog.abc.com
和www.abc.com
均是其衍生出來(lái)的二級(jí)域名加匈。
Cookie的路徑
path屬性決定允許訪問(wèn)Cookie的路徑寄症。比如,設(shè)置為"/"表示允許所有路徑都可以使用Cookie
4.應(yīng)用
Cookies最典型的應(yīng)用是判定注冊(cè)用戶是否已經(jīng)登錄網(wǎng)站矩动,用戶可能會(huì)得到提示,是否在下一次進(jìn)入此網(wǎng)站時(shí)保留用戶信息以便簡(jiǎn)化登錄手續(xù)释漆,這些都是Cookies的功用悲没。另一個(gè)重要應(yīng)用場(chǎng)合是“購(gòu)物車”之類處理。用戶可能會(huì)在一段時(shí)間內(nèi)在同一家網(wǎng)站的不同頁(yè)面中選擇不同的商品,這些信息都會(huì)寫入Cookies示姿,以便在最后付款時(shí)提取信息甜橱。
Session
1.什么是Session
在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ù)亩进。
2.Session實(shí)現(xiàn)原理
服務(wù)器創(chuàng)建session出來(lái)后,會(huì)把session的id號(hào)缩歪,以cookie的形式回寫給客戶機(jī)归薛,這樣,只要客戶機(jī)的瀏覽器不關(guān)匪蝙,再去訪問(wèn)服務(wù)器時(shí)主籍,都會(huì)帶著session的id號(hào)去,服務(wù)器發(fā)現(xiàn)客戶機(jī)瀏覽器帶session id過(guò)來(lái)了逛球,就會(huì)使用內(nèi)存中與之對(duì)應(yīng)的session為之服務(wù)千元。可以用如下的代碼證明:
@RestController
public class TestController {
@GetMapping(value = "/doGet")
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//使用request對(duì)象的getSession()獲取session需忿,如果session不存在則創(chuàng)建一個(gè)
HttpSession session = request.getSession();
//將數(shù)據(jù)存儲(chǔ)到session中
session.setAttribute("mayun", "馬云");
//獲取session的Id
String sessionId = session.getId();
//判斷session是不是新創(chuàng)建的
if (session.isNew()) {
response.getWriter().print("session創(chuàng)建成功诅炉,session的id是:"+sessionId);
}else {
response.getWriter().print("服務(wù)器已經(jīng)存在該session了,session的id是:"+sessionId);
}
}
}
第一次訪問(wèn)時(shí)屋厘,服務(wù)器會(huì)創(chuàng)建一個(gè)新的sesion涕烧,并且把session的Id以cookie的形式發(fā)送給客戶端瀏覽器,如下圖所示:
再次請(qǐng)求服務(wù)器汗洒,此時(shí)就可以看到瀏覽器再請(qǐng)求服務(wù)器時(shí)议纯,會(huì)把存儲(chǔ)到cookie中的session的Id一起傳遞到服務(wù)器端了,如下圖所示:
3.session創(chuàng)建和銷毀
在程序中第一次調(diào)用request.getSession()方法時(shí)就會(huì)創(chuàng)建一個(gè)新的Session溢谤,可以用isNew()方法來(lái)判斷Session是不是新創(chuàng)建的
//使用request對(duì)象的getSession()獲取session瞻凤,如果session不存在則創(chuàng)建一個(gè)
HttpSession session = request.getSession();
//獲取session的Id
String sessionId = session.getId();
//判斷session是不是新創(chuàng)建的
if (session.isNew()) {
response.getWriter().print("session創(chuàng)建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服務(wù)器已經(jīng)存在session世杀,session的id是:"+sessionId);
}
session對(duì)象默認(rèn)30分鐘沒(méi)有使用阀参,則服務(wù)器會(huì)自動(dòng)銷毀session,也可以手工配置session的失效時(shí)間瞻坝,例如:
session.setMaxInactiveInterval(10*60);//10分鐘后session失效
當(dāng)需要在程序中手動(dòng)設(shè)置Session失效時(shí)蛛壳,可以手工調(diào)用session.invalidate方法,摧毀session。
HttpSession session = request.getSession();
//手工調(diào)用session.invalidate方法衙荐,摧毀session
session.invalidate();
面試題:瀏覽器關(guān)閉捞挥,session就銷毀了? 不對(duì).
Session生成后忧吟,只要用戶繼續(xù)訪問(wèn)砌函,服務(wù)器就會(huì)更新Session的最后訪問(wèn)時(shí)間,并維護(hù)該Session溜族。為防止內(nèi)存溢出讹俊,服務(wù)器會(huì)把長(zhǎng)時(shí)間內(nèi)沒(méi)有活躍的Session從內(nèi)存刪除。這個(gè)時(shí)間就是Session的超時(shí)時(shí)間斩祭。如果超過(guò)了超時(shí)時(shí)間沒(méi)訪問(wèn)過(guò)服務(wù)器劣像,Session就自動(dòng)失效了。
Token
1.什么是Token
token的意思是“令牌”摧玫,是服務(wù)端生成的一串字符串耳奕,作為客戶端進(jìn)行請(qǐng)求的一個(gè)標(biāo)識(shí)。
當(dāng)用戶第一次登錄后诬像,服務(wù)器生成一個(gè)token并將此token返回給客戶端屋群,以后客戶端只需帶上這個(gè)token前來(lái)請(qǐng)求數(shù)據(jù)即可,無(wú)需再次帶上用戶名和密碼坏挠。
簡(jiǎn)單token的組成芍躏;uid(用戶唯一的身份標(biāo)識(shí))、time(當(dāng)前時(shí)間的時(shí)間戳)降狠、sign(簽名对竣,token的前幾位以哈希算法壓縮成的一定長(zhǎng)度的十六進(jìn)制字符串。為防止token泄露)榜配。
2.Token的原理
- 用戶通過(guò)用戶名和密碼發(fā)送請(qǐng)求
- 程序校驗(yàn)
- 程序返回一個(gè)Token給客戶端
- 客戶端存儲(chǔ)Token否纬,并且每次發(fā)送請(qǐng)求攜帶Token
- 服務(wù)端驗(yàn)證Token,并返回?cái)?shù)據(jù)
3.Token的使用
Spring Boot和Jwt集成示例
項(xiàng)目依賴 pom.xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
自定義注解
//需要登錄才能進(jìn)行操作的注解LoginToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
//用來(lái)跳過(guò)驗(yàn)證的PassToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
用戶實(shí)體類蛋褥、及查詢service
public class User {
private String userID;
private String userName;
private String passWord;
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
}
@Service
public class UserService {
public User getUser(String userid, String password){
if ("admin".equals(userid) && "admin".equals(password)){
User user=new User();
user.setUserID("admin");
user.setUserName("admin");
user.setPassWord("admin");
return user;
}
else{
return null;
}
}
public User getUser(String userid){
if ("admin".equals(userid)){
User user=new User();
user.setUserID("admin");
user.setUserName("admin");
user.setPassWord("admin");
return user;
}
else{
return null;
}
}
}
Token生成
@Service
public class TokenService {
/**
* 過(guò)期時(shí)間10分鐘
*/
private static final long EXPIRE_TIME = 10 * 60 * 1000;
public String getToken(User user) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
String token="";
token= JWT.create().withAudience(user.getUserID()) // 將 user id 保存到 token 里面
.withExpiresAt(date) //十分鐘后token過(guò)期
.sign(Algorithm.HMAC256(user.getPassWord())); // 以 password 作為 token 的密鑰
return token;
}
}
攔截器攔截token
package com.jw.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.jw.annotation.LoginToken;
import com.jw.annotation.PassToken;
import com.jw.entity.User;
import com.jw.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
public class JwtInterceptor implements HandlerInterceptor{
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 從 http 請(qǐng)求頭中取出 token
// 如果不是映射到方法直接通過(guò)
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//檢查是否有passtoken注釋临燃,有則跳過(guò)認(rèn)證
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//檢查有沒(méi)有需要用戶權(quán)限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
// 執(zhí)行認(rèn)證
if (token == null) {
throw new RuntimeException("無(wú)token,請(qǐng)重新登錄");
}
// 獲取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userService.getUser(userId);
if (user == null) {
throw new RuntimeException("用戶不存在烙心,請(qǐng)重新登錄");
}
// 驗(yàn)證 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassWord())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
注冊(cè)攔截器
package com.jw.config;
import com.jw.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**"); // 攔截所有請(qǐng)求膜廊,通過(guò)判斷是否有 @LoginRequired 注解 決定是否需要登錄
//注冊(cè)TestInterceptor攔截器
// InterceptorRegistration registration = registry.addInterceptor(jwtInterceptor());
// registration.addPathPatterns("/**"); //添加攔截路徑
// registration.excludePathPatterns( //添加不攔截路徑
// "/**/*.html", //html靜態(tài)資源
// "/**/*.js", //js靜態(tài)資源
// "/**/*.css", //css靜態(tài)資源
// "/**/*.woff",
// "/**/*.ttf",
// "/swagger-ui.html"
// );
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
登錄Controller
@RestController
public class LoginController {
@Autowired
private UserService userService;
@Autowired
private TokenService tokenService;
@PostMapping("login")
public Object login(String username, String password){
JSONObject jsonObject=new JSONObject();
User user=userService.getUser(username, password);
if(user==null){
jsonObject.put("message","登錄失敗淫茵!");
return jsonObject;
}else {
String token = tokenService.getToken(user);
jsonObject.put("token", token);
jsonObject.put("user", user);
return jsonObject;
}
}
@LoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "你已通過(guò)驗(yàn)證";
}
}
配置全局異常捕獲
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public Object handleException(Exception e) {
String msg = e.getMessage();
if (msg == null || msg.equals("")) {
msg = "服務(wù)器出錯(cuò)";
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 1000);
jsonObject.put("message", msg);
return jsonObject;
}
}
postman測(cè)試
獲取token
無(wú)token登錄
有token登錄
錯(cuò)誤token登錄
4.Token的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 支持跨域訪問(wèn): Cookie是不允許垮域訪問(wèn)的爪瓜,token支持;
- 無(wú)狀態(tài): token無(wú)狀態(tài)匙瘪,session有狀態(tài)的钥勋;
- 去耦: 不需要綁定到一個(gè)特定的身份驗(yàn)證方案炬转。Token可以在任何地方生成,只要在 你的API被調(diào)用的時(shí)候算灸, 你可以進(jìn)行Token生成調(diào)用即可;
- 更適用于移動(dòng)應(yīng)用: Cookie不支持手機(jī)端訪問(wèn)的驻啤;
- 性能: 在網(wǎng)絡(luò)傳輸?shù)倪^(guò)程中菲驴,性能更好;
- 基于標(biāo)準(zhǔn)化: 你的API可以采用標(biāo)準(zhǔn)化的 JSON Web Token (JWT). 這個(gè)標(biāo)準(zhǔn)已經(jīng)存在 多個(gè)后端庫(kù)(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)骑冗。
缺點(diǎn):
- 占帶寬赊瞬,正常情況下要比 session_id 更大,需要消耗更多流量贼涩,擠占更多帶寬巧涧,假如你的網(wǎng)站每月有 10 萬(wàn)次的瀏覽器,就意味著要多開(kāi)銷幾十兆的流量遥倦。聽(tīng)起來(lái)并不多谤绳,但日積月累也是不小一筆開(kāi)銷。實(shí)際上袒哥,許多人會(huì)在 JWT 中存儲(chǔ)的信息會(huì)更多缩筛;
- 無(wú)法在服務(wù)端注銷,那么久很難解決劫持問(wèn)題堡称;
- 性能問(wèn)題瞎抛,JWT 的賣點(diǎn)之一就是加密簽名,由于這個(gè)特性却紧,接收方得以驗(yàn)證 JWT 是否有效且被信任桐臊。但是大多數(shù) Web 身份認(rèn)證應(yīng)用中,JWT 都會(huì)被存儲(chǔ)到 Cookie 中晓殊,這就是說(shuō)你有了兩個(gè)層面的簽名断凶。聽(tīng)著似乎很牛逼,但是沒(méi)有任何優(yōu)勢(shì)挺物,為此懒浮,你需要花費(fèi)兩倍的 CPU 開(kāi)銷來(lái)驗(yàn)證簽名。對(duì)于有著嚴(yán)格性能要求的 Web 應(yīng)用识藤,這并不理想砚著,尤其對(duì)于單線程環(huán)境。
標(biāo)簽: [Java]