📢 本文由 gemini-2.5-flash 翻譯
會話技術
會話:使用者開啟瀏覽器,造訪網頁伺服器的資源,會話建立,直到有一方中斷連線,會話結束。在一次會話中可以包含多次請求與回應。
會話追蹤:一種維護瀏覽器狀態的方法,伺服器需要辨識多次請求是否來自同一個瀏覽器,以便在同一次會話的多個請求之間共享資料。
會話追蹤方案:
- 用戶端會話追蹤技術:Cookie
- 伺服器端會話追蹤技術:Session
- 權杖技術
Cookie
Cookie 是 HTTP 協定支援的技術,在瀏覽器第一次造訪時,伺服器端請求標頭中設定 Cookie
Set-Cookie: your_cookie,瀏覽器會自動將 Cookie 儲存在本機,並在下次造訪時自動在請求標頭中加入 Cookie Cookie: your_cookie。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @RestController
public class SessionController {
// 設定 Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_name", "yexca"));
return Result.success();
}
// 取得 Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie c : cookies){
if(c.getName().equals("login_name")){
System.out.println("login_name:"+c.getValue());
}
}
return Result.success();
}
}
|
不過行動裝置不能使用 Cookie,並且 Cookie 無法跨網域。
相同網域:相同協定、IP/網域名稱、連接埠
Session
基於 Cookie 實作,在瀏覽器第一次請求時產生一個 Session,然後在回應標頭傳回 Session 的 ID Set-Cookie: JSESSIONID=session_id,然後瀏覽器在下次請求時自動帶上該 ID。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @RestController
public class SessionController {
// 將值儲存到 HttpSession 中
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession_set:{}", session.hashCode());
session.setAttribute("login_user","yexca");
return Result.success();
}
// 從 HttpSession 中取得值
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession_get:{}", session.hashCode());
Object loginUser = session.getAttribute("login_user");
log.info("loginUser:{}", loginUser);
return Result.success();
}
}
|
Session 的問題在於,如果伺服器端使用負載平衡,亦即有多台伺服器端,如果第一次回應的是伺服器一,而第二次回應的是伺服器二,由於伺服器二沒有 Session,所以無法使用。
JWT 權杖
權杖技術支援 PC 端、行動裝置,可以解決叢集環境下的身分驗證問題,減輕伺服器端儲存壓力,但需要自行實作。
JWT 全稱 JSON Web Token,官方網站:
https://jwt.io/
,定義了一種簡潔、自包含的格式,用於在通訊雙方以 JSON 資料格式安全地傳輸資訊。由於數位簽章的存在,這些資訊是可靠的。
組成:
- 第一部分:標頭(Header),記錄權杖類型、簽章演算法等
- 第二部分:酬載(Payload,有效載荷),記錄一些自訂資訊、預設資訊等
- 第三部分:簽章(Signature),防止權杖被篡改、確保安全性。將標頭(Header)、酬載(Payload)和指定金鑰,透過指定簽章演算法計算而來。
引入依賴項
1
2
3
4
5
| <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
|
產生與解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| class WebApplicationTests {
// JWT 產生
@Test
public void jwtGenTest(){
Map<String,Object> claims = new HashMap<>();
claims.put("id", "233");
claims.put("user", "yexca");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "yexca") // 簽章演算法
.setClaims(claims) //自訂內容(酬載)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 有效期為 1 小時
.compact();
System.out.println(jwt);
}
// 輸出
// eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU
// JWT 解析
@Test
public void jwtParseTest(){
Claims claims = Jwts.parser()
.setSigningKey("yexca") // 金鑰
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU")
.getBody(); // 取得第二部分
System.out.println(claims);
}
// 輸出
// {id=233, exp=1702797088, user=yexca}
}
|
登入驗證
過濾器 (Filter)
過濾器是早期 JavaWeb 三大元件 (Servlet, Filter, Listener) 之一。過濾器可以將對資源的請求攔截下來,進而實現一些特殊的功能,例如登入驗證、統一編碼處理、敏感字元處理等。
快速入門
建立過濾器類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @WebFilter(urlPatterns = "/*") // 攔截路徑
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("過濾器初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("過濾器攔截");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
// Filter.super.destroy();
System.out.println("過濾器銷毀");
}
}
|
在 Application 上加上註解
1
2
| @ServletComponentScan // 開啟了對 servlet 元件的支援
@SpringBootApplication
|
攔截路徑
可以根據需求調整,以下為範例
| 攔截類型 | urlPatterns | 涵義 |
|---|
| 攔截特定路徑 | /login | 只有造訪 /login 才會被攔截 |
| 目錄攔截 | /emps/* | 造訪 /emps 下所有資源都會被攔截,/emps 也會被攔截 |
| 攔截所有 | /* | 造訪所有資源都會被攔截 |
單個過濾器執行邏輯
瀏覽器發送請求 -> 請求被攔截 -> 執行放行前邏輯 -> 放行 -> 執行放行後邏輯 -> 瀏覽器收到回應
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| @WebFilter(urlPatterns = "/*") // 攔截路徑
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("過濾器初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("過濾器攔截,放行前邏輯");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("過濾器攔截,放行後邏輯");
}
@Override
public void destroy() {
// Filter.super.destroy();
System.out.println("過濾器銷毀");
}
}
|
過濾器鏈
一個網頁應用程式中,可以設定多個過濾器,這些過濾器就形成了一個過濾器鏈。
順序:註解設定的過濾器優先級是透過過濾器類別名稱的自然排序。
邏輯:瀏覽器發送請求 -> 請求被 A 攔截 -> 執行 A 放行前邏輯 -> A 放行 -> 請求被 B 攔截 -> 執行 B 放行前邏輯 -> B 放行 -> 執行 B 放行後邏輯 -> 執行 A 放行後邏輯 -> 瀏覽器收到回應
登入驗證
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| @Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 取得請求 URL
String requestURI = request.getRequestURI();
log.info("請求 URL:{}",requestURI);
// 判斷是否有login
if(requestURI.contains("login")){
log.info("登入操作,放行");
filterChain.doFilter(servletRequest, servletResponse);
return; // 因為登入操作不需要以下邏輯
}
// 非登入操作,取得權杖
String jwt = request.getHeader("token");
// 判斷權杖是否有效
if(!StringUtils.hasLength(jwt)){
log.info("請求標頭 token 為空");
// 將物件轉換為 JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return;
}
// 驗證權杖
try {
JwtUtils.parseJWT(jwt);
}catch (Exception e){
e.printStackTrace();
log.info("解析權杖失敗");
// 將物件轉換為 JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return;
}
// 放行
log.info("權杖合法,放行");
filterChain.doFilter(servletRequest,servletResponse);
}
}
|
攔截器 (Interceptor)
快速入門
建立攔截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目標方法執行前執行,傳回 true 放行,false 不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// return HandlerInterceptor.super.preHandle(request, response, handler);
System.out.println("preHandle");
return true;
}
@Override // 目標方法執行後執行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("postHandle");
}
@Override // 檢視彩現完成後執行,最後執行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion");
}
}
|
建立設定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Configuration // 設定類別
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
|
攔截路徑
| 路徑 | 涵義 | 例子 |
|---|
| /* | 一層路徑 | /emps, /login 不能匹配 /emps/1 |
| /** | 任意層級路徑 | /emps, /emps/1, /emps/1/2 |
| /emps/* | /emps 下的一層路徑 | /emps/1 不能匹配 /emps, /emps/1/2 |
| /emps/** | /emps 下的任意層級路徑 | /emps, /emps/1, /emps/1/2 |
攔截流程
如果同時存在過濾器和攔截器
瀏覽器造訪 -> 過濾器放行前邏輯 -> 過濾器放行 -> DispatcherServlet -> Interceptor preHandle -> Controller -> postHandle -> afterCompletion -> DispatcherServlet -> 過濾器放行後邏輯 -> 回應瀏覽器
過濾器會攔截所有的請求,而攔截器只會攔截 Spring 環境中的資源。
登入驗證
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| @Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目標方法執行前執行,傳回 true 放行,false 不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// return HandlerInterceptor.super.preHandle(request, response, handler);
System.out.println("preHandle");
// 取得請求 URL
String requestURI = request.getRequestURI();
log.info("請求 URL:{}",requestURI);
// 判斷是否有login
if(requestURI.contains("login")){
log.info("登入操作,放行");
return true;
}
// 非登入操作,取得權杖
String jwt = request.getHeader("token");
// 判斷權杖是否有效
if(!StringUtils.hasLength(jwt)){
log.info("請求標頭 token 為空");
// 將物件轉換為 JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
// 驗證權杖
try {
JwtUtils.parseJWT(jwt);
}catch (Exception e){
e.printStackTrace();
log.info("解析權杖失敗");
// 將物件轉換為 JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
// 放行
log.info("權杖合法,放行");
return true;
}
@Override // 目標方法執行後執行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("postHandle");
}
@Override // 檢視彩現完成後執行,最後執行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion");
}
}
|