JavaWeb—— SpringBootWeb綜合案例(登錄功能、登錄校驗、異常處理)

案例-登錄認證

目錄

  • 案例-登錄認證
    • 1. 登錄功能
      • 1.1 需求
      • 1.2 接口文檔
      • 1.3 思路分析
      • 1.4 功能開發
      • 1.5 測試
    • 2. 登錄校驗
      • 2.1 問題分析
      • 2.2 會話技術
        • 2.2.1 會話技術介紹
        • 2.2.2 會話跟蹤方案
          • 2.2.2.1 方案一 - Cookie
          • 2.2.2.2 方案二 - Session
          • 2.2.2.3 方案三 - 令牌技術
      • 2.3 JWT令牌
        • 2.3.1 介紹
        • 2.3.2 生成和校驗
        • 2.3.3 登錄下發令牌
      • 2.4 過濾器Filter
        • 2.4.1 快速入門
        • 2.4.2 Filter詳解
          • 2.4.2.1 執行流程
          • 2.4.2.2 攔截路徑
          • 2.4.2.3 過濾器鏈
        • 2.4.3 登錄校驗-Filter
          • 2.4.3.1 分析
          • 2.4.3.2 具體流程
          • 2.4.3.3 代碼實現
      • 2.5 攔截器Interceptor
        • 2.5.1 快速入門
        • 2.5.2 Interceptor詳解
          • 2.5.2.1 攔截路徑
          • 2.5.2.2 執行流程
        • 2.5.3 登錄校驗- Interceptor
    • 3. 異常處理
      • 3.1 當前問題
      • 3.2 解決方案
      • 3.3 全局異常處理器

1. 登錄功能

1.1 需求

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

1.2 接口文檔

我們參照接口文檔來開發登錄功能

  • 基本信息

    請求路徑:/login請求方式:POST接口描述:該接口用于員工登錄Tlias智能學習輔助系統,登錄完畢后,系統下發JWT令牌。 
    
  • 請求參數

    參數格式:application/json

    參數說明:

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

    請求數據樣例:

    {"username": "jinyong","password": "123456"
    }
    
  • 響應數據

    參數格式:application/json

    參數說明:

    名稱類型是否必須默認值備注其他信息
    codenumber必須響應碼, 1 成功 ; 0 失敗
    msgstring非必須提示信息
    datastring必須返回的數據 , jwt令牌

    響應數據樣例:

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

1.3 思路分析

在這里插入圖片描述

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

1.4 功能開發

LoginController

@RestController
public class LoginController {@Autowiredprivate 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 {@Autowiredprivate EmpMapper empMapper;@Overridepublic Emp login(Emp emp) {//調用dao層功能:登錄Emp loginEmp = empMapper.getByUsernameAndPassword(emp);//返回查詢結果給Controllerreturn 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);//省略其他代碼...
}

在這里插入圖片描述

1.5 測試

功能開發完畢后,我們就可以啟動服務,打開postman進行測試了。

發起POST請求,訪問:http://localhost:8080/login

在這里插入圖片描述

postman測試通過了,那接下來,我們就可以結合著前端工程進行聯調測試。

先退出系統,進入到登錄頁面:

在這里插入圖片描述

在登錄頁面輸入賬戶密碼:

在這里插入圖片描述

登錄成功之后進入到后臺管理系統頁面:

在這里插入圖片描述

2. 登錄校驗

2.1 問題分析

我們已經完成了基礎登錄功能的開發與測試,在我們登錄成功后就可以進入到后臺管理系統中進行數據的操作。

但是當我們在瀏覽器中新的頁面上輸入地址:http://localhost:9528/#/system/dept,發現沒有登錄仍然可以進入到后端管理系統頁面。

在這里插入圖片描述

而真正的登錄功能應該是:登陸后才能訪問后端系統頁面,不登陸則跳轉登陸頁面進行登陸。

為什么會出現這個問題?其實原因很簡單,就是因為針對于我們當前所開發的部門管理、員工管理以及文件上傳等相關接口來說,我們在服務器端并沒有做任何的判斷,沒有去判斷用戶是否登錄了。所以無論用戶是否登錄,都可以訪問部門管理以及員工管理的相關數據。所以我們目前所開發的登錄功能,它只是徒有其表。而我們要想解決這個問題,我們就需要完成一步非常重要的操作:登錄校驗。

在這里插入圖片描述

什么是登錄校驗?

  • 所謂登錄校驗,指的是我們在服務器端接收到瀏覽器發送過來的請求之后,首先我們要對請求進行校驗。先要校驗一下用戶登錄了沒有,如果用戶已經登錄了,就直接執行對應的業務操作就可以了;如果用戶沒有登錄,此時就不允許他執行相關的業務操作,直接給前端響應一個錯誤的結果,最終跳轉到登錄頁面,要求他登錄成功之后,再來訪問對應的數據。

了解完什么是登錄校驗之后,接下來我們分析一下登錄校驗大概的實現思路。

首先我們在宏觀上先有一個認知:

前面在講解HTTP協議的時候,我們提到HTTP協議是無狀態協議。什么又是無狀態的協議?

所謂無狀態,指的是每一次請求都是獨立的,下一次請求并不會攜帶上一次請求的數據。而瀏覽器與服務器之間進行交互,基于HTTP協議也就意味著現在我們通過瀏覽器來訪問了登陸這個接口,實現了登陸的操作,接下來我們在執行其他業務操作時,服務器也并不知道這個員工到底登陸了沒有。因為HTTP協議是無狀態的,兩次請求之間是獨立的,所以是無法判斷這個員工到底登陸了沒有。

在這里插入圖片描述

那應該怎么來實現登錄校驗的操作呢?具體的實現思路可以分為兩部分:

  1. 在員工登錄成功后,需要將用戶登錄成功的信息存起來,記錄用戶已經登錄成功的標記。
  2. 在瀏覽器發起請求時,需要在服務端進行統一攔截,攔截后進行登錄校驗。

想要判斷員工是否已經登錄,我們需要在員工登錄成功之后,存儲一個登錄成功的標記,接下來在每一個接口方法執行之前,先做一個條件判斷,判斷一下這個員工到底登錄了沒有。如果是登錄了,就可以執行正常的業務操作,如果沒有登錄,會直接給前端返回一個錯誤的信息,前端拿到這個錯誤信息之后會自動的跳轉到登錄頁面。

我們程序中所開發的查詢功能、刪除功能、添加功能、修改功能,都需要使用以上套路進行登錄校驗。此時就會出現:相同代碼邏輯,每個功能都需要編寫,就會造成代碼非常繁瑣。

為了簡化這塊操作,我們可以使用一種技術:統一攔截技術。

通過統一攔截的技術,我們可以來攔截瀏覽器發送過來的所有的請求,攔截到這個請求之后,就可以通過請求來獲取之前所存入的登錄標記,在獲取到登錄標記且標記為登錄成功,就說明員工已經登錄了。如果已經登錄,我們就直接放行(意思就是可以訪問正常的業務接口了)。

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

  1. 會話技術
  2. 統一攔截技術

而統一攔截技術現實方案也有兩種:

  1. Servlet規范中的Filter過濾器
  2. Spring提供的interceptor攔截器

下面我們先學習會話技術,然后再學習統一攔截技術。

2.2 會話技術

介紹了登錄校驗的大概思路之后,我們先來學習下會話技術。

2.2.1 會話技術介紹

什么是會話?

  • 在我們日常生活當中,會話指的就是談話、交談。

  • 在web開發當中,會話指的就是瀏覽器與服務器之間的一次連接,我們就稱為一次會話。

    在用戶打開瀏覽器第一次訪問服務器的時候,這個會話就建立了,直到有任何一方斷開連接,此時會話就結束了。在一次會話當中,是可以包含多次請求和響應的。

    比如:打開了瀏覽器來訪問web服務器上的資源(瀏覽器不能關閉、服務器不能斷開)

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

    只要瀏覽器和服務器都沒有關閉,以上3次請求都屬于一次會話當中完成的。

在這里插入圖片描述

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

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

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

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

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

為什么要共享數據呢?

由于HTTP是無狀態協議,在后面請求中怎么拿到前一次請求生成的數據呢?此時就需要在一次會話的多次請求之間進行數據共享

會話跟蹤技術有兩種:

  1. Cookie(客戶端會話跟蹤技術)
    • 數據存儲在客戶端瀏覽器當中
  2. Session(服務端會話跟蹤技術)
    • 數據存儲在儲在服務端
  3. 令牌技術
2.2.2 會話跟蹤方案

上面我們介紹了什么是會話,什么是會話跟蹤,并且也提到了會話跟蹤 3 種常見的技術方案。接下來,我們就來對比一下這 3 種會話跟蹤的技術方案,來看一下具體的實現思路,以及它們之間的優缺點。

2.2.2.1 方案一 - Cookie

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

比如第一次請求了登錄接口,登錄接口執行完成之后,我們就可以設置一個cookie,在 cookie 當中我們就可以來存儲用戶相關的一些數據信息。比如我可以在 cookie 當中來存儲當前登錄用戶的用戶名,用戶的ID。

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

在這里插入圖片描述

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

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

  • 服務器會 自動 的將 cookie 響應給瀏覽器。

  • 瀏覽器接收到響應回來的數據之后,會 自動 的將 cookie 存儲在瀏覽器本地。

  • 在后續的請求當中,瀏覽器會 自動 的將 cookie 攜帶到服務器端。

為什么這一切都是自動化進行的?

是因為 cookie 它是 HTP 協議當中所支持的技術,而各大瀏覽器廠商都支持了這一標準。在 HTTP 協議官方給我們提供了一個響應頭和請求頭:

  • 響應頭 Set-Cookie :設置Cookie數據的

  • 請求頭 Cookie:攜帶Cookie數據的

在這里插入圖片描述

代碼測試

@Slf4j
@RestController
public class SessionController {//設置Cookie@GetMapping("/c1")public Result cookie1(HttpServletResponse response){response.addCookie(new Cookie("login_username","itheima")); //設置Cookie/響應Cookiereturn 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接口,設置Cookie,http://localhost:8080/c1

在這里插入圖片描述

我們可以看到,設置的cookie,通過響應頭Set-Cookie響應給瀏覽器,并且瀏覽器會將Cookie,存儲在瀏覽器端。

在這里插入圖片描述

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

在這里插入圖片描述

優缺點

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

跨域介紹:

? 在這里插入圖片描述

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

區分跨域的維度:

  • 協議
  • IP/協議
  • 端口

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

舉例:

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

? 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 [不跨域]

2.2.2.2 方案二 - Session

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

  • 獲取Session

    在這里插入圖片描述

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

  • 響應Cookie (JSESSIONID)

    在這里插入圖片描述

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

  • 查找Session

    在這里插入圖片描述

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

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

代碼測試

@Slf4j
@RestController
public class SessionController {@GetMapping("/s1")public Result session1(HttpSession session){log.info("HttpSession-s1: {}", session.hashCode());session.setAttribute("loginUser", "tom"); //往session中存儲數據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中獲取數據log.info("loginUser: {}", loginUser);return Result.success(loginUser);}
}

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

在這里插入圖片描述

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

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

在這里插入圖片描述

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

那經過這兩步測試,大家也會看到,在控制臺中輸出如下日志:

在這里插入圖片描述

兩次請求,獲取到的Session會話對象的hashcode是一樣的,就說明是同一個會話對象。而且,第一次請求時,往Session會話對象中存儲的值,第二次請求時,也獲取到了。 那這樣,我們就可以通過Session會話對象,在同一個會話的多次請求之間來進行數據共享了。

優缺點

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

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

服務器集群環境為何無法使用Session?

? 在這里插入圖片描述

  • 首先第一點,我們現在所開發的項目,一般都不會只部署在一臺服務器上,因為一臺服務器會存在一個很大的問題,就是單點故障。所謂單點故障,指的就是一旦這臺服務器掛了,整個應用都沒法訪問了。

? 在這里插入圖片描述

  • 所以在現在的企業項目開發當中,最終部署的時候都是以集群的形式來進行部署,也就是同一個項目它會部署多份。比如這個項目我們現在就部署了 3 份。

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

    在這里插入圖片描述

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

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

    好。此時假如又執行了一次查詢操作,要查詢部門的數據。這次請求到達負載均衡服務器之后,負載均衡服務器將這次請求轉給了第二臺 Tomcat 服務器,此時他就要到第二臺 Tomcat 服務器當中。根據JSESSIONID 也就是對應的 session 的 ID 值,要找對應的 session 會話對象。

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

大家會看到上面這兩種傳統的會話技術,在現在的企業開發當中是不是會存在很多的問題。 為了解決這些問題,在現在的企業開發當中,基本上都會采用第三種方案,通過令牌技術來進行會話跟蹤。接下來我們就來介紹一下令牌技術,來看一下令牌技術又是如何跟蹤會話的。

2.2.2.3 方案三 - 令牌技術

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

在這里插入圖片描述

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

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

接下來,在后續的每一次請求當中,都需要將令牌攜帶到服務端。攜帶到服務端之后,接下來我們就需要來校驗令牌的有效性。如果令牌是有效的,就說明用戶已經執行了登錄操作,如果令牌是無效的,就說明用戶之前并未執行登錄操作。

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

優缺點

  • 優點:
    • 支持PC端、移動端
    • 解決集群環境下的認證問題
    • 減輕服務器的存儲壓力(無需在服務器端存儲)
  • 缺點:需要自己實現(包括令牌的生成、令牌的傳遞、令牌的校驗)

針對于這三種方案,現在企業開發當中使用的最多的就是第三種令牌技術進行會話跟蹤。而前面的這兩種傳統的方案,現在企業項目開發當中已經很少使用了。所以在我們的課程當中,我們也將會采用令牌技術來解決案例項目當中的會話跟蹤問題。

2.3 JWT令牌

前面我們介紹了基于令牌技術來實現會話追蹤。這里所提到的令牌就是用戶身份的標識,其本質就是一個字符串。令牌的形式有很多,我們使用的是功能強大的 JWT令牌。

2.3.1 介紹

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

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

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

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

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

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

  • 第一部分:Header(頭), 記錄令牌類型、簽名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}

  • 第二部分:Payload(有效載荷),攜帶一些自定義信息、默認信息等。 例如:{“id”:“1”,“username”:“Tom”}

  • 第三部分:Signature(簽名),防止Token被篡改、確保安全性。將header、payload,并加入指定秘鑰,通過指定簽名算法計算而來。

    簽名的目的就是為了防jwt令牌被篡改,而正是因為jwt令牌最后一個部分數字簽名的存在,所以整個jwt 令牌是非常安全可靠的。一旦jwt令牌當中任何一個部分、任何一個字符被篡改了,整個令牌在校驗的時候都會失敗,所以它是非常安全可靠的。

在這里插入圖片描述

JWT是如何將原始的JSON格式數據,轉變為字符串的呢?

其實在生成JWT令牌時,會對JSON格式的數據進行一次編碼:進行base64編碼

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

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

在這里插入圖片描述

JWT令牌最典型的應用場景就是登錄認證:

  1. 在瀏覽器發起請求來執行登錄操作,此時會訪問登錄的接口,如果登錄成功之后,我們需要生成一個jwt令牌,將生成的 jwt令牌返回給前端。
  2. 前端拿到jwt令牌之后,會將jwt令牌存儲起來。在后續的每一次請求中都會將jwt令牌攜帶到服務端。
  3. 服務端統一攔截請求之后,先來判斷一下這次請求有沒有把令牌帶過來,如果沒有帶過來,直接拒絕訪問,如果帶過來了,還要校驗一下令牌是否是有效。如果有效,就直接放行進行請求的處理。

在JWT登錄認證的場景中我們發現,整個流程當中涉及到兩步操作:

  1. 在登錄成功之后,要生成令牌。
  2. 每一次請求當中,要接收令牌并對令牌進行校驗。

稍后我們再來學習如何來生成jwt令牌,以及如何來校驗jwt令牌。

2.3.2 生成和校驗

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

首先我們先來實現JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依賴:

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

在引入完JWT來賴后,就可以調用工具包中提供的API來完成JWT令牌的生成和校驗

工具類:Jwts

生成JWT代碼實現:

 @Testpublic void testGenJwt(){Map<String, Object> claims = new HashMap<>();claims.put("id",1);claims.put("name","tom");String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256,"pang2003210")//簽名秘鑰.setClaims(claims)//定義自定義內容(載荷).setExpiration(new Date(System.currentTimeMillis()+3600*1000))//設置有效期為1h.compact();System.out.println(jwt);}

運行測試方法:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk

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

在這里插入圖片描述

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

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

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

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

/*** 解析JWT*/ 
@Testpublic void parseJwt(){Claims claims = Jwts.parser().setSigningKey("pang2003210")//指定簽名密鑰(必須保證和生成令牌時使用相同的簽名密鑰).parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcwOTQzNTAwM30.veywLvCY2IxLcwYFWV2rueIiPwBeJNCmslcbkRWh92Y").getBody();System.out.println(claims);}

運行測試方法:

{name=tom, id=1, exp=1709435003}

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

下面我們做一個測試:把令牌header中的數字9變為8,運行測試方法后發現報錯:

原header: eyJhbGciOiJIUzI1NiJ9

修改為: eyJhbGciOiJIUzI1NiJ8

在這里插入圖片描述

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

我們繼續測試:修改生成令牌的時指定的過期時間,修改為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) //自定義內容(載荷)          .signWith(SignatureAlgorithm.HS256, “itheima”) //簽名算法        .setExpiration(new Date(System.currentTimeMillis() + 60*1000)) //有效期60秒   .compact();System.out.println(jwt);//輸出結果:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro
}@Test
public void parseJwt(){Claims claims = Jwts.parser().setSigningKey("itheima")//指定簽名密鑰
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro").getBody();System.out.println(claims);
}

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

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

  • JWT校驗時使用的簽名秘鑰,必須和生成JWT令牌時使用的秘鑰是配套的。

  • 如果JWT令牌解析校驗時報錯,則說明 JWT令牌被篡改 或 失效了,令牌非法。

在這里插入圖片描述

2.3.3 登錄下發令牌

JWT令牌的生成和校驗的基本操作我們已經學習完了,接下來我們就需要在案例當中通過JWT令牌技術來跟蹤會話。具體的思路我們前面已經分析過了,主要就是兩步操作:

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

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

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

  • 響應數據

    參數格式:application/json

    參數說明:

    名稱類型是否必須默認值備注其他信息
    codenumber必須響應碼, 1 成功 ; 0 失敗
    msgstring非必須提示信息
    datastring必須返回的數據 , jwt令牌

    響應數據樣例:

    {"code": 1,"msg": "success","data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
    }
    
  • 備注說明

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

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

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

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

實現步驟:

  1. 引入JWT工具類
    • 在項目工程下創建com.itheima.utils包,并把提供JWT工具類復制到該包下
  2. 登錄完成后,調用工具類生成JWT令牌并返回

JWT工具類

public class JwtUtils {private static String signKey = "itheima";//簽名密鑰private static Long expire = 43200000L; //有效時間/*** 生成JWT令牌* @param claims JWT第二部分負載 payload 中存儲的內容* @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第二部分負載 payload 中存儲的內容*/public static Claims parseJWT(String jwt){Claims claims = Jwts.parser().setSigningKey(signKey)//指定簽名密鑰.parseClaimsJws(jwt)//指定令牌Token.getBody();return claims;}
}

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

@RestController
@Slf4j
public class LoginController {//依賴業務層對象@Autowiredprivate EmpService empService;@PostMapping("/login")public Result login(@RequestBody Emp emp) {//調用業務層:登錄功能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("用戶名或密碼錯誤");}
}

重啟服務,打開postman測試登錄接口:

在這里插入圖片描述

在這里插入圖片描述

打開瀏覽器完成前后端聯調操作:利用開發者工具,抓取一下網絡請求

在這里插入圖片描述

登錄請求完成后,可以看到JWT令牌已經響應給了前端,此時前端就會將JWT令牌存儲在瀏覽器本地。

在這里插入圖片描述

服務器響應的JWT令牌存儲在本地瀏覽器哪里了呢?

  • 在當前案例中,JWT令牌存儲在瀏覽器的本地存儲空間local storage中了。 local storage是瀏覽器的本地存儲,在移動端也是支持的。

在這里插入圖片描述

我們在發起一個查詢部門數據的請求,此時我們可以看到在請求頭中包含一個token(JWT令牌),后續的每一次請求當中,都會將這個令牌攜帶到服務端。

在這里插入圖片描述

2.4 過濾器Filter

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

  1. Filter過濾器
  2. Interceptor攔截器

我們首先來學習過濾器Filter。

2.4.1 快速入門

在這里插入圖片描述

在這里插入圖片描述

什么是Filter?

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

在這里插入圖片描述

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

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

定義過濾器

//定義一個類,實現一個標準的Filter過濾器的接口
public class DemoFilter implements Filter {@Override //初始化方法, 只調用一次public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init 初始化方法執行了");}@Override //攔截到請求之后調用, 調用多次public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {System.out.println("Demo 攔截到了請求...放行前邏輯");//放行chain.doFilter(request,response);}@Override //銷毀方法, 只調用一次public void destroy() {System.out.println("destroy 銷毀方法執行了");}
}
  • init方法:過濾器的初始化方法。在web服務器啟動的時候會自動的創建Filter過濾器對象,在創建過濾器對象的時候會自動調用init初始化方法,這個方法只會被調用一次。

  • doFilter方法:這個方法是在每一次攔截到請求之后都會被調用,所以這個方法是會被調用多次的,每攔截到一次請求就會調用一次doFilter()方法。

  • destroy方法: 是銷毀的方法。當我們關閉服務器的時候,它會自動的調用銷毀方法destroy,而這個銷毀方法也只會被調用一次。

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

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

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

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

重新啟動服務,打開瀏覽器,執行部門管理的請求,可以看到控制臺輸出了過濾器中的內容:

在這里插入圖片描述

注意事項:

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

現在我們已完成了Filter過濾器的基本使用,下面我們將學習Filter過濾器在使用過程中的一些細節。

2.4.2 Filter詳解

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

  1. 過濾器的執行流程
  2. 過濾器的攔截路徑配置
  3. 過濾器鏈
2.4.2.1 執行流程

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

在這里插入圖片描述

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

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

@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {@Override //初始化方法, 只調用一次public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init 初始化方法執行了");}@Overridepublic 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 destroy() {System.out.println("destroy 銷毀方法執行了");}
}

在這里插入圖片描述

2.4.2.2 攔截路徑

執行流程我們搞清楚之后,接下來再來介紹一下過濾器的攔截路徑,Filter可以根據需求,配置不同的攔截資源路徑:

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

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

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

測試1:訪問部門管理請求,發現過濾器沒有攔截請求

在這里插入圖片描述

在這里插入圖片描述

測試2:訪問登錄請求/login,發現過濾器攔截請求

在這里插入圖片描述

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

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

測試1:訪問部門管理請求,發現過濾器攔截了請求

在這里插入圖片描述

測試2:訪問登錄請求/login,發現過濾器沒有攔截請求

在這里插入圖片描述

2.4.2.3 過濾器鏈

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

在這里插入圖片描述

比如:在我們web服務器當中,定義了兩個過濾器,這兩個過濾器就形成了一個過濾器鏈。

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

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

先要執行過濾器2放行之后的邏輯,再來執行過濾器1放行之后的邏輯,最后在給瀏覽器響應數據。

以上就是當我們在web應用當中配置了多個過濾器,形成了這樣一個過濾器鏈以及過濾器鏈的執行順序。下面我們通過idea來驗證下過濾器鏈。

驗證步驟:

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

在這里插入圖片描述

AbcFilter過濾器

@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {@Overridepublic 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 {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("DemoFilter   放行前邏輯.....");//放行請求filterChain.doFilter(servletRequest,servletResponse);System.out.println("DemoFilter   放行后邏輯.....");}
}

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

在這里插入圖片描述

通過控制臺日志的輸出,大家發現AbcFilter先執行DemoFilter后執行,這是為什么呢?

其實是和過濾器的類名有關系。以注解方式配置的Filter過濾器,它的執行優先級是按時過濾器類名的自動排序確定的,類名排名越靠前,優先級越高。

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

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

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

在這里插入圖片描述

到此,關于過濾器的使用細節,我們已經全部介紹完畢了。

在這里插入圖片描述

2.4.3 登錄校驗-Filter
2.4.3.1 分析

過濾器Filter的快速入門以及使用細節我們已經介紹完了,接下來最后一步,我們需要使用過濾器Filter來完成案例當中的登錄校驗功能。

在這里插入圖片描述

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

  • 要進入到后臺管理系統,我們必須先完成登錄操作,此時就需要訪問登錄接口login。

  • 登錄成功之后,我們會在服務端生成一個JWT令牌,并且把JWT令牌返回給前端,前端會將JWT令牌存儲下來。

  • 在后續的每一次請求當中,都會將JWT令牌攜帶到服務端,請求到達服務端之后,要想去訪問對應的業務功能,此時我們必須先要校驗令牌的有效性。

  • 對于校驗令牌的這一塊操作,我們使用登錄校驗的過濾器,在過濾器當中來校驗令牌的有效性。如果令牌是無效的,就響應一個錯誤的信息,也不會再去放行訪問對應的資源了。如果令牌存在,并且它是有效的,此時就會放行去訪問對應的web資源,執行相應的業務操作。

大概清楚了在Filter過濾器的實現步驟了,那在正式開發登錄校驗過濾器之前,我們思考兩個問題:

  1. 所有的請求,攔截到了之后,都需要校驗令牌嗎?

    • 答案:登錄請求例外
  2. 攔截到請求后,什么情況下才可以放行,執行業務操作?

    • 答案:有令牌,且令牌校驗通過(合法);否則都返回未登錄錯誤結果
2.4.3.2 具體流程

我們要完成登錄校驗,主要是利用Filter過濾器實現,而Filter過濾器的流程步驟:

在這里插入圖片描述

基于上面的業務流程,我們分析出具體的操作步驟:

  1. 獲取請求url
  2. 判斷請求url中是否包含login,如果包含,說明是登錄操作,放行
  3. 獲取請求頭中的令牌(token)
  4. 判斷令牌是否存在,如果不存在,返回錯誤結果(未登錄)
  5. 解析token,如果解析失敗,返回錯誤結果(未登錄)
  6. 放行
2.4.3.3 代碼實現

分析清楚了以上的問題后,我們就參照接口文檔來開發登錄功能了,登錄接口描述如下:

  • 基本信息

    請求路徑:/login請求方式:POST接口描述:該接口用于員工登錄Tlias智能學習輔助系統,登錄完畢后,系統下發JWT令牌。 
    
  • 請求參數

    參數格式:application/json

    參數說明:

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

    請求數據樣例:

    {"username": "jinyong","password": "123456"
    }
    
  • 響應數據

    參數格式:application/json

    參數說明:

    名稱類型是否必須默認值備注其他信息
    codenumber必須響應碼, 1 成功 ; 0 失敗
    msgstring非必須提示信息
    datastring必須返回的數據 , jwt令牌

    響應數據樣例:

    {"code": 1,"msg": "success","data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
    }
    
  • 備注說明

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

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

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

登錄校驗過濾器:LoginCheckFilter

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

在上述過濾器的功能實現中,我們使用到了一個第三方json處理的工具包fastjson。我們要想使用,需要引入如下依賴:

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

登錄校驗的過濾器我們編寫完成了,接下來我們就可以重新啟動服務來做一個測試:

測試前先把之前所編寫的測試使用的過濾器,暫時注釋掉。直接將@WebFilter注解給注釋掉即可。

  • 測試1:未登錄是否可以訪問部門管理頁面

    首先關閉瀏覽器,重新打開瀏覽器,在地址欄中輸入:http://localhost:9528/#/system/dept

    由于用戶沒有登錄,登錄校驗過濾器返回錯誤信息,前端頁面根據返回的錯誤信息結果,自動跳轉到登錄頁面了

  • 測試2:先進行登錄操作,再訪問部門管理頁面

    登錄校驗成功之后,可以正常訪問相關業務操作頁面

    在這里插入圖片描述

2.5 攔截器Interceptor

學習完了過濾器Filter之后,接下來我們繼續學習攔截器Interseptor。

攔截器我們主要分為三個方面進行講解:

  1. 介紹下什么是攔截器,并通過快速入門程序上手攔截器
  2. 攔截器的使用細節
  3. 通過攔截器Interceptor完成登錄校驗功能

我們先學習第一塊內容:攔截器快速入門

2.5.1 快速入門

什么是攔截器?

  • 是一種動態攔截方法調用的機制,類似于過濾器。
  • 攔截器是Spring框架中提供的,用來動態攔截控制器方法的執行。

攔截器的作用:

  • 攔截請求,在指定方法調用前后,根據業務需要執行預先設定的代碼。

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

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

  1. 定義攔截器

  2. 注冊配置攔截器

**自定義攔截器:**實現HandlerInterceptor接口,并重寫其所有方法

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

注意:

? preHandle方法:目標資源方法執行前執行。 返回true:放行 返回false:不放行

? postHandle方法:目標資源方法執行后執行

? afterCompletion方法:視圖渲染完畢后執行,最后執行

注冊配置攔截器:實現WebMvcConfigurer接口,并重寫addInterceptors方法

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

重新啟動SpringBoot服務,打開postman測試:

在這里插入圖片描述

在這里插入圖片描述

接下來我們再來做一個測試:將攔截器中返回值改為false

使用postman,再次點擊send發送請求后,沒有響應數據,說明請求被攔截了沒有放行

在這里插入圖片描述

2.5.2 Interceptor詳解

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

  1. 攔截器的攔截路徑配置
  2. 攔截器的執行流程
2.5.2.1 攔截路徑

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

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

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

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

攔截路徑含義舉例
/*一級路徑能匹配/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

下面主要來演示下/**/*的區別:

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

使用postman測試:http://localhost:8080/emps/1

在這里插入圖片描述

控制臺沒有輸出攔截器中的日志信息,說明/*沒有匹配到攔截路徑/emp/1

在這里插入圖片描述

2.5.2.2 執行流程

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

在這里插入圖片描述

  • 當我們打開瀏覽器來訪問部署在web服務器當中的web應用時,此時我們所定義的過濾器會攔截到這次請求。攔截到這次請求之后,它會先執行放行前的邏輯,然后再執行放行操作。而由于我們當前是基于springboot開發的,所以放行之后是進入到了spring的環境當中,也就是要來訪問我們所定義的controller當中的接口方法。

  • Tomcat并不識別所編寫的Controller程序,但是它識別Servlet程序,所以在Spring的Web環境中提供了一個非常核心的Servlet:DispatcherServlet(前端控制器),所有請求都會先進行到DispatcherServlet,再將請求轉給Controller。

  • 當我們定義了攔截器后,會在執行Controller的方法之前,請求被攔截器攔截住。執行preHandle()方法,這個方法執行完成后需要返回一個布爾類型的值,如果返回true,就表示放行本次操作,才會繼續訪問controller中的方法;如果返回false,則不會放行(controller中的方法也不會執行)。

  • 在controller當中的方法執行完畢之后,再回過來執行postHandle()這個方法以及afterCompletion() 方法,然后再返回給DispatcherServlet,最終再來執行過濾器當中放行后的這一部分邏輯的邏輯。執行完畢之后,最終給瀏覽器響應數據。

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

  • 開啟LoginCheckInterceptor攔截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle .... ");return true; //true表示放行}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle ... ");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion .... ");}
}
@Configuration  
public class WebConfig implements WebMvcConfigurer {//攔截器對象@Autowiredprivate LoginCheckInterceptor loginCheckInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注冊自定義攔截器對象registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**")//攔截所有請求.excludePathPatterns("/login");//不攔截登錄請求}
}
  • 開啟DemoFilter過濾器
@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("DemoFilter   放行前邏輯.....");//放行請求filterChain.doFilter(servletRequest,servletResponse);System.out.println("DemoFilter   放行后邏輯.....");}
}

重啟SpringBoot服務后,清空日志,打開Postman,測試查詢部門:

在這里插入圖片描述

在這里插入圖片描述

以上就是攔截器的執行流程。通過執行流程分析,大家應該已經清楚了過濾器和攔截器之間的區別,其實它們之間的區別主要是兩點:

  • 接口規范不同:過濾器需要實現Filter接口,而攔截器需要實現HandlerInterceptor接口。
  • 攔截范圍不同:過濾器Filter會攔截所有的資源,而Interceptor只會攔截Spring環境中的資源。
2.5.3 登錄校驗- Interceptor

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

登錄校驗的業務邏輯以及操作步驟我們前面已經分析過了,和登錄校驗Filter過濾器當中的邏輯是完全一致的。現在我們只需要把這個技術方案由原來的過濾器換成攔截器interceptor就可以了。

登錄校驗攔截器

//自定義攔截器
@Component //當前攔截器對象由Spring創建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {//前置方式@Overridepublic 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.判斷令牌是否存在,如果不存在,返回錯誤結果(未登錄)if(!StringUtils.hasLength(token)){log.info("Token不存在");//創建響應結果對象Result responseResult = Result.error("NOT_LOGIN");//把Result對象轉換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實現對象和json的轉換工具類)String json = JSONObject.toJSONString(responseResult);//設置響應頭(告知瀏覽器:響應的數據類型為json、響應的數據編碼表為utf-8)response.setContentType("application/json;charset=utf-8");//響應response.getWriter().write(json);return false;//不放行}//5.解析token,如果解析失敗,返回錯誤結果(未登錄)try {JwtUtils.parseJWT(token);}catch (Exception e){log.info("令牌解析失敗!");//創建響應結果對象Result responseResult = Result.error("NOT_LOGIN");//把Result對象轉換為JSON格式字符串 (fastjson是阿里巴巴提供的用于實現對象和json的轉換工具類)String json = JSONObject.toJSONString(responseResult);//設置響應頭response.setContentType("application/json;charset=utf-8");//響應response.getWriter().write(json);return false;}//6.放行return true;}

注冊配置攔截器

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

登錄校驗的攔截器編寫完成后,接下來我們就可以重新啟動服務來做一個測試: (關閉登錄校驗Filter過濾器

  • 測試1:未登錄是否可以訪問部門管理頁面

    首先關閉瀏覽器,重新打開瀏覽器,在地址欄中輸入:http://localhost:9528/#/system/dept

    由于用戶沒有登錄,校驗機制返回錯誤信息,前端頁面根據返回的錯誤信息結果,自動跳轉到登錄頁面了

    在這里插入圖片描述

  • 測試2:先進行登錄操作,再訪問部門管理頁面

    登錄校驗成功之后,可以正常訪問相關業務操作頁面

    在這里插入圖片描述

到此我們也就驗證了所開發的登錄校驗的攔截器也是沒問題的。登錄校驗的過濾器和攔截器,我們只需要使用其中的一種就可以了。

3. 異常處理

3.1 當前問題

登錄功能和登錄校驗功能我們都實現了,下面我們學習下今天最后一塊技術點:異常處理。首先我們先來看一下系統出現異常之后會發生什么現象,再來介紹異常處理的方案。

我們打開瀏覽器,訪問系統中的新增部門操作,系統中已經有了 “就業部” 這個部門,我們再來增加一個就業部,看看會發生什么現象。

在這里插入圖片描述

點擊確定之后,窗口關閉了,頁面沒有任何反應,就業部也沒有添加上。 而此時,大家會發現,網絡請求報錯了。

在這里插入圖片描述

狀態碼為500,表示服務器端異常,我們打開idea,來看一下,服務器端出了什么問題。

在這里插入圖片描述

上述錯誤信息的含義是,dept部門表的name字段的值 就業部 重復了,因為在數據庫表dept中已經有了就業部,我們之前設計這張表時,為name字段建議了唯一約束,所以該字段的值是不能重復的。

而當我們再添加就業部,這個部門時,就違反了唯一約束,此時就會報錯。

我們來看一下出現異常之后,最終服務端給前端響應回來的數據長什么樣。
在這里插入圖片描述

響應回來的數據是一個JSON格式的數據。但這種JSON格式的數據還是我們開發規范當中所提到的統一響應結果Result嗎?顯然并不是。由于返回的數據不符合開發規范,所以前端并不能解析出響應的JSON數據。

接下來我們需要思考的是出現異常之后,當前案例項目的異常是怎么處理的?

  • 答案:沒有做任何的異常處理

在這里插入圖片描述

當我們沒有做任何的異常處理時,我們三層架構處理異常的方案:

  • Mapper接口在操作數據庫的時候出錯了,此時異常會往上拋(誰調用Mapper就拋給誰),會拋給service。
  • service 中也存在異常了,會拋給controller。
  • 而在controller當中,我們也沒有做任何的異常處理,所以最終異常會再往上拋。最終拋給框架之后,框架就會返回一個JSON格式的數據,里面封裝的就是錯誤的信息,但是框架返回的JSON格式的數據并不符合我們的開發規范。

3.2 解決方案

那么在三層構架項目中,出現了異常,該如何處理?

  • 方案一:在所有Controller的所有方法中進行try…catch處理
    • 缺點:代碼臃腫(不推薦)
  • 方案二:全局異常處理器
    • 好處:簡單、優雅(推薦)

在這里插入圖片描述

3.3 全局異常處理器

我們該怎么樣定義全局異常處理器?

  • 定義全局異常處理器非常簡單,就是定義一個類,在類上加上一個注解@RestControllerAdvice,加上這個注解就代表我們定義了一個全局異常處理器。
  • 在全局異常處理器當中,需要定義一個方法來捕獲異常,在這個方法上需要加上注解@ExceptionHandler。通過@ExceptionHandler注解當中的value屬性來指定我們要捕獲的是哪一類型的異常。
@RestControllerAdvice
public class GlobalExceptionHandler {//處理異常@ExceptionHandler(Exception.class) //指定能夠處理的異常類型public Result ex(Exception e){e.printStackTrace();//打印堆棧中的異常信息//捕獲到異常之后,響應一個標準的Resultreturn Result.error("對不起,操作失敗,請聯系管理員");}
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

處理異常的方法返回值會轉換為json后再響應給前端

重新啟動SpringBoot服務,打開瀏覽器,再來測試一下添加部門這個操作,我們依然添加已存在的 “就業部” 這個部門:

在這里插入圖片描述

在這里插入圖片描述

此時,我們可以看到,出現異常之后,異常已經被全局異常處理器捕獲了。然后返回的錯誤信息,被前端程序正常解析,然后提示出了對應的錯誤提示信息。

以上就是全局異常處理器的使用,主要涉及到兩個注解:

  • @RestControllerAdvice //表示當前類為全局異常處理器
  • @ExceptionHandler //指定可以捕獲哪種類型的異常進行處理

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/717812.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/717812.shtml
英文地址,請注明出處:http://en.pswp.cn/news/717812.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

程序員眼中的“祖傳代碼”

引言 在IT界&#xff0c;特別是在Java項目中&#xff0c;“祖傳代碼”通常指的是那些經過長時間積累、由多位開發者共同維護、且蘊含深厚技術沉淀的代碼片段或模塊。這些代碼可能存在于項目的核心模塊&#xff0c;也可能是一些輔助性的工具類。它們承載著項目的歷史&#xff0…

Matlab 多項式插值(曲線擬合)

文章目錄 一、簡介二、實現代碼三、實現效果參考資料一、簡介 由于對曲線擬合有些興趣,這里就找了一些資料從最基本的方法來看一下曲線擬合的效果: 二、實現代碼 % **********

Vue.js中的路由導航守衛和其使用方法

Vue.js 中的路由導航守衛是 Vue Router 提供的一套機制&#xff0c;用于在路由切換的過程中執行自定義代碼邏輯&#xff0c;包括但不限于權限驗證、頁面滾動位置保存、加載數據等。它分為三種類型&#xff1a; 全局前置守衛 (Global beforeEach Guard) 全局前置守衛應用在整個…

python科學計算庫之Numpy庫的使用的簡單習題

Numpy庫 Numpy&#xff08;Numerical Python的縮寫&#xff09;是一個開源的Python庫&#xff0c;用于進行科學計算。它提供了一個高性能的多維數組對象&#xff08;ndarray&#xff09;及用于處理這些數組的各種工具和函數。由于其高效和靈活的數據結構以及豐富的功能&#x…

Google 地圖 API 教程--干貨(1/2)

Google Maps API 教程 在本教程中我們將學習如何使用谷歌地圖API V3創建交互式地圖。 什么是 API? API = 應用程序編程接口(Application programming interface)。 API(Application Programming Interface,應用編程接口)其實就是操作系統留給應用程序的一個調用接口,…

【d34】【Java】【力扣】27. 移除元素

題目 給你一個數組 nums 和一個值 val&#xff0c;你需要 原地 移除所有數值等于 val 的元素&#xff0c;并返回移除后數組的新長度。 不要使用額外的數組空間&#xff0c;你必須僅使用 O(1) 額外空間并 原地 修改輸入數組。 元素的順序可以改變。你不需要考慮數組中超出新長…

案例介紹:汽車售后服務網絡構建與信息抽取技術應用(開源)

一、引言 在當今競爭激烈的汽車行業中&#xff0c;售后服務的質量已成為品牌成功的關鍵因素之一。作為一位經驗豐富的項目經理&#xff0c;我曾參與構建一個全面的汽車售后服務網絡&#xff0c;旨在為客戶提供無縫的維修、保養和配件更換服務。這個項目的核心目標是通過高效的…

spring、springmvc、springboot框架的介紹

前言 我們已經學過Spring&#xff0c;SpringMVC&#xff0c;SpringBoot了&#xff0c;那這三者之間有沒有聯系或者區別呢&#xff1f; spring是一個一站式的輕量級java開發的框架&#xff0c;那我們剛開始使用spring的時候&#xff0c;是需要配置很多的配置文件以及繁瑣的過程…

狀態機實現雙擊、短按、長按等按鍵識別檢測算法

1、按鍵識別算法的作用 按鍵識別算法在不同的技術和應用背景下有不同的作用&#xff0c;但其核心目標都是準確、可靠地檢測和區分用戶通過物理或虛擬按鍵所執行的操作。按鍵識別算法在各類電子設備及系統中起到至關重要的作用&#xff0c;它確保了人機交互的有效性和準確性&…

Vue前端+快速入門【詳解】

目錄 1.Vue概述 2. 快速入門 3. Vue指令 4.表格信息案例 5. 生命周期 1.Vue概述 1.MVVM思想 原始HTMLCSSJavaScript開發存在的問題&#xff1a;操作麻煩&#xff0c;耦合性強 為了實現html標簽與數據的解耦&#xff0c;前端開發中提供了MVVM思想&#xff1a;即Model-Vi…

Mysql-主從架構篇(一主多從,半同步案例搭建)

主從架構 主從架構有什么用&#xff1f; 通過搭建MySQL主從集群&#xff0c;可以緩解MySQL的數據存儲以及訪問的壓力。 數據安全&#xff08;主備&#xff09;&#xff1a;給主服務增加一個數據備份。基于這個目的&#xff0c;可以搭建主從架構&#xff0c;或者也可以基于主…

GO語言學習筆記(與Java的比較學習)(九)

讀寫數據 讀取用戶的輸入 最簡單的辦法是使用 fmt 包提供的 Scan 和 Sscan 開頭的函數。 Scanln 掃描來自標準輸入的文本&#xff0c;將空格分隔的值依次存放到后續的參數內&#xff0c;直到碰到換行。Scanf 與其類似&#xff0c;除了 Scanf 的第一個參數用作格式字符串&…

大數據開發(Java面試真題-卷三)

大數據開發&#xff08;Java面試真題&#xff09; 1、簡要介紹以下JVM有幾種垃圾收集器&#xff1f;2、Java中Synchronized的底層原理是什么&#xff1f;3、Java String為什么是不可變的&#xff1f;為什么要設計成不可變&#xff1f;4、泛型&#xff1f;5、常用的反射方法&…

深入Java日志框架及其最佳實踐

概述 在Java應用開發中&#xff0c;日志框架是確保應用穩定性和可觀察性的關鍵組件。它幫助開發者記錄應用的行為、診斷問題&#xff0c;并監控系統的健康狀況。隨著Java生態系統的不斷發展&#xff0c;各種日志框架也應運而生&#xff0c;各有特點和優勢。本文將詳細探討幾個…

redis進階(一)

文章目錄 前言一、Redis中的對象的結構體如下&#xff1a;二、壓縮鏈表三、跳躍表 前言 Redis是一種key/value型數據庫&#xff0c;其中&#xff0c;每個key和value都是使用對象表示的。 一、Redis中的對象的結構體如下&#xff1a; /** Redis 對象*/ typedef struct redisO…

c# .net8 香橙派orangepi + hc-04藍牙 實例

這些使用c# .net8開發&#xff0c;硬件 香橙派 orangepi 3lts和 hc-04藍牙 使用場景&#xff1a;可以通過這個功能&#xff0c;手機連接orangepi進行wifi等參數配置 硬件&#xff1a; 1、帶USB口的linux開發板orangepi 2、USB 轉TTL 中轉接藍牙&#xff08;HC-04) 某寶上買…

Vue的響應式原理是如何實現的

Vue的響應式原理主要**基于JavaScript的Object.defineProperty方法實現**。具體如下&#xff1a; 1. 數據劫持&#xff08;Data Hijacking&#xff09; Vue在初始化時&#xff0c;會遍歷data對象中的所有屬性&#xff0c;并使用Object.defineProperty將這些屬性轉換為getter/s…

Flink:Temporal Table Function(時態表函數)和 Temporal Join

博主歷時三年精心創作的《大數據平臺架構與原型實現&#xff1a;數據中臺建設實戰》一書現已由知名IT圖書品牌電子工業出版社博文視點出版發行&#xff0c;點擊《重磅推薦&#xff1a;建大數據平臺太難了&#xff01;給我發個工程原型吧&#xff01;》了解圖書詳情&#xff0c;…

AR時間序列模型

AR時間序列模型&#xff08;AutoRegressive Time Series Model&#xff09;是一種用于分析和預測時間序列數據的統計模型。該模型假設未來的觀測值與過去的觀測值相關&#xff0c;且該相關性可以通過線性回歸來描述。 AR模型的基本思想是將當前時刻的觀測值表示為過去幾個時刻…

設計模式(十五)狀態模式

請直接看原文:設計模式系列 ------------------------------------------------------------------------------------------------------------------------------- 前言 建議在閱讀本文前先閱讀設計模式&#xff08;十一&#xff09;策略模式這篇文章&#xff0c;雖說狀態…