diff --git a/api-test2.http b/api-test2.http new file mode 100644 index 0000000..20bdb18 --- /dev/null +++ b/api-test2.http @@ -0,0 +1,474 @@ +### ======================================== +### ======================================== +### ๐ŸŽฏ Auth & Trip ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Final Fix) +### ======================================== + +### ์ „์—ญ ๋ณ€์ˆ˜ ์„ค์ • (๋žœ๋ค ๊ฐ’ ์‚ฌ์šฉ) +@random_id = {{$randomInt}} +@email = test{{random_id}}@naver.com +@password = password1234 +@new_password = newpassword5678 +@auth_host = http://localhost:8080 +@trip_host = http://localhost:8081 + +### ======================================== +### ๐Ÿ“Œ Phase 1: ํšŒ์›๊ฐ€์ž… ๋ฐ ํ† ํฐ ๋ฐœ๊ธ‰ ๊ฒ€์ฆ +### ======================================== + +### 1-1. [Auth] ํšŒ์›๊ฐ€์ž… +# @name signup +POST {{auth_host}}/users +Content-Type: application/json + +{ + "email": "{{email}}", + "password": "{{password}}", + "name": "ํ…Œ์Šคํ„ฐ", + "gender": "M", + "age": 25 +} + +> {% + client.test("ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", function() { + client.assert(response.status === 201, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 201์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.accessToken !== null, "Access Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + client.assert(response.body.data.refreshToken !== null, "Refresh Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + }); + + // ํ† ํฐ ์ €์žฅ + client.global.set("auth_token", response.body.data.accessToken); + client.global.set("refresh_token", response.body.data.refreshToken); + client.global.set("member_id", response.body.data.id); + + client.log("โœ… ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ"); + client.log("Member ID: " + response.body.data.id); + client.log("Access Token: " + response.body.data.accessToken.substring(0, 20) + "..."); +%} + + +### 1-2. [Auth] DB์— Refresh Token ์ €์žฅ ํ™•์ธ (๋””๋ฒ„๊น…์šฉ) +GET {{auth_host}}/debug/tokens + +> {% + client.test("Refresh Token์ด DB์— ์ €์žฅ๋จ", function() { + client.assert(response.body.length > 0, "Refresh Token์ด DB์— ์กด์žฌํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… DB์— Refresh Token ์ €์žฅ ํ™•์ธ"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 2: ๋‚ด ์ •๋ณด ์กฐํšŒ (์‹ ๊ทœ API) +### ======================================== + +### 2-1. [Auth] ๋‚ด ์ •๋ณด ์กฐํšŒ +GET {{auth_host}}/users/me +Authorization: Bearer {{auth_token}} + +> {% + client.test("๋‚ด ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.email === "test@naver.com", "์ด๋ฉ”์ผ์ด ์ผ์น˜ํ•ด์•ผ ํ•จ"); + client.assert(response.body.data.name === "ํ…Œ์Šคํ„ฐ", "์ด๋ฆ„์ด ์ผ์น˜ํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ๋‚ด ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต: " + response.body.data.name); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 3: Auth-Trip ์—ฐ๋™ ๊ฒ€์ฆ +### ======================================== + +### 3-1. [Trip] ์—ฌํ–‰ ์ƒ์„ฑ (Auth ํ† ํฐ ์‚ฌ์šฉ) +# @name createTrip +POST {{trip_host}}/trips +Content-Type: application/json +Authorization: Bearer {{auth_token}} + +{ + "destinationId": "550e8400-e29b-41d4-a716-446655440001", + "title": "์ œ์ฃผ๋„ ์šฐ์ • ์—ฌํ–‰", + "description": "์นœ๊ตฌ๋“ค๊ณผ ํ•จ๊ป˜ํ•˜๋Š” ์ฆ๊ฑฐ์šด ์ œ์ฃผ๋„ ์—ฌํ–‰์ž…๋‹ˆ๋‹ค.", + "start": "2026-07-01", + "end": "2026-07-05", + "open": true, + "maxParticipants": 4, + "category": "DOMESTIC", + "hashTags": ["์ œ์ฃผ๋„", "์šฐ์ •์—ฌํ–‰", "๋ง›์ง‘ํƒ๋ฐฉ"] +} + +> {% + client.test("์—ฌํ–‰ ์ƒ์„ฑ ์„ฑ๊ณต", function() { + client.assert(response.status === 201 || response.status === 200, "์—ฌํ–‰ ์ƒ์„ฑ ์„ฑ๊ณต"); + client.assert(response.body.data.id !== null, "Trip ID๊ฐ€ ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + }); + + client.global.set("trip_id", response.body.data.id); + client.log("โœ… ์—ฌํ–‰ ์ƒ์„ฑ ์™„๋ฃŒ"); + client.log("Trip ID: " + response.body.data.id); +%} + + + +### 3-2. [Trip] ์ƒ์„ฑ๋œ ์—ฌํ–‰ ์ƒ์„ธ ์กฐํšŒ +GET {{trip_host}}/trips/{{trip_id}} +Authorization: Bearer {{auth_token}} + +> {% + client.test("์—ฌํ–‰ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + }); + client.log("โœ… ์—ฌํ–‰ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต"); +%} + +### 3-3. [Trip] ๋‚ด ์—ฌํ–‰ ๋ชฉ๋ก ์กฐํšŒ +GET {{trip_host}}/trips/my?tripStatus=RECRUITING +Authorization: Bearer {{auth_token}} + +> {% + client.test("๋‚ด ์—ฌํ–‰ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + }); + client.log("โœ… ๋‚ด ์—ฌํ–‰ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 4: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ API (์‹ ๊ทœ) +### ======================================== + +### 4-1. [Auth] ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ - ์˜ฌ๋ฐ”๋ฅธ ๋น„๋ฐ€๋ฒˆํ˜ธ +POST {{auth_host}}/users/verify-password +Content-Type: application/json +Authorization: Bearer {{auth_token}} + +{ + "password": "{{password}}" +} + +> {% + client.test("๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.isValid === true, "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ์„ฑ๊ณต (์˜ฌ๋ฐ”๋ฅธ ๋น„๋ฐ€๋ฒˆํ˜ธ)"); +%} + +### 4-2. [Auth] ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ - ์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ +POST {{auth_host}}/users/verify-password +Content-Type: application/json +Authorization: Bearer {{auth_token}} + +{ + "password": "wrongpassword" +} + +> {% + client.test("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ ๊ฐ์ง€", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.isValid === false, "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์•ผ ํ•จ"); + }); + client.log("โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ ๊ฐ์ง€ ์„ฑ๊ณต"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 5: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๋ฐ ํ† ํฐ ๋ฌดํšจํ™” +### ======================================== + +### 5-1. [Auth] ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ (๋ชจ๋“  ํ† ํฐ ๋ฌดํšจํ™”) +# @name changePassword +PATCH {{auth_host}}/users/password +Content-Type: application/json +Authorization: Bearer {{auth_token}} + +{ + "currentPassword": "{{password}}", + "newPassword": "{{new_password}}" +} + +> {% + client.test("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.accessToken !== null, "์ƒˆ Access Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + client.assert(response.body.data.refreshToken !== null, "์ƒˆ Refresh Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + }); + + // ๊ธฐ์กด ํ† ํฐ ๋ฐฑ์—… + client.global.set("old_auth_token", client.global.get("auth_token")); + + // ์ƒˆ ํ† ํฐ ์ €์žฅ + client.global.set("auth_token", response.body.data.accessToken); + client.global.set("refresh_token", response.body.data.refreshToken); + + client.log("โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์™„๋ฃŒ"); + client.log("์ƒˆ Access Token: " + response.body.data.accessToken.substring(0, 20) + "..."); +%} + +### 5-2. [Trip] ๊ธฐ์กด ํ† ํฐ์œผ๋กœ ์—ฌํ–‰ ์กฐํšŒ ์‹œ๋„ (์‹คํŒจํ•ด์•ผ ํ•จ) +GET {{trip_host}}/trips/my?tripStatus=RECRUITING +Authorization: Bearer {{old_auth_token}} + +> {% + client.test("๊ธฐ์กด ํ† ํฐ ๋ฌดํšจํ™” ํ™•์ธ", function() { + client.assert(response.status === 401 || response.status === 403, "๊ธฐ์กด ํ† ํฐ์€ ๋ฌดํšจํ™”๋˜์–ด์•ผ ํ•จ"); + }); + client.log("โœ… ๊ธฐ์กด ํ† ํฐ ๋ฌดํšจํ™” ํ™•์ธ"); +%} + +### 5-3. [Trip] ์ƒˆ ํ† ํฐ์œผ๋กœ ์—ฌํ–‰ ์กฐํšŒ (์„ฑ๊ณตํ•ด์•ผ ํ•จ) +GET {{trip_host}}/trips/my?tripStatus=RECRUITING +Authorization: Bearer {{auth_token}} + +> {% + client.test("์ƒˆ ํ† ํฐ์œผ๋กœ ์ ‘๊ทผ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒˆ ํ† ํฐ์œผ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ์ƒˆ ํ† ํฐ์œผ๋กœ ์ •์ƒ ์ ‘๊ทผ ํ™•์ธ"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 6: ๋กœ๊ทธ์ธ (ํ‘œ์ค€ Body ๋ฐฉ์‹) +### ======================================== + +### 6-1. [Auth] ๋กœ๊ทธ์ธ (๋ณ€๊ฒฝ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ) +# @name login +POST {{auth_host}}/login +Content-Type: application/json + +{ + "id": "{{email}}", + "password": "{{new_password}}" +} + +> {% + client.test("๋กœ๊ทธ์ธ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + }); + + if (response.status === 200) { + client.global.set("auth_token", response.body.data.accessToken); + client.global.set("refresh_token", response.body.data.refreshToken); + client.log("โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต"); + } else { + client.log("โŒ ๋กœ๊ทธ์ธ ์‹คํŒจ: " + response.status); + // ์‹คํŒจ ์‹œ ์ดํ›„ ์š”์ฒญ๋“ค์ด ๋ณ€์ˆ˜ ์—†์Œ ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ฒ ์ง€๋งŒ, ์ ์–ด๋„ ๋กœ๊ทธ์— ์‹คํŒจ๊ฐ€ ๋ช…ํ™•ํžˆ ์ฐํž˜ + } +%} + +### ======================================== +### ๐Ÿ“Œ Phase 7: ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ (Reissue) +### ======================================== + +### 7-1. [Auth] Refresh Token์œผ๋กœ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ +POST {{auth_host}}/auth/reissue +Content-Type: application/json +Cookie: refreshToken={{refresh_token}} + +> {% + client.test("ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.accessToken !== null, "์ƒˆ Access Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + }); + + client.global.set("auth_token", response.body.data.accessToken); + client.global.set("refresh_token", response.body.data.refreshToken); + + client.log("โœ… ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต"); + client.log("์ƒˆ Access Token: " + response.body.data.accessToken.substring(0, 20) + "..."); +%} + +### 7-2. [Trip] ์žฌ๋ฐœ๊ธ‰๋œ ํ† ํฐ์œผ๋กœ ์—ฌํ–‰ ์กฐํšŒ +GET {{trip_host}}/trips/{{trip_id}} +Authorization: Bearer {{auth_token}} + +> {% + client.test("์žฌ๋ฐœ๊ธ‰๋œ ํ† ํฐ์œผ๋กœ ์ ‘๊ทผ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์žฌ๋ฐœ๊ธ‰๋œ ํ† ํฐ์œผ๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ์žฌ๋ฐœ๊ธ‰๋œ ํ† ํฐ์œผ๋กœ ์ •์ƒ ์ ‘๊ทผ ํ™•์ธ"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 8: ํšŒ์›์ •๋ณด ์ˆ˜์ • ๋ฐ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ +### ======================================== + +### 8-1. [Auth] ํšŒ์›์ •๋ณด ์ˆ˜์ • (์„ฑ๋ณ„, ๋‚˜์ด ํฌํ•จ) +PUT {{auth_host}}/users +Content-Type: application/json +Authorization: Bearer {{auth_token}} + +{ + "password": "{{new_password}}", + "newPassword": "{{new_password}}", + "name": "์ˆ˜์ •๋œํ…Œ์Šคํ„ฐ", + "gender": "F", + "age": 30 +} + +> {% + client.test("ํšŒ์›์ •๋ณด ์ˆ˜์ • ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.name === "์ˆ˜์ •๋œํ…Œ์Šคํ„ฐ", "์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•จ"); + client.assert(response.body.data.accessToken !== null, "์ƒˆ Access Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + }); + + client.global.set("auth_token", response.body.data.accessToken); + client.log("โœ… ํšŒ์›์ •๋ณด ์ˆ˜์ • ์™„๋ฃŒ"); + client.log("๋ณ€๊ฒฝ๋œ ์ด๋ฆ„: " + response.body.data.name); +%} + +### 8-2. [Auth] ์ˆ˜์ •๋œ ์ •๋ณด ํ™•์ธ +GET {{auth_host}}/users/me +Authorization: Bearer {{auth_token}} + +> {% + client.test("์ˆ˜์ •๋œ ์ •๋ณด ํ™•์ธ", function() { + client.assert(response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.name === "์ˆ˜์ •๋œํ…Œ์Šคํ„ฐ", "์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•จ"); + }); + client.log("โœ… ์ˆ˜์ •๋œ ์ •๋ณด ํ™•์ธ ์™„๋ฃŒ"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 9: ํšŒ์›ํƒˆํ‡ด ๋ฐ ํ† ํฐ ์‚ญ์ œ +### ======================================== + +### 9-1. [Trip] ํƒˆํ‡ด ์ „ ์—ฌํ–‰ ์กฐํšŒ (์„ฑ๊ณต) +GET {{trip_host}}/trips/my?tripStatus=RECRUITING +Authorization: Bearer {{auth_token}} + +> {% + client.test("ํƒˆํ‡ด ์ „ ์—ฌํ–‰ ์กฐํšŒ ์„ฑ๊ณต", function() { + client.assert(response.status === 200, "ํƒˆํ‡ด ์ „์—๋Š” ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ํƒˆํ‡ด ์ „ ์—ฌํ–‰ ์กฐํšŒ ์„ฑ๊ณต"); +%} + +### 9-2. [Auth] ํšŒ์›ํƒˆํ‡ด +DELETE {{auth_host}}/users +Content-Type: application/json +Authorization: Bearer {{auth_token}} + +{ + "password": "{{new_password}}" +} + +> {% + client.test("ํšŒ์›ํƒˆํ‡ด ์„ฑ๊ณต", function() { + client.assert(response.status === 204 || response.status === 200, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 204 ๋˜๋Š” 200์ด์–ด์•ผ ํ•จ"); + }); + client.log("โœ… ํšŒ์›ํƒˆํ‡ด ์™„๋ฃŒ"); +%} + +### 9-3. [Auth] DB์—์„œ Refresh Token ์‚ญ์ œ ํ™•์ธ +GET {{auth_host}}/debug/tokens + +> {% + client.test("Refresh Token ์‚ญ์ œ ํ™•์ธ", function() { + // ํƒˆํ‡ดํ•œ ํšŒ์›์˜ ํ† ํฐ์ด ์—†์–ด์•ผ ํ•จ + let memberTokenExists = response.body.some(token => + token.memberId === client.global.get("member_id") + ); + client.assert(memberTokenExists === false, "ํƒˆํ‡ดํ•œ ํšŒ์›์˜ Refresh Token์ด ์‚ญ์ œ๋˜์–ด์•ผ ํ•จ"); + }); + client.log("โœ… Refresh Token ์‚ญ์ œ ํ™•์ธ"); +%} + +### 9-4. [Trip] ํƒˆํ‡ด ํ›„ ์—ฌํ–‰ ์กฐํšŒ ์‹œ๋„ (์‹คํŒจํ•ด์•ผ ํ•จ) +GET {{trip_host}}/trips/my?tripStatus=RECRUITING +Authorization: Bearer {{auth_token}} + +> {% + client.test("ํƒˆํ‡ด ํ›„ ์ ‘๊ทผ ์ฐจ๋‹จ ํ™•์ธ", function() { + client.assert(response.status === 401 || response.status === 403, "ํƒˆํ‡ดํ•œ ํšŒ์›์€ ์ ‘๊ทผ ๋ถˆ๊ฐ€ํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ํƒˆํ‡ด ํ›„ ์ ‘๊ทผ ์ฐจ๋‹จ ํ™•์ธ"); +%} + +### 9-5. [Auth] ํƒˆํ‡ด ํ›„ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์‹œ๋„ (์‹คํŒจํ•ด์•ผ ํ•จ) +POST {{auth_host}}/auth/reissue +Content-Type: application/json +Cookie: refreshToken={{refresh_token}} + +> {% + client.test("ํƒˆํ‡ด ํ›„ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์ฐจ๋‹จ", function() { + client.assert(response.status === 401 || response.status === 404, "ํƒˆํ‡ดํ•œ ํšŒ์›์€ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ๋ถˆ๊ฐ€ํ•ด์•ผ ํ•จ"); + }); + client.log("โœ… ํƒˆํ‡ด ํ›„ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์ฐจ๋‹จ ํ™•์ธ"); +%} + +### ======================================== +### ๐Ÿ“Œ Phase 10: ์ตœ์ข… ๊ฒ€์ฆ - ์ƒˆ ํšŒ์›์œผ๋กœ ์žฌ๊ฐ€์ž… +### ======================================== + +### 10-1. [Auth] ๋™์ผ ์ด๋ฉ”์ผ๋กœ ์žฌ๊ฐ€์ž… ์‹œ๋„ (์‹คํŒจํ•ด์•ผ ํ•จ) +# @name signup_fail +POST {{auth_host}}/users +Content-Type: application/json + +{ + "email": "{{email}}", + "password": "{{password}}", + "name": "์žฌ๊ฐ€์ž…์‹œ๋„", + "gender": "M", + "age": 25 +} + +> {% + client.test("ํƒˆํ‡ดํ•œ ์ด๋ฉ”์ผ๋กœ ์žฌ๊ฐ€์ž… ๋ฐฉ์ง€", function() { + // BusinessException(400) ๋˜๋Š” GlobalExceptionHandler ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ(200) ํ™•์ธ + // ์—ฌ๊ธฐ์„œ๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€(๊ฐ€์ž…์ด ์•ˆ ๋˜๋Š”์ง€)๋งŒ ํ™•์ธํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. + client.log("โœ… ํƒˆํ‡ดํ•œ ์ด๋ฉ”์ผ ์žฌ๊ฐ€์ž… ๋ฐฉ์ง€ ํ™•์ธ"); + }); +%} + +### 10-2. [Auth] ์™„์ „ ์ƒˆ๋กœ์šด ์ด๋ฉ”์ผ๋กœ ํšŒ์›๊ฐ€์ž… (์„ฑ๋ณ„/๋‚˜์ด ํฌํ•จ) +# @name signup_new +POST {{auth_host}}/users +Content-Type: application/json + +{ + "email": "brandnew{{$randomInt}}@naver.com", + "password": "{{password}}", + "name": "์ƒˆ์‚ฌ์šฉ์ž", + "gender": "F", + "age": 22 +} + +> {% + client.test("์ƒˆ ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต", function() { + client.assert(response.status === 201, "์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 201์ด์–ด์•ผ ํ•จ"); + client.assert(response.body.data.accessToken !== null, "Access Token์ด ๋ฐ˜ํ™˜๋˜์–ด์•ผ ํ•จ"); + }); + + // [ํ•ต์‹ฌ ์ˆ˜์ •] ํ† ํฐ์„ ์ „์—ญ ๋ณ€์ˆ˜์— ์ €์žฅ + client.global.set("new_auth_token", response.body.data.accessToken); + + client.log("โœ… ์ƒˆ ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ"); + // ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ ์ถœ๋ ฅ (undefined ๋ฐฉ์ง€) + if (response.body.data.accessToken) { + client.log("New Access Token: " + response.body.data.accessToken.substring(0, 10) + "..."); + } +%} + +### 10-3. [Trip] ์ƒˆ ํšŒ์›์œผ๋กœ ์—ฌํ–‰ ์ƒ์„ฑ +POST {{trip_host}}/trips +Content-Type: application/json +Authorization: Bearer {{new_auth_token}} + +{ + "destinationId": "550e8400-e29b-41d4-a716-446655440002", + "title": "์„œ์šธ ๋ฐ์ดํŠธ", + "description": "์‹ ์ž… ํšŒ์›์˜ ์ฒซ ์—ฌํ–‰", + "start": "2026-08-01", + "end": "2026-08-03", + "open": true, + "maxParticipants": 2, + "category": "DOMESTIC", + "hashTags": ["์„œ์šธ", "๋ฐ์ดํŠธ", "์นดํŽ˜ํˆฌ์–ด"] +} + +> {% + client.test("์ƒˆ ํšŒ์›์œผ๋กœ ์—ฌํ–‰ ์ƒ์„ฑ ์„ฑ๊ณต", function() { + client.assert(response.status === 201 || response.status === 200, "์—ฌํ–‰ ์ƒ์„ฑ ์„ฑ๊ณต"); + }); + client.log("โœ… ์ƒˆ ํšŒ์›์œผ๋กœ ์—ฌํ–‰ ์ƒ์„ฑ ์™„๋ฃŒ"); + client.log("๋ชจ๋“  ํ…Œ์ŠคํŠธ ์™„๋ฃŒ! ๐ŸŽ‰"); +%} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/config/CustomUserDetails.java b/src/main/java/com/retrip/auth/application/config/CustomUserDetails.java index 83952d3..30b17a8 100644 --- a/src/main/java/com/retrip/auth/application/config/CustomUserDetails.java +++ b/src/main/java/com/retrip/auth/application/config/CustomUserDetails.java @@ -17,12 +17,12 @@ public class CustomUserDetails implements UserDetails, OAuth2User { private final Member member; private Map attributes; - + // ์ผ๋ฐ˜ ๋กœ๊ทธ์ธ์šฉ ์ƒ์„ฑ์ž public CustomUserDetails(Member member) { this.member = member; } - + // OAuth2 ๋กœ๊ทธ์ธ์šฉ ์ƒ์„ฑ์ž public CustomUserDetails(Member member, Map attributes) { this.member = member; this.attributes = attributes; @@ -37,16 +37,38 @@ public Collection getAuthorities() { @Override public String getPassword() { - + // VO์—์„œ ๊ฐ’ ๊บผ๋‚ด๊ธฐ return member.getPassword().getValue(); } @Override public String getUsername() { + // UserDetails์˜ ์‹๋ณ„์ž๋Š” PK(UUID)๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ + return member.getId().toString(); + } + + // ============================================================= + // โ˜… [์ถ”๊ฐ€] JwtProvider์—์„œ Claims ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํŽธ์˜ ๋ฉ”์„œ๋“œ๋“ค + // ============================================================= + public String getEmail() { return member.getEmail().getValue(); } + public String getRealName() { + // OAuth2User์˜ getName()๊ณผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ์ด๋ฆ„์„ ๋‹ค๋ฅด๊ฒŒ ์„ค์ • + return member.getName().getValue(); + } + + public String getGender() { + return member.getGender(); + } + public Integer getAge() { + return member.getAge(); + } + // ============================================================= + + // OAuth2User ๋ฉ”์„œ๋“œ @Override public Map getAttributes() { return attributes; @@ -54,6 +76,13 @@ public Map getAttributes() { @Override public String getName() { + // OAuth2User์˜ ์‹๋ณ„์ž๋„ PK(UUID)๋กœ ํ†ต์ผ return member.getId().toString(); } + + // UserDetails ํ•„์ˆ˜ ๋ฉ”์„œ๋“œ๋“ค (๊ณ„์ • ์ƒํƒœ ํ™•์ธ - ๋ชจ๋‘ true ๋ฐ˜ํ™˜) + @Override public boolean isAccountNonExpired() { return true; } + @Override public boolean isAccountNonLocked() { return true; } + @Override public boolean isCredentialsNonExpired() { return true; } + @Override public boolean isEnabled() { return true; } } \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/config/JwtProvider.java b/src/main/java/com/retrip/auth/application/config/JwtProvider.java index 566ded7..ecfdd20 100644 --- a/src/main/java/com/retrip/auth/application/config/JwtProvider.java +++ b/src/main/java/com/retrip/auth/application/config/JwtProvider.java @@ -1,6 +1,9 @@ package com.retrip.auth.application.config; import com.retrip.auth.application.in.response.LoginResponse; +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.vo.MemberEmail; +import com.retrip.auth.domain.vo.MemberName; import io.jsonwebtoken.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,11 +22,7 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; -import com.retrip.auth.application.config.CustomUserDetails; -/** - * JWT ํ† ํฐ์˜ ์ƒ์„ฑ(Sign) ๋ฐ ๊ฒ€์ฆ(Verify)์„ ๋‹ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค (RSA ๋ฐฉ์‹) - */ @Slf4j @Component @RequiredArgsConstructor @@ -31,54 +30,53 @@ public class JwtProvider { private final JwtConfig jwtConfig; - /** - * [์ƒ์„ฑ] ์ธ์ฆ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ RSA ์„œ๋ช…๋œ Access/Refresh Token ์ƒ์„ฑ - */ + // ... createToken, generateTokens ๋“ฑ ์ƒ์„ฑ ๋กœ์ง์€ ๊ธฐ์กด ์œ ์ง€ ... + // (์œ„์— ์ž‘์„ฑํ•˜์‹  ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ ๋‘์…”๋„ ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ getAuthentication๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.) + public LoginResponse.TokenResponse generateTokens(Authentication authentication) { Instant now = Instant.now(); String authorities = String.join(",", getAuthorities(authentication)); String memberId = authentication.getName(); - String email = authentication.getName(); + String email = ""; + String name = ""; + String gender = null; + Integer age = null; Object principal = authentication.getPrincipal(); if (principal instanceof CustomUserDetails userDetails) { - memberId = userDetails.getName(); // CustomUserDetails.getName()์€ UUID(String) ๋ฐ˜ํ™˜ - email = userDetails.getUsername(); // CustomUserDetails.getUsername()์€ ์ด๋ฉ”์ผ ๋ฐ˜ํ™˜ + memberId = userDetails.getName(); // UUID + email = userDetails.getEmail(); + name = userDetails.getRealName(); + gender = userDetails.getGender(); + age = userDetails.getAge(); + } else { + // principal์ด String์ธ ๊ฒฝ์šฐ (๋ฐฉ์–ด ์ฝ”๋“œ) + memberId = authentication.getName(); } - String accessToken = createToken( - memberId, // sub (UUID) - email, // claim: username (Email) - authorities, - now, - jwtConfig.getAccess().getExpireMin() - ); - - String refreshToken = createToken( - memberId, // sub (UUID) - email, // claim: username (Email) - authorities, - now, - jwtConfig.getRefresh().getExpireMin() - ); + String accessToken = createToken(memberId, email, name, gender, age, authorities, now, jwtConfig.getAccess().getExpireMin()); + String refreshToken = createToken(memberId, email, name, gender, age, authorities, now, jwtConfig.getRefresh().getExpireMin()); return new LoginResponse.TokenResponse(accessToken, refreshToken); } - private String createToken(String subject, String username, String authorities, Instant issuedAt, long expirationMinutes) { + private String createToken(String subject, String email, String name, String gender, Integer age, + String authorities, Instant issuedAt, long expirationMinutes) { try { PrivateKey privateKey = getPrivateKey(jwtConfig.getPrivateKey()); Instant expiration = issuedAt.plus(expirationMinutes, ChronoUnit.MINUTES); - return Jwts.builder() + JwtBuilder builder = Jwts.builder() .subject(subject) - .claims( - Map.of( - "username", username, - "authorities", authorities - ) - ) + .claim("username", email) + .claim("name", name) + .claim("authorities", authorities); + + if (gender != null) builder.claim("gender", gender); + if (age != null) builder.claim("age", age); + + return builder .issuedAt(Date.from(issuedAt)) .expiration(Date.from(expiration)) .signWith(privateKey, Jwts.SIG.RS256) @@ -88,34 +86,7 @@ private String createToken(String subject, String username, String authorities, } } - /** - * [๊ฒ€์ฆ] ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (RSA Public Key ์‚ฌ์šฉ) - */ - public boolean validateToken(String token) { - try { - PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey()); - Jwts.parser() - .verifyWith(publicKey) - .build() - .parseSignedClaims(token); - return true; - } catch (SecurityException | MalformedJwtException e) { - log.info("Invalid JWT Token", e); - } catch (ExpiredJwtException e) { - log.info("Expired JWT Token", e); - } catch (UnsupportedJwtException e) { - log.info("Unsupported JWT Token", e); - } catch (IllegalArgumentException e) { - log.info("JWT claims string is empty.", e); - } catch (Exception e) { - log.error("JWT validation error", e); - } - return false; - } - - /** - * [ํŒŒ์‹ฑ] ํ† ํฐ์—์„œ ์ธ์ฆ ๊ฐ์ฒด ์ถ”์ถœ - */ + // [์ค‘์š” ์ˆ˜์ •] Authentication ๊ฐ์ฒด ์ƒ์„ฑ ์‹œ CustomUserDetails ์žฌ๊ตฌ์„ฑ public Authentication getAuthentication(String token) { try { PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey()); @@ -125,21 +96,52 @@ public Authentication getAuthentication(String token) { .parseSignedClaims(token) .getPayload(); - String username = claims.get("username", String.class); + // 1. Claims์—์„œ ์ •๋ณด ์ถ”์ถœ + String memberId = claims.getSubject(); // UUID + String email = claims.get("username", String.class); + String name = claims.get("name", String.class); String authoritiesStr = claims.get("authorities", String.class); + String gender = claims.get("gender", String.class); + Integer age = claims.get("age", Integer.class); + // 2. ๊ถŒํ•œ ๋ชฉ๋ก ์ƒ์„ฑ List authorities = Arrays.stream(authoritiesStr.split(",")) .map(String::trim) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); - return new UsernamePasswordAuthenticationToken(username, null, authorities); + // 3. ์ž„์‹œ Member ๊ฐ์ฒด ์ƒ์„ฑ (๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” null ์ฒ˜๋ฆฌ) + Member member = Member.builder() + .id(UUID.fromString(memberId)) + .email(new MemberEmail(email)) + .name(new MemberName(name)) + .gender(gender) + .age(age) + .password(null) // ์ธ์ฆ๋œ ์ƒํƒœ์ด๋ฏ€๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆํ•„์š” + .build(); + + // 4. CustomUserDetails ์ƒ์„ฑ + CustomUserDetails principal = new CustomUserDetails(member); + + // 5. Authentication ๋ฆฌํ„ด (์ด์ œ Principal์€ CustomUserDetails์ž„) + return new UsernamePasswordAuthenticationToken(principal, token, authorities); } catch (Exception e) { throw new RuntimeException("์ธ์ฆ ์ •๋ณด ์ถ”์ถœ ์‹คํŒจ", e); } } + public boolean validateToken(String token) { + try { + PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey()); + Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(token); + return true; + } catch (Exception e) { + log.error("JWT validation error: {}", e.getMessage()); + } + return false; + } + public Claims parseClaims(String token) { try { PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey()); @@ -149,29 +151,21 @@ public Claims parseClaims(String token) { .parseSignedClaims(token) .getPayload(); } catch (ExpiredJwtException e) { - // ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ด์–ด๋„ ์ •๋ณด๋ฅผ ๊บผ๋‚ด๊ธฐ ์œ„ํ•ด Claims ๋ฐ˜ํ™˜ return e.getClaims(); } catch (Exception e) { throw new RuntimeException("ํ† ํฐ ํŒŒ์‹ฑ ์‹คํŒจ", e); } } -//ํ‚ค ํŒŒ์‹ฑ ํ—ฌํผ private PrivateKey getPrivateKey(String key) throws Exception { - String sanitizedKey = key - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replaceAll("\\s", ""); + String sanitizedKey = key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\s", ""); byte[] keyBytes = Base64.getDecoder().decode(sanitizedKey); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); return KeyFactory.getInstance("RSA").generatePrivate(spec); } private PublicKey getPublicKey(String key) throws Exception { - String sanitizedKey = key - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replaceAll("\\s", ""); + String sanitizedKey = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replaceAll("\\s", ""); byte[] keyBytes = Base64.getDecoder().decode(sanitizedKey); X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); return KeyFactory.getInstance("RSA").generatePublic(spec); diff --git a/src/main/java/com/retrip/auth/application/config/SecurityConfig.java b/src/main/java/com/retrip/auth/application/config/SecurityConfig.java index 3037104..ad6e270 100644 --- a/src/main/java/com/retrip/auth/application/config/SecurityConfig.java +++ b/src/main/java/com/retrip/auth/application/config/SecurityConfig.java @@ -5,6 +5,7 @@ import com.retrip.auth.application.out.repository.RefreshTokenRepository; import com.retrip.auth.infra.adapter.in.rest.filter.JwtAuthenticationFilter; import com.retrip.auth.infra.adapter.in.rest.filter.LoginAuthenticationFilter; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -20,6 +21,10 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.context.DelegatingSecurityContextRepository; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -41,7 +46,6 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Bean public AuthenticationManager authenticationManager( HttpSecurity http, @@ -58,13 +62,13 @@ public AuthenticationManager authenticationManager( return authenticationManagerBuilder.build(); } - @Bean public LoginAuthenticationFilter loginAuthenticationFilter( JwtConfig jwtConfig, AuthenticationManager authenticationManager, JwtProvider jwtProvider) { - LoginAuthenticationFilter filter = new LoginAuthenticationFilter(jwtConfig, authenticationManager,jwtProvider,refreshTokenRepository); + // ์ƒ์„ฑ์ž์— refreshTokenRepository๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + LoginAuthenticationFilter filter = new LoginAuthenticationFilter(jwtConfig, authenticationManager, jwtProvider, refreshTokenRepository); return filter; } @@ -73,19 +77,34 @@ public SecurityFilterChain securityFilterChain( HttpSecurity http, LoginAuthenticationFilter loginAuthenticationFilter) throws Exception { + // [ํ•ต์‹ฌ] 401 ์—๋Ÿฌ ํ•ด๊ฒฐ์„ ์œ„ํ•œ SecurityContextRepository ์„ค์ • + // ์ด ๋ถ€๋ถ„์ด ๋น ์ ธ์žˆ์–ด์„œ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์œ ์ง€๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. + SecurityContextRepository securityContextRepository = new DelegatingSecurityContextRepository( + new RequestAttributeSecurityContextRepository(), + new HttpSessionSecurityContextRepository() + ); + http + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) - // ์„ธ์…˜ ๊ด€๋ฆฌ: Stateless (JWT ํ•„์ˆ˜ ์„ค์ •) + // [ํ•ต์‹ฌ] SecurityContext ์„ค์ • ์ถ”๊ฐ€ + .securityContext(context -> context + .securityContextRepository(securityContextRepository) + ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(handler -> handler + .authenticationEntryPoint((request, response, authException) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + }) + ) - // ํ•„ํ„ฐ ๋ฐฐ์น˜ .addFilterAt(loginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - // OAuth2 ์„ค์ • .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService) @@ -93,20 +112,11 @@ public SecurityFilterChain securityFilterChain( .successHandler(oAuth2LoginSuccessHandler) ) - // URL ๊ถŒํ•œ ์„ค์ • .authorizeHttpRequests(auth -> auth - .requestMatchers(HttpMethod.POST, "/users").permitAll() - - .requestMatchers("/login/**", "/oauth2/**", "/auth/reissue", "/auth/logout","/").permitAll() - .requestMatchers( - "/swagger-ui/**", - "/v3/api-docs/**", - "/swagger-resources/**", - "/webjars/**" - ).permitAll() + .requestMatchers("/login/**", "/oauth2/**", "/auth/reissue", "/auth/logout", "/").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll() .requestMatchers("/debug/**").permitAll() - .anyRequest().authenticated() ); diff --git a/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java b/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java index 420b715..0ed40b8 100644 --- a/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java +++ b/src/main/java/com/retrip/auth/application/config/UsernamePasswordAuthenticationProvider.java @@ -1,6 +1,8 @@ package com.retrip.auth.application.config; import com.retrip.auth.application.in.MemberQueryService; +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.exception.PasswordNotMatchException; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; @@ -19,21 +21,23 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro private final PasswordEncoder passwordEncoder; @Override - public Authentication authenticate(Authentication authentication) - throws AuthenticationException { - String username = authentication.getName(); - String password = String.valueOf(authentication.getCredentials()); + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String email = authentication.getName(); + String password = (String) authentication.getCredentials(); - UserDetails user = memberQueryService.loadUserByUsername(username); + Member member = memberQueryService.getMemberByEmail(email); - if (!passwordEncoder.matches(password, user.getPassword())) { - throw new BadCredentialsException("Bad credentials"); + if (!passwordEncoder.matches(password, member.getPasswordValue())) { + throw new PasswordNotMatchException(); } + // โ˜… ํ•ต์‹ฌ: ์—ฌ๊ธฐ์„œ CustomUserDetails๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋„ฃ์–ด์ค˜์•ผ JwtProvider๊ฐ€ ์ •๋ณด๋ฅผ ๊บผ๋‚ผ ์ˆ˜ ์žˆ์Œ + CustomUserDetails userDetails = new CustomUserDetails(member); + return new UsernamePasswordAuthenticationToken( - user, - password, - user.getAuthorities().stream().toList() + userDetails, + null, + userDetails.getAuthorities() ); } diff --git a/src/main/java/com/retrip/auth/application/in/AuthService.java b/src/main/java/com/retrip/auth/application/in/AuthService.java index a85f3f4..0ff4d48 100644 --- a/src/main/java/com/retrip/auth/application/in/AuthService.java +++ b/src/main/java/com/retrip/auth/application/in/AuthService.java @@ -2,12 +2,16 @@ import com.retrip.auth.application.config.JwtProvider; import com.retrip.auth.application.in.response.LoginResponse; +import com.retrip.auth.application.out.repository.MemberRepository; import com.retrip.auth.application.out.repository.RefreshTokenRepository; +import com.retrip.auth.domain.entity.Member; import com.retrip.auth.domain.entity.RefreshToken; +import com.retrip.auth.domain.exception.MemberNotFoundException; import com.retrip.auth.domain.exception.common.ErrorCode; import com.retrip.auth.domain.exception.common.InvalidValueException; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -15,6 +19,8 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; +import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Service @@ -23,36 +29,64 @@ public class AuthService { private final JwtProvider jwtProvider; private final RefreshTokenRepository refreshTokenRepository; + private final MemberRepository memberRepository; @Transactional public LoginResponse.TokenResponse reissue(String token) { if (!jwtProvider.validateToken(token)) { - throw new InvalidValueException(ErrorCode.INVALID_INPUT_VALUE, "์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค."); + throw new BadCredentialsException("Invalid Refresh Token"); } - RefreshToken savedToken = refreshTokenRepository.findById(token) - .orElseThrow(() -> new InvalidValueException(ErrorCode.ENTITY_NOT_FOUND, "๋กœ๊ทธ์•„์›ƒ ๋˜์—ˆ๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ† ํฐ์ž…๋‹ˆ๋‹ค.")); + RefreshToken savedToken = refreshTokenRepository.findByTokenValue(token) + .orElseThrow(() -> new BadCredentialsException("Refresh Token not found or member deleted")); Claims claims = jwtProvider.parseClaims(token); String memberId = claims.getSubject(); - String authoritiesStr = (String) claims.get("authorities"); - Authentication auth = new UsernamePasswordAuthenticationToken( + // ํšŒ์› ์ƒํƒœ ํ™•์ธ + Member member = memberRepository.findById(UUID.fromString(memberId)) + .orElseThrow(MemberNotFoundException::new); + + if (member.getIsDeleted()) { + refreshTokenRepository.delete(savedToken); + throw new BadCredentialsException("ํƒˆํ‡ดํ•œ ํšŒ์›์ž…๋‹ˆ๋‹ค."); + } + + Authentication authentication = new UsernamePasswordAuthenticationToken( memberId, null, - Arrays.stream(authoritiesStr.split(",")) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()) + List.of(new SimpleGrantedAuthority("ROLE_USER")) ); - LoginResponse.TokenResponse newTokens = jwtProvider.generateTokens(auth); + LoginResponse.TokenResponse newTokens = jwtProvider.generateTokens(authentication); + // ๊ธฐ์กด Refresh Token ์‚ญ์ œ refreshTokenRepository.delete(savedToken); - refreshTokenRepository.save(new RefreshToken(newTokens.refreshToken(), memberId, authoritiesStr)); + + // ์ƒˆ Refresh Token ์ €์žฅ + RefreshToken newRefreshToken = new RefreshToken( + newTokens.refreshToken(), + memberId, + "ROLE_USER" + ); + refreshTokenRepository.save(newRefreshToken); return newTokens; } + @Transactional + public void saveRefreshToken(String tokenValue, UUID memberId) { + + refreshTokenRepository.deleteByMemberId(memberId.toString()); + + RefreshToken refreshToken = new RefreshToken( + tokenValue, + memberId.toString(), + "ROLE_USER" + ); + refreshTokenRepository.save(refreshToken); + } + @Transactional public void logout(String token) { refreshTokenRepository.deleteById(token); diff --git a/src/main/java/com/retrip/auth/application/in/MemberQueryService.java b/src/main/java/com/retrip/auth/application/in/MemberQueryService.java index 4dfd53c..38add79 100644 --- a/src/main/java/com/retrip/auth/application/in/MemberQueryService.java +++ b/src/main/java/com/retrip/auth/application/in/MemberQueryService.java @@ -20,7 +20,13 @@ public class MemberQueryService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - Member member = memberQueryRepository.findByEmailWithAuthorities(username).orElseThrow(MemberNotFoundException::new); + Member member = memberQueryRepository.findByEmailWithAuthorities(username) + .orElseThrow(MemberNotFoundException::new); return new CustomUserDetails(member); } -} + + public Member getMemberByEmail(String email) { + return memberQueryRepository.findByEmailWithAuthorities(email) + .orElseThrow(MemberNotFoundException::new); + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/MemberService.java b/src/main/java/com/retrip/auth/application/in/MemberService.java index 1eab147..b4dc196 100644 --- a/src/main/java/com/retrip/auth/application/in/MemberService.java +++ b/src/main/java/com/retrip/auth/application/in/MemberService.java @@ -1,21 +1,28 @@ package com.retrip.auth.application.in; -import com.retrip.auth.application.in.request.MemberCreateRequest; -import com.retrip.auth.application.in.request.MemberDeleteRequest; -import com.retrip.auth.application.in.request.MemberUpdateRequest; -import com.retrip.auth.application.in.response.MemberCreateResponse; -import com.retrip.auth.application.in.response.MemberUpdateResponse; +import com.retrip.auth.application.config.JwtProvider; +import com.retrip.auth.application.in.request.*; +import com.retrip.auth.application.in.response.*; import com.retrip.auth.application.in.usercase.ManageMemberUseCase; import com.retrip.auth.application.out.repository.MemberRepository; +import com.retrip.auth.application.out.repository.RefreshTokenRepository; import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.entity.RefreshToken; import com.retrip.auth.domain.exception.MemberNotFoundException; +import com.retrip.auth.domain.exception.common.BusinessException; +import com.retrip.auth.domain.exception.common.ErrorCode; import com.retrip.auth.domain.vo.MemberEmail; -import com.sun.jdi.request.DuplicateRequestException; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; @Service @RequiredArgsConstructor @@ -23,38 +30,153 @@ public class MemberService implements ManageMemberUseCase { private final MemberRepository memberRepository; + private final RefreshTokenRepository refreshTokenRepository; private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; @Override public MemberCreateResponse createUser(MemberCreateRequest request) { String encode = passwordEncoder.encode(request.password()); - boolean isDuplicate = !memberRepository.findByEmailAndIsDeletedFalse(new MemberEmail(request.email())).isEmpty(); - if (isDuplicate) { - throw new DuplicateRequestException("Email already exists"); + + // ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ (ํƒˆํ‡ด ํšŒ์› ํฌํ•จ) + List existingMembers = memberRepository.findByEmail(new MemberEmail(request.email())); + + if (!existingMembers.isEmpty()) { + Member existing = existingMembers.get(0); + if (existing.getIsDeleted()) { + throw new BusinessException(ErrorCode.DELETED_MEMBER_CANNOT_REJOIN); + } + throw new BusinessException(ErrorCode.EMAIL_ALREADY_EXISTS); } + Member member = memberRepository.save(request.to(encode)); - return MemberCreateResponse.of(member); + + // ํ† ํฐ ์ƒ์„ฑ + Authentication authentication = createAuthentication(member); + LoginResponse.TokenResponse tokens = jwtProvider.generateTokens(authentication); + + // Refresh Token ์ €์žฅ + RefreshToken refreshToken = new RefreshToken( + tokens.refreshToken(), + member.getId().toString(), + "ROLE_USER" + ); + refreshTokenRepository.save(refreshToken); + + return MemberCreateResponse.of(member, tokens.accessToken(), tokens.refreshToken()); + } + + @Override + public MemberUpdateResponse updateUser(UUID memberId, MemberUpdateRequest request) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + + // ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + if (!passwordEncoder.matches(request.password(), member.getPassword().getValue())) { + throw new BadCredentialsException("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + // ์ •๋ณด ์ˆ˜์ • + String encodedNewPassword = request.newPassword() != null + ? passwordEncoder.encode(request.newPassword()) + : member.getPassword().getValue(); + + member.update( + request.name(), + encodedNewPassword, + request.gender(), + request.age() + ); + + // Access Token ์žฌ๋ฐœ๊ธ‰ + Authentication authentication = createAuthentication(member); + LoginResponse.TokenResponse tokens = jwtProvider.generateTokens(authentication); + + return MemberUpdateResponse.of(member, tokens.accessToken()); } @Override - public MemberUpdateResponse updateUser(MemberUpdateRequest request) { - String encodeNewPassword = passwordEncoder.encode(request.newPassword()); + public void deleteUser(UUID memberId, MemberDeleteRequest request) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); - Member member = findByEmail(request.email()); - if (isSamePassword(member.getPassword().getValue(), request.password())) { //์—ฌ๊ธฐ์— ํŒจ์Šค์›Œ๋“œ ๊ฒ€์ฆ ๋กœ์ง์ด ๋“ค์–ด๊ฐ€๋Š”๊ฒŒ ๋งž๋Š”์ง€..? - member.update(encodeNewPassword, request.name()); + // ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + if (!passwordEncoder.matches(request.password(), member.getPassword().getValue())) { + throw new BadCredentialsException("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } - return MemberUpdateResponse.of(member); + + // Refresh Token ์‚ญ์ œ + refreshTokenRepository.deleteByMemberId(memberId.toString()); + member.delete(); } @Override - public void deleteUser(MemberDeleteRequest request) { - Member member = findByEmail(request.email()); - if (isSamePassword(member.getPassword().getValue(), request.password())) { //์—ฌ๊ธฐ์— ํŒจ์Šค์›Œ๋“œ ๊ฒ€์ฆ ๋กœ์ง์ด ๋“ค์–ด๊ฐ€๋Š”๊ฒŒ ๋งž๋Š”์ง€..? - member.delete(); + public ChangePasswordResponse changePassword(UUID memberId, ChangePasswordRequest request) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + + // ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + if (!passwordEncoder.matches(request.currentPassword(), member.getPassword().getValue())) { + throw new BadCredentialsException("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } + + // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ + String encodedNewPassword = passwordEncoder.encode(request.newPassword()); + member.updatePassword(encodedNewPassword); + + // ๊ธฐ์กด Refresh Token ๋ชจ๋‘ ์‚ญ์ œ + refreshTokenRepository.deleteByMemberId(memberId.toString()); + + // ์ƒˆ ํ† ํฐ ๋ฐœ๊ธ‰ + Authentication authentication = createAuthentication(member); + LoginResponse.TokenResponse tokens = jwtProvider.generateTokens(authentication); + + // ์ƒˆ Refresh Token ์ €์žฅ + RefreshToken refreshToken = new RefreshToken( + tokens.refreshToken(), + memberId.toString(), + "ROLE_USER" + ); + refreshTokenRepository.save(refreshToken); + + return new ChangePasswordResponse(tokens.accessToken(), tokens.refreshToken()); + } + + @Override + @Transactional(readOnly = true) + public MemberInfoResponse getMyInfo(UUID memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + return MemberInfoResponse.of(member); + } + + @Override + @Transactional(readOnly = true) + public VerifyPasswordResponse verifyPassword(UUID memberId, VerifyPasswordRequest request) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + + boolean isValid = passwordEncoder.matches( + request.password(), + member.getPassword().getValue() + ); + + return new VerifyPasswordResponse(isValid); } + @Override + @Transactional(readOnly = true) + public UUID findIdByEmail(String email) { + return memberRepository.findByEmailAndIsDeletedFalse(new MemberEmail(email)) + .stream() + .findFirst() + .map(Member::getId) + .orElseThrow(MemberNotFoundException::new); + } + + // ======================================== + // Private Helper Methods + // ======================================== private Member findByEmail(String email) { return memberRepository.findByEmailAndIsDeletedFalse(new MemberEmail(email)) @@ -68,4 +190,11 @@ private boolean isSamePassword(String password, String inputPassword) { return true; } -} + private Authentication createAuthentication(Member member) { + return new UsernamePasswordAuthenticationToken( + member.getId().toString(), // โœ… email โ†’ memberId๋กœ ๋ณ€๊ฒฝ + null, + List.of(new SimpleGrantedAuthority("ROLE_USER")) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/request/ChangePasswordRequest.java b/src/main/java/com/retrip/auth/application/in/request/ChangePasswordRequest.java new file mode 100644 index 0000000..9450b1a --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/request/ChangePasswordRequest.java @@ -0,0 +1,6 @@ +package com.retrip.auth.application.in.request; + +public record ChangePasswordRequest( + String currentPassword, + String newPassword +) {} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/request/LoginRequest.java b/src/main/java/com/retrip/auth/application/in/request/LoginRequest.java index 0b8f436..1f38114 100644 --- a/src/main/java/com/retrip/auth/application/in/request/LoginRequest.java +++ b/src/main/java/com/retrip/auth/application/in/request/LoginRequest.java @@ -1,9 +1,7 @@ package com.retrip.auth.application.in.request; -import org.springframework.security.crypto.password.PasswordEncoder; - -public record LoginRequest ( - String email, +public record LoginRequest( + String id, // email -> id๋กœ ๋ณ€๊ฒฝ (JSON์˜ "id" ํ•„๋“œ์™€ ๋งคํ•‘๋จ) String password ) { -} +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java b/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java index 8337743..9a57744 100644 --- a/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java +++ b/src/main/java/com/retrip/auth/application/in/request/MemberCreateRequest.java @@ -10,9 +10,23 @@ public record MemberCreateRequest( @Schema(description = "๋น„๋ฐ€๋ฒˆํ˜ธ") String password, @Schema(description = "์‚ฌ์šฉ์ž ์ด๋ฆ„") - String name + String name, + @Schema(description = "์„ฑ๋ณ„ (M/F)") + String gender, + @Schema(description = "๋‚˜์ด") + Integer age ) { public Member to(String encodePassword) { - return Member.create(name, email, encodePassword); + return Member.builder() + .email(new com.retrip.auth.domain.vo.MemberEmail(email)) + .password(new com.retrip.auth.domain.vo.MemberPassword(encodePassword)) + .name(new com.retrip.auth.domain.vo.MemberName(name)) + .gender(gender) + .age(age) + .isDeleted(false) + .provider("local") + .isVerified(false) + .id(java.util.UUID.randomUUID()) + .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/request/MemberDeleteRequest.java b/src/main/java/com/retrip/auth/application/in/request/MemberDeleteRequest.java index 5c5328c..f6c824a 100644 --- a/src/main/java/com/retrip/auth/application/in/request/MemberDeleteRequest.java +++ b/src/main/java/com/retrip/auth/application/in/request/MemberDeleteRequest.java @@ -4,8 +4,7 @@ @Schema(description = "Member ์‚ญ์ œ Request") public record MemberDeleteRequest( - @Schema(description = "์ด๋ฉ”์ผ") - String email, + @Schema(description = "๋น„๋ฐ€๋ฒˆํ˜ธ") String password ) { diff --git a/src/main/java/com/retrip/auth/application/in/request/MemberUpdateRequest.java b/src/main/java/com/retrip/auth/application/in/request/MemberUpdateRequest.java index 89db2ca..84d6f17 100644 --- a/src/main/java/com/retrip/auth/application/in/request/MemberUpdateRequest.java +++ b/src/main/java/com/retrip/auth/application/in/request/MemberUpdateRequest.java @@ -1,17 +1,18 @@ package com.retrip.auth.application.in.request; -import com.retrip.auth.domain.entity.Member; import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "Member ํšŒ์›๊ฐ€์ž… Request") +@Schema(description = "Member ์ •๋ณด ์ˆ˜์ • Request") public record MemberUpdateRequest( - @Schema(description = "์ด๋ฉ”์ผ") - String email, @Schema(description = "๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ") String password, @Schema(description = "์ƒˆ๋กœ์šด ๋น„๋ฐ€๋ฒˆํ˜ธ") String newPassword, @Schema(description = "์‚ฌ์šฉ์ž ์ด๋ฆ„") - String name + String name, + @Schema(description = "์„ฑ๋ณ„ (M/F)") + String gender, + @Schema(description = "๋‚˜์ด") + Integer age ) { -} +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/request/VerifyPasswordRequest.java b/src/main/java/com/retrip/auth/application/in/request/VerifyPasswordRequest.java new file mode 100644 index 0000000..be636f0 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/request/VerifyPasswordRequest.java @@ -0,0 +1,5 @@ +package com.retrip.auth.application.in.request; + +public record VerifyPasswordRequest( + String password +) {} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/response/ChangePasswordResponse.java b/src/main/java/com/retrip/auth/application/in/response/ChangePasswordResponse.java new file mode 100644 index 0000000..8d17000 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/response/ChangePasswordResponse.java @@ -0,0 +1,6 @@ +package com.retrip.auth.application.in.response; + +public record ChangePasswordResponse( + String accessToken, + String refreshToken +) {} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java b/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java index fbdd953..4843897 100644 --- a/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java +++ b/src/main/java/com/retrip/auth/application/in/response/MemberCreateResponse.java @@ -1,15 +1,32 @@ package com.retrip.auth.application.in.response; import com.retrip.auth.domain.entity.Member; - import java.util.UUID; public record MemberCreateResponse( UUID id, String email, - String name + String name, + String accessToken, + String refreshToken ) { public static MemberCreateResponse of(Member member) { - return new MemberCreateResponse(member.getId(), member.getEmail().getValue(), member.getName().getValue()); + return new MemberCreateResponse( + member.getId(), + member.getEmail().getValue(), + member.getName().getValue(), + null, + null + ); + } + + public static MemberCreateResponse of(Member member, String accessToken, String refreshToken) { + return new MemberCreateResponse( + member.getId(), + member.getEmail().getValue(), + member.getName().getValue(), + accessToken, + refreshToken + ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/response/MemberInfoResponse.java b/src/main/java/com/retrip/auth/application/in/response/MemberInfoResponse.java new file mode 100644 index 0000000..97bc151 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/response/MemberInfoResponse.java @@ -0,0 +1,22 @@ + +package com.retrip.auth.application.in.response; + +import com.retrip.auth.domain.entity.Member; +import java.time.LocalDateTime; +import java.util.UUID; + +public record MemberInfoResponse( + UUID id, + String email, + String name, + LocalDateTime createdAt +) { + public static MemberInfoResponse of(Member member) { + return new MemberInfoResponse( + member.getId(), + member.getEmail().getValue(), + member.getName().getValue(), + member.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/response/MemberUpdateResponse.java b/src/main/java/com/retrip/auth/application/in/response/MemberUpdateResponse.java index 133c413..78cd425 100644 --- a/src/main/java/com/retrip/auth/application/in/response/MemberUpdateResponse.java +++ b/src/main/java/com/retrip/auth/application/in/response/MemberUpdateResponse.java @@ -1,15 +1,29 @@ package com.retrip.auth.application.in.response; import com.retrip.auth.domain.entity.Member; - import java.util.UUID; public record MemberUpdateResponse( UUID id, String email, - String name + String name, + String accessToken ) { public static MemberUpdateResponse of(Member member) { - return new MemberUpdateResponse(member.getId(), member.getEmail().getValue(), member.getName().getValue()); + return new MemberUpdateResponse( + member.getId(), + member.getEmail().getValue(), + member.getName().getValue(), + null + ); + } + + public static MemberUpdateResponse of(Member member, String accessToken) { + return new MemberUpdateResponse( + member.getId(), + member.getEmail().getValue(), + member.getName().getValue(), + accessToken + ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/response/VerifyPasswordResponse.java b/src/main/java/com/retrip/auth/application/in/response/VerifyPasswordResponse.java new file mode 100644 index 0000000..6f3efe0 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/in/response/VerifyPasswordResponse.java @@ -0,0 +1,5 @@ +package com.retrip.auth.application.in.response; + +public record VerifyPasswordResponse( + boolean isValid +) {} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java b/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java index 4c1da0f..833b563 100644 --- a/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java +++ b/src/main/java/com/retrip/auth/application/in/usercase/ManageMemberUseCase.java @@ -1,16 +1,19 @@ package com.retrip.auth.application.in.usercase; +import com.retrip.auth.application.in.request.*; +import com.retrip.auth.application.in.response.*; -import com.retrip.auth.application.in.request.MemberCreateRequest; -import com.retrip.auth.application.in.request.MemberDeleteRequest; -import com.retrip.auth.application.in.request.MemberUpdateRequest; -import com.retrip.auth.application.in.response.MemberCreateResponse; -import com.retrip.auth.application.in.response.MemberUpdateResponse; +import java.util.UUID; public interface ManageMemberUseCase { MemberCreateResponse createUser(MemberCreateRequest request); - MemberUpdateResponse updateUser(MemberUpdateRequest request); + MemberUpdateResponse updateUser(UUID memberId, MemberUpdateRequest request); + void deleteUser(UUID memberId, MemberDeleteRequest request); + ChangePasswordResponse changePassword(UUID memberId, ChangePasswordRequest request); - void deleteUser(MemberDeleteRequest request); -} + MemberInfoResponse getMyInfo(UUID memberId); + VerifyPasswordResponse verifyPassword(UUID memberId, VerifyPasswordRequest request); + + UUID findIdByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java b/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java index d4ebab5..c0d4a82 100644 --- a/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java +++ b/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java @@ -1,14 +1,14 @@ + package com.retrip.auth.application.out.repository; import com.retrip.auth.domain.entity.Member; - import com.retrip.auth.domain.vo.MemberEmail; +import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; +import java.util.UUID; -public interface MemberRepository extends JpaRepository { +public interface MemberRepository extends JpaRepository { List findByEmailAndIsDeletedFalse(MemberEmail email); -} + List findByEmail(MemberEmail email); +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/out/repository/RefreshTokenRepository.java b/src/main/java/com/retrip/auth/application/out/repository/RefreshTokenRepository.java index fa05fd9..b252166 100644 --- a/src/main/java/com/retrip/auth/application/out/repository/RefreshTokenRepository.java +++ b/src/main/java/com/retrip/auth/application/out/repository/RefreshTokenRepository.java @@ -3,5 +3,11 @@ import com.retrip.auth.domain.entity.RefreshToken; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface RefreshTokenRepository extends JpaRepository { + Optional findByTokenValue(String tokenValue); + Optional findByMemberId(String memberId); + + void deleteByMemberId(String memberId); } \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/domain/entity/BaseEntity.java b/src/main/java/com/retrip/auth/domain/entity/BaseEntity.java index a4cb68d..af0ec98 100644 --- a/src/main/java/com/retrip/auth/domain/entity/BaseEntity.java +++ b/src/main/java/com/retrip/auth/domain/entity/BaseEntity.java @@ -2,6 +2,7 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import lombok.Getter; import java.time.LocalDateTime; @@ -9,6 +10,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +@Getter @EntityListeners(AuditingEntityListener.class) @MappedSuperclass public abstract class BaseEntity { @@ -18,5 +20,4 @@ public abstract class BaseEntity { @LastModifiedDate private LocalDateTime editedAt; -} - +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/domain/entity/Member.java b/src/main/java/com/retrip/auth/domain/entity/Member.java index 544de71..5cc2918 100644 --- a/src/main/java/com/retrip/auth/domain/entity/Member.java +++ b/src/main/java/com/retrip/auth/domain/entity/Member.java @@ -36,55 +36,60 @@ public class Member extends BaseEntity { private Boolean isDeleted; - // [์‹ ๊ทœ ์ถ”๊ฐ€] ์†Œ์…œ ๋กœ๊ทธ์ธ ์ •๋ณด ๋ฐ ๋ณธ์ธ์ธ์ฆ ์ •๋ณด ํ•„๋“œ @Column(length = 20) - private String provider; // e.g., "local", "kakao", "google" + private String provider; @Column(unique = true) - private String providerId; // ์†Œ์…œ ํ”Œ๋žซํผ์˜ ๊ณ ์œ  ์‚ฌ์šฉ์ž ID + private String providerId; @Column(length = 88, unique = true) - private String ci; // ๋ณธ์ธ์ธ์ฆ ์—ฐ๊ณ„์ •๋ณด (CI) + private String ci; @Column(nullable = false) - private boolean isVerified = false; // ๋ณธ์ธ์ธ์ฆ ์—ฌ๋ถ€ + @Builder.Default + private boolean isVerified = false; - public static Member create(String name, String email, String password, List authorities) { - Member member = Member.builder() - .id(UUID.randomUUID()) - .name(new MemberName(name)) - .email(new MemberEmail(email)) - .password(new MemberPassword(password)) - .isDeleted(false) - .provider("local") // [์‹ ๊ทœ] ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - .isVerified(false) // [์‹ ๊ทœ] ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - .build(); - member.authorities = new Authorities(authorities, member); - return member; + @Column(name = "gender") + private String gender; + + @Column(name = "age") + private Integer age; + + public String getPasswordValue() { + return this.password != null ? this.password.getValue() : null; } - public static Member create(String name, String email, String password) { + public String getEmailValue() { + return this.email != null ? this.email.getValue() : null; + } + + public String getNameValue() { + return this.name != null ? this.name.getValue() : null; + } + + public static Member create(String name, String email, String password, List authorities, String gender, Integer age) { Member member = Member.builder() .id(UUID.randomUUID()) .name(new MemberName(name)) .email(new MemberEmail(email)) - .isDeleted(false) .password(new MemberPassword(password)) - .provider("local") // [์‹ ๊ทœ] ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - .isVerified(false) // [์‹ ๊ทœ] ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + .isDeleted(false) + .provider("local") + .isVerified(false) + .gender(gender) + .age(age) .build(); - member.authorities = new Authorities(List.of("user"), member); + member.authorities = new Authorities(authorities, member); return member; } - // [์‹ ๊ทœ ์ถ”๊ฐ€] ์†Œ์…œ ๋กœ๊ทธ์ธ ํšŒ์› ์ƒ์„ฑ ๋ฉ”์„œ๋“œ public static Member createSocialMember(String name, String email, String provider, String providerId) { Member member = Member.builder() .id(UUID.randomUUID()) .name(new MemberName(name)) .email(new MemberEmail(email)) .isDeleted(false) - .password(new MemberPassword(null)) // ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์Œ + .password(new MemberPassword(null)) .provider(provider) .providerId(providerId) .isVerified(false) @@ -93,16 +98,21 @@ public static Member createSocialMember(String name, String email, String provid return member; } - public void update(String password, String name) { - this.password = new MemberPassword(password); - this.name = new MemberName(name); + public void update(String name, String password, String gender, Integer age) { + if (name != null) this.name = new MemberName(name); + if (password != null) this.password = new MemberPassword(password); + if (gender != null) this.gender = gender; + if (age != null) this.age = age; } - // [์‹ ๊ทœ ์ถ”๊ฐ€] ์†Œ์…œ ํšŒ์› ์ •๋ณด ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ public void updateSocialInfo(String name) { this.name = new MemberName(name); } + public void updatePassword(String encodedPassword) { + this.password = new MemberPassword(encodedPassword); + } + public void delete() { this.isDeleted = true; } diff --git a/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java b/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java index 45a157f..c362a02 100644 --- a/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java +++ b/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java @@ -15,7 +15,11 @@ public enum ErrorCode { INVALID_ACCESS(FORBIDDEN, "Common-006","์ ‘๊ทผ ๊ถŒํ•œ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), MEMBER_NOT_FOUND(HttpStatus.UNAUTHORIZED, "Member-001", "๋ฉค๋ฒ„ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), - PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "Member-001", "๋น„๋ฐ€ ๋ฒˆํ˜ธ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค."), + PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "Member-002", "๋น„๋ฐ€ ๋ฒˆํ˜ธ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค."), + + // โœ… ์ถ”๊ฐ€ + DELETED_MEMBER_CANNOT_REJOIN(HttpStatus.BAD_REQUEST, "Member-003", "ํƒˆํ‡ดํ•œ ์ด๋ฉ”์ผ์€ ์žฌ๊ฐ€์ž…ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "Member-004", "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."), ; private final HttpStatus status; @@ -27,4 +31,4 @@ public enum ErrorCode { this.code = code; this.message = message; } -} +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/AuthController.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/AuthController.java index 32aa191..1210c59 100644 --- a/src/main/java/com/retrip/auth/infra/adapter/in/rest/AuthController.java +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/AuthController.java @@ -2,6 +2,7 @@ import com.retrip.auth.application.in.AuthService; import com.retrip.auth.application.in.response.LoginResponse; +import com.retrip.auth.domain.exception.common.InvalidValueException; import com.retrip.auth.infra.adapter.in.rest.common.ApiResponse; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -21,7 +22,7 @@ public ApiResponse reissue( HttpServletResponse response ) { if (refreshToken == null) { - throw new IllegalArgumentException("Refresh Token์ด ์ฟ ํ‚ค์— ์—†์Šต๋‹ˆ๋‹ค."); + throw new InvalidValueException("Refresh Token์ด ์ฟ ํ‚ค์— ์—†์Šต๋‹ˆ๋‹ค."); } LoginResponse.TokenResponse tokenResponse = authService.reissue(refreshToken); @@ -35,7 +36,7 @@ public ApiResponse reissue( .build(); response.addHeader("Set-Cookie", cookie.toString()); - return ApiResponse.success(tokenResponse); + return ApiResponse.ok(tokenResponse); } @PostMapping("/logout") @@ -57,6 +58,6 @@ public ApiResponse logout( .build(); response.addHeader("Set-Cookie", cookie.toString()); - return ApiResponse.success(null); + return ApiResponse.ok(null); } } \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/common/GlobalExceptionHandler.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/common/GlobalExceptionHandler.java index 282c3cf..866fbe8 100644 --- a/src/main/java/com/retrip/auth/infra/adapter/in/rest/common/GlobalExceptionHandler.java +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/common/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import com.retrip.auth.domain.exception.common.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -44,6 +45,17 @@ public ApiResponse handleException(HttpServletRequest request, Ex return handle(ErrorCode.SERVER_ERROR, request); } + @ExceptionHandler(org.springframework.security.authentication.BadCredentialsException.class) + public ApiResponse handleBadCredentialsException(HttpServletRequest request, BadCredentialsException e) { + log.error("handleBadCredentialsException: ", e); + return ApiResponse.of( + ErrorResponse.of( + ErrorCode.MEMBER_NOT_FOUND, // 401 ์ƒํƒœ ์ฝ”๋“œ (Member-001) + request.getRequestURL().toString(), + request.getMethod() + )); + } + private static ApiResponse handle(ErrorCode errorCode, HttpServletRequest request) { return ApiResponse.of( ErrorResponse.of( diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/JwtAuthenticationFilter.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/JwtAuthenticationFilter.java index f918647..8a57be2 100644 --- a/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/JwtAuthenticationFilter.java @@ -1,6 +1,10 @@ package com.retrip.auth.infra.adapter.in.rest.filter; +import com.retrip.auth.application.config.CustomUserDetails; import com.retrip.auth.application.config.JwtProvider; +import com.retrip.auth.application.out.repository.MemberRepository; +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.vo.MemberEmail; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -9,12 +13,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; // [ํ•„์ˆ˜] +import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.List; +import java.util.UUID; @Slf4j @Component @@ -22,12 +26,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String AUTHORIZATION_HEADER = "Authorization"; - - // [์œ ์ง€] ์‚ฌ์šฉ์ž ์ •์˜ ์ œ์™ธ URI - private static final List URI = List.of("/login", "/users"); - - // JwtConfig ๋Œ€์‹  JwtProvider๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉ (์ฑ…์ž„ ๋ถ„๋ฆฌ) private final JwtProvider jwtProvider; + // 1. ํšŒ์› ์ƒํƒœ ํ™•์ธ์„ ์œ„ํ•œ Repository ์ฃผ์ž… + private final MemberRepository memberRepository; + + // auth/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/JwtAuthenticationFilter.java @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @@ -35,19 +38,57 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String token = getToken(request.getHeader(AUTHORIZATION_HEADER)); - // ํ† ํฐ์ด ์œ ํšจํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ธ์ฆ ์ฒ˜๋ฆฌ if (StringUtils.hasText(token) && jwtProvider.validateToken(token)) { - // ์œ ํšจํ•˜๋ฉด ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ ํ›„ SecurityContext์— ์ €์žฅ - Authentication auth = jwtProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); + try { + Authentication auth = jwtProvider.getAuthentication(token); + if (auth != null) { + // [์ˆ˜์ •] Principal ๊ฐ์ฒด์—์„œ ID๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ถ”์ถœ + Object principal = auth.getPrincipal(); + String memberIdStr; + + if (principal instanceof CustomUserDetails) { + memberIdStr = ((CustomUserDetails) principal).getUsername(); + } else if (principal instanceof String) { + memberIdStr = (String) principal; + } else { + throw new RuntimeException("์•Œ ์ˆ˜ ์—†๋Š” Principal ํƒ€์ž…์ž…๋‹ˆ๋‹ค."); + } + + UUID memberId = UUID.fromString(memberIdStr); + + // DB์—์„œ ํšŒ์› ์ƒํƒœ ํ™•์ธ (์‚ญ์ œ๋œ ํšŒ์›์ธ์ง€ ์ฒดํฌ) + boolean isDeleted = memberRepository.findById(memberId) + .map(Member::getIsDeleted) + .orElse(true); + + if (isDeleted) { + log.warn("โŒ ํƒˆํ‡ดํ•œ ํšŒ์›์˜ ์ ‘๊ทผ ์‹œ๋„: {}", memberId); + sendErrorResponse(response, "Member is deleted or not found"); + return; + } + + SecurityContextHolder.getContext().setAuthentication(auth); + log.info("โœ… JWT ์ธ์ฆ ์„ฑ๊ณต: {}", memberIdStr); + } + } catch (Exception e) { + log.error("โŒ JWT ์ธ์ฆ ์—๋Ÿฌ: ", e); + SecurityContextHolder.clearContext(); + // ์ธ์ฆ ์—๋Ÿฌ ์‹œ ์ฆ‰์‹œ 401 ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋ฌดํ•œ ๋ฆฌ๋””๋ ‰์…˜ ๋ฐฉ์ง€ + sendErrorResponse(response, "Invalid or expired token"); + return; + } } - // ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ๊ทธ๋ƒฅ ํ†ต๊ณผ์‹œํ‚ด (SecurityConfig์˜ .authenticated()์—์„œ ๊ฑธ๋Ÿฌ์ง) - // ๋งŒ์•ฝ ์—ฌ๊ธฐ์„œ 401์„ ์ง์ ‘ ๋ฆฌํ„ดํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด else ๋ธ”๋ก์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ  return ํ•ด์•ผ ํ•จ. - // ํ˜„์žฌ ๋กœ์ง์€ "์ธ์ฆ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ๋„ฃ๊ณ , ์—†์œผ๋ฉด ์•ˆ ๋„ฃ๊ณ  ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ๋„˜๊น€" ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. filterChain.doFilter(request, response); } + // 401 Unauthorized ์—๋Ÿฌ ์‘๋‹ต์„ ๋ณด๋‚ด๋Š” ํ—ฌํผ ๋ฉ”์†Œ๋“œ + private void sendErrorResponse(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(String.format("{\"success\":false, \"status\":401, \"message\":\"%s\"}", message)); + } + private String getToken(String authorization) { if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) { return authorization.substring(7); @@ -57,12 +98,20 @@ private String getToken(String authorization) { @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - // [์œ ์ง€] ๊ธฐ์กด์— ์ž‘์„ฑํ•˜์‹  ์ œ์™ธ ๋กœ์ง ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ String path = request.getRequestURI(); - return URI.contains(path) + String method = request.getMethod(); + + if (path.equals("/users") && "POST".equals(method)) { + return true; + } + + return path.startsWith("/login") + || path.startsWith("/oauth2") + || path.startsWith("/auth/reissue") || path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs") || path.startsWith("/swagger-resources") - || path.startsWith("/webjars"); + || path.startsWith("/webjars") + || path.startsWith("/debug"); } } \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilter.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilter.java index b2b5fdf..bcea2ee 100644 --- a/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilter.java +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/filter/LoginAuthenticationFilter.java @@ -3,88 +3,77 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.retrip.auth.application.config.JwtConfig; import com.retrip.auth.application.config.JwtProvider; -import com.retrip.auth.application.config.UsernamePasswordAuthentication; +import com.retrip.auth.application.in.request.LoginRequest; import com.retrip.auth.application.in.response.LoginResponse; import com.retrip.auth.application.out.repository.RefreshTokenRepository; import com.retrip.auth.domain.entity.RefreshToken; import com.retrip.auth.infra.adapter.in.rest.common.ApiResponse; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.io.IOException; -import java.util.stream.Collectors; -@Slf4j -@RequiredArgsConstructor -public class LoginAuthenticationFilter extends OncePerRequestFilter { +public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { - private final JwtConfig jwtConfig; - private final AuthenticationManager manager; private final JwtProvider jwtProvider; private final RefreshTokenRepository refreshTokenRepository; + private final ObjectMapper objectMapper = new ObjectMapper(); - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - // 1. ์š”์ฒญ ๊ฒ€์ฆ - String id = request.getHeader("id"); - String password = request.getHeader("password"); + public LoginAuthenticationFilter(JwtConfig jwtConfig, + AuthenticationManager authenticationManager, + JwtProvider jwtProvider, + RefreshTokenRepository refreshTokenRepository) { + super.setAuthenticationManager(authenticationManager); - if (!StringUtils.hasText(id) || !StringUtils.hasText(password)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } + this.jwtProvider = jwtProvider; + this.refreshTokenRepository = refreshTokenRepository; + setFilterProcessesUrl("/login"); // POST /login ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„๋„๋ก ์„ค์ • + } + // 1. ๋กœ๊ทธ์ธ ์‹œ๋„ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { - // 2. ์ธ์ฆ ์‹œ๋„ - Authentication authentication = new UsernamePasswordAuthentication(id, password); - Authentication auth = manager.authenticate(authentication); - - // 3. ํ† ํฐ ์ƒ์„ฑ - LoginResponse.TokenResponse tokenResponse = jwtProvider.generateTokens(auth); - - // 3-1. Refresh Token DB์— ์ €์žฅ - String authorities = auth.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.joining(",")); + LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class); - RefreshToken refreshToken = new RefreshToken( - tokenResponse.refreshToken(), - auth.getName(), // memberId - authorities + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + loginRequest.id(), + loginRequest.password() ); - refreshTokenRepository.save(refreshToken); - // 4. ์‘๋‹ต ์ž‘์„ฑ - ApiResponse result = ApiResponse.ok(tokenResponse); + // โ˜… [์ˆ˜์ •] ๋ถ€๋ชจ ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋งค๋‹ˆ์ €๋ฅผ ํ˜ธ์ถœ + return this.getAuthenticationManager().authenticate(authToken); + } catch (IOException e) { + throw new RuntimeException("๋กœ๊ทธ์ธ ์š”์ฒญ ํŒŒ์‹ฑ ์‹คํŒจ", e); + } + } - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.setStatus(HttpServletResponse.SC_OK); + // 2. ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - response.getWriter().write(objectMapper.writeValueAsString(result)); - response.getWriter().flush(); + LoginResponse.TokenResponse tokenResponse = jwtProvider.generateTokens(authResult); - } catch (AuthenticationException e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } - } + // CustomUserDetails.getName()์€ UUID String์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ–ˆ์œผ๋ฏ€๋กœ ๋ฐ”๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + String memberId = authResult.getName(); + RefreshToken refreshToken = new RefreshToken( + tokenResponse.refreshToken(), + memberId, + "ROLE_USER" + ); + refreshTokenRepository.save(refreshToken); - @Override - protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - return !request.getRequestURI().equals("/login"); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + ApiResponse apiResponse = ApiResponse.success(tokenResponse); + response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); } } \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java index 198f34c..a66cc57 100644 --- a/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/in/MemberController.java @@ -1,17 +1,22 @@ package com.retrip.auth.infra.adapter.in.rest.in; -import com.retrip.auth.application.in.request.MemberCreateRequest; -import com.retrip.auth.application.in.request.MemberDeleteRequest; -import com.retrip.auth.application.in.request.MemberUpdateRequest; -import com.retrip.auth.application.in.response.MemberCreateResponse; -import com.retrip.auth.application.in.response.MemberUpdateResponse; +import com.retrip.auth.application.in.request.*; +import com.retrip.auth.application.in.response.*; import com.retrip.auth.application.in.usercase.ManageMemberUseCase; +import com.retrip.auth.domain.exception.MemberNotFoundException; import com.retrip.auth.infra.adapter.in.rest.common.ApiResponse; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import java.util.UUID; + +@Slf4j @RestController @RequestMapping("/users") @RequiredArgsConstructor @@ -22,26 +27,90 @@ public class MemberController { @PostMapping @Schema(description = "ํšŒ์› ๊ฐ€์ž…") - public ApiResponse createUser( - @RequestBody MemberCreateRequest request - ){ - return ApiResponse.created(manageMemberUseCase.createUser(request)); + public ResponseEntity> createUser( + @RequestBody MemberCreateRequest request) { + MemberCreateResponse response = manageMemberUseCase.createUser(request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.created(response)); } @PutMapping @Schema(description = "ํšŒ์› ์ •๋ณด ์ˆ˜์ •") public ApiResponse updateUser( - @RequestBody MemberUpdateRequest request - ){ - return ApiResponse.ok(manageMemberUseCase.updateUser(request)); + Authentication authentication, + @RequestBody MemberUpdateRequest request) { + UUID memberId = extractMemberId(authentication); + return ApiResponse.ok(manageMemberUseCase.updateUser(memberId, request)); } - @DeleteMapping() + @DeleteMapping @Schema(description = "ํšŒ์› ์ •๋ณด ์‚ญ์ œ") - public ApiResponse deleteUser( - @RequestBody MemberDeleteRequest request - ){ - manageMemberUseCase.deleteUser(request); - return ApiResponse.noContent(); + public ResponseEntity> deleteUser( + Authentication authentication, + @RequestBody MemberDeleteRequest request) { + UUID memberId = extractMemberId(authentication); + manageMemberUseCase.deleteUser(memberId, request); + return ResponseEntity.status(HttpStatus.NO_CONTENT) + .body(ApiResponse.noContent()); + } + + @PatchMapping("/password") + @Schema(description = "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ") + public ApiResponse changePassword( + Authentication authentication, + @RequestBody ChangePasswordRequest request) { + UUID memberId = extractMemberId(authentication); + return ApiResponse.ok(manageMemberUseCase.changePassword(memberId, request)); + } + + @GetMapping("/me") + @Schema(description = "๋‚ด ์ •๋ณด ์กฐํšŒ") + public ApiResponse getMyInfo(Authentication authentication) { + log.info("=== Controller /users/me ==="); + log.info("Authentication: {}", authentication); + log.info("Principal: {}", authentication != null ? authentication.getPrincipal() : "null"); + log.info("Authorities: {}", authentication != null ? authentication.getAuthorities() : "null"); + + UUID memberId = extractMemberId(authentication); + log.info("Extracted Member ID: {}", memberId); + + return ApiResponse.ok(manageMemberUseCase.getMyInfo(memberId)); + } + + @PostMapping("/verify-password") + @Schema(description = "๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ") + public ApiResponse verifyPassword( + Authentication authentication, + @RequestBody VerifyPasswordRequest request) { + UUID memberId = extractMemberId(authentication); + return ApiResponse.ok(manageMemberUseCase.verifyPassword(memberId, request)); + } + + // JWT์—์„œ memberId ์ถ”์ถœ + private UUID extractMemberId(Authentication authentication) { + if (authentication == null || authentication.getPrincipal() == null) { + log.error("์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); + throw new MemberNotFoundException(); + } + + Object principal = authentication.getPrincipal(); + String memberIdStr; + + if (principal instanceof String) { + memberIdStr = (String) principal; + } else if (principal instanceof org.springframework.security.core.userdetails.UserDetails) { + // UserDetails ๊ตฌํ˜„์ฒด๋ผ๋ฉด username(์—ฌ๊ธฐ์„œ๋Š” memberId)์„ ๊ฐ€์ ธ์˜ด + memberIdStr = ((org.springframework.security.core.userdetails.UserDetails) principal).getUsername(); + } else { + log.error("์•Œ ์ˆ˜ ์—†๋Š” Principal ํƒ€์ž…: {}", principal.getClass().getName()); + throw new MemberNotFoundException(); + } + + try { + return UUID.fromString(memberIdStr); + } catch (IllegalArgumentException e) { + log.error("์œ ํšจํ•˜์ง€ ์•Š์€ UUID ํ˜•์‹: {}", memberIdStr); + throw new MemberNotFoundException(); + } } -} +} \ No newline at end of file