Spring Boot 4 + Spring Security 7 application demonstrating WebAuthn (Passkey) authentication with a REST API backend, JPA persistence, and PostgreSQL.
- WebAuthn Passkey Demo β Spring Boot REST API
- π ΰΈͺΰΈ²ΰΈ£ΰΈΰΈ±ΰΈ
- π ΰΈ£ΰΈ°ΰΈΰΈ Authentication ΰΈΰΈ³ΰΈΰΈ²ΰΈΰΈ’ΰΈ±ΰΈΰΉΰΈ?
- π Flow ΰΈΰΈ²ΰΈ£ΰΈΰΈ³ΰΈΰΈ²ΰΈ
- Flow 1: Password Login (ΰΉΰΈΰΉΰΈ²ΰΈͺΰΈΉΰΉΰΈ£ΰΈ°ΰΈΰΈΰΈΰΉΰΈ§ΰΈ’ Email/Password)
- Flow 2: Passkey Registration (ΰΈ₯ΰΈΰΈΰΈ°ΰΉΰΈΰΈ΅ΰΈ’ΰΈ Passkey)
- Flow 3: Passkey Login (ΰΉΰΈΰΉΰΈ²ΰΈͺΰΈΉΰΉΰΈ£ΰΈ°ΰΈΰΈΰΈΰΉΰΈ§ΰΈ’ Passkey)
- Flow 4: ΰΉΰΈΰΉΰΈ²ΰΈ«ΰΈΰΉΰΈ² index.html (ΰΈ«ΰΈ₯ΰΈ±ΰΈ Login ΰΈͺΰΈ³ΰΉΰΈ£ΰΉΰΈ)
- π ΰΉΰΈΰΈ§ΰΈΰΈ²ΰΈΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΈΰΈ²ΰΈ Cookie Session ΰΉΰΈΰΉΰΈΰΉ JWT
- ΰΈͺΰΈ΄ΰΉΰΈΰΈΰΈ΅ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ΄ΰΉΰΈ‘/ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈ
- 1. ΰΉΰΈΰΈ΄ΰΉΰΈ‘ JWT Library (
pom.xml) - 2. ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JWT Utility Class (ΰΉΰΈΰΈ₯ΰΉΰΉΰΈ«ΰΈ‘ΰΉ)
- 3. ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JWT Authentication Filter (ΰΉΰΈΰΈ₯ΰΉΰΉΰΈ«ΰΈ‘ΰΉ)
- 4. ΰΉΰΈΰΉΰΉΰΈ
SecurityConfig.javaβ οΈ (ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΉΰΈ’ΰΈΰΈ°ΰΈΰΈ΅ΰΉΰΈͺΰΈΈΰΈ) - 5. ΰΉΰΈΰΉΰΉΰΈ
AuthController.java - 6. ΰΉΰΈΰΉΰΉΰΈ Frontend (
login.html,index.html) - 7.
β οΈ ΰΉΰΈΰΉΰΉΰΈ WebAuthn Authentication (ΰΈͺΰΉΰΈ§ΰΈΰΈΰΈ΅ΰΉΰΈ’ΰΈ²ΰΈΰΈΰΈ΅ΰΉΰΈͺΰΈΈΰΈ)
- 1. ΰΉΰΈΰΈ΄ΰΉΰΈ‘ JWT Library (
- ΰΈͺΰΈ£ΰΈΈΰΈΰΉΰΈΰΈ₯ΰΉΰΈΰΈ΅ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΉ
- ΰΈͺΰΈ΄ΰΉΰΈΰΈΰΈ΅ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ΄ΰΉΰΈ‘/ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈ
- Architecture
- Prerequisites
- π Quick Start
- Database
- π‘ REST API
- π Project Structure
- Test UI
- π ΰΉΰΈ«ΰΈ₯ΰΉΰΈΰΈΰΉΰΈ²ΰΈΰΈΰΈ΄ΰΈ
ΰΈ£ΰΈ°ΰΈΰΈΰΈΰΈ΅ΰΉΰΉΰΈΰΉ Cookie-based Session (ΰΉΰΈ‘ΰΉΰΉΰΈΰΉ JWT) ΰΉΰΈΰΈΰΈ²ΰΈ£ΰΈ’ΰΈ·ΰΈΰΈ’ΰΈ±ΰΈΰΈΰΈ±ΰΈ§ΰΈΰΈ ΰΈΰΈΆΰΉΰΈΰΉΰΈΰΉΰΈ default ΰΈΰΈΰΈ Spring Security
ΰΈ«ΰΈ₯ΰΈ±ΰΈΰΈΰΈ²ΰΈ£ΰΈΰΈ³ΰΈΰΈ²ΰΈΰΉΰΈΰΈΰΈΰΉΰΈ²ΰΈ’ΰΉ:
ΰΈΰΈ΄ΰΈΰΉΰΈ«ΰΈ‘ΰΈ·ΰΈΰΈ "ΰΈΰΈ±ΰΈΰΈ£ΰΉΰΈΰΉΰΈ²ΰΈΰΈ²ΰΈ Event"
1. ΰΈΰΈΈΰΈΰΉΰΈΰΈΰΈ΅ΰΉΰΈΰΈ£ΰΈ°ΰΈΰΈΉ (Login) β ΰΈ’ΰΈ·ΰΉΰΈΰΈΰΈ±ΰΈΰΈ£ΰΈΰΈ£ΰΈ°ΰΈΰΈ²ΰΈΰΈ (Email + Password)
2. ΰΉΰΈΰΉΰΈ²ΰΈ«ΰΈΰΉΰΈ²ΰΈΰΈ΅ΰΉΰΈΰΈ£ΰΈ§ΰΈΰΈͺΰΈΰΈ β ΰΈΰΈΉΰΈΰΈΰΉΰΈΰΈ! β ΰΉΰΈ«ΰΉ "ΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈΰΈΰΉΰΈΰΈ‘ΰΈ·ΰΈ" (Session Cookie)
3. ΰΈΰΈ₯ΰΈΰΈΰΈΰΈ±ΰΉΰΈΰΈΰΈ²ΰΈ β ΰΉΰΈΰΉΰΉΰΈΰΈ§ΰΉΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈΰΈΰΉΰΈΰΈ‘ΰΈ·ΰΈ β ΰΉΰΈΰΉΰΈ²ΰΉΰΈΰΉΰΉΰΈ₯ΰΈ’ ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈΰΈ’ΰΈ·ΰΉΰΈΰΈΰΈ±ΰΈΰΈ£ΰΈΰΈ΅ΰΈ
4. ΰΈΰΈΰΈΰΈΰΈ²ΰΈΰΈΰΈ²ΰΈ (Logout) β ΰΈΰΈ±ΰΈΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈΰΈΰΉΰΈΰΈ‘ΰΈ·ΰΈΰΈΰΈ΄ΰΉΰΈ β ΰΉΰΈΰΉΰΈ²ΰΉΰΈ‘ΰΉΰΉΰΈΰΉΰΉΰΈ₯ΰΉΰΈ§
"ΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈΰΈΰΉΰΈΰΈ‘ΰΈ·ΰΈ" = JSESSIONID Cookie
"ΰΈ£ΰΈ²ΰΈ’ΰΈΰΈ·ΰΉΰΈΰΈΰΈΉΰΉΰΉΰΈΰΉΰΈ²ΰΈΰΈ²ΰΈ" = Session store ΰΈΰΈ±ΰΉΰΈ Server (ΰΉΰΈΰΉΰΈΰΉΰΈ Memory)
ΰΉΰΈΰΈΰΈ²ΰΈ Technical:
| ΰΈͺΰΉΰΈ§ΰΈΰΈΰΈ£ΰΈ°ΰΈΰΈΰΈ | ΰΈΰΈ³ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ |
|---|---|
| JSESSIONID | Cookie ΰΈΰΈ΅ΰΉ Server ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈ«ΰΉ Browser ΰΉΰΈΰΉΰΈΰΉΰΈ§ΰΉ ΰΉΰΈΰΉΰΈΰΉΰΈΰΉ "ID ΰΈͺΰΈΈΰΉΰΈ‘" ΰΉΰΈΰΉΰΈ JSESSIONID=A1B2C3D4E5 |
| HttpSession | Object ΰΈΰΈ±ΰΉΰΈ Server ΰΈΰΈ΅ΰΉΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User (ΰΉΰΈΰΉΰΈ ΰΉΰΈΰΈ£ΰΈ₯ΰΉΰΈΰΈΰΈΰΈ΄ΰΈ, ΰΈ‘ΰΈ΅ Role ΰΈΰΈ°ΰΉΰΈ£) |
| SecurityContext | Object ΰΈΰΈΰΈ Spring Security ΰΈΰΈ΅ΰΉΰΉΰΈΰΉΰΈ Authentication info ΰΉΰΈ§ΰΉΰΉΰΈ Session |
Browser Server (Spring Boot)
β β
β Request + Cookie: β
β JSESSIONID=A1B2C3D4E5 β
β ββββββββββββββββββββββββββββββββββΊ β
β β Server ΰΈΰΈΆΰΈ: "A1B2C3D4E5 = user@example.com, ROLE_USER"
β β ΰΈΰΈ²ΰΈ HttpSession (ΰΉΰΈΰΉΰΈΰΉΰΈ Memory/Redis)
β ββββββββββββββββββββββββββββββββββ β
β Response: 200 OK (ΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User) β
ΰΉΰΈ‘ΰΈ·ΰΉΰΈ Login ΰΈͺΰΈ³ΰΉΰΈ£ΰΉΰΈ Spring Security ΰΈΰΈ°ΰΈΰΈ³ 3 ΰΈͺΰΈ΄ΰΉΰΈΰΈΰΈ΅ΰΉ:
// 1. ΰΈ’ΰΈ·ΰΈΰΈ’ΰΈ±ΰΈΰΈΰΈ±ΰΈ§ΰΈΰΈ (Authenticate)
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(email, password)
);
// 2. ΰΉΰΈΰΉΰΈΰΈΰΈ₯ΰΈ₯ΰΈ±ΰΈΰΈΰΉΰΉΰΈ SecurityContext
SecurityContextHolder.getContext().setAuthentication(auth);
// 3. ΰΈΰΈ±ΰΈΰΈΰΈΆΰΈΰΈ₯ΰΈ HttpSession (ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Session ΰΉΰΈ«ΰΈ‘ΰΉΰΈΰΉΰΈ²ΰΈ’ΰΈ±ΰΈΰΉΰΈ‘ΰΉΰΈ‘ΰΈ΅)
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());ΰΈΰΈ±ΰΉΰΈΰΈΰΈΰΈ 3 ΰΈΰΈ·ΰΈΰΈΰΈΈΰΈΰΈͺΰΈ³ΰΈΰΈ±ΰΈ! β Server ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Session ID ΰΈͺΰΈΈΰΉΰΈ‘ΰΈΰΈΆΰΉΰΈΰΈ‘ΰΈ² ΰΉΰΈ₯ΰΉΰΈ§ΰΉΰΈΰΉΰΈ Authentication object ΰΉΰΈ§ΰΉΰΉΰΈΰΈΰΈ±ΰΉΰΈ ΰΈΰΈ²ΰΈΰΈΰΈ±ΰΉΰΈΰΈͺΰΉΰΈ Session ID ΰΈΰΈ₯ΰΈ±ΰΈΰΉΰΈΰΉΰΈΰΉΰΈ Cookie
Cookie ΰΈΰΈΉΰΈΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈΰΈ’ Tomcat (Servlet Container ΰΈΰΈ΅ΰΉ Spring Boot ΰΉΰΈΰΉ) ΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄:
Login ΰΈͺΰΈ³ΰΉΰΈ£ΰΉΰΈ:
Server Response Headers:
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=A1B2C3D4E5F6; Path=/; HttpOnly β ΰΈΰΈ±ΰΈ§ΰΈΰΈ΅ΰΉ!
Content-Type: application/json
Browser ΰΉΰΈ«ΰΉΰΈ Header "Set-Cookie" β ΰΉΰΈΰΉΰΈ Cookie ΰΉΰΈ§ΰΉΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄
β ΰΈΰΈΈΰΈ Request ΰΈΰΉΰΈΰΉΰΈΰΈΰΈ΅ΰΉΰΈͺΰΉΰΈΰΉΰΈΰΈ’ΰΈ±ΰΈ Server ΰΉΰΈΰΈ΅ΰΈ’ΰΈ§ΰΈΰΈ±ΰΈ ΰΈΰΈ°ΰΉΰΈΰΈ Cookie ΰΈΰΈ΅ΰΉΰΉΰΈΰΈΰΉΰΈ§ΰΈ’
ΰΉΰΈΰΉΰΈΰΈ£ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ΅ΰΉΰΈ‘ΰΈ΅ Cookie 2 ΰΈΰΈ±ΰΈ§:
| Cookie | ΰΈ«ΰΈΰΉΰΈ²ΰΈΰΈ΅ΰΉ | ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈΰΈ’ΰΉΰΈΰΈ£ |
|---|---|---|
JSESSIONID |
Session ID β ΰΉΰΈΰΉΰΈ£ΰΈ°ΰΈΰΈΈΰΈΰΈ±ΰΈ§ΰΈΰΈ User | Tomcat (Servlet Container) ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄ |
XSRF-TOKEN |
CSRF Protection Token β ΰΈΰΉΰΈΰΈΰΈΰΈ±ΰΈΰΈΰΈ²ΰΈ£ΰΉΰΈΰΈ‘ΰΈΰΈ΅ Cross-Site Request Forgery | Spring Security (CookieCsrfTokenRepository) |
JSESSIONID ΰΈͺΰΈ³ΰΈΰΈ±ΰΈΰΈΰΈ΅ΰΉΰΈͺΰΈΈΰΈ β ΰΈΰΉΰΈ²ΰΉΰΈ‘ΰΉΰΈ‘ΰΈ΅ Cookie ΰΈΰΈ΅ΰΉ Server ΰΈΰΈ°ΰΉΰΈ‘ΰΉΰΈ£ΰΈΉΰΉΰΈ§ΰΉΰΈ²ΰΈΰΈΈΰΈΰΉΰΈΰΉΰΈΰΉΰΈΰΈ£ β ΰΈͺΰΉΰΈ 401 Unauthorized ΰΈΰΈ₯ΰΈ±ΰΈΰΈ‘ΰΈ²
XSRF-TOKEN ΰΉΰΈΰΉΰΉΰΈΰΈΰΈ²ΰΈ°ΰΈΰΈ±ΰΈ POST/PUT/DELETE β ΰΈΰΉΰΈΰΈΰΈΰΉΰΈ²ΰΈ Cookie ΰΉΰΈ₯ΰΉΰΈ§ΰΈͺΰΉΰΈΰΈΰΈ₯ΰΈ±ΰΈΰΉΰΈΰΉΰΈ Header X-XSRF-TOKEN ΰΉΰΈΰΈ·ΰΉΰΈΰΈΰΈ΄ΰΈͺΰΈΉΰΈΰΈΰΉΰΈ§ΰΉΰΈ² Request ΰΈ‘ΰΈ²ΰΈΰΈ²ΰΈΰΈ«ΰΈΰΉΰΈ²ΰΉΰΈ§ΰΉΰΈΰΈΰΈ£ΰΈ΄ΰΈΰΉ ΰΉΰΈ‘ΰΉΰΉΰΈΰΉΰΈΰΈ²ΰΈΰΉΰΈ§ΰΉΰΈΰΈΰΈ·ΰΉΰΈΰΈΰΈ₯ΰΈΰΈ‘ΰΈ‘ΰΈ²
// ΰΈΰΈ±ΰΈ§ΰΈΰΈ’ΰΉΰΈ²ΰΈΰΈΰΈ²ΰΈ£ΰΈΰΉΰΈ²ΰΈ XSRF-TOKEN ΰΉΰΈ₯ΰΉΰΈ§ΰΈͺΰΉΰΈΰΈΰΈ₯ΰΈ±ΰΈΰΉΰΈ Header
const xsrf = document.cookie
.split("; ")
.find(c => c.startsWith("XSRF-TOKEN="))
?.split("=")[1];
fetch("/login/webauthn", {
method: "POST",
credentials: "include", // β ΰΈͺΰΈ³ΰΈΰΈ±ΰΈ! ΰΈΰΈΰΈΰΉΰΈ«ΰΉ Browser ΰΈͺΰΉΰΈ Cookie ΰΉΰΈΰΈΰΉΰΈ§ΰΈ’
headers: {
"Content-Type": "application/json",
"X-XSRF-TOKEN": decodeURIComponent(xsrf) // β ΰΈͺΰΉΰΈ CSRF token ΰΈΰΈ₯ΰΈ±ΰΈΰΉΰΈ
},
body: JSON.stringify(data)
});βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cookie-based Session (ΰΉΰΈΰΈ£ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ΅ΰΉ) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Browser Server β
β ββββββββββββ ββββββββββββββββββββββββ β
β β Cookie: β ββ Request βββΊ β Session Store: β β
β β JSESSION β β A1B2 β {user: "a"} β β
β β =A1B2 β βββ Response ββ β C3D4 β {user: "b"} β β
β ββββββββββββ β E5F6 β {user: "c"} β β
β ββββββββββββββββββββββββ β
β Cookie ΰΉΰΈΰΉΰΈΰΉΰΈΰΉ "ID" Server ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User ΰΈΰΈ±ΰΉΰΈΰΈ«ΰΈ‘ΰΈ β
β ΰΉΰΈ‘ΰΉΰΈ‘ΰΈ΅ΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ΰΈΰΈ°ΰΉΰΈ£ΰΉΰΈ₯ΰΈ’ ΰΉΰΈ Memory (ΰΈ«ΰΈ£ΰΈ·ΰΈ Redis) β
β β
β β
ΰΈΰΉΰΈ²ΰΈ’, Spring Security ΰΈΰΈ³ΰΉΰΈ«ΰΉΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄ β
β β
ΰΉΰΈΰΈ΄ΰΈΰΈΰΈΰΈ (Logout) ΰΉΰΈΰΉΰΈΰΈ±ΰΈΰΈΰΈ΅ β ΰΈ₯ΰΈ Session ΰΈΰΈ±ΰΉΰΈ Server β
β β
ΰΈΰΈ₯ΰΈΰΈΰΈ ΰΈ±ΰΈ’ β Cookie HttpOnly ΰΈΰΉΰΈΰΈΰΈΰΈ±ΰΈ XSS ΰΈΰΉΰΈ²ΰΈΰΉΰΈ‘ΰΉΰΉΰΈΰΉ β
β β ΰΉΰΈ‘ΰΉ Stateless β Server ΰΈΰΉΰΈΰΈΰΈΰΈ³ Session ΰΈΰΈΈΰΈΰΈΰΈ β
β β Scale ΰΈ’ΰΈ²ΰΈ β ΰΈΰΉΰΈ²ΰΈ‘ΰΈ΅ΰΈ«ΰΈ₯ΰΈ²ΰΈ’ Server ΰΈΰΉΰΈΰΈΰΉΰΈΰΈ£ΰΉ Session (Redis) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JWT (JSON Web Token) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Browser Server β
β ββββββββββββββββββββ ββββββββββββββββββββββββ β
β β Header: β β Req βββΊ β ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΉΰΈΰΈΰΈ°ΰΉΰΈ£! β β
β β Authorization: β β ΰΉΰΈΰΉ verify signature β β
β β Bearer eyJhbG... β ββ Res ββ β ΰΈΰΉΰΈ§ΰΈ’ Secret Key β β
β ββββββββββββββββββββ ββββββββββββββββββββββββ β
β β
β Token ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User Server Stateless β
β ΰΉΰΈΰΉΰΈ²ΰΈ£ΰΈ«ΰΈ±ΰΈͺ + ΰΈ₯ΰΈ²ΰΈ’ΰΉΰΈΰΉΰΈ ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈΰΈΰΈ³ΰΉΰΈΰΈ£ΰΉΰΈ₯ΰΈ’ β
β β
β β
Stateless β Server ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΉΰΈ Session β
β β
Scale ΰΈΰΉΰΈ²ΰΈ’ β Server ΰΈΰΈ±ΰΈ§ΰΉΰΈ«ΰΈΰΈΰΉ verify ΰΉΰΈΰΉ β
β β
ΰΉΰΈ«ΰΈ‘ΰΈ²ΰΈ°ΰΈΰΈ±ΰΈ Mobile App / Microservices β
β β ΰΉΰΈΰΈ΄ΰΈΰΈΰΈΰΈΰΈ’ΰΈ²ΰΈ β Token ΰΈ’ΰΈ±ΰΈΰΉΰΈΰΉΰΉΰΈΰΉΰΈΰΈΰΈ«ΰΈ‘ΰΈΰΈΰΈ²ΰΈ’ΰΈΈ (ΰΈΰΉΰΈΰΈΰΈΰΈ³ Blacklist) β
β β Token ΰΉΰΈ«ΰΈΰΉΰΈΰΈ§ΰΉΰΈ² Session ID β
β β ΰΈΰΉΰΈΰΈ implement ΰΉΰΈΰΈ β Spring Security ΰΉΰΈ‘ΰΉΰΉΰΈΰΉΰΈΰΈ³ΰΉΰΈ«ΰΉΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Browser (login.html) Server (Spring Boot)
βββββββββββββββββββ ββββββββββββββββββββ
ββ 1. User ΰΈΰΈ£ΰΈΰΈ Email + Password ΰΉΰΈ₯ΰΉΰΈ§ΰΈΰΈ Login
β
β POST /api/auth/login
β Body: {"email":"a@b.com","password":"1234"}
β βββββββββββββββββββββββββββββββββββββββββββΊ
β ββ 2. AuthController.login()
β β authenticationManager.authenticate()
β β βββΊ JpaUserDetailsService.loadUserByUsername("a@b.com")
β β βββΊ SELECT * FROM app_users WHERE email = 'a@b.com'
β β βββΊ ΰΉΰΈΰΈ£ΰΈ΅ΰΈ’ΰΈΰΉΰΈΰΈ΅ΰΈ’ΰΈ password (BCrypt)
β β β
ΰΈΰΈΉΰΈΰΈΰΉΰΈΰΈ!
β β
β β SecurityContextHolder.getContext().setAuthentication(auth)
β β session.setAttribute("SPRING_SECURITY_CONTEXT", context)
β β β Tomcat ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JSESSIONID Cookie ΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄
β ββ
β
β βββββββββββββββββββββββββββββββββββββββββββ
β 200 OK
β Set-Cookie: JSESSIONID=ABC123; Path=/; HttpOnly
β Body: {"id":1,"email":"a@b.com","displayName":"John"}
β
ββ 3. Browser ΰΉΰΈΰΉΰΈ JSESSIONID Cookie
JavaScript: window.location.href = "/index.html"
β redirect ΰΉΰΈΰΈ«ΰΈΰΉΰΈ² Profile
ββ 4. ΰΉΰΈΰΉΰΈ²ΰΈ«ΰΈΰΉΰΈ² index.html β ΰΉΰΈ£ΰΈ΅ΰΈ’ΰΈ /api/auth/me
β
β GET /api/auth/me
β Cookie: JSESSIONID=ABC123 β Browser ΰΉΰΈΰΈ Cookie ΰΉΰΈΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄
β βββββββββββββββββββββββββββββββββββββββββββΊ
β ββ 5. Spring Security Filter
β β ΰΈΰΉΰΈ²ΰΈ JSESSIONID=ABC123
β β ΰΈ«ΰΈ² Session β ΰΈΰΈ SecurityContext
β β β User "a@b.com" ΰΈ’ΰΈ±ΰΈΰΈ₯ΰΉΰΈΰΈΰΈΰΈ΄ΰΈΰΈΰΈ’ΰΈΉΰΉ
β β β
ΰΈΰΉΰΈ²ΰΈ!
β β
β β AuthController.me()
β β β ΰΈΰΈΆΰΈ Principal ΰΈΰΈ²ΰΈ SecurityContext
β β β query DB β return User info
β ββ
β
β βββββββββββββββββββββββββββββββββββββββββββ
β 200 OK {"id":1,"email":"a@b.com",...}
β
ββ 6. ΰΉΰΈͺΰΈΰΈΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User ΰΈΰΈΰΈ«ΰΈΰΉΰΈ² Profile β
β οΈ ΰΈΰΉΰΈΰΈ Login ΰΈΰΉΰΈ§ΰΈ’ Password ΰΈΰΉΰΈΰΈ ΰΈΰΈΆΰΈΰΈΰΈ° Register Passkey ΰΉΰΈΰΉ
Browser (index.html) Server Authenticator
ββββββββββββββββββββ ββββββ (Windows Hello /
Touch ID / YubiKey)
βββββββββββββββ
ββ 1. User ΰΈΰΈΰΈΰΈΈΰΉΰΈ‘ "Register New Passkey"
β
β POST /webauthn/register/options
β Cookie: JSESSIONID=ABC123
β βββββββββββββββββββββββββββΊ
β ββ 2. Spring Security WebAuthn Filter
β β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Challenge (ΰΈΰΉΰΈ²ΰΈͺΰΈΈΰΉΰΈ‘)
β β + ΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ Relying Party (rpId, rpName)
β β + ΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User (id, name)
β ββ
β βββββββββββββββββββββββββββ
β 200 OK (PublicKeyCredentialCreationOptions)
β {challenge, rp, user, pubKeyCredParams,...}
β
β 3. JavaScript ΰΉΰΈ£ΰΈ΅ΰΈ’ΰΈ Web API:
β navigator.credentials.create({publicKey: options})
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ
β ββ 4. Authenticator
β β ΰΉΰΈͺΰΈΰΈ Prompt
β β (ΰΈͺΰΉΰΈΰΈΰΈΰΈ΄ΰΉΰΈ§/Face/PIN)
β β β
User ΰΈ’ΰΈ·ΰΈΰΈ’ΰΈ±ΰΈ!
β β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Key Pair:
β β - Private Key (ΰΉΰΈΰΉΰΈΰΉΰΈΰΈΰΈΈΰΈΰΈΰΈ£ΰΈΰΉ)
β β - Public Key (ΰΈͺΰΉΰΈΰΈΰΈ₯ΰΈ±ΰΈ)
β ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β credential (id, publicKey, attestation)
β
β POST /webauthn/register
β Body: {credential, label: "My Passkey"}
β βββββββββββββββββββββββββββΊ
β ββ 5. Spring Security WebAuthn Filter
β β ΰΈΰΈ£ΰΈ§ΰΈΰΈͺΰΈΰΈ Challenge
β β ΰΈΰΈ£ΰΈ§ΰΈΰΈͺΰΈΰΈ Attestation
β β ΰΈΰΈ±ΰΈΰΈΰΈΆΰΈ Public Key ΰΈ₯ΰΈ DB
β β (table: user_credentials)
β ββ
β βββββββββββββββββββββββββββ
β 200 OK (Passkey saved!)
β
ββ 6. ΰΉΰΈͺΰΈΰΈ "Passkey registered! β
"
π ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈΰΈΰΈ£ΰΈΰΈ Email/Password ΰΉΰΈ₯ΰΈ’!
Browser (login.html) Server Authenticator
ββββββββββββββββββββ ββββββ βββββββββββββββ
ββ 1. User ΰΈΰΈΰΈΰΈΈΰΉΰΈ‘ "Login with Passkey"
β
β POST /webauthn/authenticate/options
β Body: {}
β βββββββββββββββββββββββββββΊ
β ββ 2. Spring Security WebAuthn Filter
β β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Challenge (ΰΈΰΉΰΈ²ΰΈͺΰΈΈΰΉΰΈ‘ΰΉΰΈ«ΰΈ‘ΰΉ)
β β + allowCredentials (Passkey ΰΈΰΈ΅ΰΉΰΈ₯ΰΈΰΈΰΈ°ΰΉΰΈΰΈ΅ΰΈ’ΰΈΰΉΰΈ§ΰΉ)
β ββ
β βββββββββββββββββββββββββββ
β 200 OK (PublicKeyCredentialRequestOptions)
β {challenge, allowCredentials,...}
β
β 3. JavaScript ΰΉΰΈ£ΰΈ΅ΰΈ’ΰΈ Web API:
β navigator.credentials.get({publicKey: options})
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ
β ββ 4. Authenticator
β β ΰΉΰΈͺΰΈΰΈ Prompt
β β "ΰΉΰΈ₯ΰΈ·ΰΈΰΈ Passkey"
β β (ΰΈͺΰΉΰΈΰΈΰΈΰΈ΄ΰΉΰΈ§/Face/PIN)
β β β
User ΰΈ’ΰΈ·ΰΈΰΈ’ΰΈ±ΰΈ!
β β ΰΉΰΈΰΉ Private Key ΰΈΰΈ΅ΰΉΰΉΰΈΰΉΰΈΰΉΰΈ§ΰΉ
β β ΰΉΰΈΰΉΰΈ Challenge β Signature
β ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β assertion (id, signature, authenticatorData, clientDataJSON)
β
β POST /login/webauthn
β Body: {id, rawId, response: {signature, authenticatorData,...}}
β βββββββββββββββββββββββββββΊ
β ββ 5. Spring Security WebAuthn Filter
β β ΰΈΰΉΰΈΰΈ«ΰΈ² Credential ΰΈΰΈ²ΰΈ DB
β β ΰΈΰΈΆΰΈ Public Key ΰΈΰΈ΅ΰΉΰΉΰΈΰΉΰΈΰΉΰΈ§ΰΉ
β β Verify: signature + challenge + publicKey
β β β
ΰΈΰΈΉΰΈΰΈΰΉΰΈΰΈ!
β β
β β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ WebAuthnAuthentication
β β Principal = PublicKeyCredentialUserEntity β βΌοΈ ΰΉΰΈ‘ΰΉΰΉΰΈΰΉ UserDetails!
β β ΰΈΰΈ±ΰΈΰΈΰΈΆΰΈΰΈ₯ΰΈ Session (ΰΉΰΈ«ΰΈ‘ΰΈ·ΰΈΰΈ Password Login)
β β β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JSESSIONID Cookie
β ββ
β βββββββββββββββββββββββββββ
β 200 OK
β Set-Cookie: JSESSIONID=XYZ789; Path=/; HttpOnly
β Body: {"redirectUrl":"/","authenticated":true}
β
ββ 6. JavaScript: window.location.href = "/index.html"
β redirect ΰΉΰΈΰΈ«ΰΈΰΉΰΈ² Profile β
β οΈ ΰΈΰΈΈΰΈΰΈΰΈ΅ΰΉΰΈΰΉΰΈΰΈΰΈ£ΰΈ°ΰΈ§ΰΈ±ΰΈ (Bug ΰΈΰΈ΅ΰΉΰΉΰΈ£ΰΈ²ΰΉΰΈΰΉΰΉΰΈ):ΰΉΰΈ‘ΰΈ·ΰΉΰΈ Login ΰΈΰΉΰΈ§ΰΈ’ Password β Principal ΰΉΰΈΰΉΰΈ
UserDetailsΰΉΰΈ‘ΰΈ·ΰΉΰΈ Login ΰΈΰΉΰΈ§ΰΈ’ Passkey β Principal ΰΉΰΈΰΉΰΈPublicKeyCredentialUserEntityΰΈΰΈ±ΰΈΰΈΰΈ±ΰΉΰΈ Controller ΰΈΰΉΰΈΰΈΰΈ£ΰΈΰΈΰΈ£ΰΈ±ΰΈ ΰΈΰΈ±ΰΉΰΈ 2 ΰΈΰΈ£ΰΈ°ΰΉΰΈ ΰΈ (ΰΈΰΈΉΰΉΰΈ
AuthController.me()ΰΉΰΈ₯ΰΈ°PasskeyController)
Browser (index.html) Server
ββββββββββββββββββββ ββββββ
ββ 1. Browser ΰΉΰΈ«ΰΈ₯ΰΈ index.html (permitAll β ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈ auth)
β
β 2. JavaScript ΰΈΰΈ³ΰΈΰΈ²ΰΈ:
β loadUser() β GET /api/auth/me + Cookie: JSESSIONID=XYZ789
β loadPasskeys() β GET /api/passkeys + Cookie: JSESSIONID=XYZ789
β (Browser ΰΈͺΰΉΰΈ Cookie ΰΉΰΈΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄ΰΉΰΈΰΈ£ΰΈ²ΰΈ°ΰΉΰΈΰΉΰΈ same-origin)
β
β βββββββββββββββββββββββββββββββββββββββββββΊ
β ββ 3. Spring Security Filter Chain
β β ΰΈΰΉΰΈ²ΰΈ JSESSIONID=XYZ789
β β ΰΈ«ΰΈ² Session β ΰΈΰΈ SecurityContext
β β β User authenticated β
β β
β β AuthController.me()
β β β ΰΈΰΈΆΰΈ Principal (UserDetails ΰΈ«ΰΈ£ΰΈ·ΰΈ WebAuthnUser)
β β β resolveUsername() β ΰΈ«ΰΈ² email
β β β query app_users β return User info
β β
β β PasskeyController.listPasskeys()
β β β resolveUsername() β ΰΈ«ΰΈ² email
β β β query user_credentials β return passkey list
β ββ
β βββββββββββββββββββββββββββββββββββββββββββ
β /api/auth/me β 200 OK {email, displayName,...}
β /api/passkeys β 200 OK [{credentialId, label,...}]
β
ββ 4. ΰΉΰΈͺΰΈΰΈΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ User + Passkey List ΰΈΰΈΰΈ«ΰΈΰΉΰΈ² Profile β
ΰΈΰΉΰΈ²ΰΈΰΉΰΈΰΈΰΈΰΈ²ΰΈ£ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΈΰΈ²ΰΈ Cookie-based Session ΰΉΰΈΰΉΰΈΰΉ JWT (ΰΉΰΈΰΉΰΈ ΰΈͺΰΈ³ΰΈ«ΰΈ£ΰΈ±ΰΈ Mobile App ΰΈ«ΰΈ£ΰΈ·ΰΈ Microservices) ΰΈΰΉΰΈΰΈΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΈͺΰΉΰΈ§ΰΈΰΈΰΉΰΈ²ΰΈΰΉ ΰΈΰΈ±ΰΈΰΈΰΈ΅ΰΉ:
<!-- ΰΉΰΈΰΈ΄ΰΉΰΈ‘ dependency -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>π ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈΰΈ₯ΰΉΰΉΰΈ«ΰΈ‘ΰΉ: src/main/java/.../util/JwtUtil.java
// ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ class ΰΈΰΈ΅ΰΉΰΈͺΰΈ²ΰΈ‘ΰΈ²ΰΈ£ΰΈ:
// - generateToken(String email) β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JWT Token
// - validateToken(String token) β ΰΈΰΈ£ΰΈ§ΰΈΰΈͺΰΈΰΈΰΈ§ΰΉΰΈ² Token ΰΈΰΈΉΰΈΰΈΰΉΰΈΰΈΰΉΰΈ«ΰΈ‘
// - getEmailFromToken(String token) β ΰΈΰΈΆΰΈ email ΰΈΰΈ²ΰΈ Tokenπ ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈΰΈ₯ΰΉΰΉΰΈ«ΰΈ‘ΰΉ: src/main/java/.../config/JwtAuthenticationFilter.java
// ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Filter ΰΈΰΈ΅ΰΉΰΈΰΈ³ΰΈΰΈ²ΰΈΰΈΰΈΈΰΈ Request:
// - ΰΈΰΉΰΈ²ΰΈ Header "Authorization: Bearer <token>"
// - Validate Token
// - ΰΈΰΉΰΈ²ΰΈΰΈΉΰΈΰΈΰΉΰΈΰΈ β ΰΈΰΈ±ΰΉΰΈ SecurityContext
//
// extends OncePerRequestFilter// ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΈΰΈ²ΰΈ:
.formLogin(form -> form.loginPage("/login.html")...)
// ΰΉΰΈΰΉΰΈ:
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // β ΰΉΰΈ‘ΰΉΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Session!
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// ΰΈ₯ΰΈ .formLogin() ΰΈΰΈΰΈ
// ΰΈ₯ΰΈ .logout() ΰΈΰΈΰΈ (JWT ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈ logout ΰΈΰΈ±ΰΉΰΈ Server)// ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈ login() ΰΉΰΈ«ΰΉ return JWT Token ΰΉΰΈΰΈ Session:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// Authenticate ΰΉΰΈ«ΰΈ‘ΰΈ·ΰΈΰΈΰΉΰΈΰΈ΄ΰΈ‘
Authentication auth = authenticationManager.authenticate(...);
// β ΰΈ₯ΰΈ: session.setAttribute(...)
// β
ΰΉΰΈΰΈ΄ΰΉΰΈ‘: ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JWT Token
String token = jwtUtil.generateToken(request.email());
return ResponseEntity.ok(Map.of("token", token));
}
// ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈ me() ΰΉΰΈ«ΰΉΰΈΰΉΰΈ²ΰΈΰΈΰΈ²ΰΈ SecurityContext (ΰΉΰΈ‘ΰΉΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΉΰΈ’ΰΈΰΈ° ΰΉΰΈΰΈ£ΰΈ²ΰΈ°ΰΉΰΈΰΉΰΉΰΈΰΉΰΈ₯ΰΉΰΈ§)// ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΈΰΈ²ΰΈ:
// (ΰΉΰΈ‘ΰΉΰΈΰΉΰΈΰΈΰΈΰΈ³ΰΈΰΈ°ΰΉΰΈ£ β Cookie ΰΈͺΰΉΰΈΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄)
// ΰΉΰΈΰΉΰΈ:
// ΰΈΰΉΰΈΰΈΰΉΰΈΰΉΰΈ Token ΰΉΰΈ localStorage ΰΉΰΈ₯ΰΉΰΈ§ΰΉΰΈΰΈΰΈΰΈΈΰΈ Request
// Login:
const data = await res.json();
localStorage.setItem("token", data.token);
// ΰΈΰΈΈΰΈ API call:
fetch("/api/auth/me", {
headers: {
"Authorization": "Bearer " + localStorage.getItem("token")
}
});
// Logout:
localStorage.removeItem("token");7. β οΈ ΰΉΰΈΰΉΰΉΰΈ WebAuthn Authentication (ΰΈͺΰΉΰΈ§ΰΈΰΈΰΈ΅ΰΉΰΈ’ΰΈ²ΰΈΰΈΰΈ΅ΰΉΰΈͺΰΈΈΰΈ)
ΰΈΰΈ±ΰΈΰΈ«ΰΈ²: Spring Security's WebAuthn filter ΰΈΰΈΉΰΈΰΈΰΈΰΈΰΉΰΈΰΈΰΈ‘ΰΈ²ΰΉΰΈ«ΰΉΰΉΰΈΰΉΰΈΰΈ±ΰΈ Session
ΰΉΰΈ‘ΰΉΰΉΰΈΰΉΰΈΰΈΰΈΰΉΰΈΰΈΰΈ‘ΰΈ²ΰΉΰΈ«ΰΉΰΈΰΈ³ΰΈΰΈ²ΰΈΰΈΰΈ±ΰΈ JWT ΰΉΰΈΰΈ’ΰΈΰΈ£ΰΈ
ΰΈΰΈ²ΰΈΰΉΰΈ₯ΰΈ·ΰΈΰΈ:
A) ΰΉΰΈΰΉ "Hybrid" β WebAuthn ΰΈ’ΰΈ±ΰΈΰΉΰΈΰΉ Session, ΰΉΰΈΰΉΰΈ«ΰΈ₯ΰΈ±ΰΈ login ΰΈͺΰΈ³ΰΉΰΈ£ΰΉΰΈ
ΰΉΰΈ«ΰΉΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JWT Token ΰΉΰΈ₯ΰΉΰΈ§ΰΈͺΰΉΰΈΰΈΰΈ₯ΰΈ±ΰΈΰΉΰΈ (ΰΉΰΈΰΈ°ΰΈΰΈ³)
B) Override WebAuthnAuthenticationSuccessHandler ΰΉΰΈ«ΰΉ return JWT
ΰΉΰΈΰΈΰΈΰΈ΅ΰΉΰΈΰΈ°ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ Session
C) ΰΉΰΈΰΈ΅ΰΈ’ΰΈ WebAuthn authentication ΰΉΰΈΰΈ ΰΉΰΈ‘ΰΉΰΉΰΈΰΉ built-in filter ΰΈΰΈΰΈ Spring Security
ΰΉΰΈΰΈ₯ΰΉΰΈΰΈ΅ΰΉΰΈΰΉΰΈΰΈΰΉΰΈΰΉ/ΰΉΰΈΰΈ΄ΰΉΰΈ‘ ΰΈ£ΰΈ°ΰΈΰΈ±ΰΈΰΈΰΈ§ΰΈ²ΰΈ‘ΰΈ’ΰΈ²ΰΈ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π pom.xml β ΰΉΰΈΰΈ΄ΰΉΰΈ‘ jjwt dependency π’ ΰΈΰΉΰΈ²ΰΈ’
π JwtUtil.java β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈ«ΰΈ‘ΰΉ π‘ ΰΈΰΈ²ΰΈΰΈΰΈ₯ΰΈ²ΰΈ
π JwtAuthenticationFilter.java β ΰΈͺΰΈ£ΰΉΰΈ²ΰΈΰΉΰΈ«ΰΈ‘ΰΉ π‘ ΰΈΰΈ²ΰΈΰΈΰΈ₯ΰΈ²ΰΈ
π SecurityConfig.java β ΰΉΰΈΰΈ₯ΰΈ΅ΰΉΰΈ’ΰΈΰΉΰΈ’ΰΈΰΈ° (STATELESS + Filter) π΄ ΰΈ’ΰΈ²ΰΈ
π AuthController.java β ΰΉΰΈΰΉ login() return token π’ ΰΈΰΉΰΈ²ΰΈ’
π login.html β ΰΉΰΈΰΉΰΈ token + ΰΈͺΰΉΰΈ Header π‘ ΰΈΰΈ²ΰΈΰΈΰΈ₯ΰΈ²ΰΈ
π index.html β ΰΈͺΰΉΰΈ Authorization Header π‘ ΰΈΰΈ²ΰΈΰΈΰΈ₯ΰΈ²ΰΈ
π WebAuthn Integration β ΰΈΰΈ£ΰΈ±ΰΈ success handler π΄ ΰΈ’ΰΈ²ΰΈ
π‘ ΰΈΰΈ³ΰΉΰΈΰΈ°ΰΈΰΈ³: ΰΈΰΉΰΈ²ΰΉΰΈΰΉΰΈΰΉΰΈ§ΰΉΰΈ (Browser-based application) ΰΉΰΈΰΈ°ΰΈΰΈ³ΰΉΰΈ«ΰΉΰΉΰΈΰΉ Cookie-based Session ΰΉΰΈΰΈ£ΰΈ²ΰΈ°ΰΈΰΈ₯ΰΈΰΈΰΈ ΰΈ±ΰΈ’ΰΈΰΈ§ΰΉΰΈ² (Cookie HttpOnly ΰΈΰΉΰΈΰΈΰΈΰΈ±ΰΈ XSS ΰΉΰΈΰΉ) ΰΉΰΈ₯ΰΈ° Spring Security ΰΈΰΈ³ΰΉΰΈ«ΰΉΰΈΰΈ±ΰΈΰΉΰΈΰΈ‘ΰΈ±ΰΈΰΈ΄ ΰΉΰΈΰΉ JWT ΰΉΰΈ‘ΰΈ·ΰΉΰΈΰΈΰΉΰΈΰΈ support Mobile App ΰΈ«ΰΈ£ΰΈ·ΰΈ Microservices ΰΈΰΈ£ΰΈ΄ΰΈΰΉ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spring Security WebAuthn β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PublicKeyCredentialUserEntityRepository (interface) β β
β β UserCredentialRepository (interface) β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ β
β β implements β
β ββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ β
β β JpaPublicKeyCredentialUserEntityRepository (@Component)β
β β JpaUserCredentialRepository (@Component) β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ β
β β uses β
β ββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ β
β β UserEntityJpaRepository (Spring Data JPA) β β
β β CredentialRecordJpaRepository (Spring Data JPA) β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ β
β β maps β
β ββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ β
β β UserEntityRecord (@Entity) β β
β β CredentialRecordEntity (@Entity) β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ β
β β Hibernate auto-DDL β
β ββββββΌβββββ β
β βPostgreSQLβ β
β βββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Java 21+
- Docker & Docker Compose
- Maven (or use the included
mvnwwrapper)
# 1. Start PostgreSQL
docker compose up -d
# 2. Run the application
./mvnw spring-boot:run
# Windows: .\mvnw spring-boot:run
# 3. Open test UI
# http://localhost:8080/login.html| Setting | Value |
|---|---|
| Host | localhost:5432 |
| Database | webauthn |
| Username | webauthn |
| Password | webauthn |
| JDBC URL | jdbc:postgresql://localhost:5432/webauthn |
Tables are auto-created by Hibernate (ddl-auto: update):
| Table | Description |
|---|---|
app_users |
Application users (email/password) |
user_entities |
WebAuthn user entities (passkey owner) |
user_credentials |
WebAuthn credentials (passkeys) |
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "MyPassword123",
"displayName": "John Doe"
}'Response 201 Created:
{
"id": 1,
"email": "user@example.com",
"displayName": "John Doe",
"createdAt": "2026-02-12T07:00:00Z"
}Errors:
400β Validation error (missing email, password < 8 chars)409β Email already registered
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "user@example.com",
"password": "MyPassword123"
}'Response 200 OK + Set-Cookie: JSESSIONID=...:
{
"id": 1,
"email": "user@example.com",
"displayName": "John Doe",
"createdAt": "2026-02-12T07:00:00Z"
}Error 401:
{ "error": "AUTH_FAILED", "message": "Invalid email or password" }curl http://localhost:8080/api/auth/me -b cookies.txtResponse 200 OK:
{
"id": 1,
"email": "user@example.com",
"displayName": "John Doe",
"createdAt": "2026-02-12T07:00:00Z"
}curl -X POST http://localhost:8080/api/auth/logout -b cookies.txtResponse 200 OK:
{ "message": "Logged out successfully" }curl http://localhost:8080/api/passkeys -b cookies.txtResponse 200 OK:
[
{
"credentialId": "NxxppSNYduUBLSESigyfCA",
"label": "My Passkey",
"created": "2026-02-12T07:05:00Z",
"lastUsed": "2026-02-12T07:10:00Z"
}
]curl -X DELETE http://localhost:8080/api/passkeys/{credentialId} \
-b cookies.txt \
-H "X-XSRF-TOKEN: <token>"Response 200 OK:
{ "message": "Passkey deleted successfully" }These endpoints are handled by Spring Security's WebAuthn filters.
Client Server
β β
β POST /webauthn/register/options
β (authenticated, + XSRF token) β
β βββββββββββββββββββββββββββββββΊβ
β β
β ββββ PublicKeyCredentialCreationOptions (challenge, rp, user)
β β
β navigator.credentials.create()β
β (browser prompts biometric) β
β β
β POST /webauthn/register β
β (credential + XSRF token) β
β βββββββββββββββββββββββββββββββΊβ
β β
β ββββ 200 OK (passkey saved) β
Client Server
β β
β POST /webauthn/authenticate/options
β (public, no auth needed) β
β βββββββββββββββββββββββββββββββΊβ
β β
β ββββ PublicKeyCredentialRequestOptions (challenge, allowCredentials)
β β
β navigator.credentials.get() β
β (browser prompts biometric) β
β β
β POST /login/webauthn β
β (assertion response) β
β βββββββββββββββββββββββββββββββΊβ
β β
β ββββ 200 OK + session cookie β
src/main/java/dev/yutsuki/webauthn/
βββ WebauthnApplication.java # Spring Boot entry point
βββ config/
β βββ SecurityConfig.java # Security: CORS, CSRF, auth, WebAuthn
βββ controller/
β βββ AuthController.java # /api/auth/* (register, login, logout, me)
β βββ PasskeyController.java # /api/passkeys (list, delete)
βββ dto/
β βββ RegisterRequest.java # { email, password, displayName }
β βββ LoginRequest.java # { email, password }
β βββ UserResponse.java # { id, email, displayName, createdAt }
β βββ PasskeyResponse.java # { credentialId, label, created, lastUsed }
β βββ ApiError.java # { error, message }
βββ entity/
β βββ AppUser.java # app_users table
β βββ UserEntityRecord.java # user_entities table (WebAuthn)
β βββ CredentialRecordEntity.java # user_credentials table (WebAuthn)
βββ repository/
β βββ AppUserRepository.java # Spring Data JPA for AppUser
β βββ UserEntityJpaRepository.java # Spring Data JPA for UserEntityRecord
β βββ CredentialRecordJpaRepository.java # Spring Data JPA for CredentialRecordEntity
β βββ JpaPublicKeyCredentialUserEntityRepository.java # Adapter β Spring Security
β βββ JpaUserCredentialRepository.java # Adapter β Spring Security
βββ service/
βββ JpaUserDetailsService.java # UserDetailsService backed by PostgreSQL
Static HTML pages are included at /login.html and /index.html for testing.
They call the REST API endpoints β they are not required for production use.
- W3C Web Authentication (WebAuthn) Spec β ΰΈ‘ΰΈ²ΰΈΰΈ£ΰΈΰΈ²ΰΈ WebAuthn ΰΈΰΈ²ΰΈ W3C (spec ΰΉΰΈΰΉΰΈ‘)
- MDN Web Docs β Web Authentication API β ΰΈΰΈ³ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ WebAuthn API ΰΈΰΈ΅ΰΉΰΉΰΈΰΉΰΈ²ΰΉΰΈΰΈΰΉΰΈ²ΰΈ’
- passkeys.dev β ΰΉΰΈ«ΰΈ₯ΰΉΰΈΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ΰΉΰΈΰΈ΅ΰΉΰΈ’ΰΈ§ΰΈΰΈ±ΰΈ Passkeys ΰΈ£ΰΈ§ΰΈ‘ΰΈΰΈΈΰΈΰΈΰΈ’ΰΉΰΈ²ΰΈ
- FIDO Alliance β Passkeys β ΰΈΰΉΰΈΰΈ‘ΰΈΉΰΈ₯ΰΈΰΈ²ΰΈ FIDO Alliance ΰΈΰΈΉΰΉΰΈΰΈ³ΰΈ«ΰΈΰΈΰΈ‘ΰΈ²ΰΈΰΈ£ΰΈΰΈ²ΰΈ
- Spring Security Reference β WebAuthn β ΰΉΰΈΰΈΰΈͺΰΈ²ΰΈ£ Official: ΰΈ§ΰΈ΄ΰΈΰΈ΅ΰΈΰΈ±ΰΉΰΈΰΈΰΉΰΈ² Passkeys ΰΉΰΈ Spring Security
- Spring Blog β Passkeys Support β Blog ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ΰΈΰΈ²ΰΈ£ support Passkeys ΰΉΰΈ Spring Security 6.4+
- Spring Security β Session Management β ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ HttpSession, SecurityContext, ΰΉΰΈ₯ΰΈ° Session Fixation Protection
- Spring Security β Architecture β Filter Chain Architecture (SecurityFilterChain, DelegatingFilterProxy)
- MDN β HTTP Cookies β ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ Cookie ΰΈΰΈ·ΰΈΰΈΰΈ°ΰΉΰΈ£, HttpOnly, Secure, SameSite
- Spring Security β CSRF Protection β ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ CSRF Protection ΰΉΰΈ Spring Security
- OWASP β Cross-Site Request Forgery (CSRF) β ΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ΰΈΰΈ²ΰΈ£ΰΉΰΈΰΈ‘ΰΈΰΈ΅ CSRF
- JWT.io β ΰΉΰΈ§ΰΉΰΈΰΈΰΈΰΈ΄ΰΈΰΈ²ΰΈ’ JWT + Debugger
- RFC 7519 β JSON Web Token β Spec ΰΈΰΈΰΈ JWT
- JJWT (Java JWT) β Library ΰΈͺΰΈ³ΰΈ«ΰΈ£ΰΈ±ΰΈ Java ΰΈΰΈ΅ΰΉΰΉΰΈΰΈ°ΰΈΰΈ³
- Baeldung β Spring Security JWT β Tutorial ΰΈͺΰΈ£ΰΉΰΈ²ΰΈ JWT Authentication ΰΈΰΈ±ΰΈ Spring Security