欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文掌握J(rèn)avaWeb登錄認(rèn)證

 更新時間:2024年09月25日 16:12:56   作者:SunnyRivers  
登錄認(rèn)證是每個系統(tǒng)中必不可少的功能,通過用戶名和密碼來驗(yàn)證用戶身份,JavaWeb中實(shí)現(xiàn)登錄認(rèn)證通常需要處理HTTP協(xié)議的無狀態(tài)性,涉及會話管理、令牌技術(shù)等,本文給大家介紹JavaWeb登錄認(rèn)證的相關(guān)知識,感興趣的朋友跟隨小編一起看看吧

前言

與開發(fā)接口比起來,登錄認(rèn)證功能對于新手來說理解起來更難,但是登錄又是每個系統(tǒng)中必不可少的功能,很多系統(tǒng)中的功能,沒有登錄是無法使用的,比如商場中的購物車,你不用賬號登錄,根本不知道購物車中有啥。本篇博客主題就是登錄認(rèn)證。 最終我們要實(shí)現(xiàn)的效果就是用戶必須登錄之后,才可以訪問后臺系統(tǒng)中的功能。

需求

在登錄界面中,我們可以輸入用戶的用戶名以及密碼,然后點(diǎn)擊 “登錄” 按鈕就要請求服務(wù)器,服務(wù)端判斷用戶輸入的用戶名或者密碼是否正確。如果正確,則返回成功結(jié)果,前端跳轉(zhuǎn)至系統(tǒng)首頁面。

接口文檔

基本信息

請求路徑:/login

請求方式:POST

接口描述:該接口用于員工登錄系統(tǒng),登錄完畢后,系統(tǒng)下發(fā)JWT令牌。 

請求參數(shù)
參數(shù)格式:application/json

參數(shù)說明:

名稱類型是否必須備注
usernamestring必須用戶名
passwordstring必須密碼

請求數(shù)據(jù)樣例:

{
   "username": "jinyong",
   "password": "123456"
}

響應(yīng)數(shù)據(jù)
參數(shù)格式:application/json

參數(shù)說明:

名稱類型是否必須默認(rèn)值備注其他信息
codenumber必須響應(yīng)碼, 1 成功 ; 0 失敗
msgstring非必須提示信息
datastring必須返回的數(shù)據(jù) , jwt令牌

響應(yīng)數(shù)據(jù)樣例:

{
  "code": 1,
  "msg": "success",
  "data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
}

思路分析

登錄服務(wù)端的核心邏輯就是:接收前端請求傳遞的用戶名和密碼 ,然后再根據(jù)用戶名和密碼查詢用戶信息,如果用戶信息存在,則說明用戶輸入的用戶名和密碼正確。如果查詢到的用戶不存在,則說明用戶輸入的用戶名和密碼錯誤。

功能開發(fā)

LoginController

@RestController
public class LoginController {
    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        Emp e = empService.login(emp);
        return  e != null ? Result.success():Result.error("用戶名或密碼錯誤");
    }
}

EmpService

public interface EmpService {
    /**
     * 用戶登錄
     * @param emp
     * @return
     */
    public Emp login(Emp emp);
    //省略其他代碼...
}

EmpServiceImpl

@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;
    @Override
    public Emp login(Emp emp) {
        //調(diào)用dao層功能:登錄
        Emp loginEmp = empMapper.getByUsernameAndPassword(emp);
        //返回查詢結(jié)果給Controller
        return loginEmp;
    }   
    //省略其他代碼...
}

EmpMapper

@Mapper
public interface EmpMapper {
    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time " +
            "from emp " +
            "where username=#{username} and password =#{password}")
    public Emp getByUsernameAndPassword(Emp emp);
    //省略其他代碼...
}

問題分析

我們已經(jīng)完成了基礎(chǔ)登錄功能的開發(fā),在我們登錄成功后就可以進(jìn)入到后臺管理系統(tǒng)中進(jìn)行數(shù)據(jù)的操作。

但是真正的登錄功能應(yīng)該是:登陸后才能訪問后端系統(tǒng)頁面,不登陸則跳轉(zhuǎn)登陸頁面進(jìn)行登陸。

為什么會出現(xiàn)這個問題?其實(shí)原因很簡單,就是因?yàn)獒槍τ谖覀儺?dāng)前所開發(fā)的部門管理、員工管理以及文件上傳等相關(guān)接口來說,我們在服務(wù)器端并沒有做任何的判斷,沒有去判斷用戶是否登錄了。所以無論用戶是否登錄,都可以訪問部門管理以及員工管理的相關(guān)數(shù)據(jù)。所以我們目前所開發(fā)的登錄功能,它只是徒有其表。而我們要想解決這個問題,我們就需要完成一步非常重要的操作:登錄校驗(yàn)。

什么是登錄校驗(yàn)?

所謂登錄校驗(yàn),指的是我們在服務(wù)器端接收到瀏覽器發(fā)送過來的請求之后,首先我們要對請求進(jìn)行校驗(yàn)。先要校驗(yàn)一下用戶登錄了沒有,如果用戶已經(jīng)登錄了,就直接執(zhí)行對應(yīng)的業(yè)務(wù)操作就可以了;如果用戶沒有登錄,此時就不允許他執(zhí)行相關(guān)的業(yè)務(wù)操作,直接給前端響應(yīng)一個錯誤的結(jié)果,最終跳轉(zhuǎn)到登錄頁面,要求他登錄成功之后,再來訪問對應(yīng)的數(shù)據(jù)。

了解完什么是登錄校驗(yàn)之后,接下來我們分析一下登錄校驗(yàn)大概的實(shí)現(xiàn)思路。

首先我們在宏觀上先有一個認(rèn)知:
前面在講解HTTP協(xié)議的時候,我們提到HTTP協(xié)議是無狀態(tài)協(xié)議。什么又是無狀態(tài)的協(xié)議?

所謂無狀態(tài),指的是每一次請求都是獨(dú)立的,下一次請求并不會攜帶上一次請求的數(shù)據(jù)。而瀏覽器與服務(wù)器之間進(jìn)行交互,基于HTTP協(xié)議也就意味著現(xiàn)在我們通過瀏覽器來訪問了登陸這個接口,實(shí)現(xiàn)了登陸的操作,接下來我們在執(zhí)行其他業(yè)務(wù)操作時,服務(wù)器也并不知道這個員工到底登陸了沒有。因?yàn)镠TTP協(xié)議是無狀態(tài)的,兩次請求之間是獨(dú)立的,所以是無法判斷這個員工到底登陸了沒有。

那應(yīng)該怎么來實(shí)現(xiàn)登錄校驗(yàn)的操作呢?具體的實(shí)現(xiàn)思路可以分為兩部分:

  • 在員工登錄成功后,需要將用戶登錄成功的信息存起來,記錄用戶已經(jīng)登錄成功的標(biāo)記。
  • 在瀏覽器發(fā)起請求時,需要在服務(wù)端進(jìn)行統(tǒng)一攔截,攔截后進(jìn)行登錄校驗(yàn)。

想要判斷員工是否已經(jīng)登錄,我們需要在員工登錄成功之后,存儲一個登錄成功的標(biāo)記,接下來在每一個接口方法執(zhí)行之前,先做一個條件判斷,判斷一下這個員工到底登錄了沒有。如果是登錄了,就可以執(zhí)行正常的業(yè)務(wù)操作,如果沒有登錄,會直接給前端返回一個錯誤的信息,前端拿到這個錯誤信息之后會自動的跳轉(zhuǎn)到登錄頁面。
我們程序中所開發(fā)的查詢功能、刪除功能、添加功能、修改功能,都需要使用以上套路進(jìn)行登錄校驗(yàn)。此時就會出現(xiàn):相同代碼邏輯,每個功能都需要編寫,就會造成代碼非常繁瑣。
為了簡化這塊操作,我們可以使用一種技術(shù):統(tǒng)一攔截技術(shù)。
通過統(tǒng)一攔截的技術(shù),我們可以來攔截瀏覽器發(fā)送過來的所有的請求,攔截到這個請求之后,就可以通過請求來獲取之前所存入的登錄標(biāo)記,在獲取到登錄標(biāo)記且標(biāo)記為登錄成功,就說明員工已經(jīng)登錄了。如果已經(jīng)登錄,我們就直接放行(意思就是可以訪問正常的業(yè)務(wù)接口了)。

我們要完成以上操作,會涉及到web開發(fā)中的兩個技術(shù):

  • 會話技術(shù)
  • 統(tǒng)一攔截技術(shù)

而統(tǒng)一攔截技術(shù)現(xiàn)實(shí)方案也有兩種:

  • Servlet規(guī)范中的Filter過濾器
  • Spring提供的interceptor攔截器

下面我們先學(xué)習(xí)會話技術(shù),然后再學(xué)習(xí)統(tǒng)一攔截技術(shù)。

會話技術(shù)介紹

什么是會話?

  • 在我們?nèi)粘I町?dāng)中,會話指的就是談話、交談。
  • 在web開發(fā)當(dāng)中,會話指的就是瀏覽器與服務(wù)器之間的一次連接,我們就稱為一次會話。

在用戶打開瀏覽器第一次訪問服務(wù)器的時候,這個會話就建立了,直到有任何一方斷開連接,此時會話就結(jié)束了。在一次會話當(dāng)中,是可以包含多次請求和響應(yīng)的。
比如:打開了瀏覽器來訪問web服務(wù)器上的資源(瀏覽器不能關(guān)閉、服務(wù)器不能斷開)

  • 第1次:訪問的是登錄的接口,完成登錄操作
  • 第2次:訪問的是部門管理接口,查詢所有部門數(shù)據(jù)
  • 第3次:訪問的是員工管理接口,查詢員工數(shù)據(jù)

只要瀏覽器和服務(wù)器都沒有關(guān)閉,以上3次請求都屬于一次會話當(dāng)中完成的。

需要注意的是:會話是和瀏覽器關(guān)聯(lián)的,當(dāng)有三個瀏覽器客戶端和服務(wù)器建立了連接時,就會有三個會話。同一個瀏覽器在未關(guān)閉之前請求了多次服務(wù)器,這多次請求是屬于同一個會話。比如:1、2、3這三個請求都是屬于同一個會話。當(dāng)我們關(guān)閉瀏覽器之后,這次會話就結(jié)束了。而如果我們是直接把web服務(wù)器關(guān)了,那么所有的會話就都結(jié)束了。

知道了會話的概念了,接下來我們再來了解下會話跟蹤。

會話跟蹤:一種維護(hù)瀏覽器狀態(tài)的方法,服務(wù)器需要識別多次請求是否來自于同一瀏覽器,以便在同一次會話的多次請求間共享數(shù)據(jù)。

服務(wù)器會接收很多的請求,但是服務(wù)器是需要識別出這些請求是不是同一個瀏覽器發(fā)出來的。比如:1和2這兩個請求是不是同一個瀏覽器發(fā)出來的,3和5這兩個請求不是同一個瀏覽器發(fā)出來的。如果是同一個瀏覽器發(fā)出來的,就說明是同一個會話。如果是不同的瀏覽器發(fā)出來的,就說明是不同的會話。而識別多次請求是否來自于同一瀏覽器的過程,我們就稱為會話跟蹤。

我們使用會話跟蹤技術(shù)就是要完成在同一個會話中,多個請求之間進(jìn)行共享數(shù)據(jù)。

為什么要共享數(shù)據(jù)呢?
由于HTTP是無狀態(tài)協(xié)議,在后面請求中怎么拿到前一次請求生成的數(shù)據(jù)呢?此時就需要在一次會話的多次請求之間進(jìn)行數(shù)據(jù)共享。

會話跟蹤技術(shù)有三種:

  • Cookie(客戶端會話跟蹤技術(shù))
  • 數(shù)據(jù)存儲在客戶端瀏覽器當(dāng)中
  • Session(服務(wù)端會話跟蹤技術(shù))
  • 數(shù)據(jù)存儲在儲在服務(wù)端
  • 令牌技術(shù)

會話跟蹤技術(shù)一:Cookie

cookie 是客戶端會話跟蹤技術(shù),它是存儲在客戶端瀏覽器的,我們使用 cookie 來跟蹤會話,我們就可以在瀏覽器第一次發(fā)起請求來請求服務(wù)器的時候,我們在服務(wù)器端來設(shè)置一個cookie。

比如第一次請求了登錄接口,登錄接口執(zhí)行完成之后,我們就可以設(shè)置一個cookie,在 cookie 當(dāng)中我們就可以來存儲用戶相關(guān)的一些數(shù)據(jù)信息。比如我可以在 cookie 當(dāng)中來存儲當(dāng)前登錄用戶的用戶名,用戶的ID。

服務(wù)器端在給客戶端在響應(yīng)數(shù)據(jù)的時候,會自動的將 cookie 響應(yīng)給瀏覽器,瀏覽器接收到響應(yīng)回來的 cookie 之后,會自動的將 cookie 的值存儲在瀏覽器本地。接下來在后續(xù)的每一次請求當(dāng)中,都會將瀏覽器本地所存儲的 cookie 自動地攜帶到服務(wù)端。

接下來在服務(wù)端我們就可以獲取到 cookie 的值。我們可以去判斷一下這個 cookie 的值是否存在,如果不存在這個cookie,就說明客戶端之前是沒有訪問登錄接口的;如果存在 cookie 的值,就說明客戶端之前已經(jīng)登錄完成了。這樣我們就可以基于 cookie 在同一次會話的不同請求之間來共享數(shù)據(jù)。

我剛才在介紹流程的時候,用了 3 個自動:

  • 服務(wù)器會 自動 的將 cookie 響應(yīng)給瀏覽器。
  • 瀏覽器接收到響應(yīng)回來的數(shù)據(jù)之后,會 自動 的將 cookie 存儲在瀏覽器本地。
  • 在后續(xù)的請求當(dāng)中,瀏覽器會 自動 的將 cookie 攜帶到服務(wù)器端。

為什么這一切都是自動化進(jìn)行的?

是因?yàn)?cookie 它是 HTP 協(xié)議當(dāng)中所支持的技術(shù),而各大瀏覽器廠商都支持了這一標(biāo)準(zhǔn)。在 HTTP 協(xié)議官方給我們提供了一個響應(yīng)頭和請求頭:

  • 響應(yīng)頭 Set-Cookie :設(shè)置Cookie數(shù)據(jù)的
  • 請求頭 Cookie:攜帶Cookie數(shù)據(jù)的

代碼測試

@Slf4j
@RestController
public class SessionController {
    //設(shè)置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","xc")); //設(shè)置Cookie/響應(yīng)Cookie
        return Result.success();
    }
    //獲取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //輸出name為login_username的cookie
            }
        }
        return Result.success();
    }
}    

A. 訪問c1接口,設(shè)置Cookie,http://localhost:8080/c1
我們可以看到,設(shè)置的cookie,通過響應(yīng)頭Set-Cookie響應(yīng)給瀏覽器,并且瀏覽器會將Cookie,存儲在瀏覽器端。

B. 訪問c2接口 http://localhost:8080/c2,此時瀏覽器會自動的將Cookie攜帶到服務(wù)端,是通過請求頭Cookie,攜帶的。

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

  • 優(yōu)點(diǎn):HTTP協(xié)議中支持的技術(shù)(像Set-Cookie 響應(yīng)頭的解析以及 Cookie 請求頭數(shù)據(jù)的攜帶,都是瀏覽器自動進(jìn)行的,是無需我們手動操作的)
  • 缺點(diǎn):
    • 移動端APP(Android、IOS)中無法使用Cookie
    • 不安全,用戶可以自己禁用Cookie
    • Cookie不能跨域

跨域介紹:

  • 現(xiàn)在的項(xiàng)目,大部分都是前后端分離的,前后端最終也會分開部署,前端部署在服務(wù)器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
  • 我們打開瀏覽器直接訪問前端工程,訪問url:http://192.168.150.200/login.html
  • 然后在該頁面發(fā)起請求到服務(wù)端,而服務(wù)端所在地址不再是localhost,而是服務(wù)器的IP地址192.168.150.100,假設(shè)訪問接口地址為:http://192.168.150.100:8080/login
  • 那此時就存在跨域操作了,因?yàn)槲覀兪窃?http://192.168.150.200/login.html 這個頁面上訪問了http://192.168.150.100:8080/login 接口
  • 此時如果服務(wù)器設(shè)置了一個Cookie,這個Cookie是不能使用的,因?yàn)镃ookie無法跨域

區(qū)分跨域的維度:

  • 協(xié)議
  • IP/協(xié)議
  • 端口

只要上述的三個維度有任何一個維度不同,那就是跨域操作

舉例:

? http://192.168.150.200/login.html ----------> https://192.168.150.200/login [協(xié)議不同,跨域]

? http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域]

? http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域]

? http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]

會話跟蹤技術(shù)二:Session

前面介紹的時候,我們提到Session,它是服務(wù)器端會話跟蹤技術(shù),所以它是存儲在服務(wù)器端的。而 Session 的底層其實(shí)就是基于我們剛才所介紹的 Cookie 來實(shí)現(xiàn)的。

獲取Session

如果我們現(xiàn)在要基于 Session 來進(jìn)行會話跟蹤,瀏覽器在第一次請求服務(wù)器的時候,我們就可以直接在服務(wù)器當(dāng)中來獲取到會話對象Session。如果是第一次請求Session ,會話對象是不存在的,這個時候服務(wù)器會自動的創(chuàng)建一個會話對象Session 。而每一個會話對象Session ,它都有一個ID(示意圖中Session后面括號中的1,就表示ID),我們稱之為 Session 的ID。

響應(yīng)Cookie (JSESSIONID)

接下來,服務(wù)器端在給瀏覽器響應(yīng)數(shù)據(jù)的時候,它會將 Session 的 ID 通過 Cookie 響應(yīng)給瀏覽器。其實(shí)在響應(yīng)頭當(dāng)中增加了一個 Set-Cookie 響應(yīng)頭。這個 Set-Cookie 響應(yīng)頭對應(yīng)的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服務(wù)器端會話對象 Session 的 ID。瀏覽器會自動識別這個響應(yīng)頭,然后自動將Cookie存儲在瀏覽器本地。

查找Session

接下來,在后續(xù)的每一次請求當(dāng)中,都會將 Cookie 的數(shù)據(jù)獲取出來,并且攜帶到服務(wù)端。接下來服務(wù)器拿到JSESSIONID這個 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就會從眾多的 Session 當(dāng)中來找到當(dāng)前請求對應(yīng)的會話對象Session。

這樣我們是不是就可以通過 Session 會話對象在同一次會話的多次請求之間來共享數(shù)據(jù)了?好,這就是基于 Session 進(jìn)行會話跟蹤的流程。

代碼測試

@Slf4j
@RestController
public class SessionController {
    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1: {}", session.hashCode());
        session.setAttribute("loginUser", "tom"); //往session中存儲數(shù)據(jù)
        return Result.success();
    }
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info("HttpSession-s2: {}", session.hashCode());
        Object loginUser = session.getAttribute("loginUser"); //從session中獲取數(shù)據(jù)
        log.info("loginUser: {}", loginUser);
        return Result.success(loginUser);
    }
}

A. 訪問 s1 接口,http://localhost:8080/s1

請求完成之后,在響應(yīng)頭中,就會看到有一個Set-Cookie的響應(yīng)頭,里面響應(yīng)回來了一個Cookie,就是JSESSIONID,這個就是服務(wù)端會話對象 Session 的ID。

B. 訪問 s2 接口,http://localhost:8080/s2

接下來,在后續(xù)的每次請求時,都會將Cookie的值,攜帶到服務(wù)端,那服務(wù)端呢,接收到Cookie之后,會自動的根據(jù)JSESSIONID的值,找到對應(yīng)的會話對象Session。

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

  • 優(yōu)點(diǎn):Session是存儲在服務(wù)端的,安全
  • 缺點(diǎn):
    • 服務(wù)器集群環(huán)境下無法直接使用Session
    • 移動端APP(Android、IOS)中無法使用Cookie
    • 用戶可以自己禁用Cookie
    • Cookie不能跨域

PS:Session 底層是基于Cookie實(shí)現(xiàn)的會話跟蹤,如果Cookie不可用,則該方案,也就失效了。

服務(wù)器集群環(huán)境為何無法使用Session?

首先第一點(diǎn),我們現(xiàn)在所開發(fā)的項(xiàng)目,一般都不會只部署在一臺服務(wù)器上,因?yàn)橐慌_服務(wù)器會存在一個很大的問題,就是單點(diǎn)故障。所謂單點(diǎn)故障,指的就是一旦這臺服務(wù)器掛了,整個應(yīng)用都沒法訪問了。

所以在現(xiàn)在的企業(yè)項(xiàng)目開發(fā)當(dāng)中,最終部署的時候都是以集群的形式來進(jìn)行部署,也就是同一個項(xiàng)目它會部署多份。比如這個項(xiàng)目我們現(xiàn)在就部署了 3 份。

而用戶在訪問的時候,到底訪問這三臺其中的哪一臺?其實(shí)用戶在訪問的時候,他會訪問一臺前置的服務(wù)器,我們叫負(fù)載均衡服務(wù)器,我們在后面項(xiàng)目當(dāng)中會詳細(xì)講解。目前大家先有一個印象負(fù)載均衡服務(wù)器,它的作用就是將前端發(fā)起的請求均勻的分發(fā)給后面的這三臺服務(wù)器。

此時假如我們通過 session 來進(jìn)行會話跟蹤,可能就會存在這樣一個問題。用戶打開瀏覽器要進(jìn)行登錄操作,此時會發(fā)起登錄請求。登錄請求到達(dá)負(fù)載均衡服務(wù)器,將這個請求轉(zhuǎn)給了第一臺 Tomcat 服務(wù)器。

Tomcat 服務(wù)器接收到請求之后,要獲取到會話對象session。獲取到會話對象 session 之后,要給瀏覽器響應(yīng)數(shù)據(jù),最終在給瀏覽器響應(yīng)數(shù)據(jù)的時候,就會攜帶這么一個 cookie 的名字,就是 JSESSIONID ,下一次再請求的時候,是不是又會將 Cookie 攜帶到服務(wù)端?

好。此時假如又執(zhí)行了一次查詢操作,要查詢部門的數(shù)據(jù)。這次請求到達(dá)負(fù)載均衡服務(wù)器之后,負(fù)載均衡服務(wù)器將這次請求轉(zhuǎn)給了第二臺 Tomcat 服務(wù)器,此時他就要到第二臺 Tomcat 服務(wù)器當(dāng)中。根據(jù)JSESSIONID 也就是對應(yīng)的 session 的 ID 值,要找對應(yīng)的 session 會話對象。

我想請問在第二臺服務(wù)器當(dāng)中有沒有這個ID的會話對象 Session, 是沒有的。此時是不是就出現(xiàn)問題了?我同一個瀏覽器發(fā)起了 2 次請求,結(jié)果獲取到的不是同一個會話對象,這就是Session這種會話跟蹤方案它的缺點(diǎn),在服務(wù)器集群環(huán)境下無法直接使用Session。

大家會看到上面這兩種傳統(tǒng)的會話技術(shù),在現(xiàn)在的企業(yè)開發(fā)當(dāng)中是不是會存在很多的問題。 為了解決這些問題,在現(xiàn)在的企業(yè)開發(fā)當(dāng)中,基本上都會采用第三種方案,通過令牌技術(shù)來進(jìn)行會話跟蹤。接下來我們就來介紹一下令牌技術(shù),來看一下令牌技術(shù)又是如何跟蹤會話的。

會話跟蹤技術(shù)三:令牌技術(shù)

這里我們所提到的令牌,其實(shí)它就是一個用戶身份的標(biāo)識,看似很高大上,很神秘,其實(shí)本質(zhì)就是一個字符串。

如果通過令牌技術(shù)來跟蹤會話,我們就可以在瀏覽器發(fā)起請求。在請求登錄接口的時候,如果登錄成功,我就可以生成一個令牌,令牌就是用戶的合法身份憑證。接下來我在響應(yīng)數(shù)據(jù)的時候,我就可以直接將令牌響應(yīng)給前端。

接下來我們在前端程序當(dāng)中接收到令牌之后,就需要將這個令牌存儲起來。這個存儲可以存儲在 cookie 當(dāng)中,也可以存儲在其他的存儲空間(比如:localStorage)當(dāng)中。

接下來,在后續(xù)的每一次請求當(dāng)中,都需要將令牌攜帶到服務(wù)端。攜帶到服務(wù)端之后,接下來我們就需要來校驗(yàn)令牌的有效性。如果令牌是有效的,就說明用戶已經(jīng)執(zhí)行了登錄操作,如果令牌是無效的,就說明用戶之前并未執(zhí)行登錄操作。

此時,如果是在同一次會話的多次請求之間,我們想共享數(shù)據(jù),我們就可以將共享的數(shù)據(jù)存儲在令牌當(dāng)中就可以了。

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

  • 優(yōu)點(diǎn):
    • 支持PC端、移動端
    • 解決集群環(huán)境下的認(rèn)證問題
    • 減輕服務(wù)器的存儲壓力(無需在服務(wù)器端存儲)
  • 缺點(diǎn):需要自己實(shí)現(xiàn)(包括令牌的生成、令牌的傳遞、令牌的校驗(yàn))

針對于這三種方案,現(xiàn)在企業(yè)開發(fā)當(dāng)中使用的最多的就是第三種令牌技術(shù)進(jìn)行會話跟蹤。而前面的這兩種傳統(tǒng)的方案,現(xiàn)在企業(yè)項(xiàng)目開發(fā)當(dāng)中已經(jīng)很少使用了。所以在我們的課程當(dāng)中,我們也將會采用令牌技術(shù)來解決案例項(xiàng)目當(dāng)中的會話跟蹤問題。

詳解JWT令牌 介紹

JWT全稱:JSON Web Token (官網(wǎng):https://jwt.io/)

  • 定義了一種簡潔的、自包含的格式,用于在通信雙方以json數(shù)據(jù)格式安全的傳輸信息。由于數(shù)字簽名的存在,這些信息是可靠的。

簡潔:是指jwt就是一個簡單的字符串。可以在請求參數(shù)或者是請求頭當(dāng)中直接傳遞。

自包含:指的是jwt令牌,看似是一個隨機(jī)的字符串,但是我們是可以根據(jù)自身的需求在jwt令牌中存儲自定義的數(shù)據(jù)內(nèi)容。如:可以直接在jwt令牌中存儲用戶的相關(guān)信息。

簡單來講,jwt就是將原始的json數(shù)據(jù)格式進(jìn)行了安全的封裝,這樣就可以直接基于jwt在通信雙方安全的進(jìn)行信息傳輸了。

JWT的組成: (JWT令牌由三個部分組成,三個部分之間使用英文的點(diǎn)來分割)

  • 第一部分:Header(頭), 記錄令牌類型、簽名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}
  • 第二部分:Payload(有效載荷),攜帶一些自定義信息、默認(rèn)信息等。 例如:{“id”:“1”,“username”:“Tom”}
  • 第三部分:Signature(簽名),防止Token被篡改、確保安全性。將header、payload,并加入指定秘鑰,通過指定簽名算法計算而來。

簽名的目的就是為了防jwt令牌被篡改,而正是因?yàn)閖wt令牌最后一個部分?jǐn)?shù)字簽名的存在,所以整個jwt 令牌是非常安全可靠的。一旦jwt令牌當(dāng)中任何一個部分、任何一個字符被篡改了,整個令牌在校驗(yàn)的時候都會失敗,所以它是非常安全可靠的。

JWT是如何將原始的JSON格式數(shù)據(jù),轉(zhuǎn)變?yōu)樽址哪兀?/p>

其實(shí)在生成JWT令牌時,會對JSON格式的數(shù)據(jù)進(jìn)行一次編碼:進(jìn)行base64編碼

Base64:是一種基于64個可打印的字符來表示二進(jìn)制數(shù)據(jù)的編碼方式。既然能編碼,那也就意味著也能解碼。所使用的64個字符分別是A到Z、a到z、 0- 9,一個加號,一個斜杠,加起來就是64個字符。任何數(shù)據(jù)經(jīng)過base64編碼之后,最終就會通過這64個字符來表示。當(dāng)然還有一個符號,那就是等號。等號它是一個補(bǔ)位的符號

需要注意的是Base64是編碼方式,而不是加密方式。

JWT令牌最典型的應(yīng)用場景就是登錄認(rèn)證:

  • 在瀏覽器發(fā)起請求來執(zhí)行登錄操作,此時會訪問登錄的接口,如果登錄成功之后,我們需要生成一個jwt令牌,將生成的 jwt令牌返回給前端。
  • 前端拿到j(luò)wt令牌之后,會將jwt令牌存儲起來。在后續(xù)的每一次請求中都會將jwt令牌攜帶到服務(wù)端。
  • 服務(wù)端統(tǒng)一攔截請求之后,先來判斷一下這次請求有沒有把令牌帶過來,如果沒有帶過來,直接拒絕訪問,如果帶過來了,還要校驗(yàn)一下令牌是否是有效。如果有效,就直接放行進(jìn)行請求的處理。

在JWT登錄認(rèn)證的場景中我們發(fā)現(xiàn),整個流程當(dāng)中涉及到兩步操作:

  • 在登錄成功之后,要生成令牌。
  • 每一次請求當(dāng)中,要接收令牌并對令牌進(jìn)行校驗(yàn)。

稍后我們再來學(xué)習(xí)如何來生成jwt令牌,以及如何來校驗(yàn)jwt令牌。

生成和校驗(yàn)

簡單介紹了JWT令牌以及JWT令牌的組成之后,接下來我們就來學(xué)習(xí)基于Java代碼如何生成和校驗(yàn)JWT令牌。

首先我們先來實(shí)現(xiàn)JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依賴:

<!-- JWT依賴-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

在引入完JWT來賴后,就可以調(diào)用工具包中提供的API來完成JWT令牌的生成和校驗(yàn)
工具類:Jwts

生成JWT代碼實(shí)現(xiàn):

@Test
public void genJwt(){
    Map<String,Object> claims = new HashMap<>();
    claims.put("id",1);
    claims.put("username","Tom");
    String jwt = Jwts.builder()
        .setClaims(claims) //自定義內(nèi)容(載荷)          
        .signWith(SignatureAlgorithm.HS256, "xc") //簽名算法        
        .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期   
        .compact();
    System.out.println(jwt);
}

運(yùn)行測試方法:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk

輸出的結(jié)果就是生成的JWT令牌,,通過英文的點(diǎn)分割對三個部分進(jìn)行分割,我們可以將生成的令牌復(fù)制一下,然后打開JWT的官網(wǎng),將生成的令牌直接放在Encoded位置,此時就會自動的將令牌解析出來。

第一部分解析出來,看到JSON格式的原始數(shù)據(jù),所使用的簽名算法為HS256。

第二個部分是我們自定義的數(shù)據(jù),之前我們自定義的數(shù)據(jù)就是id,還有一個exp代表的是我們所設(shè)置的過期時間。

由于前兩個部分是base64編碼,所以是可以直接解碼出來。但最后一個部分并不是base64編碼,是經(jīng)過簽名算法計算出來的,所以最后一個部分是不會解析的。

實(shí)現(xiàn)了JWT令牌的生成,下面我們接著使用Java代碼來校驗(yàn)JWT令牌(解析生成的令牌):

@Test
public void parseJwt(){
    Claims claims = Jwts.parser()
        .setSigningKey("xc")//指定簽名密鑰(必須保證和生成令牌時使用相同的簽名密鑰)  
        .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
        .getBody();
    System.out.println(claims);
}

運(yùn)行測試方法:

{id=1, exp=1672729730}

令牌解析后,我們可以看到id和過期時間,如果在解析的過程當(dāng)中沒有報錯,就說明解析成功了。

下面我們做一個測試:把令牌header中的數(shù)字9變?yōu)?,運(yùn)行測試方法后發(fā)現(xiàn)報錯:

原h(huán)eader: eyJhbGciOiJIUzI1NiJ9

修改為: eyJhbGciOiJIUzI1NiJ8

結(jié)論:篡改令牌中的任何一個字符,在對令牌進(jìn)行解析時都會報錯,所以JWT令牌是非常安全可靠的。

我們繼續(xù)測試:修改生成令牌的時指定的過期時間,修改為1分鐘

@Test
public void genJwt(){
    Map<String,Object> claims = new HashMap<>();
    claims.put(“id”,1);
    claims.put(“username”,“Tom”);
    String jwt = Jwts.builder()
        .setClaims(claims) //自定義內(nèi)容(載荷)          
        .signWith(SignatureAlgorithm.HS256, “xc”) //簽名算法        
        .setExpiration(new Date(System.currentTimeMillis() + 60*1000)) //有效期60秒   
        .compact();
    System.out.println(jwt);
    //輸出結(jié)果:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro
}
@Test
public void parseJwt(){
    Claims claims = Jwts.parser()
        .setSigningKey("xc")//指定簽名密鑰
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro")
        .getBody();
    System.out.println(claims);
}

等待1分鐘之后運(yùn)行測試方法發(fā)現(xiàn)也報錯了,說明:JWT令牌過期后,令牌就失效了,解析的為非法令牌。

通過以上測試,我們在使用JWT令牌時需要注意:

  • JWT校驗(yàn)時使用的簽名秘鑰,必須和生成JWT令牌時使用的秘鑰是配套的。
  • 如果JWT令牌解析校驗(yàn)時報錯,則說明 JWT令牌被篡改 或 失效了,令牌非法。

登錄下發(fā)令牌

JWT令牌的生成和校驗(yàn)的基本操作我們已經(jīng)學(xué)習(xí)完了,接下來我們就需要在案例當(dāng)中通過JWT令牌技術(shù)來跟蹤會話。具體的思路我們前面已經(jīng)分析過了,主要就是兩步操作:

  • 生成令牌
  • 在登錄成功之后來生成一個JWT令牌,并且把這個令牌直接返回給前端
  • 校驗(yàn)令牌
  • 攔截前端請求,從請求中獲取到令牌,對令牌進(jìn)行解析校驗(yàn)

那我們首先來完成:登錄成功之后生成JWT令牌,并且把令牌返回給前端。

JWT令牌怎么返回給前端呢?此時我們就需要再來看一下接口文檔當(dāng)中關(guān)于登錄接口的描述(主要看響應(yīng)數(shù)據(jù)):

  • 響應(yīng)數(shù)據(jù)

參數(shù)格式:application/json

參數(shù)說明:

名稱類型是否必須默認(rèn)值備注其他信息
codenumber必須響應(yīng)碼, 1 成功 ; 0 失敗
msgstring非必須提示信息
datastring必須返回的數(shù)據(jù) , jwt令牌

響應(yīng)數(shù)據(jù)樣例:

{
  "code": 1,
  "msg": "success",
  "data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
}

備注說明

用戶登錄成功后,系統(tǒng)會自動下發(fā)JWT令牌,然后在后續(xù)的每次請求中,都需要在請求頭header中攜帶到服務(wù)端,請求頭的名稱為 token ,值為 登錄時下發(fā)的JWT令牌。

如果檢測到用戶未登錄,則會返回如下固定錯誤信息:

{
    "code": 0,
    "msg": "NOT_LOGIN",
    "data": null
}

解讀完接口文檔中的描述了,目前我們先來完成令牌的生成和令牌的下發(fā),我們只需要生成一個令牌返回給前端就可以了。

實(shí)現(xiàn)步驟:

  • 引入JWT工具類
  • 在項(xiàng)目工程下創(chuàng)建com.example.utils包,并把提供JWT工具類復(fù)制到該包下
  • 登錄完成后,調(diào)用工具類生成JWT令牌并返回

JWT工具類

public class JwtUtils {
    private static String signKey = "xc";//簽名密鑰
    private static Long expire = 43200000L; //有效時間
    /**
     * 生成JWT令牌
     * @param claims JWT第二部分負(fù)載 payload 中存儲的內(nèi)容
     * @return
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)//自定義信息(有效載荷)
                .signWith(SignatureAlgorithm.HS256, signKey)//簽名算法(頭部)
                .setExpiration(new Date(System.currentTimeMillis() + expire))//過期時間
                .compact();
        return jwt;
    }
    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分負(fù)載 payload 中存儲的內(nèi)容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)//指定簽名密鑰
                .parseClaimsJws(jwt)//指定令牌Token
                .getBody();
        return claims;
    }
}

登錄成功,生成JWT令牌并返回

@RestController
@Slf4j
public class LoginController {
    //依賴業(yè)務(wù)層對象
    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        //調(diào)用業(yè)務(wù)層:登錄功能
        Emp loginEmp = empService.login(emp);
        //判斷:登錄用戶是否存在
        if(loginEmp !=null ){
            //自定義信息
            Map<String , Object> claims = new HashMap<>();
            claims.put("id", loginEmp.getId());
            claims.put("username",loginEmp.getUsername());
            claims.put("name",loginEmp.getName());
            //使用JWT工具類,生成身份令牌
            String token = JwtUtils.generateJwt(claims);
            return Result.success(token);
        }
        return Result.error("用戶名或密碼錯誤");
    }
}

服務(wù)器響應(yīng)的JWT令牌存儲在本地瀏覽器哪里了呢?
在當(dāng)前案例中,JWT令牌存儲在瀏覽器的本地存儲空間local storage中了。 local storage是瀏覽器的本地存儲,在移動端也是支持的。

過濾器Filter

剛才通過瀏覽器的開發(fā)者工具,我們可以看到在后續(xù)的請求當(dāng)中,都會在請求頭中攜帶JWT令牌到服務(wù)端,而服務(wù)端需要統(tǒng)一攔截所有的請求,從而判斷是否攜帶的有合法的JWT令牌。
那怎么樣來統(tǒng)一攔截到所有的請求校驗(yàn)令牌的有效性呢?這里我們會學(xué)習(xí)兩種解決方案:

  • Filter過濾器
  • Interceptor攔截器

我們首先來學(xué)習(xí)過濾器Filter。

快速入門

什么是Filter?

  • Filter表示過濾器,是 JavaWeb三大組件(Servlet、Filter、Listener)之一。
  • 過濾器可以把對資源的請求攔截下來,從而實(shí)現(xiàn)一些特殊的功能
    • 使用了過濾器之后,要想訪問web服務(wù)器上的資源,必須先經(jīng)過濾器,過濾器處理完畢之后,才可以訪問對應(yīng)的資源。
  • 過濾器一般完成一些通用的操作,比如:登錄校驗(yàn)、統(tǒng)一編碼處理、敏感字符處理等。

下面我們通過Filter快速入門程序掌握過濾器的基本使用操作:

  • 第1步,定義過濾器 :1.定義一個類,實(shí)現(xiàn) Filter 接口,并重寫其所有方法。
  • 第2步,配置過濾器:Filter類上加 @WebFilter 注解,配置攔截資源的路徑。引導(dǎo)類上加 @ServletComponentScan 開啟Servlet組件支持。

定義過濾器

//定義一個類,實(shí)現(xiàn)一個標(biāo)準(zhǔn)的Filter過濾器的接口
public class DemoFilter implements Filter {
    @Override //初始化方法, 只調(diào)用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法執(zhí)行了");
    }
    @Override //攔截到請求之后調(diào)用, 調(diào)用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Demo 攔截到了請求...放行前邏輯");
        //放行
        chain.doFilter(request,response);
    }
    @Override //銷毀方法, 只調(diào)用一次
    public void destroy() {
        System.out.println("destroy 銷毀方法執(zhí)行了");
    }
}
  • init方法:過濾器的初始化方法。在web服務(wù)器啟動的時候會自動的創(chuàng)建Filter過濾器對象,在創(chuàng)建過濾器對象的時候會自動調(diào)用init初始化方法,這個方法只會被調(diào)用一次。
  • doFilter方法:這個方法是在每一次攔截到請求之后都會被調(diào)用,所以這個方法是會被調(diào)用多次的,每攔截到一次請求就會調(diào)用一次doFilter()方法。
  • destroy方法: 是銷毀的方法。當(dāng)我們關(guān)閉服務(wù)器的時候,它會自動的調(diào)用銷毀方法destroy,而這個銷毀方法也只會被調(diào)用一次。

在定義完Filter之后,F(xiàn)ilter其實(shí)并不會生效,還需要完成Filter的配置,F(xiàn)ilter的配置非常簡單,只需要在Filter類上添加一個注解:@WebFilter,并指定屬性urlPatterns,通過這個屬性指定過濾器要攔截哪些請求

@WebFilter(urlPatterns = "/*") //配置過濾器要攔截的請求路徑( /* 表示攔截瀏覽器的所有請求 )
public class DemoFilter implements Filter {
    @Override //初始化方法, 只調(diào)用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法執(zhí)行了");
    }
    @Override //攔截到請求之后調(diào)用, 調(diào)用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Demo 攔截到了請求...放行前邏輯");
        //放行
        chain.doFilter(request,response);
    }
    @Override //銷毀方法, 只調(diào)用一次
    public void destroy() {
        System.out.println("destroy 銷毀方法執(zhí)行了");
    }
}

當(dāng)我們在Filter類上面加了@WebFilter注解之后,接下來我們還需要在啟動類上面加上一個注解@ServletComponentScan,通過這個@ServletComponentScan注解來開啟SpringBoot項(xiàng)目對于Servlet組件的支持。

@ServletComponentScan
@SpringBootApplication
public class WebManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebManagementApplication.class, args);
    }
}

注意事項(xiàng):
? 在過濾器Filter中,如果不執(zhí)行放行操作,將無法訪問后面的資源。 放行操作:chain.doFilter(request, response);

現(xiàn)在我們已完成了Filter過濾器的基本使用,下面我們將學(xué)習(xí)Filter過濾器在使用過程中的一些細(xì)節(jié)。

Filter詳解

Filter過濾器的快速入門程序我們已經(jīng)完成了,接下來我們就要詳細(xì)的介紹一下過濾器Filter在使用中的一些細(xì)節(jié)。主要介紹以下3個方面的細(xì)節(jié):

  • 過濾器的執(zhí)行流程
  • 過濾器的攔截路徑配置
  • 過濾器鏈

執(zhí)行流程
首先我們先來看下過濾器的執(zhí)行流程:

過濾器當(dāng)中我們攔截到了請求之后,如果希望繼續(xù)訪問后面的web資源,就要執(zhí)行放行操作,放行就是調(diào)用 FilterChain對象當(dāng)中的doFilter()方法,在調(diào)用doFilter()這個方法之前所編寫的代碼屬于放行之前的邏輯。

在放行后訪問完 web 資源之后還會回到過濾器當(dāng)中,回到過濾器之后如有需求還可以執(zhí)行放行之后的邏輯,放行之后的邏輯我們寫在doFilter()這行代碼之后。

@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {
    @Override //初始化方法, 只調(diào)用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法執(zhí)行了");
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("DemoFilter   放行前邏輯.....");
        //放行請求
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("DemoFilter   放行后邏輯.....");
    }
    @Override //銷毀方法, 只調(diào)用一次
    public void destroy() {
        System.out.println("destroy 銷毀方法執(zhí)行了");
    }
}

攔截路徑
執(zhí)行流程我們搞清楚之后,接下來再來介紹一下過濾器的攔截路徑,F(xiàn)ilter可以根據(jù)需求,配置不同的攔截資源路徑:

攔截路徑urlPatterns值含義
攔截具體路徑/login只有訪問 /login 路徑時,才會被攔截
目錄攔截/emps/*訪問/emps下的所有資源,都會被攔截
攔截所有/*訪問所有資源,都會被攔截

下面我們來測試"攔截具體路徑":

@WebFilter(urlPatterns = "/login")  //攔截/login具體路徑
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("DemoFilter   放行前邏輯.....");
        //放行請求
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("DemoFilter   放行后邏輯.....");
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }
    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

測試1:訪問部門管理請求,發(fā)現(xiàn)過濾器沒有攔截請求

測試2:訪問登錄請求/login,發(fā)現(xiàn)過濾器攔截請求

下面我們來測試"目錄攔截":

@WebFilter(urlPatterns = "/depts/*") //攔截所有以/depts開頭,后面是什么無所謂
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("DemoFilter   放行前邏輯.....");
        //放行請求
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("DemoFilter   放行后邏輯.....");
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }
    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

測試1:訪問部門管理請求,發(fā)現(xiàn)過濾器攔截了請求

測試2:訪問登錄請求/login,發(fā)現(xiàn)過濾器沒有攔截請求

過濾器鏈
最后我們在來介紹下過濾器鏈,什么是過濾器鏈呢?所謂過濾器鏈指的是在一個web應(yīng)用程序當(dāng)中,可以配置多個過濾器,多個過濾器就形成了一個過濾器鏈。

比如:在我們web服務(wù)器當(dāng)中,定義了兩個過濾器,這兩個過濾器就形成了一個過濾器鏈。

而這個鏈上的過濾器在執(zhí)行的時候會一個一個的執(zhí)行,會先執(zhí)行第一個Filter,放行之后再來執(zhí)行第二個Filter,如果執(zhí)行到了最后一個過濾器放行之后,才會訪問對應(yīng)的web資源。

訪問完web資源之后,按照我們剛才所介紹的過濾器的執(zhí)行流程,還會回到過濾器當(dāng)中來執(zhí)行過濾器放行后的邏輯,而在執(zhí)行放行后的邏輯的時候,順序是反著的。

先要執(zhí)行過濾器2放行之后的邏輯,再來執(zhí)行過濾器1放行之后的邏輯,最后在給瀏覽器響應(yīng)數(shù)據(jù)。

以上就是當(dāng)我們在web應(yīng)用當(dāng)中配置了多個過濾器,形成了這樣一個過濾器鏈以及過濾器鏈的執(zhí)行順序。下面我們通過idea來驗(yàn)證下過濾器鏈。

驗(yàn)證步驟:

  • 在filter包下再來新建一個Filter過濾器類:AbcFilter
  • 在AbcFilter過濾器中編寫放行前和放行后邏輯
  • 配置AbcFilter過濾器攔截請求路徑為:/*
  • 重啟SpringBoot服務(wù),查看DemoFilter、AbcFilter的執(zhí)行日志

AbcFilter過濾器

@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Abc 攔截到了請求... 放行前邏輯");
        //放行
        chain.doFilter(request,response);
        System.out.println("Abc 攔截到了請求... 放行后邏輯");
    }
}

DemoFilter過濾器

@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("DemoFilter   放行前邏輯.....");
        //放行請求
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("DemoFilter   放行后邏輯.....");
    }
}

打開瀏覽器訪問登錄接口:

通過控制臺日志的輸出,大家發(fā)現(xiàn)AbcFilter先執(zhí)行DemoFilter后執(zhí)行,這是為什么呢?

其實(shí)是和過濾器的類名有關(guān)系。以注解方式配置的Filter過濾器,它的執(zhí)行優(yōu)先級是按時過濾器類名的自動排序確定的,類名排名越靠前,優(yōu)先級越高。

假如我們想讓DemoFilter先執(zhí)行,怎么辦呢?答案就是修改類名。

測試:修改AbcFilter類名為XbcFilter,運(yùn)行程序查看控制臺日志

@WebFilter(urlPatterns = "/*")
public class XbcFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Xbc 攔截到了請求...放行前邏輯");
        //放行
        chain.doFilter(request,response);
        System.out.println("Xbc 攔截到了請求...放行后邏輯");
    }
}

到此,關(guān)于過濾器的使用細(xì)節(jié),我們已經(jīng)全部介紹完畢了。

登錄校驗(yàn)-Filter 分析

過濾器Filter的快速入門以及使用細(xì)節(jié)我們已經(jīng)介紹完了,接下來最后一步,我們需要使用過濾器Filter來完成案例當(dāng)中的登錄校驗(yàn)功能。

我們先來回顧下前面分析過的登錄校驗(yàn)的基本流程:

  • 要進(jìn)入到后臺管理系統(tǒng),我們必須先完成登錄操作,此時就需要訪問登錄接口login。
  • 登錄成功之后,我們會在服務(wù)端生成一個JWT令牌,并且把JWT令牌返回給前端,前端會將JWT令牌存儲下來。
  • 在后續(xù)的每一次請求當(dāng)中,都會將JWT令牌攜帶到服務(wù)端,請求到達(dá)服務(wù)端之后,要想去訪問對應(yīng)的業(yè)務(wù)功能,此時我們必須先要校驗(yàn)令牌的有效性。
  • 對于校驗(yàn)令牌的這一塊操作,我們使用登錄校驗(yàn)的過濾器,在過濾器當(dāng)中來校驗(yàn)令牌的有效性。如果令牌是無效的,就響應(yīng)一個錯誤的信息,也不會再去放行訪問對應(yīng)的資源了。如果令牌存在,并且它是有效的,此時就會放行去訪問對應(yīng)的web資源,執(zhí)行相應(yīng)的業(yè)務(wù)操作。

大概清楚了在Filter過濾器的實(shí)現(xiàn)步驟了,那在正式開發(fā)登錄校驗(yàn)過濾器之前,我們思考兩個問題:

  • 所有的請求,攔截到了之后,都需要校驗(yàn)令牌嗎?
  • 答案:登錄請求例外
  • 攔截到請求后,什么情況下才可以放行,執(zhí)行業(yè)務(wù)操作?
  • 答案:**有令牌,且令牌校驗(yàn)通過(合法);否則都返回未登錄錯誤結(jié)果

具體流程

我們要完成登錄校驗(yàn),主要是利用Filter過濾器實(shí)現(xiàn),而Filter過濾器的流程步驟:

基于上面的業(yè)務(wù)流程,我們分析出具體的操作步驟:

  • 獲取請求url
  • 判斷請求url中是否包含login,如果包含,說明是登錄操作,放行
  • 獲取請求頭中的令牌(token)
  • 判斷令牌是否存在,如果不存在,返回錯誤結(jié)果(未登錄)
  • 解析token,如果解析失敗,返回錯誤結(jié)果(未登錄)
  • 放行

代碼實(shí)現(xiàn)
分析清楚了以上的問題后,我們就參照接口文檔來開發(fā)登錄功能了,登錄接口描述如下:

基本信息

請求路徑:/login
請求方式:POST
接口描述:該接口用于員工登錄Tlias智能學(xué)習(xí)輔助系統(tǒng),登錄完畢后,系統(tǒng)下發(fā)JWT令牌。 

請求參數(shù)

參數(shù)格式:application/json

參數(shù)說明:

名稱類型是否必須備注
usernamestring必須用戶名
passwordstring必須密碼

請求數(shù)據(jù)樣例:

{
    "username": "jinyong",
    "password": "123456"
}

響應(yīng)數(shù)據(jù)

參數(shù)格式:application/json

參數(shù)說明:

名稱類型是否必須默認(rèn)值備注其他信息
codenumber必須響應(yīng)碼, 1 成功 ; 0 失敗
msgstring非必須提示信息
datastring必須返回的數(shù)據(jù) , jwt令牌

響應(yīng)數(shù)據(jù)樣例:

{
  "code": 1,
  "msg": "success",
  "data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
}

備注說明

用戶登錄成功后,系統(tǒng)會自動下發(fā)JWT令牌,然后在后續(xù)的每次請求中,都需要在請求頭header中攜帶到服務(wù)端,請求頭的名稱為 token ,值為 登錄時下發(fā)的JWT令牌。

如果檢測到用戶未登錄,則會返回如下固定錯誤信息:

{
    "code": 0,
    "msg": "NOT_LOGIN",
    "data": null
}

登錄校驗(yàn)過濾器:LoginCheckFilter

@Slf4j
@WebFilter(urlPatterns = "/*") //攔截所有請求
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        //前置:強(qiáng)制轉(zhuǎn)換為http協(xié)議的請求對象、響應(yīng)對象 (轉(zhuǎn)換原因:要使用子類中特有方法)
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //1.獲取請求url
        String url = request.getRequestURL().toString();
        log.info("請求路徑:{}", url); //請求路徑:http://localhost:8080/login
        //2.判斷請求url中是否包含login,如果包含,說明是登錄操作,放行
        if(url.contains("/login")){
            chain.doFilter(request, response);//放行請求
            return;//結(jié)束當(dāng)前方法的執(zhí)行
        }
        //3.獲取請求頭中的令牌(token)
        String token = request.getHeader("token");
        log.info("從請求頭中獲取的令牌:{}",token);
        //4.判斷令牌是否存在,如果不存在,返回錯誤結(jié)果(未登錄)
        if(!StringUtils.hasLength(token)){
            log.info("Token不存在");
            Result responseResult = Result.error("NOT_LOGIN");
            //把Result對象轉(zhuǎn)換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實(shí)現(xiàn)對象和json的轉(zhuǎn)換工具類)
            String json = JSONObject.toJSONString(responseResult);
            response.setContentType("application/json;charset=utf-8");
            //響應(yīng)
            response.getWriter().write(json);
            return;
        }
        //5.解析token,如果解析失敗,返回錯誤結(jié)果(未登錄)
        try {
            JwtUtils.parseJWT(token);
        }catch (Exception e){
            log.info("令牌解析失敗!");
            Result responseResult = Result.error("NOT_LOGIN");
            //把Result對象轉(zhuǎn)換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實(shí)現(xiàn)對象和json的轉(zhuǎn)換工具類)
            String json = JSONObject.toJSONString(responseResult);
            response.setContentType("application/json;charset=utf-8");
            //響應(yīng)
            response.getWriter().write(json);
            return;
        }
        //6.放行
        chain.doFilter(request, response);
    }
}

在上述過濾器的功能實(shí)現(xiàn)中,我們使用到了一個第三方j(luò)son處理的工具包fastjson。我們要想使用,需要引入如下依賴:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
</dependency>

攔截器Interceptor

學(xué)習(xí)完了過濾器Filter之后,接下來我們繼續(xù)學(xué)習(xí)攔截器Interseptor。

攔截器我們主要分為三個方面進(jìn)行講解:

  • 介紹下什么是攔截器,并通過快速入門程序上手?jǐn)r截器
  • 攔截器的使用細(xì)節(jié)
  • 通過攔截器Interceptor完成登錄校驗(yàn)功能

我們先學(xué)習(xí)第一塊內(nèi)容:攔截器快速入門

快速入門

  • 什么是攔截器?
  • 是一種動態(tài)攔截方法調(diào)用的機(jī)制,類似于過濾器。攔截器是Spring框架中提供的,用來動態(tài)攔截控制器方法的執(zhí)行。

攔截器的作用:

  • 攔截請求,在指定方法調(diào)用前后,根據(jù)業(yè)務(wù)需要執(zhí)行預(yù)先設(shè)定的代碼。

在攔截器當(dāng)中,我們通常也是做一些通用性的操作,比如:我們可以通過攔截器來攔截前端發(fā)起的請求,將登錄校驗(yàn)的邏輯全部編寫在攔截器當(dāng)中。在校驗(yàn)的過程當(dāng)中,如發(fā)現(xiàn)用戶登錄了(攜帶JWT令牌且是合法令牌),就可以直接放行,去訪問spring當(dāng)中的資源。如果校驗(yàn)時發(fā)現(xiàn)并沒有登錄或是非法令牌,就可以直接給前端響應(yīng)未登錄的錯誤信息。

下面我們通過快速入門程序,來學(xué)習(xí)下攔截器的基本使用。攔截器的使用步驟和過濾器類似,也分為兩步:

  • 定義攔截器
  • 注冊配置攔截器

自定義攔截器:實(shí)現(xiàn)HandlerInterceptor接口,并重寫其所有方法

//自定義攔截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //目標(biāo)資源方法執(zhí)行前執(zhí)行。 返回true:放行    返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        return true; //true表示放行
    }
    //目標(biāo)資源方法執(zhí)行后執(zhí)行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ... ");
    }
    //視圖渲染完畢后執(zhí)行,最后執(zhí)行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion .... ");
    }
}

注意:
? preHandle方法:目標(biāo)資源方法執(zhí)行前執(zhí)行。 返回true:放行 返回false:不放行
? postHandle方法:目標(biāo)資源方法執(zhí)行后執(zhí)行
? afterCompletion方法:視圖渲染完畢后執(zhí)行,最后執(zhí)行

注冊配置攔截器:實(shí)現(xiàn)WebMvcConfigurer接口,并重寫addInterceptors方法

@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //自定義的攔截器對象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注冊自定義攔截器對象
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//設(shè)置攔截器攔截的請求路徑( /** 表示攔截所有請求)
    }
}

Interceptor詳解

攔截器的入門程序完成之后,接下來我們來介紹攔截器的使用細(xì)節(jié)。攔截器的使用細(xì)節(jié)我們主要介紹兩個部分:

  • 攔截器的攔截路徑配置
  • 攔截器的執(zhí)行流程

攔截路徑
首先我們先來看攔截器的攔截路徑的配置,在注冊配置攔截器的時候,我們要指定攔截器的攔截路徑,通過addPathPatterns("要攔截路徑")方法,就可以指定要攔截哪些資源。

在入門程序中我們配置的是/**,表示攔截所有資源,而在配置攔截器時,不僅可以指定要攔截哪些資源,還可以指定不攔截哪些資源,只需要調(diào)用excludePathPatterns("不攔截路徑")方法,指定哪些資源不需要攔截。

@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //攔截器對象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注冊自定義攔截器對象
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")//設(shè)置攔截器攔截的請求路徑( /** 表示攔截所有請求)
                .excludePathPatterns("/login");//設(shè)置不攔截的請求路徑
    }
}

在攔截器中除了可以設(shè)置/**攔截所有資源外,還有一些常見攔截路徑設(shè)置:

攔截路徑含義舉例
/*一級路徑能匹配/depts,/emps,/login,不能匹配 /depts/1
/**任意級路徑能匹配/depts,/depts/1,/depts/1/2
/depts/*/depts下的一級路徑能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**/depts下的任意級路徑能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

下面主要來演示下/**/*的區(qū)別:

  • 修改攔截器配置,把攔截路徑設(shè)置為/*
@Configuration 
public class WebConfig implements WebMvcConfigurer {
    //攔截器對象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注冊自定義攔截器對象
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/*")
                .excludePathPatterns("/login");//設(shè)置不攔截的請求路徑
    }
}

執(zhí)行流程
介紹完攔截路徑的配置之后,接下來我們再來介紹攔截器的執(zhí)行流程。通過執(zhí)行流程,大家就能夠清晰的知道過濾器與攔截器的執(zhí)行時機(jī)。

  • 當(dāng)我們打開瀏覽器來訪問部署在web服務(wù)器當(dāng)中的web應(yīng)用時,此時我們所定義的過濾器會攔截到這次請求。攔截到這次請求之后,它會先執(zhí)行放行前的邏輯,然后再執(zhí)行放行操作。而由于我們當(dāng)前是基于springboot開發(fā)的,所以放行之后是進(jìn)入到了spring的環(huán)境當(dāng)中,也就是要來訪問我們所定義的controller當(dāng)中的接口方法。
  • Tomcat并不識別所編寫的Controller程序,但是它識別Servlet程序,所以在Spring的Web環(huán)境中提供了一個非常核心的Servlet:DispatcherServlet(前端控制器),所有請求都會先進(jìn)行到DispatcherServlet,再將請求轉(zhuǎn)給Controller。
  • 當(dāng)我們定義了攔截器后,會在執(zhí)行Controller的方法之前,請求被攔截器攔截住。執(zhí)行preHandle()方法,這個方法執(zhí)行完成后需要返回一個布爾類型的值,如果返回true,就表示放行本次操作,才會繼續(xù)訪問controller中的方法;如果返回false,則不會放行(controller中的方法也不會執(zhí)行)。
  • 在controller當(dāng)中的方法執(zhí)行完畢之后,再回過來執(zhí)行postHandle()這個方法以及afterCompletion() 方法,然后再返回給DispatcherServlet,最終再來執(zhí)行過濾器當(dāng)中放行后的這一部分邏輯的邏輯。執(zhí)行完畢之后,最終給瀏覽器響應(yīng)數(shù)據(jù)。

接下來我們就來演示下過濾器和攔截器同時存在的執(zhí)行流程:

  • 開啟LoginCheckInterceptor攔截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        return true; //true表示放行
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ... ");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion .... ");
    }
}
@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //攔截器對象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注冊自定義攔截器對象
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")//攔截所有請求
                .excludePathPatterns("/login");//不攔截登錄請求
    }
}

開啟DemoFilter過濾器

@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("DemoFilter   放行前邏輯.....");
        //放行請求
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("DemoFilter   放行后邏輯.....");
    }
}

以上就是攔截器的執(zhí)行流程。通過執(zhí)行流程分析,大家應(yīng)該已經(jīng)清楚了過濾器和攔截器之間的區(qū)別,其實(shí)它們之間的區(qū)別主要是兩點(diǎn):

  • 接口規(guī)范不同:過濾器需要實(shí)現(xiàn)Filter接口,而攔截器需要實(shí)現(xiàn)HandlerInterceptor接口。
  • 攔截范圍不同:過濾器Filter會攔截所有的資源,而Interceptor只會攔截Spring環(huán)境中的資源。

登錄校驗(yàn)- Interceptor

講解完了攔截器的基本操作之后,接下來我們需要完成最后一步操作:通過攔截器來完成案例當(dāng)中的登錄校驗(yàn)功能。

登錄校驗(yàn)的業(yè)務(wù)邏輯以及操作步驟我們前面已經(jīng)分析過了,和登錄校驗(yàn)Filter過濾器當(dāng)中的邏輯是完全一致的?,F(xiàn)在我們只需要把這個技術(shù)方案由原來的過濾器換成攔截器interceptor就可以了。
登錄校驗(yàn)攔截器

//自定義攔截器
@Component //當(dāng)前攔截器對象由Spring創(chuàng)建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    //前置方式
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        //1.獲取請求url
        //2.判斷請求url中是否包含login,如果包含,說明是登錄操作,放行
        //3.獲取請求頭中的令牌(token)
        String token = request.getHeader("token");
        log.info("從請求頭中獲取的令牌:{}",token);
        //4.判斷令牌是否存在,如果不存在,返回錯誤結(jié)果(未登錄)
        if(!StringUtils.hasLength(token)){
            log.info("Token不存在");
            //創(chuàng)建響應(yīng)結(jié)果對象
            Result responseResult = Result.error("NOT_LOGIN");
            //把Result對象轉(zhuǎn)換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實(shí)現(xiàn)對象和json的轉(zhuǎn)換工具類)
            String json = JSONObject.toJSONString(responseResult);
            //設(shè)置響應(yīng)頭(告知瀏覽器:響應(yīng)的數(shù)據(jù)類型為json、響應(yīng)的數(shù)據(jù)編碼表為utf-8)
            response.setContentType("application/json;charset=utf-8");
            //響應(yīng)
            response.getWriter().write(json);
            return false;//不放行
        }
        //5.解析token,如果解析失敗,返回錯誤結(jié)果(未登錄)
        try {
            JwtUtils.parseJWT(token);
        }catch (Exception e){
            log.info("令牌解析失敗!");
            //創(chuàng)建響應(yīng)結(jié)果對象
            Result responseResult = Result.error("NOT_LOGIN");
            //把Result對象轉(zhuǎn)換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實(shí)現(xiàn)對象和json的轉(zhuǎn)換工具類)
            String json = JSONObject.toJSONString(responseResult);
            //設(shè)置響應(yīng)頭
            response.setContentType("application/json;charset=utf-8");
            //響應(yīng)
            response.getWriter().write(json);
            return false;
        }
        //6.放行
        return true;
    }

注冊配置攔截器

@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //攔截器對象
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注冊自定義攔截器對象
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

到此這篇關(guān)于一文掌握J(rèn)avaWeb登錄認(rèn)證的文章就介紹到這了,更多相關(guān)JavaWeb登錄認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳解Spring Security 中的四種權(quán)限控制方式

    詳解Spring Security 中的四種權(quán)限控制方式

    這篇文章主要介紹了詳解Spring Security 中的四種權(quán)限控制方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-10-10
  • 關(guān)于MD5算法原理與常用實(shí)現(xiàn)方式

    關(guān)于MD5算法原理與常用實(shí)現(xiàn)方式

    這篇文章主要介紹了關(guān)于MD5算法原理與常用實(shí)現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • 關(guān)于@Configuration的作用說明

    關(guān)于@Configuration的作用說明

    這篇文章主要介紹了關(guān)于@Configuration的作用說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • MyBatis-Plus實(shí)現(xiàn)邏輯刪除功能解析

    MyBatis-Plus實(shí)現(xiàn)邏輯刪除功能解析

    這篇文章主要介紹了MyBatis-Plus實(shí)現(xiàn)邏輯刪除功能解析,有時候并不需要真正的刪除數(shù)據(jù),而是想邏輯刪除,方便數(shù)據(jù)恢復(fù),MyBatis-Plus可以很方便的實(shí)現(xiàn)邏輯刪除的功能,需要的朋友可以參考下
    2023-11-11
  • 詳細(xì)分析Java中String、StringBuffer、StringBuilder類的性能

    詳細(xì)分析Java中String、StringBuffer、StringBuilder類的性能

    在Java中,String類和StringBuffer類以及StringBuilder類都能用于創(chuàng)建字符串對象,而在分別操作這些對象時我們會發(fā)現(xiàn)JVM執(zhí)行它們的性能并不相同,下面我們就來詳細(xì)分析Java中String、StringBuffer、StringBuilder類的性能
    2016-05-05
  • Java并發(fā)Futures和Callables類實(shí)例詳解

    Java并發(fā)Futures和Callables類實(shí)例詳解

    Callable對象返回Future對象,該對象提供監(jiān)視線程執(zhí)行的任務(wù)進(jìn)度的方法, Future對象可用于檢查Callable的狀態(tài),然后線程完成后從Callable中檢索結(jié)果,這篇文章給大家介紹Java并發(fā)Futures和Callables類的相關(guān)知識,感興趣的朋友一起看看吧
    2024-05-05
  • JavaEE Filter敏感詞過濾的方法實(shí)例詳解

    JavaEE Filter敏感詞過濾的方法實(shí)例詳解

    我們無論是在聊天還是在留言時,都有一些信息不希望別人看到。那么如果過濾這些關(guān)鍵詞呢?下面小編給大家分享JavaEE Filter敏感詞過濾的方法實(shí)例詳解,感興趣的朋友一起學(xué)習(xí)吧
    2016-05-05
  • Mybatis注解方式操作Oracle數(shù)據(jù)庫詳解

    Mybatis注解方式操作Oracle數(shù)據(jù)庫詳解

    這篇文章主要介紹了Mybatis注解方式操作Oracle數(shù)據(jù)庫詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-11-11
  • Springboot連接數(shù)據(jù)庫及查詢數(shù)據(jù)完整流程

    Springboot連接數(shù)據(jù)庫及查詢數(shù)據(jù)完整流程

    今天給大家?guī)淼氖顷P(guān)于Springboot的相關(guān)知識,文章圍繞著Springboot連接數(shù)據(jù)庫及查詢數(shù)據(jù)完整流程展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • 使用idea2017搭建SSM框架(圖文步驟)

    使用idea2017搭建SSM框架(圖文步驟)

    這篇文章主要介紹了使用idea2017搭建SSM框架(圖文步驟),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-12-12

最新評論