---
# System prepended metadata

title: spring  Security 1

---

# 1. 基礎概念入門
## 1. HTTP 與 TLS
1. 理論
    1. HTTP 是「無狀態」的，伺服器不會自動記住用戶。
    2. HTTPS 透過 TLS 加密，避免竊聽與中間人攻擊。
    3. HSTS 強制瀏覽器只能走 HTTPS。
2. 實作:在 Nginx 加上 HSTS：
```
//nginx 

server {
    listen 443 ssl;
    server_name example.com;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
}
```
3. 檢查:用瀏覽器開發者工具（Network → Headers）確認 Strict-Transport-Security 是否存在。
## 2. Cookie 與 Session
1. 理論
    1. Cookie 屬性：
        1. Secure：僅限 HTTPS
        2. HttpOnly：JS 無法存取
        3.SameSite：防止 CSRF
    2. Session 管理：
        1. 登入成功後應 重新產生 Session ID
        2. 設定閒置逾時（如 30 分鐘）
2. 實作:LoginServlet.java
```java=
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        if ("user".equals(username) && "1234".equals(password)) {
            // 登入成功 → 清掉舊 session，建立新 session
            HttpSession oldSession = req.getSession(false);
            if (oldSession != null) oldSession.invalidate();

            HttpSession newSession = req.getSession(true);
            newSession.setMaxInactiveInterval(600); // 10 分鐘
            newSession.setAttribute("username", username);

            resp.sendRedirect("welcome.jsp");
        } else {
            resp.sendRedirect("login.jsp?error=true");
        }
    }
}
```
3. web.xml
```xml=
//xml

<web-app>
  <session-config>
    <session-timeout>30</session-timeout> <!-- 30 分鐘 -->
  </session-config>
</web-app>
```
4. 測試
    1. 部署到 Tomcat，訪問 http://localhost:8080/login.jsp
    2. 登入成功 → 10 分鐘內閒置會自動登出
## 3. 認證 vs 授權
1. 理論
    1. 認證 Authentication：你是誰？
    2. 授權 Authorization：你能做什麼？
    3. 權限模型：
        1. RBAC（角色基礎）
        2. ABAC（屬性基礎，例如地點、時間）
        3. Scopes/Claims（Token 中聲明）
2. 實作（檢查登入狀態）
```java=
@WebServlet("/admin")
public class AdminServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        HttpSession session = req.getSession(false);
        if (session == null || session.getAttribute("username") == null) {
            resp.sendRedirect("login.jsp");
        } else {
            resp.getWriter().println("Welcome, Admin!");
        }
    }
}
```
## 4. 密碼學與密碼儲存
1. 理論
    1. 千萬不要存明碼
    2. 使用 bcrypt/argon2 這類演算法
    3. 每個帳號應有獨立 Salt
2. 實作:BCryptDemo.java
```java=
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BCryptDemo {
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); // cost=10
        String rawPassword = "123456";
        String hashed = encoder.encode(rawPassword);

        System.out.println("Raw: " + rawPassword);
        System.out.println("Hashed: " + hashed);
        System.out.println("Match: " + encoder.matches("123456", hashed));
    }
}
```
3. 執行
```
//bash

mvn -q exec:java -Dexec.mainClass="BCryptDemo"
```
## 5. Token 世界觀
1. JWT 結構
```
//css

header.payload.signature
```
2. 範例程式碼:JwtExample.java
```java=
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;

public class JwtExample {
    public static void main(String[] args) {
        String key = "mySecretKey";
        String jwt = Jwts.builder()
                .setSubject("user1")
                .setIssuer("demo-app")
                .setExpiration(new Date(System.currentTimeMillis() + 60000))
                .signWith(SignatureAlgorithm.HS256, key)
                .compact();

        System.out.println("JWT: " + jwt);
    }
}
```
## 6. 瀏覽器安全：CSRF / XSS / CORS
1. CSRF 防護
    1. SameSite Cookie
    2. CSRF Token
2. XSS 防護
    1. 輸出前 HTML encode
    2. 使用 textContent 而非 innerHTML
3. CORS
    1. 僅允許指定來源
    2. 不要 * + Credentials
## 7. 多因子驗證（MFA）
範例：TOTP
```java=
// 用 Google Authenticator 類似的演算法產生一次性密碼
```
## 8. 章末檢核清單
1. HTTPS 全站強制
2. Cookie 有 Secure / HttpOnly / SameSite
3. Session 登入後會重生 ID
4. 密碼儲存使用 bcrypt
5. JWT 有效期設置合理
6. CSRF / XSS / CORS 防護到位
7. 日誌不記敏感資料
## 9. 練習
### 1. 概覽（5 件練習）
1. 修改 Session Timeout → 10 分鐘
2. 新增 /admin 頁面 → 需登入才能訪問
3. 將 JWT 演算法改為 RSA (RS256) 並示範 kid 金鑰輪替
4. 為表單加入 CSRF Token（伺服器端驗證）
5. 設定 CORS 只允許 http://localhost:3000
### 2. 前置說明 / 約定
1. 假設專案目錄結構與先前 starter-kit 相同（module 分別：02-cookie-session, 03-passwords, 04-jwt, 05-csrf-xss, 06-cors）。
2. 下面所有 Java 類都放在相對 package：com.example.*，放在 src/main/java 下。
3. 若你使用 Tomcat：把每個 module mvn package 的 war 丟到 Tomcat webapps/ 即可。沒有 Tomcat 的話我也給出 Jetty 直接跑的指令。
4. 測試我用 curl 範例（必要時會演示如何抓 CSRF token）。
### A. 02-cookie-session：Session timeout 10 分鐘、/admin、CSRF Token、登入流程（完整檔案）
把以下檔案放到 02-cookie-session 專案對應位置（覆寫原檔）。
#### 1. src/main/java/com/example/basics/LoginServlet.java
```java=
package com.example.basics;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;

@WebServlet(urlPatterns = {"/login", "/logout", "/me"})
public class LoginServlet extends HttpServlet {

    // 測試用硬編碼帳密（示範用，實務請改 DB + bcrypt）
    private static final String USER = "alice";
    private static final String PASS = "123456";

    // session idle timeout = 10 分鐘（秒）
    private static final int SESSION_TIMEOUT_SECONDS = 10 * 60;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 提供 /me 與 /logout 的 GET 介面
        resp.setCharacterEncoding("UTF-8");
        String path = req.getServletPath();
        if ("/logout".equals(path)) {
            HttpSession session = req.getSession(false);
            if (session != null) session.invalidate();
            resp.getWriter().println("👋 已登出");
            return;
        } else if ("/me".equals(path)) {
            HttpSession session = req.getSession(false);
            Object user = (session == null) ? null : session.getAttribute("user");
            if (user == null) {
                resp.setStatus(401);
                resp.getWriter().println("未登入");
            } else {
                resp.getWriter().println("目前使用者：" + user);
            }
            return;
        }

        // 其他 GET 不處理（login.jsp 直接作為靜態頁顯示）
        resp.setStatus(404);
        resp.getWriter().println("Not Found");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");

        // --- CSRF 驗證 ---
        HttpSession session = req.getSession(false);
        String sessionCsrf = (session == null) ? null : (String) session.getAttribute("csrfToken");
        String requestCsrf = req.getParameter("csrfToken");
        if (sessionCsrf == null || requestCsrf == null || !sessionCsrf.equals(requestCsrf)) {
            resp.setStatus(403);
            resp.getWriter().println("CSRF token invalid or missing");
            return;
        }
        // --- end CSRF ---

        String username = req.getParameter("username");
        String password = req.getParameter("password");

        if (USER.equals(username) && PASS.equals(password)) {
            // 防 Session Fixation：先 invalidate 舊 session，再建立新 session
            if (session != null) session.invalidate();
            HttpSession newSession = req.getSession(true);
            newSession.setAttribute("user", username);

            // 設定 idle timeout = 10 分鐘
            newSession.setMaxInactiveInterval(SESSION_TIMEOUT_SECONDS);

            // 重新產生 CSRF token（登入後可以更新）
            String newCsrf = UUID.randomUUID().toString();
            newSession.setAttribute("csrfToken", newCsrf);

            resp.getWriter().println("✅ 登入成功，Hi " + username + "（session timeout 10 分鐘）");
        } else {
            resp.setStatus(401);
            resp.getWriter().println("❌ 帳號或密碼錯誤");
        }
    }
}
```
說明（要點）
1. CSRF token 由 login.jsp / session 產生並放在 hidden 欄位，伺服器端在 doPost 驗證。
2. 登入成功前先驗 CSRF，通過才比對密碼 — 這可避免 CSRF 導致的未授權行為。
3. 登入時先 invalidate() 舊 session 再 getSession(true) 以防 Session Fixation。
4. setMaxInactiveInterval(SESSION_TIMEOUT_SECONDS) 設為 10 分鐘。
#### 2. src/main/java/com/example/basics/AdminServlet.java
```java=
package com.example.basics;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

@WebServlet("/admin")
public class AdminServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        HttpSession session = req.getSession(false);
        Object user = (session == null) ? null : session.getAttribute("user");
        if (user == null) {
            // 未登入 -> 導回 login 頁
            resp.sendRedirect("login.jsp");
            return;
        }
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().println("<html><body>");
        resp.getWriter().println("<h2>Admin area</h2>");
        resp.getWriter().println("<p>Welcome, " + user + "</p>");
        resp.getWriter().println("<a href='logout'>Logout</a>");
        resp.getWriter().println("</body></html>");
    }
}
```
說明:簡單的訪問控制：只檢查 session 中是否存在 user attribute。實務上你會再檢查角色/權限。
#### 3. src/main/webapp/login.jsp（含 CSRF token）
```jsp=
//jsp

<%@ page session="true" %>
<%
    // 取得或建立 CSRF token 存進 session
    String csrf = (String) session.getAttribute("csrfToken");
    if (csrf == null) {
        java.util.UUID uuid = java.util.UUID.randomUUID();
        csrf = uuid.toString();
        session.setAttribute("csrfToken", csrf);
    }
%>
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Login</title></head>
<body>
<form method="post" action="login">
    <div>帳號：<input type="text" name="username"></div>
    <div>密碼：<input type="password" name="password"></div>
    <input type="hidden" name="csrfToken" value="<%= csrf %>" />
    <button type="submit">登入</button>
</form>
<a href="me">查看目前使用者</a> |
<a href="admin">系統管理區</a>
</body>
</html>
```
說明:
1. JSP 在被 GET 時，會把 csrfToken 放到 session 並寫到表單 hidden 欄位。
2. 重要：session cookie 要在 production 加 Secure; HttpOnly; SameSite（可由容器或反向代理設定）。
#### 4. src/main/webapp/WEB-INF/web.xml（Session timeout 設 10 分鐘）
```
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <session-config>
        <session-timeout>10</session-timeout> <!-- 分鐘 -->
    </session-config>

</web-app>
```
#### Build & Run（02-cookie-session）
##### 方法 A — 用 Tomcat（推薦生產測試）
1. cd 02-cookie-session
2. mvn package（會產生 target/cookie-session.war）
3. 把 target/cookie-session.war 複製到 Tomcat 的 webapps/，啟動 Tomcat（或直接在 IDE 中 Run）。
##### 方法 B — 用 Jetty Maven plugin 快速測試
```
//bash

cd 02-cookie-session
mvn org.eclipse.jetty:jetty-maven-plugin:9.4.48.v20220622:run
# 服務會在 http://localhost:8080/（context 可能是 /, 視 pom 配置）
```
#### 測試（以 Tomcat context 假設為 /cookie-session）
##### 1. 測 CSRF protection（缺 token）
```
//bash

# 嘗試直接 POST（沒有 token） -> 應該得到 403
curl -i -c cookie.txt -d "username=alice&password=123456" http://localhost:8080/cookie-session/login
# 回應應包含 HTTP/1.1 403 並訊息 "CSRF token invalid or missing"
```
##### 2. 正確流程（先 GET 抓 csrf，再 POST 帶 cookie 與 token）
```
//bash

# 1) 先 GET login page 並保存 cookie
curl -s -c cookie.txt http://localhost:8080/cookie-session/login.jsp -o loginpage.html

# 2) 從 loginpage.html 解析 csrf token（Linux: grep+sed）
TOKEN=$(grep -oP 'name="csrfToken" value="\K[^"]+' loginpage.html)
echo "csrf: $TOKEN"

# 3) 使用 cookie 與 token POST 登入
curl -i -b cookie.txt -c cookie.txt -d "username=alice&password=123456&csrfToken=${TOKEN}" http://localhost:8080/cookie-session/login
# 回應應為登入成功訊息

# 4) 用 cookie 訪問 /admin（成功）
curl -i -b cookie.txt http://localhost:8080/cookie-session/admin
```
##### 3. 測 session timeout（10 分鐘）
登入後等待 >10 分鐘再訪問 /me 或 /admin，應失去 session（/me 回 401、/admin 跳回 login.jsp）。
##### 4. 注意事項（02-cookie-session）
1. 現在 CSRF token 存在 session；若在 scale-out 環境（多台 app servers），session 需要共用（Redis 等）。
2. 切勿把 csrfToken 放到 URL 或用 GET 攜帶敏感資訊。
3. Cookie Flags（Secure/HttpOnly/SameSite）應在容器或 reverse proxy 設置，不能只靠前端。
### B. 04-jwt：把 JWT 改為 RS256（RSA）並示範 kid 金鑰輪替
下面是完整的程式與金鑰產生指令。放到 04-jwt 模組。
#### 1. 產生 RSA 金鑰（在你的 shell 執行）
```
//bash

mkdir -p keys && cd keys

# 產生第一組私鑰/公鑰（rsa1）
openssl genpkey -algorithm RSA -out rsa1_private_pkcs8.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in rsa1_private_pkcs8.pem -pubout -out rsa1_public.pem

# 產生第二組私鑰/公鑰（rsa2，模擬新 key）
openssl genpkey -algorithm RSA -out rsa2_private_pkcs8.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in rsa2_private_pkcs8.pem -pubout -out rsa2_public.pem

# 回到專案根目錄
cd ..
```
我們用 PKCS#8 格式的 private key (-----BEGIN PRIVATE KEY-----)，方便 Java 直接解析。
#### 2. src/main/java/com/example/jwt/JwtRsaExample.java（完整）
```java=
package com.example.jwt;

import io.jsonwebtoken.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.*;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtRsaExample {

    private static RSAPrivateKey loadPrivateKey(String path) throws Exception {
        String pem = new String(Files.readAllBytes(Paths.get(path))).replaceAll("\\r|\\n", "");
        pem = pem.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "");
        byte[] der = Base64.getDecoder().decode(pem);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(der);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return (RSAPrivateKey) kf.generatePrivate(spec);
    }

    private static RSAPublicKey loadPublicKey(String path) throws Exception {
        String pem = new String(Files.readAllBytes(Paths.get(path))).replaceAll("\\r|\\n", "");
        pem = pem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
        byte[] der = Base64.getDecoder().decode(pem);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(der);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return (RSAPublicKey) kf.generatePublic(spec);
    }

    public static void main(String[] args) throws Exception {
        RSAPrivateKey priv1 = loadPrivateKey("keys/rsa1_private_pkcs8.pem");
        RSAPublicKey pub1 = loadPublicKey("keys/rsa1_public.pem");

        RSAPrivateKey priv2 = loadPrivateKey("keys/rsa2_private_pkcs8.pem");
        RSAPublicKey pub2 = loadPublicKey("keys/rsa2_public.pem");

        Map<String, RSAPublicKey> keyset = new HashMap<>();
        keyset.put("rsa1", pub1);
        keyset.put("rsa2", pub2);

        // 使用 rsa1 簽名 (kid = rsa1)
        String token1 = Jwts.builder()
                .setSubject("alice")
                .setIssuer("demo-app")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
                .setHeaderParam("kid", "rsa1")
                .signWith(priv1, SignatureAlgorithm.RS256)
                .compact();

        System.out.println("=== token1 (signed with rsa1) ===");
        System.out.println(token1);
        System.out.println();

        // 使用 rsa2 簽名 (kid = rsa2)
        String token2 = Jwts.builder()
                .setSubject("bob")
                .setIssuer("demo-app")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
                .setHeaderParam("kid", "rsa2")
                .signWith(priv2, SignatureAlgorithm.RS256)
                .compact();

        System.out.println("=== token2 (signed with rsa2) ===");
        System.out.println(token2);
        System.out.println();

        // 驗證 token1: 先 parse header 取得 kid，再從 keyset 中取回對應公鑰驗證
        JwsHeader header1 = Jwts.parserBuilder().build().parseClaimsJws(token1).getHeader();
        String kid1 = (String) header1.get("kid");
        RSAPublicKey pubFor1 = keyset.get(kid1);

        try {
            Jws<Claims> claims1 = Jwts.parserBuilder()
                    .setSigningKey(pubFor1)
                    .build()
                    .parseClaimsJws(token1);
            System.out.println("token1 verified; subject=" + claims1.getBody().getSubject());
        } catch (JwtException e) {
            System.out.println("token1 verification failed: " + e.getMessage());
        }

        // 驗證 token2
        JwsHeader header2 = Jwts.parserBuilder().build().parseClaimsJws(token2).getHeader();
        String kid2 = (String) header2.get("kid");
        RSAPublicKey pubFor2 = keyset.get(kid2);

        try {
            Jws<Claims> claims2 = Jwts.parserBuilder()
                    .setSigningKey(pubFor2)
                    .build()
                    .parseClaimsJws(token2);
            System.out.println("token2 verified; subject=" + claims2.getBody().getSubject());
        } catch (JwtException e) {
            System.out.println("token2 verification failed: " + e.getMessage());
        }

        // 嘗試用錯的 public key 驗證 token1 (應該失敗)
        try {
            Jwts.parserBuilder().setSigningKey(pub2).build().parseClaimsJws(token1);
            System.out.println("unexpectedly verified token1 with pub2");
        } catch (JwtException e) {
            System.out.println("As expected, token1 cannot be verified with pub2: " + e.getMessage());
        }
    }
}
```
#### 3. pom.xml（04-jwt/pom.xml）應包含 jjwt 依賴（先前 starter-kit 已放入），若沒有請加：
```xml=
//xml

<dependencies>
  <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
  </dependency>
  <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>
```
#### 4. Build & Run（04-jwt）
```
//bash

# 先建立 keys（如上述 openssl 指令），確保 keys/ 裡有 rsa1_private_pkcs8.pem, rsa1_public.pem, rsa2_*.

cd 04-jwt
mvn compile exec:java -Dexec.mainClass=com.example.jwt.JwtRsaExample
```
你會看到：兩個 token（token1、token2）列印出來，並且解析驗證成功；嘗試用錯 key 驗證會失敗。
##### 5. 為什麼用 RS256 + kid（實務要點）
1. 非對稱簽章：私鑰只給簽發者（auth server），公鑰給資源伺服器（resource server），降低祕密洩漏風險。
2. kid 支援 key rotation（可同時保留舊 key 以驗證既有未過期 token，並用新 key 簽發新 token）。實務上會發佈一個 JWKS endpoint（JSON Web Key Set），供驗證端定期拉取。
##### 6. 生產注意：
1. 不要把 private key 放在專案裸檔。請使用 vault 或 KMS（AWS KMS / GCP KMS / Azure Key Vault），或以檔案外部注入，並限制檔案權限。
2. 若要撤銷 token，請將 Refresh Token 和 access token 設計為短期 + 使用黑名單/版本策略。
### C. 06-cors：只允許 http://localhost:3000（完整 server code + 測試）
把以下 server 放到 06-cors/server/src/main/java/... 覆寫。
#### 1. CorsServer.java
```java=
package com.example.cors;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.Set;

public class CorsServer {
    private static final Set<String> ALLOWED_ORIGINS = Set.of("http://localhost:3000");

    private static final String ALLOWED_METHODS = "GET, POST, OPTIONS";
    private static final String ALLOWED_HEADERS = "Content-Type, Authorization";

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8081), 0);
        server.createContext("/api/echo", new EchoHandler());
        server.start();
        System.out.println("CORS Server on http://localhost:8081");
    }

    static class EchoHandler implements HttpHandler {
        @Override public void handle(HttpExchange ex) throws IOException {
            Headers reqHeaders = ex.getRequestHeaders();
            String origin = reqHeaders.getFirst("Origin");
            Headers h = ex.getResponseHeaders();

            boolean originAllowed = origin != null && ALLOWED_ORIGINS.contains(origin);
            if (originAllowed) {
                h.add("Access-Control-Allow-Origin", origin);
                h.add("Access-Control-Allow-Credentials", "true");
            }

            h.add("Vary", "Origin");
            h.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
            h.add("Access-Control-Allow-Methods", ALLOWED_METHODS);

            if ("OPTIONS".equalsIgnoreCase(ex.getRequestMethod())) {
                ex.sendResponseHeaders(204, -1);
                return;
            }

            byte[] body = "ok".getBytes();
            ex.sendResponseHeaders(200, body.length);
            try (OutputStream os = ex.getResponseBody()) { os.write(body); }
        }
    }
}
```
#### 2. Build & Run（06-cors）
```
//bash

cd 06-cors/server
mvn compile exec:java -Dexec.mainClass=com.example.cors.CorsServer
# 或用 IDE 執行 main
```
#### 3. 測試（模擬 preflight 與實際請求）
##### 1. Preflight request (OPTIONS) — allowed origin
```
//bash

curl -i -X OPTIONS http://localhost:8081/api/echo \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type"
```
預期：204，headers 含 Access-Control-Allow-Origin: http://localhost:3000 以及 Access-Control-Allow-Credentials: true。
##### 2. Preflight — not allowed origin
```
//bash

curl -i -X OPTIONS http://localhost:8081/api/echo \
  -H "Origin: http://evil.com" \
  -H "Access-Control-Request-Method: POST"
```
預期：回應沒有 Access-Control-Allow-Origin（瀏覽器會拒絕前端跨域請求）。
##### 3. 實際 GET
```
//bash

curl -i -X GET http://localhost:8081/api/echo -H "Origin: http://localhost:3000"
```
預期：200 + Access-Control-Allow-Origin: http://localhost:3000
##### 4. 為什麼這樣做
1. 只允許白名單 domain（http://localhost:3000），並在 response 中 echo 回 Origin，這是正確做法，能同時允許 Access-Control-Allow-Credentials: true。
2. 別用 * 搭配 Credentials（瀏覽器限制）。生產時把白名單外移到 config，或使用動態檢核。
### D. 一次性把整個流程測試（整合範例）
下面示範一個完整整合測試流程（假設你已經啟動 02-cookie-session 在 Tomcat，context = /cookie-session，並啟動 06-cors 在 8081，04-jwt 可執行以產生 RSA token）：
1. 開 http://localhost:8080/cookie-session/login.jsp（或用 curl 下載 loginpage.html，抓 csrfToken）
2. 用 cookie + csrfToken POST 到 /login，應得到登入成功。
3. 使用 cookie 訪問 /admin，應返回 admin 頁面。
4. 等超過 10 分鐘，訪問 /me 或 /admin 應失效（401 或 redirect 到 login）。
5. 執行 04-jwt program，得到 RSA-signed token （可在其他 microservice 驗證）。
6. 啟動 06-cors，用 http://localhost:3000 origin 在前端 fetch 成功；其他 origin 失敗。
### E. 補充：如何用 Shell 自動抓 CSRF token（實務測試腳本）
```
//bash

# 1) 抓 login page 並存 cookie
curl -s -c cookie.txt http://localhost:8080/cookie-session/login.jsp -o loginpage.html

# 2) 從 loginpage.html 擷取 CSRF token（Linux with grep -P）
TOKEN=$(grep -oP 'name="csrfToken" value="\K[^"]+' loginpage.html)
echo "Got CSRF: $TOKEN"

# 3) POST 帶 cookie 與 token
curl -i -b cookie.txt -c cookie.txt -d "username=alice&password=123456&csrfToken=${TOKEN}" http://localhost:8080/cookie-session/login

# 4) 以 cookie 訪問 admin
curl -i -b cookie.txt http://localhost:8080/cookie-session/admin
```
### F. 生產 / 安全最佳實務（重要）
1. HTTPS & Cookie flags：在 Nginx 或 Tomcat 設 Set-Cookie 的 Secure; HttpOnly; SameSite=Lax（或適合你的 UX 的 SameSite）。
2. 密碼儲存：所有密碼在 DB 內 use bcrypt/argon2 with sufficient cost and per-user salt。
3. JWT keys：Private key 放在 KMS/Vault；公鑰透過 JWKS endpoint 公開給 resource servers。
4. Token 撤銷：Access Token 短期、Refresh Token 使用旋轉策略並可撤銷（維護一個 revocation list 或 token version）。
5. Session 在叢集：像使用 Redis session store 或 sticky sessions（但 sticky sessions 有可用性/可擴展疑慮）。
6. CSP / XSS：設定 Content-Security-Policy、HTML encode，並使用框架的防 XSS 工具。
7. 日誌：日誌不要寫明碼或 token；對敏感操作做審計。



# 2. Spring Security 快速上手：完整教學
1. 專案說明
    1. 專案名稱：security-spring-security
    2. 打包方式：war（部署至 Tomcat 7+）
    3. 技術棧：Spring MVC、Spring Security、Servlet 3.0（無 web.xml）
2. 最終功能清單
    1. /login：使用 Spring Security 內建登入頁（本章使用預設頁）
    2. /login-success：登入成功後跳轉到這個端點
    3. /r/**：受保護資源，必須登入後才可訪問
    4. 兩位測試用戶（記憶體）：
        1. zhangsan / 123（授權：p1）
        2. lisi / 456（授權：p2）
3. 專案結構（建議）
```
security-spring-security
├─ pom.xml
├─ src
   ├─ main
      ├─ java
      │  └─ com.example.security
      │     ├─ config
      │     │  ├─ ApplicationConfig.java
      │     │  ├─ WebConfig.java
      │     │  └─ WebSecurityConfig.java
      │     └─ init
      │        ├─ SpringApplicationInitializer.java
      │        └─ SpringSecurityApplicationInitializer.java
      ├─ resources
      └─ webapp
         └─ WEB-INF
            └─ views
               └─ login-success.jsp
```
備註：本章使用 Spring Security 預設登入頁，故無需 login.jsp；若想自訂登入頁，請見附錄 A。
## 1. Step 0｜建立 Maven 專案（WAR）
pom.xml（核心片段）
```xml=
//pom.xml

<project>
  <properties>
    <java.version>1.8</java.version>
    <spring.version>5.3.39</spring.version>
    <spring.security.version>5.8.12</spring.security.version>
    <javax.servlet.version>4.0.1</javax.servlet.version>
  </properties>


  <dependencies>
    <!-- Spring MVC -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>


    <!-- Spring Security -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>${spring.security.version}</version>
    </dependency>


    <!-- Servlet API（Provided by container） -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>${javax.servlet.version}</version>
      <scope>provided</scope>
    </dependency>


    <!-- JSP 支援（若使用 JSP 視圖） -->
    <dependency>
      <groupId>jakarta.servlet.jsp</groupId>
      <artifactId>jakarta.servlet.jsp-api</artifactId>
      <version>3.1.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>10.1.26</version>
      <scope>provided</scope>
    </dependency>


    <!-- JSTL（若使用 JSP） -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
  </dependencies>
</project>
```
為了對齊新版容器，這裡示範使用 jakarta.servlet 套件命名。若你部署於舊版 Tomcat，請改回 javax.servlet 版本。
## 2. Step 1｜Spring 容器 & MVC 基礎設定（無 web.xml）
### 1-1. Spring 根容器（非 Web MVC）
ApplicationConfig.java
```java=
package com.example.security.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = "com.example.security")
@EnableAspectJAutoProxy
public class ApplicationConfig {
}
```
用來載入服務層、資料層等 Bean。
### 1-2. Spring MVC 設定（視圖解析、靜態資源）
WebConfig.java
```java=
package com.example.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.security")
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    // 預設根路徑導向 /login（Spring Security 內建登入頁）
    registry.addViewController("/").setViewName("redirect:/login");
  }

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver vr = new InternalResourceViewResolver();
    vr.setPrefix("/WEB-INF/views/");
    vr.setSuffix(".jsp");
    return vr;
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**").addResourceLocations("/static/");
  }
}
```
這裡用 ViewController 直接把 / 轉到 /login，方便一啟動就能看到登入頁。
### 1-3. Servlet 3.0 無 web.xml 啟動
SpringApplicationInitializer.java（載入根與MVC容器）
```java=
package com.example.security.init;

import com.example.security.config.ApplicationConfig;
import com.example.security.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;


public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {


  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[]{ ApplicationConfig.class };
  }


  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[]{ WebConfig.class };
  }


  @Override
  protected String[] getServletMappings() {
    return new String[]{ "/" };
  }
}
```
## 3. Step 2｜導入 Spring Security
這一步完成「用戶來源 + 安全攔截規則 + 表單登入」。
### 2-1. 安全設定類
WebSecurityConfig.java:
```java=
package com.example.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
@EnableWebSecurity
@SuppressWarnings("deprecation") // Demo 使用 NoOp（教學目的）
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


  @Bean
  public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager mgr = new InMemoryUserDetailsManager();
    mgr.createUser(
      User.withUsername("zhangsan").password("123").authorities("p1").build()
    );
    mgr.createUser(
      User.withUsername("lisi").password("456").authorities("p2").build()
    );
    return mgr;
  }


  @Bean
  public PasswordEncoder passwordEncoder() {
    // 教學用：純字串比對。實務請改用 BCrypt。
    return NoOpPasswordEncoder.getInstance();
  }


  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
  }


  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/", "/login", "/static/**").permitAll()
        .antMatchers("/r/**").authenticated()
        .anyRequest().permitAll()
      .and()
      .formLogin()
        .loginPage("/login") // 使用預設登入頁的 URL（框架自動提供）
        .defaultSuccessUrl("/login-success", true)
        .permitAll()
      .and()
      .logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll()
      .and()
      .csrf(); // 保持預設開啟（附錄 B 說明如何處理 403）
  }
}
```

關鍵解釋：
1. userDetailsService()：這裡採 記憶體 用戶（最小可行）。實務會改成資料庫版本。
2. authorities("p1") / authorities("p2")：先用「權限字串」示範，稍後可延伸為角色/權限模型。
3. HttpSecurity 規則：/r/** 需登入；其他開放。表單登入成功導向 /login-success。
### 2-2. 把 Security 啟動進 Servlet
SpringSecurityApplicationInitializer.java:
```java=
package com.example.security.init;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;


public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
  // 空即可：父類已自動註冊 springSecurityFilterChain
}
```
這一步會自動在你的 DispatcherServlet 之前插入 SpringSecurityFilterChain。
## 4. Step 3｜Controller 與視圖
### 3-1. 登入成功頁（Controller + JSP）
1. LoginController.java:
```java=
package com.example.security.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class LoginController {


  @GetMapping("/login-success")
  @ResponseBody
  public String loginSuccess() {
    return "Login Success!";
  }
}
```
    為了最小化依賴，這裡先回傳字串。若你偏好 JSP，請把 @ResponseBody 拿掉，並在 /WEB-INF/views/ 放 login-success.jsp：
2. /WEB-INF/views/login-success.jsp
```
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Login Success</title></head>
<body>
  <h2>Login Success</h2>
  <p>你已成功登入，歡迎回來。</p>
  <p><a href="/logout">Log Out</a></p>
</body>
</html>
```
## 5. Step 4｜執行與驗收
1. 部署：將 war 丟到 Tomcat webapps/，啟動伺服器。
2. 開啟：瀏覽器進入 http://localhost:8080/security-spring-security/ → 會自動導向 /login。
3. 測試帳密：
    1. zhangsan / 123 → 登入後轉 /login-success
    2. lisi / 456 → 同上
4. 受保護資源：登入後訪問 /r/hello（需自行建立 Controller），未登入時會被引導至 /login。

    快速測試受保護端點：
    ```java=
    // com.example.security.controller.ResourceController
    @Controller
    @RequestMapping("/r")
    public class ResourceController {
      @GetMapping("/hello")
      @ResponseBody
      public String hello() { return "Hello Protected"; }
    }
    ```


# 3. Spring Security 進階與實戰（完整教學）
## 重要觀念
1. Modern config：Spring Security 自 5.7+ 建議以 SecurityFilterChain 與 component-style beans 取代 WebSecurityConfigurerAdapter。這讓配置更可組合、測試性更好。 (參考：Spring 官方)。
2. Stateful vs Stateless：傳統表單登入/Session 是有狀態的；REST API 常用 JWT（無狀態）。無狀態設計要 sessionCreationPolicy(SessionCreationPolicy.STATELESS)，並處理 CSRF 與 CORS 的差異。
3. Role vs Authority：ROLE_ 前綴是 Spring 的慣例（hasRole("ADMIN") 等於 hasAuthority("ROLE_ADMIN")）。系統可用 GrantedAuthority 更細的權限字串。
4. Method security：把細粒度授權放在 service / controller 方法上通常更穩健（@PreAuthorize("hasRole('ADMIN') and #id == principal.id")）。
## 範例前置：建議的 POM（關鍵依賴）
```xml=
<!-- 片段 -> 需自行合併到你已有的 pom.xml -->
<dependencies>
  <!-- Spring Web / MVC -->
  <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> </dependency>


  <!-- Spring Security core -->
  <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency>
  <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency>


  <!-- OAuth2 Resource Server (JWT support) -->
  <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> </dependency>
  <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>


  <!-- JWT helper (選擇) -> JJWT 或 Nimbus 可以任一 -->
  <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency>
  <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
  <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>


  <!-- JDBC / Data -->
  <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency>
  <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>


  <!-- Password encoder (BCrypt already in security) -->
</dependencies>
```
## 1. 現代化安全設定（SecurityFilterChain）
1. 要點：不用再繼承 WebSecurityConfigurerAdapter，改為註冊 SecurityFilterChain 與 AuthenticationManager / UserDetailsService / PasswordEncoder 的 @Bean。
2. 範例：最小化的 SecurityFilterChain（含 stateless 與 stateful 範例）
```java=
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 開啟方法層授權（之後章節會解釋）
public class SecurityConfig {


  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }


  @Bean
  public UserDetailsService userDetailsService() {
    // 簡單示範：記憶體
    UserDetails u1 = User.withUsername("zhangsan").password(passwordEncoder().encode("123")).authorities("p1").build();
    UserDetails u2 = User.withUsername("lisi").password(passwordEncoder().encode("456")).authorities("p2").build();
    return new InMemoryUserDetailsManager(u1, u2);
  }


  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .csrf(csrf -> csrf.disable()) // 例：API 用例（實務請評估）
      .authorizeHttpRequests(auth -> auth
        .requestMatchers("/public/**", "/login").permitAll()
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
      )
      .formLogin(withDefaults())
      .httpBasic(withDefaults());


    return http.build();
  }
}
```
3. 解說（逐步）
    1. @EnableMethodSecurity：允許在方法上使用 @PreAuthorize、@PostAuthorize 等。與舊的 @EnableGlobalMethodSecurity 相比，為官方建議的替代方式。
    2. PasswordEncoder：示例使用 BCryptPasswordEncoder（生產環境必備）。
    3. SecurityFilterChain：使用 http DSL 設定授權規則、csrf、登入等，最後 http.build() 產生 Bean。
## 2. 方法層級授權（Method-level Security）
1. 為何要用方法層級授權:URL 層授權適合保護「路由」，但有時權限判斷應基於方法參數或業務邏輯（例如：只允許資源擁有者編輯），此時 @PreAuthorize 更合適。
2. 啟用與示例
```java=
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig { }

@Service
public class AccountService {


  @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
  public AccountDto getAccount(Long userId) { ... }
}
```
3. 解說
    1. #userId 代表方法參數，authentication.principal 是當前使用者（你可以改成 principal.username 或自訂 UserDetails）。
    2. Spring 在呼叫方法前會做安全檢查，若不通過將丟出 AccessDeniedException。
## 3. UserDetails 與 JDBC（實務：連資料庫）
1. 建議資料表（示例：與 Spring 的 JDBC schema 類似）
```sql=
CREATE TABLE users (
  username VARCHAR(50) PRIMARY KEY,
  password VARCHAR(200) NOT NULL,
  enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
  username VARCHAR(50) NOT NULL,
  authority VARCHAR(50) NOT NULL,
  CONSTRAINT fk_user FOREIGN KEY(username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);
```
2. 自訂 UserDetailsService 範例
```java=
@Service
public class JdbcUserDetailsService implements UserDetailsService {


  private final JdbcTemplate jdbc;


  public JdbcUserDetailsService(JdbcTemplate jdbc) { this.jdbc = jdbc; }


  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserRecord user = jdbc.queryForObject("select username,password,enabled from users where username=?",
      (rs, rowNum) -> new UserRecord(rs.getString("username"), rs.getString("password"), rs.getBoolean("enabled")), username);


    List<GrantedAuthority> auths = jdbc.query("select authority from authorities where username=?",
      (rs, rowNum) -> new SimpleGrantedAuthority(rs.getString("authority")), username);


    return new org.springframework.security.core.userdetails.User(user.username, user.password, user.enabled, true, true, true, auths);
  }
}
```
3. 與 DaoAuthenticationProvider 整合
```java=
@Bean
public AuthenticationProvider daoAuthProvider(UserDetailsService uds, PasswordEncoder encoder) {
  DaoAuthenticationProvider p = new DaoAuthenticationProvider();
  p.setUserDetailsService(uds);
  p.setPasswordEncoder(encoder);
  return p;
}
```
## 4. JWT（REST）— 兩種策略
核心選擇：如果你是 標準 OAuth2 / OpenID Connect 生態（Authorization Server exist），建議採用 Resource Server 自動驗證 JWT（官方支援）。若你需要完全自訂或沒有授權伺服器，則可用自訂 JWT filter（自行驗證 token）。
### A. 推薦：Spring OAuth2 Resource Server（最少自寫驗證邏輯）
1. POM 依賴:
```xml=
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
```
2. application.yml 範例（使用 issuer-uri）
```xml=
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://example-issuer.com/
```
3. SecurityFilterChain（Resource Server）
```java=
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
  http
    .csrf(csrf -> csrf.disable())
    .authorizeHttpRequests(auth -> auth
      .requestMatchers("/public/**").permitAll()
      .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
    .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
  return http.build();
}
```
4. 優點:
    1. 官方支援 jwks 自動發現/驗證
    2. 可直接把 claims 映射成 Authorities
    3. 更少自訂程式碼，較安全
### B. 教學/控制：自訂 JwtAuthenticationFilter（OncePerRequestFilter）
1. JwtFilter（概念）
```java=
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  private final JwtService jwtService; // 你實作的 token 驗證/解析

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    String header = request.getHeader("Authorization");
    if (header != null && header.startsWith("Bearer ")) {
      String token = header.substring(7);
      if (jwtService.validateToken(token)) {
        String username = jwtService.getUsername(token);
        List<GrantedAuthority> auths = jwtService.getAuthorities(token);
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, auths);
        SecurityContextHolder.getContext().setAuthentication(auth);
      }
    }
    filterChain.doFilter(request, response);
  }
}
```
2. 加入過濾器到 FilterChain
```java=
http.addFilterBefore(new JwtAuthenticationFilter(jwtService), UsernamePasswordAuthenticationFilter.class);
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
```
3. 風險與注意
    1. 自己負責簽章驗證、失效、演算法切換、鑰匙輪替（key rotation）等
    2. 若使用 HMAC，伺服器需安全保存密鑰；若使用 RSA，需處理公鑰/私鑰與 JWKS
## 5 Remember-me（持久化 Token）
1. 原理:使用者勾選 "Remember me" 後，伺服器會發一個 token 存在 cookie，下一次可自動登入。
2. Persistent Token 範例資料表
```sql=
CREATE TABLE persistent_logins (
  username VARCHAR(64) NOT NULL,
  series VARCHAR(64) PRIMARY KEY,
  token VARCHAR(64) NOT NULL,
  last_used TIMESTAMP NOT NULL
);
```
2. 設定範例
```
@Autowired
DataSource dataSource;


http
  .rememberMe(r -> r
    .tokenRepository(new JdbcTokenRepositoryImpl() {{ setDataSource(dataSource); }})
    .tokenValiditySeconds(1209600) // 14 days
    .userDetailsService(userDetailsService)
  );
```
3. 注意
    1. token 必須安全生成（不可預測），且需定期清理過期紀錄
    2. persistent token 比單純 cookie 更安全，但仍有被盜用風險（配合 rotating tokens / device fingerprint 可加強）
## 6 Session 管理（Session Fixation、Concurrent Sessions）
Session Fixation（預設保障）:Spring Security 會在成功登入後替使用者建立新的 session id（session fixation protection）。若需自訂：
```
http.sessionManagement(sm -> sm.sessionFixation(SessionFixation
```

# 4. 身份與授權協定：OAuth2、OpenID Connect、SSO 與授權伺服器
## 1. OAuth2 與 OIDC 核心概念（先讀）
1. 基本角色
    1. Resource Owner：使用者（擁有受保護資源）。
    2. Client：代表應用程式（例如 SPA、Web App 或後端服務）。
    3. Authorization Server (AS)：處理使用者認證、同意（consent）與 token 發放。
    4. Resource Server (RS)：實際提供受保護資源的 API（驗證 access token）。
2. 常見 Grant Types
    1. Authorization Code（推薦，適用於 Web App + server-side、也可配 PKCE 用於 SPA）
    2. Implicit（不推薦，因安全問題）
    3. Resource Owner Password Credentials (ROPC)（僅限特定情境，已不建議）
    4. Client Credentials（機器到機器，沒有使用者）
    5. Refresh Token（用於換取新的 access token）
    6. Device Flow（IoT / limited-input devices）
3. OIDC（OpenID Connect）關鍵差異
    1. OIDC 在 OAuth2 基礎上新增 id_token（JWT），用於標示已驗證的身分（subject、issuer、aud 等 claim）。
    2. userinfo endpoint：可請求更多 profile 資料。
## 2. Spring Authorization Server（SAS）入門（建立你的授權伺服器）
注意：Spring 官方在 2022 之後提供 Spring Authorization Server（SAS）作為推薦方案（取代已退役的 Spring Security OAuth project）。
### A. 最小可用範例（Spring Boot 3 + Spring Authorization Server）
1. pom.xml（片段）
```xml=
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
  <version>1.1.0</version> <!-- 對應當前 Spring Authorization Server 版本 -->
</dependency>
```
2. 基本設定與註冊 client
```java=
@Configuration
public class AuthorizationServerConfig {

  @Bean
  public RegisteredClientRepository registeredClientRepository(PasswordEncoder encoder) {
    RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("web-client")
        .clientSecret(encoder.encode("secret"))
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri("http://localhost:8080/login/oauth2/code/web-client-oidc")
        .scope(OidcScopes.OPENID)
        .scope("read")
        .build();
    return new InMemoryRegisteredClientRepository(client);
  }

  @Bean
  public JWKSource<SecurityContext> jwkSource() {
    RSAKey rsaKey = Jwks.generateRsa(); // helper to generate RSA key pair
    JWKSet jwkSet = new JWKSet(rsaKey);
    return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
  }

  @Bean
  public ProviderSettings providerSettings() {
    return ProviderSettings.builder().issuer("http://auth-server:9000").build();
  }
}
```
3. 啟動 AuthorizationServer endpoints
    1. 授權端點（/oauth2/authorize）
    2. token endpoint（/oauth2/token）
    3. jwks endpoint（/oauth2/jwks）
    4. OIDC 的 /.well-known/openid-configuration
4. 解說
    1. RegisteredClient：代表註冊在授權伺服器的 client（client_id, client_secret, grant types, redirect uris, scopes）。
    2. JWKSource：用於簽發 JWT token 的金鑰來源（建議 RSA）、/oauth2/jwks 會對外公開公鑰。
    3. ProviderSettings.issuer：很重要，JWT 的 iss claim 會帶上此值，Resource Server 與 clients 可基於此值發現 JWKS。
## 3. Authorization Code Flow（含 PKCE）範例（完整流程）
### A. Authorization Code（Server-side web app）
1. 使用者在 Client（Web App）點擊「使用者登入」→ 導向 AS 的 /oauth2/authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid%20read。
2. AS 顯示登入頁面（及 consent 頁）→ 使用者同意。
3. AS redirect 回 Client 的 redirect_uri，包含 code。
4. Client Server 以 POST /oauth2/token（帶 client credentials）交換 code 換取 access_token 以及 id_token（若 request 含 openid scope）。
5. Client 以 access_token 訪問 Resource Server。
### B. PKCE（適用於 SPA / 原生 app）
1. PKCE（Proof Key for Code Exchange）是為了防止 code interception：
    1. 在發起 /authorize 前，Client 產生 code_verifier（高熵隨機字串），並以 S256 將其雜湊為 code_challenge。
    2. AS 在授權階段存下 code_challenge，當 Client 用 code 呼叫 /token 時，必須帶回 code_verifier，AS 驗證匹配後發 token。
2. 範例（交換 token 的 curl）
```
curl -X POST \
  -u "web-client:secret" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:8080/login/oauth2/code/web-client-oidc&code_verifier=CODE_VERIFIER" \
  http://localhost:9000/oauth2/token
```
## 4. Client Credentials Flow（machine-to-machine）
1. 使用情境:背後服務 A 需要呼叫資源伺服器 B，並不代表使用者，則使用 Client Credentials。
2. 範例
```
curl -X POST -u "service-client:secret" \
  -d "grant_type=client_credentials&scope=read" \
  http://localhost:9000/oauth2/token
```
3. 回傳 access_token（通常為 JWT 或 opaque token）。
## 5. Refresh Token 與 Refresh Token Rotation
1. 基本:Refresh token 用於在 access token 到期後換取新 token。refresh token 必須安全保管，且通常只給 server-side clients。
2. Refresh Token Rotation（安全強化）
    1. 每次使用 refresh token 換 new token 時，AS 發一個新的 refresh token 並使舊的過期（prevent replay attack）。
    2. 若偵測到重複使用舊的 token（可能是竊取），可主動撤銷所有 token 或封鎖帳號。
3. Spring Authorization Server 範例設定
    1. 在 AuthorizationServerSettings 或 Token 的設定中啟用 refresh token rotation（視版本 API 而定）。
## 6. Token Revocation / Introspection
1. Revocation Endpoint:RFC 7009：授權伺服器應提供 revocation endpoint，讓 client 可撤銷 access 或 refresh token。
2. 範例（client 撤銷 token）
```
curl -X POST -u "web-client:secret" -d "token=THE_REFRESH_TOKEN&token_type_hint=refresh_token" http://localhost:9000/oauth2/revoke
```
3. Introspection Endpoint:RFC 7662：resource server（或 introspection 客戶端）帶 token 到 /introspect 檢查是否有效（若使用 opaque token）。
## 7. OpenID Connect（id_token 與 userinfo）
1. id_token:id_token 是 JWT（或 signed）包含 sub（subject）、iss、aud、exp、iat 等 claim，並可包含 email, name 等。
2. Userinfo Endpoint:用 access_token 呼叫 /userinfo 取得更多 profile 資訊（例如：email_verified 等）。
3. 範例（取得 userinfo）
```
curl -H "Authorization: Bearer ACCESS_TOKEN" http://localhost:9000/userinfo
```
## 8. Single Sign-On（SSO）實務
1. 原理:SSO 的核心是讓多個應用共享一個 Authorization Server 的 session（通常由 AS 以 cookie 管理），使用者只要在 AS 登入過，一個 Client 的授權步驟可被其他 Client 省去。
2. 實作要點
    1. 各 Client 設為 OIDC Client（Authorization Code + PKCE / server-side），共用同一 Authorization Server。
    2. AS 的 session cookie（同一頂級網域或透過跨域方案）會保證已登入狀態。
    3. 若要達到跨子域 SSO，AS 的 cookie domain 可以設為 .example.com，或採用 central domain 與 redirect 線路。
## 9. 社群登入（OAuth2 Login）— 以 Google 為例
1. Spring Security OAuth2 Client（快速設定）:
```
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: <client-id>
            client-secret: <client-secret>
            scope: openid,profile,email
        provider:
          google:
            issuer-uri: https://accounts.google.com
```
2. 解說:
    1. Spring Boot 會自動建立 OAuth2Login 的 ClientRegistration，並提供 /oauth2/authorization/google 跳轉入口。
    2. 在回呼時可以透過 OidcUser 或 OAuth2User 取用 claims，並結合本地帳戶資料。
## 10. Logout（Front-channel / Back-channel / RP-Initiated）
1. Front-channel logout:Browser 重導到 AS 的 logout endpoint，會清掉 AS 的 session cookie，並可能導向各 Client 的 logout URI。適合簡單場景。
2. Back-channel logout:AS 向各 Client 發送 server-to-server 的 logout notification，Client 伺服器收到後內部清除 session。適合更嚴格一致性的需求。
3. RP-Initiated logout（OIDC）:Client 呼叫 AS 的 end_session_endpoint 並帶 id_token_hint 與 post_logout_redirect_uri。
## 11. Security Considerations 與最佳實務
1. 永遠使用 HTTPS（Token 與 redirect_uri 都必須是安全的）。
2. 使用 PKCE：SPA / Native App 必須啟用。
3. 最小 scope：只請求必要的權限與 claims。
4. 避免 long-lived access tokens：盡量使用短期限 access token 與刷新機制。
5. 保護 refresh token：server-side 儲存（不要放在 localStorage for SPAs）。
6. consent 與 scopes 的呈現：清楚告知使用者將授權的權限。
7. 監控與稽核：token 使用、撤銷、登入地點/時間等的日誌。
## 12. 測試與調試小技巧
1. 使用 Postman / OAuth2 Playground 測試 flow。
2. 觀察 .well-known/openid-configuration 與 /oauth2/jwks 是否正確回應。
3. 在 Resource Server 用 jwt introspection 或自行驗證 JWT signature 錯誤常見原因：issuer、aud mismatch、clock skew。
4. 檢查 redirect_uri 是否完全相符（含結尾 slash）。
## 13. 範例練習（逐步）
1. 建立一個最小 SAS（授權伺服器）：用 InMemoryRegisteredClient 註冊兩個 client：一個 web client（authorization_code + refresh_token），一個 machine client（client_credentials）。測試 /oauth2/token 與 /oauth2/authorize。
2. 部署 Resource Server：配置 spring.security.oauth2.resourceserver.jwt.issuer-uri 指向你的 SAS，測試受保護 API。
3. 實作 PKCE 的 SPA 登入：使用一個簡單的前端（或 curl 模擬）完成 code+PKCE 流程。
4. 啟用 refresh token rotation：模擬 refresh token 重複使用的偵測。
5. 社群登入整合：把 Google 註冊為 client（在 Google Console 設定 redirect URI），並測試 /oauth2/authorization/google 流程。
### 1. 建立最小 SAS（Authorization Server） — 註冊兩個 client（web + machine）
目標:
1. 啟動 Authorization Server，並用 InMemoryRegisteredClient 註冊兩個 client：
    1. web-client：grant types = authorization_code + refresh_token（適用於 server-side web app）
    2. machine-client：grant type = client_credentials（server-to-server）
2. 可以測試 /oauth2/authorize 與 /oauth2/token。

註：Spring Authorization Server 提供註冊 client 的中心元件 RegisteredClientRepository，並且預設會把一套最小 endpoint（/oauth2/authorize, /oauth2/token, /oauth2/jwks, /.well-known/openid-configuration）暴露出來。
#### 1. 依賴（pom 片段）
（把這些加到 auth-server 專案的 pom.xml）
```xml=
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version><!-- 對應當前可用版本，例如 1.1.x --></version>
</dependency>
<!-- Nimbus JOSE for JWK/JWT handling if needed -->
<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
</dependency>
```
#### 2. 最小的 Authorization Server 設定（範例程式碼）
AuthorizationServerConfig.java
```java=
@Configuration
public class AuthorizationServerConfig {

    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder encoder) {
        RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("web-client")
            // 若為機密客戶端可設定 clientSecret；若用 PKCE 的 public client 則不給 secret
            .clientSecret(encoder.encode("web-secret"))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://localhost:8080/login/oauth2/code/web-client-oidc")
            .scope(OidcScopes.OPENID)
            .scope("read")
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(15))
                .refreshTokenTimeToLive(Duration.ofHours(8))
                .reuseRefreshTokens(true) // 若要啟用 rotation 改為 false（後面會示範）
                .build())
            .build();

        RegisteredClient machineClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("machine-client")
            .clientSecret(encoder.encode("machine-secret"))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .scope("read")
            .build();

        return new InMemoryRegisteredClientRepository(webClient, machineClient);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa(); // helper: 產生 RSAKey 的程式，可用 Nimbus helper
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
                .issuer("http://localhost:9000")
                .build();
    }

    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        // 啟用 OIDC 的 endpoint（如果需要）
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
        // 其餘保護（例如登入）由 Spring Security 預設處理
        return http.formLogin(Customizer.withDefaults()).build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```
解說（重點）
1. RegisteredClient 表示在授權伺服器上註冊的 client（id/secret/redirect uri/授權類型/scopes...）。RegisteredClientRepository 是存放這些 client 的地方；範例使用 InMemory（測試用）。
2. JWKSource 提供簽發 access_token/ID token 的金鑰，SAS 會暴露 /oauth2/jwks 給 Resource Server 或第三方驗證。
3. ProviderSettings.issuer 很重要，JWT 的 iss 會用到，Resource Server 也會使用此 issuer 來發現 JWKS 等 metadata。
Home
#### 3. 啟動與測試（手把手）
1. 啟動 auth-server（預設 http://localhost:9000）。
2. 測試 machine-client（client_credentials）取得 token：
```
//bash

curl -u "machine-client:machine-secret" -X POST http://localhost:9000/oauth2/token \
  -d "grant_type=client_credentials&scope=read"
```
會回傳 JSON 包含 access_token（通常是 JWT 或 opaque token），可用於 Resource Server 測試。
3. 測試 web-client 的 Authorization Code 流程（手動流程）：
    1. 在瀏覽器開啟（或貼到地址列）：
    ```
    //bash

    http://localhost:9000/oauth2/authorize?response_type=code&client_id=web-client&scope=openid%20read&redirect_uri=http://localhost:8080/login/oauth2/code/web-client-oidc&state=xyz
    ```
    2. AS 會顯示登入頁（或自訂），登入並同意後會 redirect 回 redirect_uri 並附 code：
    ```
    //bash

    http://localhost:8080/login/oauth2/code/web-client-oidc?code=AUTH_CODE&state=xyz
    ```
    3. 用 code 換 token：
    ```
    //bash

    curl -u "web-client:web-secret" -X POST http://localhost:9000/oauth2/token \
      -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:8080/login/oauth2/code/web-client-oidc"
    ```
    4. 你會拿到 access_token 與 refresh_token（若 scope 含 refresh）可用來呼叫 Resource Server。
### 2 部署 Resource Server（以驗證 SAS 發的 JWT）
#### 1. Resource Server 設定（application.yml）
在 resource-server 的 application.yml：
```xml=
server:
  port: 8081

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:9000
```
Resource Server 會透過 issuer-uri 查 .well-known/openid-configuration / jwks_uri 來自動設定 JWT 驗證器（會自動抓 jwks 並驗 signature）。這是 Spring Resource Server 的啟動預期行為。
Home
#### 2. SecurityConfig（簡短）
```
@Configuration
public class ResourceServerConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public").permitAll()
            .anyRequest().authenticated()
        ).oauth2ResourceServer(oauth2 -> oauth2.jwt());
        return http.build();
    }
}
```
#### 3. 測試 Resource Server
1. 先用 machine-client 從 auth-server 拿 token（參考上面 curl）。
2. 呼叫受保護端點：
```
//bash

curl -H "Authorization: Bearer <ACCESS_TOKEN>" http://localhost:8081/api/protected
```
    1. 若 JWT 由 auth-server 簽章且 issuer/aud 合法，Resource Server 會回應 200；否則會回 401/403。
    2. 若想要 debug，檢查 .well-known/openid-configuration 與 /oauth2/jwks 是否可被 resource-server 存取與解析（網域/防火牆/HTTP），以及 token 的 iss、aud 與 exp。
### 3. PKCE（SPA）— 完整 code + PKCE 流程（含產生 code_challenge 的範例）
#### 1. 為什麼要 PKCE？
PKCE（RFC 7636）是為了防止 Authorization Code 被截取（public clients 無法安全保存 client_secret），它要求在授權請求階段先提交 code_challenge（雜湊），在 token 交換階段提交 code_verifier（原始字串）來驗證。PKCE 已成為對 public clients（SPA、native apps）強烈建議/必要的保護機制。
#### 2. 前端（或測試腳本）如何產生 code_verifier / code_challenge（JavaScript 範例）
```javascript=
//javascript

// 產生 code_verifier（長度 43~128, 隨機字元）
function generateCodeVerifier() {
  const array = new Uint8Array(64);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest("SHA-256", data);
  return base64UrlEncode(new Uint8Array(digest));
}

function base64UrlEncode(arr) {
  // from byte array to base64url string
  let str = btoa(String.fromCharCode.apply(null, [...arr]));
  return str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
```
#### 3. 流程（步驟）
1. 前端產生 code_verifier，並用 SHA256 產生 code_challenge（S256）。
2. 導向授權端點（例）：
```
//bash

GET /oauth2/authorize?
  response_type=code
  &client_id=public-spa-client
  &redirect_uri=http://localhost:5173/callback
  &scope=openid%20read
  &code_challenge=<CODE_CHALLENGE>
  &code_challenge_method=S256
```
3. 使用者在授權伺服器登入/同意 → redirect 回 redirect_uri?code=...。
4. 前端（或後端）用 code 搭配 code_verifier 到 /oauth2/token 換 token：
```
//bash

# 若 client 是 public（無 secret），token exchange 不提供 client secret：
curl -X POST http://localhost:9000/oauth2/token \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:5173/callback&client_id=public-spa-client&code_verifier=THE_VERIFIER"
```
    （若 client 被註冊為 confidential，則需同時帶 client_secret 以 Basic auth。）
5. 伺服器驗證 code_verifier 是否與流程階段存下的 code_challenge 對應，如通過則下發 access_token（和 refresh token，視註冊而定）。

6. 範例測試（用 curl）:
    1. 先準備 code_verifier / code_challenge（可用上面 JS 在瀏覽器 console 生成，或用 openssl / python 生成）。
    2. 用瀏覽器導向 authorize URL（含 code_challenge），完成同意後用 curl 換 token。
    3. 注意：若是 SPA，最好把 client 註冊為 public（不含 secret）並使用 PKCE；切記不要把 long-lived refresh token 放在 localStorage（XSS 風險），也要考慮 refresh token rotation 或將 refresh token 儲存在 httpOnly cookie（但會帶來 CSRF 要處理）。
    IETF Datatracker
    Auth0
### 4. 啟用 Refresh Token Rotation（旋轉）與偵測重複使用（reuse detection）
#### 1. 概念簡述
Refresh token rotation：每次使用 refresh token 換新 access token 時，AS 同時發一個新的 refresh token 並使舊的失效。若舊的被重複使用（可能表示 token 被竊），AS 可檢測並採取措施（例如撤銷該帳號所有 token）。這能顯著降低 refresh token 被盜用的衝擊。多家廠商與文件建議使用 rotation。
#### 2. Spring Authorization Server 的設定（範例）
在建立 RegisteredClient 時可以透過 TokenSettings 設定 reuseRefreshTokens(false)：
```java=
RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("web-client")
        .clientSecret(encoder.encode("web-secret"))
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .redirectUri("http://localhost:8080/login/oauth2/code/web-client-oidc")
        .scope("read")
        .tokenSettings(TokenSettings.builder()
            .accessTokenTimeToLive(Duration.ofMinutes(15))
            .refreshTokenTimeToLive(Duration.ofDays(7))
            .reuseRefreshTokens(false) // 這會啟用 rotation（發新 refresh token 並使舊 token 失效）
            .build())
        .build();
```
#### 3. Token persistence 與偵測
為了做到 rotation 與偵測（detect reuse），必須把 token / 授權資料持久化（例如 JDBC JdbcOAuth2AuthorizationService），因為「是否已被使用/撤銷」需要存在伺服器端可查詢的狀態。範例：
```java=
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
    return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
```
當 reuseRefreshTokens(false) 時，SAS 會在 refresh 時發新的 refresh token 並把舊的標記為已使用。若舊 token 再次出現，伺服器可以視為 replay attack 並拒絕，或進一步撤銷該帳號的所有授權。
#### 4. 如何模擬與測試重複使用的情境
1. 使用 Authorization Code（或其他流）拿到 refresh_token（稱為 R1）。
2. 用 R1 呼叫 /oauth2/token?grant_type=refresh_token&refresh_token=R1 → 伺服器回傳新 access token + new refresh token R2（而 R1 被視為失效）。
3. 再次用 R1 嘗試呼叫 /oauth2/token → 若 rotation 被正確實作，伺服器會回 400/error（例如 invalid_grant），代表偵測到 reuse（表示可能遭竊）。你可以在伺服器端記錄/封鎖該 account。
4. 驗證伺服器在重複使用舊 R1 時能夠正確拒絕，並在需要時撤銷該使用者其他授權。
### 5. 社群登入整合（以 Google 為例） — 作為 OAuth2 Client
#### 1. 概念與前置（Google Console）
1. 到 Google Cloud Console → OAuth consent screen → 設定應用、同意畫面（scopes/公開性）。
2. 在 Credentials → Create Credentials → OAuth client ID，選擇 Web Application，並把 Authorized redirect URIs 加入，例如：
```
//bash

http://localhost:8080/login/oauth2/code/google
```
    （redirect URI 必須與 Spring client registration 的 redirectUri 相同且精確）
#### 2. Spring Boot（OAuth2 Client）設定（application.yml）
```xml=
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: <GOOGLE_CLIENT_ID>
            client-secret: <GOOGLE_CLIENT_SECRET>
            scope:
              - openid
              - profile
              - email
        provider:
          google:
            issuer-uri: https://accounts.google.com
```
#### 3. SecurityConfig（接入 OAuth2 Login）
```java=
@Configuration
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .authorizeHttpRequests(auth -> auth
              .requestMatchers("/", "/login**", "/oauth2/**").permitAll()
              .anyRequest().authenticated()
          )
          .oauth2Login(oauth2 -> oauth2
              // 可設定 successHandler, userService 等
          );
        return http.build();
    }
}
```
#### 4. 取得 user info / token 並與本地帳號關聯
1. Spring 會在成功 callback 時建立 OidcUser 或 OAuth2User。你可以在 OAuth2UserService 或一個 OAuth2AuthenticationSuccessHandler 中抓取 OidcUser 的 claims（例如 email），在你本地系統找現有帳號或建立新帳號，然後建立應用內 session。
2. 測試：在瀏覽器開啟 http://localhost:8080/oauth2/authorization/google，Google 會跳轉到 Google 的登入頁，完成同意後回到你的 redirect_uri。
3. Google 對於 web server flow、redirect-uri 的說明請參考官方文件（建立憑證、設定 redirect URI、授權流程）。
### 6. 常見錯誤與排查（針對以上練習）
1. redirect_uri_mismatch：redirect uri 必須完全吻合（包含尾斜線與 http/https）。（常在 Google OAuth 設定錯誤）
2. invalid_grant on token exchange：code 過期或已被使用，或 PKCE 的 code_verifier 不匹配。檢查時間差、code 是否已使用。
3. Resource Server 回 401：檢查 JWT 的 iss（issuer）是否為 resource-server 所期望，jwks 是否能讀取，以及 token 是否過期。
4. Refresh token 再使用未被拒絕：確認你是否把 token persist 到 DB 並啟用 reuseRefreshTokens(false)（及用 JDBC 的 OAuth2AuthorizationService 來追蹤與標記狀態）。
### 7. 測試腳本（快速流程摘要）
1. 啟動 auth-server（9000）, resource-server（8081）, client（8080）。
2. 取得 machine-client token：
```
//bash

curl -u "machine-client:machine-secret" -d "grant_type=client_credentials&scope=read" http://localhost:9000/oauth2/token
```
3. 用 token 呼叫 resource：
```
//bash

curl -H "Authorization: Bearer <ACCESS_TOKEN>" http://localhost:8081/api/protected
```
4. 授權碼流程測試（web client）：在瀏覽器開啟 authorize URL → 同意 → 用 code 用 curl 換 token（帶 client secret）
5. PKCE（SPA）測試：在前端產生 code_verifier / code_challenge → 導向 authorize URL（含 code_challenge）→ 用 code + code_verifier 呼叫 token endpoint（若是 public client，不帶 secret）
## 14. 常見問題與排查清單
1. invalid_grant → code 過期、已使用或 code_verifier mismatch
2. unauthorized_client → client 未允許該 grant type
3. invalid_client → client_id/secret 錯誤或未使用正確的 client auth method
4. insufficient_scope → access token 缺少所需 scope
5. token signature validation failed → wrong public key / issuer mismatch

附錄 A：範例程式碼片段（SAS + Client）:
1. 更完整的可跑範例，我可以依你的需求建立並打包成 Spring Boot 範例（包含 SAS + Resource Server + Client 三套微服務）並上傳給你。

# 5. CSRF（跨站請求偽造）與 XSS（跨站腳本）的完整參考教學
## 1. CSRF（Cross-Site Request Forgery）深入
1. 什麼是 CSRF（概念）:CSRF 是攻擊者利用使用者在 另一站（受信任站） 的登入狀態，誘導瀏覽器在受信任站發出有狀態改變的請求（例如轉帳、改密碼）。由於瀏覽器會自動帶上 cookies，目標站若沒有驗證請求來源，就會把該請求當作合法使用者的操作。這是 OWASP 上常見定義與防範核心。
2. 攻擊前提（威脅模型）
    1. 受害者已在目標站登入（具有有效 session cookie 或其他自動帶的憑證）。
    2. 攻擊者能誘導受害者（例如寄信、造訪惡意頁面）去對目標站發出請求（通常為 POST/PUT/DELETE 等非安全方法）。
    3. 目標站無法區分該請求是來自使用者在本站的操作還是外站發起。
3. 常見 CSRF 防禦策略（比較）
    1. Synchronizer (server-side) token（同步化 token） — 最可靠：伺服器在 session 中放 token，表單內放 hidden field，提交時比對。適合 server-rendered pages 和大多數情境。OWASP 建議為首選。
    2. Double Submit Cookie — 伺服器發一個可被 JS 讀的 cookie（XSRF token），JS 將 cookie 讀出放到 header 或 body，再由伺服器比對 cookie 與 header。適合 stateless 或某些 SPA 情境，但若 cookie 被 JS 讀取，XSS 可洩露，需慎用。
    3. SameSite Cookie — 設 SameSite=Lax/Strict/None 可在瀏覽器層阻止大部份跨站帶 cookie 的情形；為非常有效的補強，但並非萬能（瀏覽器相容性、同源情境/導航情境差異）。建議搭配 token 使用。MDN 的 Set-Cookie 文件詳列屬性與注意事項。
    4. 要求 re-auth / MFA / 確認碼 — 對高風險操作（轉帳、金流）要求二次驗證。這是風險緩解（risk-based）策略。
4. 作模式（選用建議）
    1. Server-rendered (常規 MVC)：使用 同步化 token（session 中 token + hidden input），最直接、最被 OWASP 推薦。
    2. SPA（前後端分離）：推薦用 Cookie + header（例如 Spring 的 CookieCsrfTokenRepository 寫入 XSRF-TOKEN cookie，前端讀 cookie 並在 X-XSRF-TOKEN header 傳回），或在登入後把 token 放到 initial HTML（meta tag）再由 SPA 記住。不要把 token 存到 localStorage。Spring docs 舉例說明。
## 2. CSRF：同步化 token（Synchronizer Token Pattern）詳細教學（Java Servlet 範例）
### 1. 設計思路（要點）
    1. 在使用者建立 session 時（或在 GET 顯示表單時），在 server-side 生成一個隨機 token（cryptographically random），把它放到 session（或 server-side token store）。
    2. 把 token 放到表單的 hidden 欄位或 meta tag（SPA），並在提交時把 token 一併送到 server。
    3. Server 比對收到的 token 與 session 中保存的 token 是否相符，若不同或缺失則拒絕（HTTP 403）。
        1. 要點：token 必須具備不可預測性，且每個 session（或 form）不同。不要把 token 放到 URL。請在 session invalidation（例如 logout）時清掉 token。
### 2. Java Servlet 實作（從零到尾）
#### 步驟 A — 產生 token 的 helper
src/main/java/com/example/csrf/CsrfUtil.java:
```
package com.example.csrf;

import java.security.SecureRandom;
import java.util.Base64;

public final class CsrfUtil {
    private static final SecureRandom RNG = new SecureRandom();

    public static String generateToken() {
        byte[] b = new byte[32];
        RNG.nextBytes(b);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(b);
    }
}
```
解說：使用 SecureRandom 產生 32 bytes 隨機值，用 Base64URL 編碼（不含 + / =）以便放到 HTML 欄位或 header。這比使用 UUID 更難被猜測。
#### 步驟 B — GET 時放 token 到 session 與 form（JSP 範例）
login.jsp:
```jsp=
<%@ page session="true" import="com.example.csrf.CsrfUtil" %>
<%
    String csrf = (String) session.getAttribute("csrfToken");
    if (csrf == null) {
        csrf = CsrfUtil.generateToken();
        session.setAttribute("csrfToken", csrf);
    }
%>
<form action="login" method="post">
  <input type="hidden" name="csrfToken" value="<%= csrf %>" />
  <!-- 其餘欄位 -->
</form>
```
解說：JSP 在渲染時取得或建立 token，並放入 hidden input。若網站允許快取 form page，請考慮設置 Cache-Control: no-store 以免舊 token 被快取導致 mismatch。
#### 步驟 C — POST 時驗證 token（Servlet）
LoginServlet#doPost(...)
```java=
HttpSession session = req.getSession(false);
String sessionCsrf = (session == null) ? null : (String) session.getAttribute("csrfToken");
String requestCsrf = req.getParameter("csrfToken");

if (sessionCsrf == null || requestCsrf == null || !sessionCsrf.equals(requestCsrf)) {
    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
    resp.getWriter().println("CSRF token invalid or missing");
    return;
}

// 繼續處理登入驗證...
```
解說：先比對 token 才進行任何狀態改變。若驗證失敗直接回 403 並終止流程。
#### 步驟 D — 測試（curl 範例）
1. 先 GET login 頁面並存 cookie：
```
//bash

curl -s -c cookies.txt http://localhost:8080/app/login.jsp -o loginpage.html
```
2. 從 loginpage.html 擷取 csrfToken（Linux 範例）：
```
//basah

TOKEN=$(grep -oP 'name="csrfToken" value="\K[^"]+' loginpage.html)
```
3. 再 POST 帶 cookie 與 token：
```
//bash

curl -i -b cookies.txt -c cookies.txt -d "username=alice&password=123&csrfToken=${TOKEN}" http://localhost:8080/app/login
```
若 token 缺失或錯誤會回 403。
### 3. 注意與進階
1. Session 共用 / Scale：在多節點部署時 session 需集中儲存（Redis、DB），或改為 stateless token 並另做 CSRF 措施。
2. 表單快取：若反向 proxy 或 CDN 快取 HTML（不安全），會導致舊 token 被用。對 form page 設 Cache-Control: no-store。
3. 高風險操作加強：對重要轉帳或改密等操作，要求使用者再次輸入密碼或 MFA。
## 3. CSRF 在 SPA / AJAX / Spring Security 的實作（現代做法）
### 1. Spring Security 的支援（重點）
Spring Security 預設啟用 CSRF 防護（適用於瀏覽器使用的應用）。它會把 _csrf token 暴露為 request attribute，且可自動插入到使用 Spring form tags 的表單中；對 AJAX 則常用 CookieCsrfTokenRepository（把 token 寫入 cookie），並在前端把 token 加入 header（例如 X-XSRF-TOKEN 或 X-CSRF-TOKEN）。參考 Spring 官方文件。
### 2. Spring Boot + 前端（簡要範例）
1. Server side（Spring Security config）（示意）：
@EnableWebSecurity
```java=
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        return http.build();
    }
}
```
說明：CookieCsrfTokenRepository.withHttpOnlyFalse() 會把 token 寫到 cookie（名稱 XSRF-TOKEN），cookie 設為可被 JS 讀取（HttpOnly=false）以便 SPA 把它放到 header。

2. Client side（fetch 範例）：
```//javascript=
//javascript

function getCookie(name) {
  return document.cookie.split('; ').find(row => row.startsWith(name+'='))
         ?.split('=')[1];
}
const token = getCookie('XSRF-TOKEN');
fetch('/api/modify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-XSRF-TOKEN': token
  },
  body: JSON.stringify({...})
});
```
說明：Spring 會檢查 header 或 request param 中的 token 與 session/cookie 中儲存的 token 是否匹配。詳細配置與範例請參考 Spring Security 文件與 Baeldung 等教學。
## 4. XSS（Cross-Site Scripting）深入
### 1. XSS 類型與風險
1. Reflected XSS：惡意 payload 隨參數回顯，通常透過釣魚 URL 觸發。
2. Stored XSS：惡意 payload 存在伺服器（留言、設定），被其他使用者讀取時執行（最嚴重）。
3. DOM-based XSS：payload 在客戶端 DOM 操作時被執行（例如 innerHTML、eval），不經伺服器判斷。
    1. XSS 可導致 cookie/LocalStorage 被竊取（若不是 HttpOnly）、執行惡意行為、偽造 UI、進一步的 CSRF 等。OWASP 與 PortSwigger 的資源為權威參考。
### 2. 防禦總原則（最重要）
1. Contextual output encoding（情境式輸出編碼）：根據輸出位置（HTML body、attribute、JS、URL、CSS）使用正確的編碼器。輸入驗證不是充分防線，正確的輸出編碼才是關鍵。
2. Use framework/template auto-escaping：JSTL <c:out>, Thymeleaf 等都有自動 escape 機制 — 儘量使用並避免直接 innerHTML。
3. CSP（Content Security Policy）做為 defense-in-depth：CSP 能大幅降低攻擊面（阻止 inline script、只允許特定來源或 nonce），但不能取代正確的輸出編碼。MDN 與 OWASP 都強調 CSP 為第二層防線。
4. Trusted Types（限制 DOM sink 的非受信值）：新興 API，用於強化 CSP 與輸出管控，對抗 DOM-based XSS。MDN 與 W3C 有相關說明（2024-2025 年日益被採用）。
### 3. XSS 實作範例（從易犯錯到正確）
#### 範例 A — Vulnerable（反射 / DOM）
vuln.jsp:
```jsp=
<% String q = request.getParameter("q"); %>
<html>
<body>
  <h3>搜尋結果</h3>
  <div id="results">
    <!-- 危險：直接輸出 user 輸入到 innerHTML -->
    <script>
      document.getElementById('results').innerHTML = "<p>結果：<%= q %></p>";
    </script>
  </div>
</body>
</html>
```
危險點：把使用者輸入直接注入到 innerHTML。若 q 包含 <img src=x onerror=...> 等，會執行。這是經典 DOM-based XSS。
#### 範例 B — 安全修補（伺服器端做 HTML encode + 前端用 textContent）
伺服器端用 OWASP Java Encoder（建議）或 JSTL c:out 編碼:safe.jsp
```jsp=
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<% String q = request.getParameter("q"); %>
<html>
<body>
  <h3>搜尋結果</h3>
  <div id="results">
    <p>結果：<c:out value="${param.q}" /></p>
  </div>
</body>
</html>
```
或若要 JS 更新 DOM：
```javascript=
//javascript

// server 放入 JSON-escaped string 或直接放 data-* attribute
const q = /* safely-escaped server-inserted value */;
document.getElementById('results').textContent = "結果：" + q;
```

解說：用 textContent 或 server-side HTML-escape 工具（OWASP Java Encoder、Apache Commons Lang StringEscapeUtils.escapeHtml4）來避免 XSS。不要用 innerHTML 來放未淨化的字串。
### 4. CSP（Content Security Policy）實作與實例
1. 目標：防止 inline script 與不受信任來源載入。
2. 範例 header（嚴格但實用）：
```
//csharp

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-<RANDOM>' https://apis.examplecdn.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  report-uri /csp-report;
```
3. 說明：
    1. 使用 nonce- 給可信 inline script（由 server 隨每次回應產生隨機 nonce 並插入對應 script 的 nonce 屬性），可允許必要的 inline script 又防止注入腳本。
    2. report-uri / report-to 幫助你收集 CSP 違規事件以觀察是否有被利用。MDN 的 CSP 指南非常詳細。
4. 在 Java Servlet 中設定 CSP header（簡單 filter）：
```java=
public class SecurityHeadersFilter implements Filter {
  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    HttpServletResponse httpResp = (HttpServletResponse) res;
    httpResp.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none';");
    chain.doFilter(req, res);
  }
}
```
### 5. Trusted Types（前端強化）
概念：限制把任意 string 傳入 DOM sinks（例如 innerHTML），必須從 Trusted Types API 產生的受信物件。可配合 CSP 的 require-trusted-types-for 'script' 或 trusted-types 指令。這在 2024-2025 年被建議於大型單頁應用逐步採用以防 DOM XSS。
## 5. 補充標頭與防線（SRI / Referrer-Policy / X-Frame-Options / Security headers）
1. SRI (Subresource Integrity)：當從 third-party CDN 載入 JS/CSS 時，加上 hash 確保內容未被篡改。MDN 有實作教學。
2. Referrer-Policy：限制 referer 的泄露，避免敏感 URL 透過 Referer header 洩出。
3. X-Frame-Options / CSP frame-ancestors：防止 clickjacking。
4. 不要只依賴 X-XSS-Protection header：現代瀏覽器已廢用或有限支持，應以 CSP 與輸出編碼為主。
## 6. 測試、驗證與工具
1.靜態分析（SAST）與動態掃描（DAST）皆不可少。PortSwigger / Burp Suite 非常適合發現 XSS 與 CSRF 相關問題。
2. 自動化測試腳本：用 curl、Selenium、Playwright 測試表單提交（包含不帶 CSRF token 的情形）與用例。
3. CSP report：部署 report-uri / report-to 來收集 CSP 違規事件並分析是否有惡意嘗試。
## 7. 實務上線檢查清單（Deploy checklist）
1. 所有 state-changing 請求均有 CSRF 防護（POST/PUT/PATCH/DELETE）。
2. cookie 標記：Secure; HttpOnly; SameSite（視 UX 需求選 Lax/Strict/None）並留意 SameSite 的瀏覽器行為。
3. 對所有輸出使用情境式編碼（HTML/Attribute/JS/URL/CSS），並使用框架的安全模板或 OWASP Java Encoder。
4. 設置 CSP（從寬到嚴逐步收緊，先設 report-only 模式觀察）。
5. 為第三方資源加 SRI，並限制來源。
6. 在 SPA 中使用 cookie + header 或 meta tag 的 CSRF 流程（不要把 Token 存 localStorage），並對 AJAX 設置適當 header。
7. 如果採用 double submit cookie，評估 XSS 風險與 JS-cookie 可視性。
## 8.  練習實驗（你可以照著做，step-by-step）
### A. 練習 1 — 同步化 CSRF token（Servlet + JSP，放到 02-cookie-session）
目標：在 login.jsp 產生一個不可預測的 CSRF token 存入 session，表單以 hidden 欄位帶回；LoginServlet 在 POST 時比對 session 中 token，不符回 403；成功則登入（模擬）。
#### 1. 專案結構（範例）
```
02-cookie-session/
 ├─ pom.xml
 └─ src/main/java/com/example/csrf/
 │    ├─ CsrfUtil.java
 │    ├─ LoginServlet.java
 │    └─ AdminServlet.java
 └─ src/main/webapp/
      ├─ login.jsp
      ├─ welcome.jsp
      └─ vuln.jsp         (選作：XSS 測試頁)
```
#### 2. 必要檔案（完整貼上）
##### 1. pom.xml（使用 Servlet 4.0 + JSP 支援 + Jetty plugin）
```xml=
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>csrf-demo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <dependencies>
    <!-- Servlet API (provided by container) -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>

    <!-- JSP runtime (Tomcat Jasper) -->
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>9.0.73</version>
    </dependency>

    <!-- JSTL (若使用 safe.jsp) -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>9.4.48.v20220622</version>
        <configuration>
          <scanIntervalSeconds>3</scanIntervalSeconds>
          <webApp>
            <contextPath>/</contextPath>
          </webApp>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
```
##### 2. src/main/java/com/example/csrf/CsrfUtil.java
```java=
package com.example.csrf;

import java.security.SecureRandom;
import java.util.Base64;

public final class CsrfUtil {
    private static final SecureRandom RNG = new SecureRandom();

    private CsrfUtil() {}

    public static String generateToken() {
        byte[] b = new byte[32];
        RNG.nextBytes(b);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(b);
    }
}
```
說明：使用 SecureRandom 產生 32 bytes → Base64URL，產出難猜的 token。
##### 3. src/main/java/com/example/csrf/LoginServlet.java
```java=
package com.example.csrf;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    // 範例硬編碼帳密（僅示範）
    private static final String DEMO_USER = "user";
    private static final String DEMO_PASS = "1234";

    @Override
    protected void doPost(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp)
            throws IOException {
        // 先驗 CSRF token
        HttpSession session = req.getSession(false);
        String sessionToken = (session == null) ? null : (String) session.getAttribute("csrfToken");
        String requestToken = req.getParameter("csrfToken");

        if (sessionToken == null || requestToken == null || !sessionToken.equals(requestToken)) {
            resp.setStatus(javax.servlet.http.HttpServletResponse.SC_FORBIDDEN);
            resp.getWriter().println("CSRF token invalid or missing");
            return;
        }

        // 驗證帳密 (示範)
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        if (DEMO_USER.equals(username) && DEMO_PASS.equals(password)) {
            // 成功 -> 若沒有 session 創建一個（或可先 invalidate 舊 session 做防 Fixation）
            if (session != null) session.invalidate();
            HttpSession newSession = req.getSession(true);
            newSession.setAttribute("username", username);

            // OPTIONAL: 重新產生一個 CSRF token（登入後）
            newSession.setAttribute("csrfToken", CsrfUtil.generateToken());

            // Redirect 到 welcome 頁
            resp.sendRedirect("welcome.jsp");
        } else {
            resp.getWriter().println("帳號或密碼錯誤");
        }
    }
}
```
重點說明：
1. 在執行任何「會改變伺服器狀態」的操作（這裡是登入流程）時，先比對 CSRF 才進行驗證或變更。
2. 若之前已有 session 建議 invalidate() 後再 getSession(true) 以防 Session Fixation。
##### 4. src/main/java/com/example/csrf/AdminServlet.java（可選，用來測試 /admin 需登入）
```java=
package com.example.csrf;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

@WebServlet("/admin")
public class AdminServlet extends HttpServlet {
    @Override
    protected void doGet(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp)
            throws IOException {
        HttpSession s = req.getSession(false);
        String user = (s == null) ? null : (String) s.getAttribute("username");
        if (user == null) {
            resp.sendRedirect("login.jsp");
            return;
        }
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().println("<h2>Admin area - welcome " + user + "</h2>");
    }
}
```
##### 5. src/main/webapp/login.jsp
```jsp=
<%@ page session="true" import="com.example.csrf.CsrfUtil" %>
<%
  String csrf = (String) session.getAttribute("csrfToken");
  if (csrf == null) {
      csrf = CsrfUtil.generateToken();
      session.setAttribute("csrfToken", csrf);
  }
%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>Login</title>
</head>
<body>
  <h2>登入（示範）</h2>
  <form method="post" action="login">
    <input type="hidden" name="csrfToken" value="<%= csrf %>" />
    帳號: <input type="text" name="username"/><br/>
    密碼: <input type="password" name="password"/><br/>
    <input type="submit" value="登入"/>
  </form>
</body>
</html>
```
##### 6. src/main/webapp/welcome.jsp
```jsp=
<%@ page session="true" %>
<%
  String user = (String) session.getAttribute("username");
  if (user == null) {
      response.sendRedirect("login.jsp");
      return;
  }
%>
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"/><title>Welcome</title></head>
<body>
  <h2>歡迎, <%= user %>！</h2>
  <a href="admin">Admin</a>
</body>
</html>
```
#### 3. 建置 & 執行
1. 使用 Jetty plugin（快速）
```
//bash

cd 02-cookie-session
mvn clean package
mvn org.eclipse.jetty:jetty-maven-plugin:9.4.48.v20220622:run
# 之後開瀏覽器 http://localhost:8080/login.jsp
```
2. 使用 Tomcat（部署 WAR）
```
//bash

cd 02-cookie-session
mvn clean package
# 會在 target/csrf-demo.war（或 artifactId-wrong? 檢查 target）
# 把 war deploy 到 Tomcat webapps 下，啟動 Tomcat
```
#### 4. 測試（curl 範例，演示「不帶 token → 403」與「帶 token → 登入成功」）
1. 取得 login page（並儲存 cookie 與 html）
```
//bash

curl -s -c cookies.txt http://localhost:8080/login.jsp -o loginpage.html
```
2. 擷取 CSRF token（Linux）
```
//bash

TOKEN=$(grep -oP 'name="csrfToken" value="\K[^"]+' loginpage.html)
echo "token=$TOKEN"
```
3. 正常 POST（帶 token）
```
//bash

curl -i -b cookies.txt -c cookies.txt \
  -d "username=user&password=1234&csrfToken=${TOKEN}" \
  http://localhost:8080/login
```
預期：若成功會看到 302 redirect 到 welcome.jsp 或直接成功回應。

4. 攻擊模擬：漏掉 token
```
//bash

curl -i -b cookies.txt -c cookies.txt -d "username=user&password=1234" http://localhost:8080/login
```

預期：HTTP/1.1 403 Forbidden，回應 CSRF token invalid or missing。
#### 5. 每一步詳細說明（教學式）
1. 為什麼要在 GET 時產生 token？因為表單需要一個隱藏值（hidden field）送回 server 作比對；把 token 存在 session 能確保每個 session 的 token 唯一且不可預測。
2. 為什麼 token 要用 SecureRandom？一般 UUID 雖有隨機性，但 cryptographic-strength 的 SecureRandom 更難被猜中。
3. 比對時應在任何 state-changing 請求前驗 token:先驗 token 再做密碼檢查/DB 操作，可避免 CSRF 觸發不必要的查詢/邏輯。
4. 如何在 scale-out（多台機）環境維持 token？Session 需集中儲存（例如 Redis），或把 token 與 session id 綁在同一儲存後端。
5. 如何保護 cookie？Production 要在 Set-Cookie 時加 Secure; HttpOnly; SameSite=Lax/Strict（在容器或 reverse proxy 中設定）。
### B. 練習 2 — Spring Security + SPA（CookieCsrfTokenRepository.withHttpOnlyFalse()）
目標：示範用 Spring Security 把 CSRF token 寫到 cookie（XSRF-TOKEN），前端（React/Vue）讀 cookie 把值放入 X-XSRF-TOKEN header，伺服器端驗證 token。這個流程是現代 SPA 常用的做法。
#### 1. 專案（簡化 Spring Boot demo）
1. 專案 04-spring-spa-demo/ 主要檔案：
```
04-spring-spa-demo/
 ├─ pom.xml
 └─ src/main/java/com/example/spa/
     ├─ SpaApplication.java
     ├─ SecurityConfig.java
     └─ DemoController.java
```
2. pom.xml（Spring Boot）
```xml=
<project ...>
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.9</version>
  </parent>
  <artifactId>spring-spa-demo</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
```
3. SpaApplication.java
```java=
package com.example.spa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpaApplication.class, args);
    }
}
```
4. SecurityConfig.java
```java=
package com.example.spa;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // Cookie 存放 CSRF token，允許 JS 可讀取（HttpOnly=false）
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            // for demo, allow all requests (簡化測試)
            .authorizeRequests(auth -> auth.anyRequest().permitAll());
        return http.build();
    }
}
```
重點：CookieCsrfTokenRepository.withHttpOnlyFalse() 會在 GET 回應將 token 寫入 XSRF-TOKEN cookie（且 cookie 可被 JavaScript 讀取），前端拿到後放入 X-XSRF-TOKEN header。

5. DemoController.java（一個需要 CSRF 的 POST endpoint）
```java=
package com.example.spa;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class DemoController {

    @PostMapping("/modify")
    public ResponseEntity<?> modify(@RequestBody Map<String,Object> body) {
        // 只回傳收到資料 — 但 Spring Security 已會驗 CSRF token
        return ResponseEntity.ok(Map.of("status","ok","received", body));
    }

    @GetMapping("/")
    public String home() {
        return "SPA CSRF demo home (GET sets XSRF-TOKEN cookie)";
    }
}
```
#### 2. 建置 & 執行
```
//bash

cd 04-spring-spa-demo
mvn spring-boot:run
```
應啟動在 http://localhost:8080。
#### 3. 前端（React / 任意 SPA）示範：把 cookie 中的 XSRF-TOKEN 取出並放到 X-XSRF-TOKEN header

簡單的 fetch 範例（瀏覽器環境）:
```javascript=
//javascript

function getCookie(name) {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? decodeURIComponent(match[2]) : null;
}

async function postModify(payload) {
  const token = getCookie('XSRF-TOKEN'); // 由 Spring 寫入 cookie
  const res = await fetch('/api/modify', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-XSRF-TOKEN': token
    },
    credentials: 'include',
    body: JSON.stringify(payload)
  });
  return res.json();
}
```


說明：
1. credentials: 'include' 若服務在不同 domain（需允許 CORS with credentials）才需要；本地同域可省略。
2. 這種模式避免把 token 存在 localStorage（XSS 泄露風險），token 存在 cookie 並由 JS 讀取再放 header，server 比對 cookie/token 機制。
#### 4. 測試（curl 模擬）
1. 先 GET 根頁面把 cookie 取回：
```
//bash

curl -i -c cookie.txt http://localhost:8080/
```
2. 從 cookie.txt 抓出 XSRF-TOKEN（示範用 awk，視 cookie 文件格式而定）：
```
//bash

# BSD/Linux 的 cookie file 格式不同，這是一般方法嘗試抓 cookie value
TOKEN=$(grep XSRF-TOKEN cookie.txt | awk '{print $7}')
echo "token=$TOKEN"
```
3. 再帶 cookie 與 header 發 POST：
```
//bash

curl -i -b cookie.txt -H "X-XSRF-TOKEN: ${TOKEN}" -H "Content-Type: application/json" \
  -d '{"msg":"hello"}' http://localhost:8080/api/modify
```
預期：200 + JSON 回應。如果缺少 header 或 header 值不對，Spring Security 會回 403。
#### 5. 詳細解釋（為什麼這樣做）
1. 為 SPA 設計：因為 SPA 常用 AJAX，使用 CookieCsrfTokenRepository 把 token 放 cookie，前端再放 header 是 industry practice。
2. 為何 cookie 要 HttpOnly=false？ 因為 JS 必須讀取 token 再放 header。若 HttpOnly=true 就沒法用此策略。
3. 風險：若應用存在 XSS，攻擊者可讀取 cookie 與 token → 所以仍要防範 XSS（輸出編碼、CSP）。
4. 替代：若採用 Bearer token 放在 Authorization header（而非 cookie），瀏覽器不會自動在跨站載入時設定 Authorization header → CSRF 風險不同（但需另處理 token 存放安全性）。
### C. 練習 3 — XSS 實驗（vulnerable → fix）與加入 CSP header
目標：建立一個「易受攻擊的頁面」vuln.jsp（使用 innerHTML 或在 JSP 直接未編碼輸出）來示範 XSS，然後示範 safe.jsp（用 JSTL c:out 或 OWASP Encoder）修補，並加上 CSP header 觀察防護效果。
#### 1. 必要檔案（放在 02-cookie-session 或 05-csrf-xss）
1. vuln.jsp（示範反射/DOM XSS）
2. safe.jsp（使用 JSTL c:out 或 OWASP Encoder）
3. SecurityHeadersFilter.java（設定 CSP header 的 servlet filter）
##### 1. src/main/webapp/vuln.jsp
```jsp=
<%@ page contentType="text/html;charset=UTF-8" %>
<% String q = request.getParameter("q"); if (q == null) q = ""; %>
<!DOCTYPE html>
<html>
<head><title>Vulnerable</title></head>
<body>
  <h3>Vulnerable page (DOM-based XSS demo)</h3>
  <div id="out"></div>

  <script>
    // 危險作法：直接把伺服器回傳的字串放到 innerHTML
    var serverValue = "<%= q %>";
    document.getElementById('out').innerHTML = "Search: " + serverValue;
  </script>
</body>
</html>
```
測試：開啟 http://localhost:8080/vuln.jsp?q=<img src=x onerror=alert(1)> 應會彈出 alert（或執行 payload），表示存在 XSS。
##### 2. src/main/webapp/safe.jsp（使用 JSTL c:out）
```jsp=
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head><title>Safe</title></head>
<body>
  <h3>Safe page (server-side encoding)</h3>
  <div id="out">
    <p>Search: <c:out value="${param.q}" /></p>
  </div>
</body>
</html>
```
說明：c:out 自動 HTML escape，會把 < > 等特殊字元轉成 HTML entity，避免執行。
##### 3. src/main/java/com/example/csrf/SecurityHeadersFilter.java（設定 CSP header）
```java=
package com.example.csrf;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SecurityHeadersFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse) response;
        // 簡單的 CSP policy（示範） — 嚴格的 CSP 會需要 nonce 或外部 resource 設定
        resp.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none';");
        resp.setHeader("X-Content-Type-Options", "nosniff");
        resp.setHeader("Referrer-Policy", "no-referrer-when-downgrade");
        resp.setHeader("X-Frame-Options", "DENY");
        chain.doFilter(request, response);
    }
}
```
在 web.xml 加註冊 filter：
```xml=
<filter>
  <filter-name>securityHeaders</filter-name>
  <filter-class>com.example.csrf.SecurityHeadersFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>securityHeaders</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
```
#### 2. 測試流程
1. 先試 vulnerable 頁：
    1. 開 http://localhost:8080/vuln.jsp?q=<img src=x onerror=alert('XSS')>
    2. 結果：alert 跳出 → 表示頁面存在 XSS（危險）。
2. 換成 safe.jsp：
    1. 開 http://localhost:8080/safe.jsp?q=<img src=x onerror=alert('XSS')>
    2. 結果：頁面會顯示 &lt;img src=x onerror=alert('XSS')&gt;（被 escape），不會執行。
3. 加入 CSP header（Filter 已註冊）：
    1. 適用情況：若攻擊者能把 script 注入，CSP 可以阻擋外部 script 與 inline script（視 policy）。
    2. 例如你把 script-src 'self' 加入，且 payload 含 inline onerror，CSP 可能阻止執行（不同瀏覽器對 inline event handler 的處理可能不同，所以不要完全依賴 CSP，仍要做正確的編碼）。
#### 3. 註解與實務建議
1. 情境式（contextual）編碼：根據你輸出的位置（HTML body / attribute / JS string / URL），使用不同的 escaping 方法。c:out 處理 HTML body。若輸出在 JavaScript 中需做 JS encoding。
2. Trusted Types + CSP：對大型 SPA 建議採用 Trusted Types 與嚴格 CSP（含 nonce）作為 defense-in-depth。
3. 不要只用 input validation：input validation 有用，但 XSS 防禦的核心是輸出編碼。
4. 部署時：把 CSP 初期放在 report-only 模式收集違規，再逐步收緊 policy。
### D. 常見測試指令（總結）
1. 取得頁面並儲存 cookie：
```
//bash

curl -s -c cookies.txt http://localhost:8080/login.jsp -o loginpage.html
```
2. 抽 token：
```
//bash

TOKEN=$(grep -oP 'name="csrfToken" value="\K[^"]+' loginpage.html)
```
3. 正常登入：
```
//bash

curl -i -b cookies.txt -c cookies.txt -d "username=user&password=1234&csrfToken=${TOKEN}" http://localhost:8080/login
```
4. 忽略 token（應 403）：
```
//bash

curl -i -b cookies.txt -c cookies.txt -d "username=user&password=1234" http://localhost:8080/login
```
5. Spring SPA cookie→header 測試（範例）：
```
//bash

curl -i -c cookie.txt http://localhost:8080/
TOKEN=$(grep XSRF-TOKEN cookie.txt | awk '{print $7}')
curl -i -b cookie.txt -H "X-XSRF-TOKEN: ${TOKEN}" -H "Content-Type: application/json" -d '{"m":"x"}' http://localhost:8080/api/modify
```
(註：cookie 索引位置可能因系統而異，若抓不到可用 cat cookie.txt 查看格式並調整 awk/gawk 的欄位)
### E. 常見陷阱與注意事項（實務教學重點）
1. 不要把 token 放到 URL（GET query） — 會被紀錄在日誌與 Referer 中。
2. 不要把 token 在 localStorage 中長期存放；若你的 token 可被 JS 讀取（如 SPA 需），務必防 XSS。
3. SameSite 可以減少 CSRF（但不要單靠 SameSite，仍應用 token）。
4. 表單頁面不要被公共 CDN 或代理快取（會導致 token 被複用 / 過期），對此設 Cache-Control: no-store。
5. 在多節點部署，session token 需要 centralized session store 或換成可辨識的 stateless token design（但那會影響 CSRF 策略）。
6. XSS 與 CSRF 有交互風險：若系統有 XSS，CSRF 很容易被繞過（XSS 導致 cookie/token 被竊），所以 XSS 的防範等級應優先。
## 9. 重要參考與延伸閱讀（權威來源）
1. [OWASP CSRF Prevention Cheat Sheet.](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html?utm_source=chatgpt.com) 
2. [OWASP XSS Prevention Cheat Sheet. ](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html?utm_source=chatgpt.com)
3. [MDN — Set-Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie?utm_source=chatgpt.com)（SameSite / Secure / HttpOnly）與 Cookies 指南. 
4. [MDN — Content Security Policy 指南](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP?utm_source=chatgpt.com)（CSP）。
5.[ MDN — Trusted Types API（2025 年重要防護）](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API?utm_source=chatgpt.com)與 W3C Trusted Types spec。
6. [Spring Security CSRF documentation](https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html?utm_source=chatgpt.com)（如何用 CookieCsrfTokenRepository，AJAX 範例）。
7. [MDN — Subresource Integrity（SRI）實作](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/SRI?utm_source=chatgpt.com)。
## 10.  小結（最短速成清單）
1. CSRF：在所有會改變 server state 的請求上，務必有 token（Server-synchronizer 或 SPA 的 cookie+header）。OWASP 優先建議同步化 token。
2. XSS：永遠做情境式輸出編碼；不要相信輸入驗證；CSP/Trusted Types 作為第二道防線。
3. Cookie：Secure; HttpOnly; SameSite；SameSite 能減少 CSRF 風險但不應完全取代 token。
4. 測試：用自動化與手動工具（Burp/PortSwigger）歸納攻擊向量並設報告機制（CSP report）。

 