面試java開(kāi)發(fā)被問(wèn)最多的“cookie速和、session歹垫、token”問(wèn)題,安排颠放!

前言

作為一個(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)搬去, 快累死了饺汹。

image-20210712162145823

后來(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ì)得被人罵死。

image-20210712162600049

也嘗試把這個(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了伪嫁。

image-20210712163843772

這個(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)證元镀。

image-20210712163519280

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è)步驟:

  1. 客戶端發(fā)送一個(gè)請(qǐng)求到服務(wù)器倘感;

  2. 服務(wù)器發(fā)送一個(gè)HttpResponse響應(yīng)到客戶端放坏,其中包含Set-Cookie的頭部;

  3. 客戶端保存cookie老玛,之后向服務(wù)器發(fā)送請(qǐng)求時(shí)淤年,HttpRequest請(qǐng)求中會(huì)包含一個(gè)Cookie的頭部;

  4. 服務(wù)器返回響應(yīng)數(shù)據(jù)蜡豹。

image-20210709083823647

為了探究這個(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é)果如圖所示:

image-20210708111331506

可見(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.coma2.jiangwang.com,因?yàn)槎叩挠蛎煌耆嗤埂H绻胍?code>jiangwnag.com名下的二級(jí)域名都可以使用該Cookie敞咧,需要設(shè)置Cookie的domain參數(shù)為.jiangwang.com,這樣使用a1.jiangwang.coma2.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.comwww.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ā)送給客戶端瀏覽器,如下圖所示:

image-20210708145107823

再次請(qǐng)求服務(wù)器汗洒,此時(shí)就可以看到瀏覽器再請(qǐng)求服務(wù)器時(shí)议纯,會(huì)把存儲(chǔ)到cookie中的session的Id一起傳遞到服務(wù)器端了,如下圖所示:

image-20210708145337564

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的原理

  1. 用戶通過(guò)用戶名和密碼發(fā)送請(qǐng)求
  2. 程序校驗(yàn)
  3. 程序返回一個(gè)Token給客戶端
  4. 客戶端存儲(chǔ)Token否纬,并且每次發(fā)送請(qǐng)求攜帶Token
  5. 服務(wù)端驗(yàn)證Token,并返回?cái)?shù)據(jù)
image-20210708162203287

3.Token的使用

Spring Boot和Jwt集成示例

image-20210714062557183

項(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

image-20210714063353922

無(wú)token登錄

image-20210714063447601

有token登錄

image-20210714063557098

錯(cuò)誤token登錄

image-20210714063830525
image-20210714064150202

4.Token的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  1. 支持跨域訪問(wèn): Cookie是不允許垮域訪問(wèn)的爪瓜,token支持;
  2. 無(wú)狀態(tài): token無(wú)狀態(tài)匙瘪,session有狀態(tài)的钥勋;
  3. 去耦: 不需要綁定到一個(gè)特定的身份驗(yàn)證方案炬转。Token可以在任何地方生成,只要在 你的API被調(diào)用的時(shí)候算灸, 你可以進(jìn)行Token生成調(diào)用即可;
  4. 更適用于移動(dòng)應(yīng)用: Cookie不支持手機(jī)端訪問(wèn)的驻啤;
  5. 性能: 在網(wǎng)絡(luò)傳輸?shù)倪^(guò)程中菲驴,性能更好;
  6. 基于標(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):

  1. 占帶寬赊瞬,正常情況下要比 session_id 更大,需要消耗更多流量贼涩,擠占更多帶寬巧涧,假如你的網(wǎng)站每月有 10 萬(wàn)次的瀏覽器,就意味著要多開(kāi)銷幾十兆的流量遥倦。聽(tīng)起來(lái)并不多谤绳,但日積月累也是不小一筆開(kāi)銷。實(shí)際上袒哥,許多人會(huì)在 JWT 中存儲(chǔ)的信息會(huì)更多缩筛;
  2. 無(wú)法在服務(wù)端注銷,那么久很難解決劫持問(wèn)題堡称;
  3. 性能問(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]

在這里插入圖片描述
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痴昧,一起剝皮案震驚了整個(gè)濱河市稽穆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赶撰,老刑警劉巖舌镶,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柱彻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡餐胀,警方通過(guò)查閱死者的電腦和手機(jī)哟楷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)否灾,“玉大人卖擅,你說(shuō)我怎么就攤上這事∧迹” “怎么了惩阶?”我有些...
    開(kāi)封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)扣汪。 經(jīng)常有香客問(wèn)我断楷,道長(zhǎng),這世上最難降的妖魔是什么崭别? 我笑而不...
    開(kāi)封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任冬筒,我火速辦了婚禮,結(jié)果婚禮上紊遵,老公的妹妹穿的比我還像新娘账千。我一直安慰自己,他們只是感情好暗膜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布匀奏。 她就那樣靜靜地躺著,像睡著了一般学搜。 火紅的嫁衣襯著肌膚如雪娃善。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天瑞佩,我揣著相機(jī)與錄音聚磺,去河邊找鬼。 笑死炬丸,一個(gè)胖子當(dāng)著我的面吹牛瘫寝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稠炬,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼焕阿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了首启?” 一聲冷哼從身側(cè)響起暮屡,我...
    開(kāi)封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毅桃,沒(méi)想到半個(gè)月后褒纲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體准夷,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年莺掠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了衫嵌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汁蝶,死狀恐怖渐扮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掖棉,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布膀估,位于F島的核電站幔亥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏察纯。R本人自食惡果不足惜帕棉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饼记。 院中可真熱鬧香伴,春花似錦、人聲如沸具则。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)博肋。三九已至低斋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間匪凡,已是汗流浹背膊畴。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留病游,地道東北人唇跨。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像衬衬,于是被迫代替她去往敵國(guó)和親买猖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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