diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..9f11b755
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea/
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index 8713e6d2..00000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1775064304989
-
-
- 1775064304989
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Gibeom/umc10th/.env b/Gibeom/umc10th/.env
deleted file mode 100644
index 43b455e5..00000000
--- a/Gibeom/umc10th/.env
+++ /dev/null
@@ -1,3 +0,0 @@
-DB_USER=USER
-DB_PW=MY_PASSWORD_HERE
-DB_URL=MY_URL
\ No newline at end of file
diff --git a/Gibeom/umc10th/build.gradle b/Gibeom/umc10th/build.gradle
index 2314bead..b3624447 100644
--- a/Gibeom/umc10th/build.gradle
+++ b/Gibeom/umc10th/build.gradle
@@ -22,6 +22,13 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-security'
+
+ // Jwt
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
+ implementation 'org.springframework.boot:spring-boot-configuration-processor'
+
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
@@ -31,9 +38,16 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
+ // Security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ testImplementation 'org.springframework.security:spring-security-test'
+
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'
+
+ // OAuth
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
tasks.named('test') {
diff --git a/Gibeom/umc10th/feedback_summary/FeedBack_Study b/Gibeom/umc10th/feedback_summary/FeedBack_Study
deleted file mode 100644
index 04d93ed2..00000000
--- a/Gibeom/umc10th/feedback_summary/FeedBack_Study
+++ /dev/null
@@ -1,26 +0,0 @@
-ResponseEntity
-- 정의 : 스프링 프레임워크에서 제공하는 클래스.
-- Http요청 또는 응답에 해당하는 HttpStatus, HttpHeader와 HttpBody를 포함하는 클래스
-- Http 응답의 주권을 가짐. 커스텀 가능
-- 상속 구현 클래스 (RequestBody, ResponseEntity)
-
-기존 상태
-- 클라이언트 측에서는 ApiResponse로 응답시 정해진 응답 (예 : 200이면 200, 500이면 500)
-- 현재 ApiResponse 클래스는 정적 팩토리 메서드 형식.
-- ApiResponse.java
- ```
- //성공한 경우
- public static ApiResponse onSuccess(BaseSuccessCode code, T result){
- return new ApiResponse<>(true, code.getCode(), code.getMessage(), result);
- }
- ```
-기존 방식의 한계
-- 클라이언트 측은 먼저 http status를 확인하고 바디를 확인함
-- 네트워크 불일치 : ApiResponse만 반환하면 내부 로직이 실패하더라도 Http 상태코드는 기본(200)으로 나가는 경우가 많았음
-- 상세한 정보가 Json바디 안에 숨겨져 있어 클라이언트가 Http 표준방식으로 1차 판단을 내리기 어려웠음
-- 단순한 소통 : 200, 500 위주의 단순한 소통만 가능했고, 201, 204 등 을 활용 못했음
-
-Why?
-- API 사용자(프론트엔드 등등)는 응답바디 안의 Json뿐 아닌 브라우저나 앱이 수신하는 Status Code를 보고 1차 판단을 내림.
-- 유연성 : ResponseEntity를 래핑해 감싼 응답을 보낸다면 커스텀 응답, 쿠키, 헤더 등 표준화된 응답을 유연하게 보낼 수 있음.
-- ApiResponse의 데이터 규격을 유지하고 ResponseEntity로 감싸 네트워크 수준 상태코드로 명시적 관리
\ No newline at end of file
diff --git a/Gibeom/umc10th/keyword_summary/ch07.md b/Gibeom/umc10th/keyword_summary/ch07.md
deleted file mode 100644
index 701095b1..00000000
--- a/Gibeom/umc10th/keyword_summary/ch07.md
+++ /dev/null
@@ -1,90 +0,0 @@
-- Page와 Slice
-
- Spring Data JPA는 페이징을 위해 두가지 객체를 제공한다. Slice & Page
-
- DB에 저장된 데이터들을 페이지에 맞춰 몇개씩 뿌릴 건지 알려주는 것
-
- - Page
- - pageable 객체를 사용해 오프셋 기반 페이지네이션을 구현할 수 있는 객체.
- - Slice를 상속함
- - Slice
- - Page 보다 좀 더 추상적.
- - 커서 기반 페이지네이션을 구현할 때 사용할 수 있는 객체
- - Response에 총 데이터 갯수는 보내지 않음
- - Pageable 객체
- - JPA가 제공하는 페이지네이션을 위한 객체
- - 페이징 정보(페이지 번호, 페이지 크기, 정렬방식 등..)를 담고 있는 인터페이스
- - 스프링 Data JPA에서 제공하는 `PageRequest` 클래스를 통해 인스턴스화 가능
- - 사용 이유
- - 모든 데이터를 한번에 뿌리게 되면 성능 저하 등 문제가 발생
- - 페이지네이션을 사용해 적절한 수의 정보만 올려주면 프론트에서 적절한 처리가능.
-- Java stream API
-
- 자바가 지원하는 함수형 프로그래밍 방식.
-
- 함수형 프로그래밍이란?
-
- - 코드가 “어떻게” 에서 “무엇을”로 포커싱이 옮겨간 프로그래밍 방식
- - 함수형 프로그래밍에서 함수는 `Int` ,`String` 같은 1급 객체로 취급.
- - 이 특성을 활용하는 고차함수 : map, filter, reduce 등…
- - (기존 객체의 상태를 변경하지 않고, 새로운 결과를 반환)
- - 사용 이유 : 가독성, 불변성, 유지보수성, 병렬 처리성 (쓰레드 풀)
-
- 단점
-
- - for문 같은 단순 반복문에 익숙한 개발자들에게는 람다식과 스트림이 이해가 어려울 수 있음
- - 중단점을 걸어서 디버깅하기 어려움.
- - for문 보다 오버헤드가 조금 더 큼.
-
- 병렬 처리
-
- - 작업을 여러 스레드에게 분할해 병렬적으로 처리하는 방법
- - `parallelStram()` 키워드와 `ForkJoinPool()` 을 사용해 스레드 지정 가능
- - CommonPool : 미리 생성해 놓은 스레드를 모든 애플리케이션이 이용하는 방식의 풀
- - 장점 : 정해진 수의 스레드를 미리 생성해 놓고 사용하기 때문에 스레드 생성/삭제 오버헤드 적음. 적은 양의 처리에 유리
- - 단점 : 각각의 애플리케이션 마다 알맞은 처리를 제공하기 힘들 수 있음
- - (군대 배식 느낌)
- - ThreadPool : 각각의 컴포넌트마다 설정할 수 있는 풀
- - 장점 : 성능 튜닝 가능
- - 메모리 관리 등 신경써야 할 것들이 있음.
- - (오마카세 느낌)
-- 객체 그래프 탐색
-
- 객체 그래프 탐색 (Object Graph Navigation)
-
- - 객체지향언어에서 참조를 사용해 연관된 객체를 타고 들어가 데이터를 조회하는 방식
- - ex) 멤버 객체에서 member.getTeam()처럼 연관관계를 통해 팀 정보를 가져오는 방식
-
- 특징 : 객체 간 연관관계를 통해 자유롭게 메모리상의 객체를 이동하며 탐색
-
- 장점 : SQL 직접 작성 시 필요한 조인 제약에서 벗어나 논리적인 도메인 모델 구조에 따라 데이터 조회 가능
-
- JPA에서의 활용 : JPA는 연관된 객체를 처음부터 로딩하지 않고 실제 사용 시점에 조회하는 지연로딩을 사용해 객체그래프 탐색을 지원
-
- 주의점 : 객체 그래프를 무분별 탐색하는 경우, 하이버네이트 등에서 N+1 문제가 발생할 수 있음.
-
- **내 요약**
-
- - Store store = member.getReviewList().get(0).getStore()와 같은 코드가 있다고 가정.
- 1. 멤버의 리뷰리스트를 가져오고
- 2. 0번 객체를 가져오고
- 3. 그 리뷰의 가게를 가져오는 흐름을 표현 가능.
- - 여기서 문제 : 어디까지 조회가 가능한가?
- - 실행시 모든 데이터를 올려둘 수는 없음. 그렇다면 사용할 때만 필요한 정보를 가져오는 방식 Lazy-Loading(지연로딩)을 사용해 `.` 을 사용해 다음 객체를 불러올때마다 데이터를 가져옴.
- - 만약 객체간 연결이 없거나, 다음 데이터를 조회할 수 없다면 null이 조회되거나 오류가 날 것.
-- @Valid vs @Validated
-
- @Valid
-
- - 컨트롤러 단에서 객체의 유효성을 검사할 때 사용
- - 계층 구조 검증이 가능 (User 안에 있는 Address 객체도 검사하고 싶으면 Address 필드 위에 @Valid 를 붙여야 함)
- - 한계 : 그룹화 검증이 불가능
-
- @Validated
-
- - 스프링에서 자바표준기능을 확장해 만든 어노테이션
- - 용도 : Service 나 Repository 등 Bean 계층에서 검증이 필요할 때, 혹은 그룹검증이 필요할 때 사용.
- - 특징
- - 제약조건 그룹화 : “회원가입때는 이 필드 검사하고, 정보수정때는 하지마” 같은 상황을 `groups` 속성으로 지정가능
- - 클래스 레벨 선언 : 클래스 상단에 `@Validated` 어노테이션을 붙여야 해당 클래스 내부의 메서드 파라미터에 대한 검증이 작동
- - 한계 : 계층구조 검증은 직접적으로 지원하지 않아, 내부 객체에는 여전히 @Valid를 써야함.
\ No newline at end of file
diff --git a/Gibeom/umc10th/keyword_summary/ch08.md b/Gibeom/umc10th/keyword_summary/ch08.md
new file mode 100644
index 00000000..737accb1
--- /dev/null
+++ b/Gibeom/umc10th/keyword_summary/ch08.md
@@ -0,0 +1,42 @@
+- Spring Security가 무엇인가?
+
+ 스프링 기반 애플리케이션의 보안을 담당하는 강력하고 포괄적인 하위 프레임워크
+
+ 복잡한 보안 로직을 직접 구현할 필요 없이 표준화된 필터 기반의 설정을 통해 시스템을 안전하게 보호한다.
+
+- 인증(Authentication)vs 인가(Authorization)
+
+ 비슷해보이지만 서로 다른 개념이다.
+
+ 인증 (Authentication)
+
+ - 본인확인 절차
+ - 사용자가 자신이 주장하는 사람이 맞는지 확인하는 과정
+
+ 인가 (Authorization)
+
+ - 권한확인 절차
+ - 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정
+
+- Stateful vs Stateless
+
+ 논점 : 서버가 클라이언트의 세션 정보를 기억하는가?
+
+ Stateful(상태유지) : 세션 정보를 기억함
+
+ Stateless(토큰 기반) : 서버가 상태를 유지하지 않으므로 요청에 포함된 토큰(JWT)로 검증
+
+ | 구분 | Stateful | Stateless |
+ | --- | --- | --- |
+ | 특징 | 서버가 세션 저장소에 로그인 상태 유지 | 서버가 상태를 유지하지 않음, 요청에 포함된 토큰으로 검증 |
+ | 인증방식 | JSESSION쿠키를 통해 서버 메모리/DB의 세션 조회 | 매 요청시 HTTP헤더에 토큰을 담아서 전송 (Authorization:Bearer) |
+ | 서버 확장 | 세션 불일치 문제 발생 가능 | 각 요청이 독립적이므로 서버 증설에 유리 |
+ | 메모리 및 비용 | 동시접속자가 많을수록 서버 세션 메모리 소비 증가 | 토큰 검증 연산이 필요하며, 서버 메모리 사용량은 적음 |
+ | 주요 활용처 | 전톤적인 웹 애플리케이션 | REST API, 모바일 앱, MSA |
+
+ 서버 확장 방법
+
+ - Scale-up : 단일 서버 성능 향상
+ - Scale-out : 서버의 개수를 늘리기
+ - 로드밸런서 : 서버 부하를 분산시키는 H/W, S/W
+ - 클라이언트와 서버Pool 사이에 위치해 서버의 부하를 분산시키는 하드웨어나 S/W
\ No newline at end of file
diff --git a/Gibeom/umc10th/keyword_summary/ch09.md b/Gibeom/umc10th/keyword_summary/ch09.md
new file mode 100644
index 00000000..df76c882
--- /dev/null
+++ b/Gibeom/umc10th/keyword_summary/ch09.md
@@ -0,0 +1,53 @@
+- 세션과 토큰의 차이는?
+
+ 세션 방식은 사용자의 정보를 서버가 저장함
+
+ - 이용자 수가 많으면 서버 부하가 커짐(모든 사용자 정보를 테이블로 보유하고 있음)
+
+ 토큰 방식은 사용자가 내 정보가 포함된 토큰을 내밀기만 하면 됨
+
+ - 서버의 부담이 적음.
+ - 다만 사용자의 정보를 저장하지 않기 때문에 할 수 없는 기능도 있음.
+
+ 세션 : 입장권인데 적혀있는 게 거의 없음. 사이트가 입장권 검사하고 저장해놓음.
+
+ 토큰 : 뭐가 많이 적혀있는 입장권. 사이트는 입장권 들고있으면 입장 허용해줌.
+
+ (참고자료) https://youtu.be/XXseiON9CV0?si=Am8JlSoLV0SiZX0W
+
+- 엑세스 토큰과 리프레시 토큰이란?
+
+ 액세스 토큰 : 접근하기 위해 필요한 기본적인 토큰, 해킹 공격 방지를 위해 유효기간이 짧다.
+
+ 리프레쉬 토큰 : 만료된 액세스 토큰을 재생성해주는 토큰.
+
+- OAuth 1.0과 OAuth 2.0의 차이는?
+
+ OAuth 2.0 은 1.0과 달리 복잡한 암호화 서명 과정을 간소화하고, 웹/모바일 등 다양한 환경에 맞춘 인증 방식을 도입해 속도와 확장성을 대폭 개선한 버전
+
+ 핵심 차이점
+
+ | 구분 | OAuth 1.0 | OAuth 2.0 |
+ | --- | --- | --- |
+ | 핵심 매커니즘 | 모든 요청에 디지털 서명 필요 | 발급받은 액세스 토큰만으로 인증 |
+ | 암호화 요구사항 | HTTPS가 필수가 아님 | HTTPS(TLS) 필수 |
+ | 역할의 분리 | 단순함(Client, Server, User) | 인증서버와 리소스 서버의 분리 |
+ | 토큰 만료 | 토큰 만료 개념이 없거나 복잡 | 만료기간이 존재하며, 리프레시 토큰으로 갱신가능 |
+ | 지원 환경 | 웹 브라우저 기반 애플리케이션 중심 | 모바일 앱, Iot, 데스크톱 등 다양한 환경 지원 |
+
+
+ 왜 2.0으로 바뀌었을까?
+
+ 1. 개발의 복잡성 해결(디지털 서명의 폐지)
+
+ 1.0은 API를 호출할 때마다 복잡한 암호화 알고리즘으로 디지털 서명을 생성하고 이를 헤더에 담아 보내야 했음. 이 서명을 만드는 과정이 조금만 틀려도 인증이 실패했기 때문에 매우 불편했음.
+
+ 2.0은 이 서명 과정을 과감히 버렸음. 대신 HTTPS통신을 필수로 규정해 연결 자체를 암호화하고, 발급받은 문자열 토큰만 헤더에 얹어서 보내면 되도록 단순화 했음
+
+ 2. 모바일 및 다양한 디바이스 지원
+
+ OAuth 1.0이 나올 당시에는 웹브라우저 중심의 환경이었음. 하지만 모바일 앱, IoT 등의 기기들이 생기면서 브라우저가 없거나 화면이 없는 환경에서도 인증을 처리해야 할 필요가 생김. OAuth 2.0은 시나리오별 인증방식을 여러개 제공해 맞춤형 인증을 제공함
+
+ 3. 대규모 서비스 분할(확장성)
+
+ OAuth 2.0은 인증을 담당하는 서버와 실제 데이터를 가지고 있는 서버의 역할을 명확히 나눴음. 덕분에 대기업이나 대규모 서비스에서는 인증서버만 따로 구축해 트래픽을 분산할 수 있음.
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
index c61e09c2..7ce6a23e 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java
@@ -1,5 +1,6 @@
package com.example.umc10th.domain.member.controller;
+import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.enums.MissionStatus;
import com.example.umc10th.domain.member.service.MemberService;
@@ -7,8 +8,9 @@
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
+import com.example.umc10th.global.security.entity.AuthMember;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@@ -17,37 +19,35 @@
@RestController
@RequiredArgsConstructor
-@RequestMapping("/api/v1/members")
+@RequestMapping("/api")
public class MemberController {
private final MemberService memberService;
//마이페이지
- @GetMapping("/me")
+ @GetMapping("/v1/members/me")
public ResponseEntity> getInfo(
- @AuthenticationPrincipal Long memberId
- ){
+ @AuthenticationPrincipal AuthMember member
+ ){
BaseSuccessCode code = MemberSuccessCode.OK;
- MemberResDTO.GetInfo result = memberService.getInfo(memberId);
return ResponseEntity
.status(code.getStatus())
- .body(ApiResponse.onSuccess(code, result));
+ .body(ApiResponse.onSuccess(code, memberService.getInfo(member)));
}
// 홈화면
- @GetMapping("/home")
+ @GetMapping("/v1/members/home")
public ResponseEntity> getHome(
- @AuthenticationPrincipal Long memberId,
+ @AuthenticationPrincipal AuthMember authMember,
@RequestParam(defaultValue = "0") int page
){
- BaseSuccessCode code = MemberSuccessCode.OK;
- MemberResDTO.HomeResultDto result = memberService.getHome(memberId, page);
+ MemberResDTO.HomeResultDto result = memberService.getHome(authMember.getMember().getId(), page);
return ResponseEntity
- .status(code.getStatus())
- .body(ApiResponse.onSuccess(code, result));
+ .status(MemberSuccessCode.OK.getStatus())
+ .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result));
}
// 진행중/완료 미션 목록 조회
- @GetMapping("/missions")
+ @GetMapping("/v1/members/missions")
public ResponseEntity>> getMissionsByStatus(
@AuthenticationPrincipal Long memberId,
@RequestParam MissionStatus status,
@@ -62,4 +62,28 @@ public ResponseEntity>> getMissionsBy
.body(ApiResponse.onSuccess(code, result));
}
+
+ //회원가입
+ @PostMapping("/auth/sign-up")
+ public ResponseEntity> signUp(
+ @RequestBody @Valid MemberReqDTO.SignUp req
+ ){
+ memberService.signUp(req);
+ return ResponseEntity
+ .status(MemberSuccessCode.CREATED.getStatus())
+ .body(ApiResponse.onSuccess(MemberSuccessCode.CREATED, null));
+ }
+
+ //로그인
+ @PostMapping("/auth/login")
+ public ResponseEntity> login(
+ @RequestBody @Valid MemberReqDTO.Login req
+ ){
+ MemberResDTO.LoginResult result = memberService.login(req);
+ return ResponseEntity
+ .status(MemberSuccessCode.OK.getStatus())
+ .body(ApiResponse.onSuccess(MemberSuccessCode.OK, result));
+ }
+
+
}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
index a7412b8f..baf0a23e 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java
@@ -4,9 +4,14 @@
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.entity.mapping.MemberMission;
+import com.example.umc10th.global.security.dto.OAuthDTO;
import org.springframework.data.domain.Page;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -44,4 +49,39 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM
.status(memberMission.getStatus().name())
.build();
}
+
+ public static MemberResDTO.LoginResult toLoginResult(String accessToken) {
+ return MemberResDTO.LoginResult.builder()
+ .accessToken(accessToken)
+ .build();
+ }
+
+ public static Member toMember(MemberReqDTO.SignUp req, String encodedPasssword){
+ return Member.builder()
+ .name(req.name())
+ .password(encodedPasssword)
+ .phoneNumber(req.phoneNumber())
+ .email(req.email())
+ .gender(req.gender())
+ .userPoint(0)
+ .nickname(req.nickname())
+ .build();
+ }
+
+ public static Member toMember(OAuthDTO dto) {
+ return Member.builder()
+ .name(dto.getName())
+ .nickname(dto.getName())
+ .email(dto.getSocialEmail())
+ .socialType(dto.getSocialType())
+ .socialUid(dto.getSocialUid())
+ .userPoint(0)
+ .build();
+ }
+
+ public static MemberResDTO.Login toLogin(String accessToken) {
+ return MemberResDTO.Login.builder()
+ .accessToken(accessToken)
+ .build();
+ }
}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
index 943a141f..b16230df 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java
@@ -1,5 +1,31 @@
package com.example.umc10th.domain.member.dto;
+import com.example.umc10th.domain.member.enums.Gender;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+
public class MemberReqDTO {
+ public record SignUp (
+ @NotBlank
+ String name,
+ @NotBlank
+ String nickname,
+ @Email @NotBlank
+ String email,
+ @NotBlank
+ String password,
+ @NotBlank
+ String phoneNumber,
+ Gender gender
+ ){}
+
+ public record Login(
+ @Email @NotBlank
+ String email,
+ @NotBlank
+ String password
+ ){}
+
}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java
index 9da8ee56..4b672d47 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java
@@ -15,6 +15,16 @@ public record GetInfo(
Integer userPoint
){}
+ @Builder
+ public record LoginResult(
+ String accessToken
+ ){}
+
+ @Builder
+ public record Login(
+ String accessToken
+ ){}
+
//홈 화면
@Builder
public record HomeResultDto(
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java
index 1f7fca6b..fb197716 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/entity/Member.java
@@ -4,6 +4,7 @@
import com.example.umc10th.domain.member.enums.Gender;
import com.example.umc10th.domain.store.entity.Region;
import com.example.umc10th.global.entity.BaseEntity;
+import com.example.umc10th.global.security.dto.SocialType;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -37,7 +38,7 @@ public class Member extends BaseEntity{
@Column(name = "nickname", nullable = false)
private String nickname;
- @Column(name = "phoneNumber", nullable = false)
+ @Column(name = "phoneNumber")
private String phoneNumber;
@Column(name = "user_point")
@@ -46,6 +47,16 @@ public class Member extends BaseEntity{
@Column(name = "email")
private String email;
+ @Column(name = "password")
+ private String password;
+
+ @Column(name = "social_type")
+ @Enumerated(EnumType.STRING)
+ private SocialType socialType;
+
+ @Column(name = "social_uid")
+ private String socialUid;
+
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "region_id")
private Region region;
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java
index 98c24940..844b6740 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java
@@ -12,7 +12,23 @@ public enum MemberErrorCode implements BaseErrorCode {
"COMMON404_1",
"해당 사용자를 찾을 수 없습니다."
),
+ EMAIL_DUPLICATED(HttpStatus.CONFLICT,
+ "COMMON404_2",
+ "해당 사용자를 찾을 수 없습니다."
+ ),
+ NICKNAME_DUPLICATED(HttpStatus.CONFLICT,
+ "COMMON409_2",
+ "이미 존재하는 닉네임입니다."
+ ),
+ INVALID_PASSWORD(HttpStatus.UNAUTHORIZED,
+ "MEMBER401_1",
+ "비밀번호가 올바르지 않습니다."
+ ),
+ NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST,
+ "404_3",
+ "제공하지 않는 인증자입니다."
+ ),
;
private final HttpStatus status;
private final String code;
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java
index 5a5c3362..2d76db0b 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java
@@ -11,7 +11,7 @@ public enum MemberSuccessCode implements BaseSuccessCode {
"MEMBER200_1",
"성공적으로 유저를 조회했습니다."),
- SIGNUP_SUCCESS(HttpStatus.CREATED,
+ CREATED(HttpStatus.CREATED,
"MEMBER_201_1",
"회원가입이 성공적으로 완료되었습니다.")
;
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
index 054ad34f..70a4b847 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java
@@ -1,7 +1,14 @@
package com.example.umc10th.domain.member.repository;
import com.example.umc10th.domain.member.entity.Member;
+import com.example.umc10th.global.security.dto.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.Optional;
+
public interface MemberRepository extends JpaRepository {
+ Boolean existsByEmail(String email);
+ Optional findByEmail(String email);
+ Boolean existsByNickname(String nickname);
+ Optional findBySocialTypeAndSocialUid(SocialType socialType, String socialUid);
}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
index 70b5aae5..bfb68f16 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java
@@ -13,10 +13,13 @@
import com.example.umc10th.domain.mission.converter.MissionConverter;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.domain.mission.entity.Mission;
+import com.example.umc10th.global.security.entity.AuthMember;
+import com.example.umc10th.global.security.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -27,11 +30,16 @@
public class MemberService {
private final MemberRepository memberRepository;
private final MemberMissionRepository memberMissionRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtUtil jwtUtil;
- public MemberResDTO.GetInfo getInfo(Long memberId) {
- Member member = memberRepository.findById(memberId)
+ public MemberResDTO.GetInfo getInfo(
+ AuthMember member
+ ) {
+ Member User = member.getMember();
+ memberRepository.findByEmail(User.getEmail())
.orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND));
- return MemberConverter.toGetInfo(member);
+ return MemberConverter.toGetInfo(User);
}
//홈화면 (진행중인 미션 10개씩 페이징)
@@ -65,4 +73,27 @@ public List getMissionsByStatus(
.collect(Collectors.toList());
return MissionConverter.toMissionDtoList(missions);
}
+
+ public MemberResDTO.LoginResult login(MemberReqDTO.Login req) {
+ Member member = memberRepository.findByEmail(req.email())
+ .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND));
+ if (!passwordEncoder.matches(req.password(), member.getPassword())) {
+ throw new MemberException(MemberErrorCode.INVALID_PASSWORD);
+ }
+ String accessToken = jwtUtil.createAccessToken(new AuthMember(member));
+ return MemberConverter.toLoginResult(accessToken);
+ }
+
+ public void signUp(MemberReqDTO.SignUp req) {
+ //닉네임 혹은 이메일이 이미 존재할 때
+ if(memberRepository.existsByEmail(req.email())){
+ throw new MemberException(MemberErrorCode.EMAIL_DUPLICATED);
+ } else if (memberRepository.existsByNickname(req.nickname())){
+ throw new MemberException(MemberErrorCode.NICKNAME_DUPLICATED);
+ }
+
+ String encodedPassword = passwordEncoder.encode(req.password());
+ Member member = MemberConverter.toMember(req, encodedPassword);
+ memberRepository.save(member);
+ }
}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java
index b784e565..240ba037 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java
@@ -26,7 +26,6 @@ public class MissionController {
private final MissionService missionService;
-
//리뷰 작성 (완료된 미션에 한해서)
@PostMapping("v1/missions/{missionId}/reviews")
public ResponseEntity> writeReview(
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java
index e78133cb..bcdfabaa 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java
@@ -30,7 +30,7 @@ public ResponseEntity result = reviewService.getMemberReviewsOrderByScore(memberId, pageSize, cursor, query);
+ ReviewResDTO.Pagination result = reviewService.getMemberReviews(memberId, pageSize, cursor, query);
return ResponseEntity
.status(code.getStatus())
.body(ApiResponse.onSuccess(code, result));
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java
index f6c18235..8870cd00 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java
@@ -7,10 +7,8 @@
import com.example.umc10th.domain.review.exception.code.ReviewErrorCode;
import com.example.umc10th.domain.review.repository.ReviewRepository;
import lombok.RequiredArgsConstructor;
-import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
-import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -20,40 +18,24 @@
public class ReviewService {
private final ReviewRepository reviewRepository;
//id 페이지네이션
- public ReviewResDTO.Pagination getMemberReviewsOrderById(
+ private ReviewResDTO.Pagination getMemberReviewsOrderById(
Long memberId,
Integer pageSize,
- String cursor,
- String query
+ String cursor
) {
-
- //페이지 정보 PageRequest만들기
PageRequest pageRequest = PageRequest.of(0, pageSize);
- long idCursor;
Slice reviewList;
String nextCursor;
- //커서가 있는 경우
if (!cursor.equals("-1")) {
-
- // 커서 분리
- String[] cursorSplit = cursor.split(":");
- switch (query.toLowerCase()) {
- case "id":
- idCursor = Long.parseLong(cursorSplit[1]);
- // 멤버의 리뷰들 조회 & where 절에 커서값 기입
- reviewList = reviewRepository.findReviewsByMember_IdAndIdLessThanOrderByIdDesc(
- memberId,
- idCursor,
- pageRequest
- );
- break;
- default:
- throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID);
- }
+ long idCursor = Long.parseLong(cursor.split(":")[1]);
+ reviewList = reviewRepository.findReviewsByMember_IdAndIdLessThanOrderByIdDesc(
+ memberId,
+ idCursor,
+ pageRequest
+ );
} else {
- //커서 없이 조회
reviewList = reviewRepository.findReviewsByMember_IdOrderByIdDesc(memberId, pageRequest);
}
@@ -70,38 +52,40 @@ public ReviewResDTO.Pagination getMemberReviewsOrderById
reviewList.getSize()
);
}
- //별점 순 페이징
- public ReviewResDTO.Pagination getMemberReviewsOrderByScore(
+ // query 값에 따라 정렬 전략 선택 — 컨트롤러는 이 메서드만 호출한다
+ public ReviewResDTO.Pagination getMemberReviews(
Long memberId,
Integer pageSize,
String cursor,
String query
+ ) {
+ return switch (query.toLowerCase()) {
+ case "id" -> getMemberReviewsOrderById(memberId, pageSize, cursor);
+ case "score" -> getMemberReviewsOrderByScore(memberId, pageSize, cursor);
+ default -> throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID);
+ };
+ }
+
+ //별점 순 페이징
+ private ReviewResDTO.Pagination getMemberReviewsOrderByScore(
+ Long memberId,
+ Integer pageSize,
+ String cursor
) {
PageRequest pageRequest = PageRequest.of(0, pageSize);
Slice reviewList;
String nextCursor;
- //커서가 있는 경우
+
if (!cursor.equals("-1")) {
- // 커서 분리
String[] cursorSplit = cursor.split(":");
- switch (query.toLowerCase()) {
- case "score":
- // 커서 타입 변환
- long scoreCursor = Long.parseLong(cursorSplit[0]);
- long idCursor = Long.parseLong(cursorSplit[1]);
-
- //리뷰들 조회 & where절에 커서 값 기입
- reviewList = reviewRepository.findReviewsByScoreCursor(
- memberId,
- scoreCursor,
- idCursor,
- pageRequest);
- break;
- default:
- throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID);
- }
+ long scoreCursor = Long.parseLong(cursorSplit[0]);
+ long idCursor = Long.parseLong(cursorSplit[1]);
+ reviewList = reviewRepository.findReviewsByScoreCursor(
+ memberId,
+ scoreCursor,
+ idCursor,
+ pageRequest);
} else {
- //커서 없이 조회
reviewList = reviewRepository.findReviewsByMember_IdOrderByScoreDescIdDesc(memberId, pageRequest);
}
List content = reviewList.getContent();
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java
index b05e24f2..87c93a9c 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/GeneralErrorCode.java
@@ -10,9 +10,6 @@ public enum GeneralErrorCode implements BaseErrorCode{
BAD_REQUEST(HttpStatus.BAD_REQUEST,
"COMMON400_1",
"잘못된 요청입니다."),
- UNAUTHORTIZED(HttpStatus.UNAUTHORIZED,
- "COMMON401_1",
- "인증되지 않았습니다."),
FORBIDDEN(HttpStatus.FORBIDDEN,
"COMMON403_1",
"접근이 금지되었습니다."),
@@ -29,6 +26,10 @@ public enum GeneralErrorCode implements BaseErrorCode{
"COMMON500",
"서버 내부 오류입니다."
),
+ UNAUTHORIZED(HttpStatus.UNAUTHORIZED,
+ "COMMON401_2",
+ "승인되지 않았습니다."
+ )
;
private final HttpStatus status;
private final String code;
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java
index 6b8c1df0..50603137 100644
--- a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java
@@ -1,23 +1,116 @@
package com.example.umc10th.global.config;
+import com.example.umc10th.global.security.filter.JwtAuthFilter;
+import com.example.umc10th.global.security.CustomAccessDenied;
+import com.example.umc10th.global.security.CustomEntryPoint;
+import com.example.umc10th.global.security.handler.OAuthSuccessHandler;
+import com.example.umc10th.global.security.service.CustomOAuthService;
+import com.example.umc10th.global.security.service.CustomUserDetailsService;
+import com.example.umc10th.global.security.util.JwtUtil;
+import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
+@RequiredArgsConstructor
public class SecurityConfig {
+ private final JwtUtil jwtUtil;
+ private final CustomUserDetailsService customUserDetailsService;
+ private final CustomOAuthService customOAuthService;
+
+ private final String[] allowUris = {
+ //swagger 허용
+ // 자유롭게 이용할 수 있는 주소 (비로그인)
+ "/swagger-ui/**",
+ "/swagger-resources/**",
+ "/v3/api-docs/**",
+ //로그인
+ "/api/auth/**",
+ "/oauth/**",
+ };
+
@Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
- .authorizeHttpRequests(auth -> auth
- .anyRequest().permitAll()
- );
+ // URI 허용 여부
+ .authorizeHttpRequests(requests ->requests
+ // public API 허용
+ .requestMatchers(allowUris).permitAll()
+ .requestMatchers("/oauth2/authorization/**").permitAll() // 로그인 진입 주소
+ .requestMatchers("/oauth/callback/kakao").permitAll() // ⭐️ 카카오 콜백 주소 허용!
+ // 그 이외의 API는 인증 필요
+ .anyRequest().authenticated()
+ )
+ //폼 로그인
+ .formLogin(AbstractHttpConfigurer::disable)
+
+ //세션
+ .sessionManagement(AbstractHttpConfigurer::disable)
+ .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
+ .logout(logout -> logout
+ .logoutUrl("/logout")
+ .logoutSuccessUrl("/login?logout")
+ .permitAll()
+ )
+ //예외 상황 핸들러
+ .exceptionHandling(exception -> exception
+ .accessDeniedHandler(customAccessDenied())
+ .authenticationEntryPoint(customEntryPoint())
+ )
+ //OAuth
+ .oauth2Login(oauth -> oauth
+ // 인증 엔트리 포인트
+ .authorizationEndpoint(auth -> auth
+ .baseUri("/oauth/authorize")
+ )
+ // 콜백 주소
+ .redirectionEndpoint(redirect -> redirect
+ .baseUri("/oauth/callback/**")
+ )
+ //인증 완료 후 정보 활용
+ .userInfoEndpoint(userInfo -> userInfo
+ .userService(customOAuthService)
+ )
+ // 성공 시 JWT 토큰 발행할 핸들러
+ .successHandler(oAuthSuccessHandler())
+ )
+ ;
return http.build();
}
+
+ //해시 알고리즘을 이용해 함호화된 Bcrypt 알고리즘 객체 반환
+ //알고리즘은 실행때마다 매번 다른 결과물 생성
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public CustomAccessDenied customAccessDenied() {
+ return new CustomAccessDenied();
+ }
+
+ @Bean
+ public CustomEntryPoint customEntryPoint() {
+ return new CustomEntryPoint();
+ }
+
+ private JwtAuthFilter jwtAuthFilter() {
+ return new JwtAuthFilter(jwtUtil, customUserDetailsService);
+ }
+
+ @Bean
+ public OAuthSuccessHandler oAuthSuccessHandler() {
+ return new OAuthSuccessHandler(jwtUtil);
+ }
}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java
new file mode 100644
index 00000000..0a9e8f40
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/entity/AuthMember.java
@@ -0,0 +1,29 @@
+package com.example.umc10th.global.entity;
+
+import com.example.umc10th.domain.member.entity.Member;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Getter
+@RequiredArgsConstructor
+public class AuthMember implements UserDetails {
+ private final Member member;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+ @Override
+ public String getPassword() {
+ return member.getPassword();
+ }
+ @Override
+ public String getUsername() {
+ return member.getEmail();
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java
new file mode 100644
index 00000000..deaea964
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/filter/JwtAuthFilter.java
@@ -0,0 +1,72 @@
+package com.example.umc10th.global.filter;
+
+import com.example.umc10th.global.apiPayload.ApiResponse;
+import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
+import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
+import com.example.umc10th.global.service.CustomUserDetailsService;
+import com.example.umc10th.global.util.JwtUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.jsonwebtoken.io.IOException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+@RequiredArgsConstructor
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+ private final JwtUtil jwtUtil;
+ private final CustomUserDetailsService customUserDetailsService;
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain
+ ) throws ServletException, IOException, java.io.IOException {
+
+ try {
+ // 토큰 가져오기
+ String token = request.getHeader("Authorization");
+ // token이 없거나 Bearer가 아니면 넘기기
+ if (token == null || !token.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ // Bearer이면 추출
+ token = token.replace("Bearer ", "");
+ // AccessToken 검증하기: 올바른 토큰이면
+ if (jwtUtil.isValid(token)) {
+ // 토큰에서 이메일 추출
+ String email = jwtUtil.getEmail(token);
+ // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성
+ UserDetails user = customUserDetailsService.loadUserByUsername(email);
+ Authentication auth = new UsernamePasswordAuthenticationToken(
+ user,
+ null,
+ user.getAuthorities()
+ );
+ // 인증 완료 후 SecurityContextHolder에 넣기
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ }
+ filterChain.doFilter(request, response);
+ } catch (Exception e) {
+ ObjectMapper mapper = new ObjectMapper();
+ BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;
+
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(code.getStatus().value());
+
+ ApiResponse errorResponse = ApiResponse.onFailure(code,null);
+
+ mapper.writeValue(response.getOutputStream(), errorResponse);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java
new file mode 100644
index 00000000..fc4ad57b
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDenied.java
@@ -0,0 +1,21 @@
+package com.example.umc10th.global.security;
+
+import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import java.io.IOException;
+
+public class CustomAccessDenied implements AccessDeniedHandler {
+
+ @Override
+ public void handle(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AccessDeniedException accessDeniedException
+ ) throws IOException {
+ SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.FORBIDDEN);
+ }
+}
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java
new file mode 100644
index 00000000..60e7713b
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/CustomEntryPoint.java
@@ -0,0 +1,21 @@
+package com.example.umc10th.global.security;
+
+import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import java.io.IOException;
+
+public class CustomEntryPoint implements AuthenticationEntryPoint {
+
+ @Override
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException
+ ) throws IOException {
+ SecurityResponseUtil.writeErrorResponse(response, GeneralErrorCode.UNAUTHORIZED);
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java
new file mode 100644
index 00000000..251ca496
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/SecurityResponseUtil.java
@@ -0,0 +1,19 @@
+package com.example.umc10th.global.security;
+
+import com.example.umc10th.global.apiPayload.ApiResponse;
+import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+public class SecurityResponseUtil {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ public static void writeErrorResponse(HttpServletResponse response, BaseErrorCode code) throws IOException {
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(code.getStatus().value());
+ objectMapper.writeValue(response.getOutputStream(), ApiResponse.onFailure(code, null));
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java
new file mode 100644
index 00000000..d1dcdbfd
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java
@@ -0,0 +1,32 @@
+package com.example.umc10th.global.security.dto;
+
+
+import lombok.RequiredArgsConstructor;
+
+//카카오용 DTO
+@RequiredArgsConstructor
+public class KakaoDTO implements OAuthDTO {
+ private final String id;
+ private final String name;
+ private final String email;
+
+ @Override
+ public SocialType getSocialType(){
+ return SocialType.KAKAO;
+ }
+
+ @Override
+ public String getSocialUid(){
+ return id;
+ }
+
+ @Override
+ public String getSocialEmail(){
+ return email;
+ }
+
+ @Override
+ public String getName(){
+ return name;
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java
new file mode 100644
index 00000000..dd9aefe5
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java
@@ -0,0 +1,10 @@
+package com.example.umc10th.global.security.dto;
+
+
+//공통 DTO (OAuth 제공자에 따라 정보가 다를 것을 대비한 OAuthDTO)
+public interface OAuthDTO {
+ SocialType getSocialType();
+ String getSocialUid();
+ String getSocialEmail();
+ String getName();
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java
new file mode 100644
index 00000000..7b94aaca
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/dto/SocialType.java
@@ -0,0 +1,5 @@
+package com.example.umc10th.global.security.dto;
+
+public enum SocialType {
+ KAKAO
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java
new file mode 100644
index 00000000..4eb2018f
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java
@@ -0,0 +1,29 @@
+package com.example.umc10th.global.security.entity;
+
+import com.example.umc10th.domain.member.entity.Member;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Getter
+@RequiredArgsConstructor
+public class AuthMember implements UserDetails {
+ private final Member member;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+ @Override
+ public String getPassword() {
+ return null;
+ }
+ @Override
+ public String getUsername() {
+ return member.getSocialUid();
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java
new file mode 100644
index 00000000..2998ef75
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java
@@ -0,0 +1,37 @@
+package com.example.umc10th.global.security.entity;
+
+
+import com.example.umc10th.domain.member.entity.Member;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+//OAuth 작업을 위한 객체
+@RequiredArgsConstructor
+public class OAuthMember implements OAuth2User {
+
+ @Getter
+ private final Member member;
+ private final Map attributes;
+
+ @Override
+ public Map getAttributes(){
+ return attributes;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities(){
+ return List.of();
+ }
+
+ @Override
+ public String getName() {
+ return member.getSocialUid();
+ }
+
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java
new file mode 100644
index 00000000..5ae3d288
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java
@@ -0,0 +1,75 @@
+package com.example.umc10th.global.security.filter;
+
+import com.example.umc10th.global.apiPayload.ApiResponse;
+import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
+import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
+import com.example.umc10th.global.security.dto.SocialType;
+import com.example.umc10th.global.security.service.CustomUserDetailsService;
+import com.example.umc10th.global.security.util.JwtUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+@RequiredArgsConstructor
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+ private final JwtUtil jwtUtil;
+ private final CustomUserDetailsService customUserDetailsService;
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain
+ ) throws ServletException, IOException {
+
+ try {
+ // 토큰 가져오기
+ String token = request.getHeader("Authorization");
+ // token이 없거나 Bearer가 아니면 넘기기
+ if (token == null || !token.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ // Bearer이면 추출
+ token = token.replace("Bearer ", "");
+ // AccessToken 검증하기: 올바른 토큰이면
+ if (jwtUtil.isValid(token)) {
+ // 토큰에서 이메일 추출
+ String uid = jwtUtil.getUid(token);
+ SocialType socialType = jwtUtil.getSocialType(token);
+ // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성
+ UserDetails member = customUserDetailsService.loadUserByUidAndSocialType(socialType, uid);
+ Authentication auth = new UsernamePasswordAuthenticationToken(
+ member,
+ null,
+ member.getAuthorities()
+ );
+ // 인증 완료 후 SecurityContextHolder에 넣기
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ }
+ filterChain.doFilter(request, response);
+ } catch (Exception e) {
+ ObjectMapper mapper = new ObjectMapper();
+ BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;
+
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(code.getStatus().value());
+
+ ApiResponse errorResponse = ApiResponse.onFailure(code,null);
+
+ mapper.writeValue(response.getOutputStream(), errorResponse);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java
new file mode 100644
index 00000000..61e01d70
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java
@@ -0,0 +1,58 @@
+package com.example.umc10th.global.security.handler;
+
+
+import com.example.umc10th.domain.member.converter.MemberConverter;
+import com.example.umc10th.domain.member.dto.MemberResDTO;
+import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
+import com.example.umc10th.global.apiPayload.ApiResponse;
+import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
+import com.example.umc10th.global.security.entity.AuthMember;
+import com.example.umc10th.global.security.entity.OAuthMember;
+import com.example.umc10th.global.security.util.JwtUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+
+import java.io.IOException;
+
+//사용자 정보를 가져오고 서비스가 실행된 다음 실행되는 핸들러
+@RequiredArgsConstructor
+public class OAuthSuccessHandler implements AuthenticationSuccessHandler {
+
+ private final JwtUtil jwtUtil;
+
+ @Override
+ public void onAuthenticationSuccess(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ Authentication authentication
+ ) throws IOException, ServletException {
+ // 사전 작업: Response 매핑할 ObjectMapper 선언
+ ObjectMapper objectMapper = new ObjectMapper();
+ BaseSuccessCode code = MemberSuccessCode.OK;
+
+ // Content-Type, Status 설정
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(code.getStatus().value());
+
+ // 인증 객체 컨테이너에서 OAuth 인증 객체 가져오기
+ OAuthMember member = (OAuthMember) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+
+ // 토큰 제작을 위해 OAuth 인증 객체에서 Member 추출 -> AuthMember 제작
+ String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember()));
+
+ // 응답 통일 객체 래핑
+ ApiResponse responseBody = ApiResponse.onSuccess(
+ code,
+ MemberConverter.toLogin(accessToken)
+ );
+
+ // 응답 출력
+ objectMapper.writeValue(response.getOutputStream(), responseBody);
+ }
+}
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java
new file mode 100644
index 00000000..44817c03
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java
@@ -0,0 +1,71 @@
+package com.example.umc10th.global.security.service;
+
+
+import com.example.umc10th.domain.member.converter.MemberConverter;
+import com.example.umc10th.domain.member.entity.Member;
+import com.example.umc10th.domain.member.exception.MemberException;
+import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
+import com.example.umc10th.domain.member.repository.MemberRepository;
+import com.example.umc10th.global.security.dto.KakaoDTO;
+import com.example.umc10th.global.security.dto.OAuthDTO;
+import com.example.umc10th.global.security.dto.SocialType;
+import com.example.umc10th.global.security.entity.OAuthMember;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+//OAuth 과정 중 사용자 정보를 가져온 다음 실행되는 서비스
+@Service
+@RequiredArgsConstructor
+public class CustomOAuthService extends DefaultOAuth2UserService {
+
+ private final MemberRepository memberRepository;
+
+ @Override
+ public OAuth2User loadUser(
+ OAuth2UserRequest userRequest
+ ) throws OAuth2AuthenticationException {
+ // (필수) 인증 서버의 일회성 토큰을 이용해 정보 조회 & 유저 객체 생성
+ OAuth2User oAuthMember = super.loadUser(userRequest);
+
+ // 유저 객체에서 정보 추출
+ SocialType providerId;
+ String socialUid;
+ try {
+ providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase());
+ socialUid = String.valueOf((Long) oAuthMember.getAttribute("id"));
+ } catch (IllegalArgumentException e) {
+ throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
+ }
+
+ // OAuth 공통 정보 DTO로 매핑
+ OAuthDTO dto;
+ switch (providerId) {
+ case KAKAO -> {
+ Map kakaoAccount = oAuthMember.getAttribute("kakao_account");
+ if (kakaoAccount == null) {
+ throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
+ }
+ Map profile = (Map) kakaoAccount.get("profile");
+ String email = kakaoAccount.get("email") != null ? kakaoAccount.get("email").toString() : "";
+ String name = (profile != null && profile.get("nickname") != null) ? profile.get("nickname").toString() : "";
+ dto = new KakaoDTO(socialUid, name, email);
+ }
+ default -> throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
+ }
+
+ // DB 저장: 있다면 그 데이터 가져오고 없으면 새로 저장
+ Member member = memberRepository.findBySocialTypeAndSocialUid(providerId, socialUid)
+ .orElseGet(() -> {
+ Member newMember = MemberConverter.toMember(dto);
+ memberRepository.save(newMember);
+ return newMember;
+ });
+ return new OAuthMember(member, oAuthMember.getAttributes());
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java
new file mode 100644
index 00000000..e8c3e7b8
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java
@@ -0,0 +1,28 @@
+package com.example.umc10th.global.security.service;
+
+import com.example.umc10th.domain.member.entity.Member;
+import com.example.umc10th.domain.member.exception.MemberException;
+import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
+import com.example.umc10th.domain.member.repository.MemberRepository;
+import com.example.umc10th.global.security.dto.SocialType;
+import com.example.umc10th.global.security.entity.AuthMember;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService{
+
+ private final MemberRepository memberRepository;
+
+ public UserDetails loadUserByUidAndSocialType(
+ SocialType socialType,
+ String username
+ ) throws UsernameNotFoundException{
+ Member member = memberRepository.findBySocialTypeAndSocialUid(socialType, username)
+ .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND));
+ return new AuthMember(member);
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java
new file mode 100644
index 00000000..7dcd97c6
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java
@@ -0,0 +1,109 @@
+package com.example.umc10th.global.security.util;
+
+import com.example.umc10th.global.security.dto.SocialType;
+import com.example.umc10th.global.security.entity.AuthMember;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.stream.Collectors;
+
+@Component
+public class JwtUtil {
+
+ private final SecretKey secretKey;
+ private final Duration accessExpiration;
+
+ public JwtUtil(
+ @Value("${jwt.token.secretKey}") String secret,
+ @Value("${jwt.token.expiration.access}") Long accessExpiration
+ ) {
+ this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ this.accessExpiration = Duration.ofMillis(accessExpiration);
+ }
+
+ // AccessToken 생성
+ public String createAccessToken(AuthMember member) {
+ return createToken(member, accessExpiration);
+ }
+
+ /** 토큰에서 유저 식별자(이메일) 가져오기
+ *
+ * @param token 유저 정보를 추출할 토큰
+ * @return subject에 저장된 유저 식별자를 반환합니다
+ */
+ public String getUid(String token) {
+ try {
+ return getClaims(token).getPayload().getSubject();
+ } catch (JwtException e) {
+ return null;
+ }
+ }
+
+ /** 토큰에서 소셜 로그인 타입 가져오기
+ *
+ * @param token 유저 정보를 추출할 토큰
+ * @return 유저 소셜 로그인타입을 토큰에서 추출합니다
+ */
+ public SocialType getSocialType(String token) {
+ try {
+ return SocialType.valueOf(getClaims(token).getPayload().get("social_type").toString().toUpperCase()); // Parsing해서 Subject 가져오기
+ } catch (JwtException e) {
+ return null;
+ }
+ }
+
+ /** 토큰 유효성 확인
+ *
+ * @param token 유효한지 확인할 토큰
+ * @return True, False 반환합니다
+ */
+ public boolean isValid(String token) {
+ try {
+ getClaims(token);
+ return true;
+ } catch (JwtException e) {
+ return false;
+ }
+ }
+
+ // 토큰 생성
+ private String createToken(AuthMember member, Duration expiration) {
+ Instant now = Instant.now();
+
+ // 인가 정보
+ String authorities = member.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.joining(","));
+
+ return Jwts.builder()
+ .subject(member.getUsername()) // User 이메일을 Subject로
+ .claim("role", authorities)
+ .claim("social_type", member.getMember().getSocialType())
+ .issuedAt(Date.from(now)) // 언제 발급한지
+ .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지
+ .signWith(secretKey) // sign할 Key
+ .compact();
+ }
+
+ // 토큰 정보 가져오기
+ private Jws getClaims(String token) throws JwtException {
+ return Jwts.parser()
+ .verifyWith(secretKey)
+ .clockSkewSeconds(60)
+ .build()
+ .parseSignedClaims(token);
+ }
+
+
+}
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java
new file mode 100644
index 00000000..173be810
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/service/CustomUserDetailsService.java
@@ -0,0 +1,27 @@
+package com.example.umc10th.global.service;
+
+import com.example.umc10th.domain.member.entity.Member;
+import com.example.umc10th.domain.member.exception.MemberException;
+import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
+import com.example.umc10th.domain.member.repository.MemberRepository;
+import com.example.umc10th.global.entity.AuthMember;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+ private final MemberRepository memberRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(
+ String username
+ ) throws UsernameNotFoundException {
+ Member member = memberRepository.findByEmail(username)
+ .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND));
+ return new AuthMember(member);
+ }
+}
diff --git a/Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java
new file mode 100644
index 00000000..48c2c1c5
--- /dev/null
+++ b/Gibeom/umc10th/src/main/java/com/example/umc10th/global/util/JwtUtil.java
@@ -0,0 +1,93 @@
+package com.example.umc10th.global.util;
+
+import com.example.umc10th.global.entity.AuthMember;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.stream.Collectors;
+
+@Component
+public class JwtUtil {
+
+ private final SecretKey secretKey;
+ private final Duration accessExpiration;
+
+ public JwtUtil(
+ @Value("${jwt.token.secretKey}") String secret,
+ @Value("${jwt.token.expiration.access}") Long accessExpiration
+ ) {
+ this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ this.accessExpiration = Duration.ofMillis(accessExpiration);
+ }
+
+ // AccessToken 생성
+ public String createAccessToken(AuthMember member) {
+ return createToken(member, accessExpiration);
+ }
+
+ /** 토큰에서 이메일 가져오기
+ *
+ * @param token 유저 정보를 추출할 토큰
+ * @return 유저 이메일을 토큰에서 추출합니다
+ */
+ public String getEmail(String token) {
+ try {
+ return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기
+ } catch (JwtException e) {
+ return null;
+ }
+ }
+
+ /** 토큰 유효성 확인
+ *
+ * @param token 유효한지 확인할 토큰
+ * @return True, False 반환합니다
+ */
+ public boolean isValid(String token) {
+ try {
+ getClaims(token);
+ return true;
+ } catch (JwtException e) {
+ return false;
+ }
+ }
+
+ // 토큰 생성
+ private String createToken(AuthMember member, Duration expiration) {
+ Instant now = Instant.now();
+
+ // 인가 정보
+ String authorities = member.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.joining(","));
+
+ return Jwts.builder()
+ .subject(member.getUsername()) // User 이메일을 Subject로
+ .claim("role", authorities)
+ .claim("email", member.getUsername())
+ .issuedAt(Date.from(now)) // 언제 발급한지
+ .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지
+ .signWith(secretKey) // sign할 Key
+ .compact();
+ }
+
+ // 토큰 정보 가져오기
+ private Jws getClaims(String token) throws JwtException {
+ return Jwts.parser()
+ .verifyWith(secretKey)
+ .clockSkewSeconds(60)
+ .build()
+ .parseSignedClaims(token);
+ }
+}
\ No newline at end of file
diff --git a/Gibeom/umc10th/src/main/resources/application.yml b/Gibeom/umc10th/src/main/resources/application.yml
index f7681ea6..e8dc9d77 100644
--- a/Gibeom/umc10th/src/main/resources/application.yml
+++ b/Gibeom/umc10th/src/main/resources/application.yml
@@ -3,17 +3,40 @@ spring:
name: "umc10th" # "umc10th"
datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC ???? ??? ??
+ driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC
url: ${DB_URL} # jdbc:mysql://localhost:3306/{???????}
- username: ${DB_USER} # MySQL ?? ??
- password: ${DB_PW} # MySQL ????
+ username: ${DB_USER} # MySQL
+ password: ${DB_PW} # MySQL
jpa:
- database: mysql # ??? ?????? ?? ?? (MySQL)
+ database: mysql
database-platform: org.hibernate.dialect.MySQLDialect # Hibernate?? ??? MySQL ??(dialect) ??
- show-sql: true # ??? SQL ??? ??? ???? ?? ??
+ show-sql: true
hibernate:
- ddl-auto: update # ?????? ?? ? ?????? ???? ??? ??
+ ddl-auto: update
properties:
hibernate:
- format_sql: true # ???? SQL ??? ?? ?? ???
\ No newline at end of file
+ format_sql: true
+ security:
+ oauth2:
+ client:
+ registration:
+ kakao:
+ client-id: ${KAKAO_REST_API_KEY}
+ client-secret: ${KAKAO_REST_API_SECRET}
+ authorization-grant-type: authorization_code
+ redirect-uri: "http://localhost:8080/oauth/callback/kakao"
+ scope:
+ - profile_nickname
+ - account_email
+ provider:
+ kakao:
+ authorization-uri: "https://kauth.kakao.com/oauth/authorize"
+ token-uri: "https://kauth.kakao.com/oauth/token"
+ user-info-uri: "https://kapi.kakao.com/v2/user/me"
+ userNameAttribute: id
+jwt:
+ token:
+ secretKey: ${JWT_SECRET_KEY}
+ expiration:
+ access: 1800000 #30분
\ No newline at end of file