diff --git a/Chanyeol/.env b/Chanyeol/.env deleted file mode 100644 index 941a143d..00000000 --- a/Chanyeol/.env +++ /dev/null @@ -1,3 +0,0 @@ -DB_USER=bcy1234 -DB_PW=asdasdasd2@ -DB_URL=jdbc:mysql://localhost:3306/umc_db \ No newline at end of file diff --git a/Chanyeol/build.gradle b/Chanyeol/build.gradle index 1c467859..f763c095 100644 --- a/Chanyeol/build.gradle +++ b/Chanyeol/build.gradle @@ -33,6 +33,10 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/Chanyeol/keyword-summary/ch07.md b/Chanyeol/keyword-summary/ch07.md deleted file mode 100644 index 8a7bad67..00000000 --- a/Chanyeol/keyword-summary/ch07.md +++ /dev/null @@ -1,63 +0,0 @@ -- Page와 Slice - - Page와 Slice는 페이징 시 쓰이는 반환 타입이다. - - Page는 Slice와 상속 관계인데, Slice가 가진 모든 메소드를 Page도 사용이 가능하다. - - 이 둘의 차이는 Page만 조회 쿼리 이후 전체 데이터 개수를 조회하는 쿼리가 한번 더 실행된다는 것이다. - - 따라서 Page에서는 총 개수와 전체 페이지 수를 알 수 있어 이러한 요소들이 필요할 때 쓰이고, - - Slice에서는 쿼리가 덜 실행이 되므로 더 빠른 성능을 가진다 → 커서 기반, 무한 스크롤에 최적 - -- Java stream API - - 배열, List, Set과 같은 컬렉션의 데이터를 함수형으로 처리하기 위한 API - - 원본 데이터를 변경하지 않고 지연 실행을 통해 효율적으로 데이터를 처리할 수 있게 해주는 추상 객체이다. - - Stream의 처리 구조는 다음과 같다: 생성 → 중간 연산 → 최종 연산 - - 중간 연산은 Stream을 받아서 또 다른 Stream을 반환하는 연산이고 - 원본 데이터를 변경하지 않고 여러 개를 연결하여 사용할 수 있다는 특징이 있다. - - 최종 연산은 Stream을 받아서 Stream이 아닌 결과를 반환하거나, 부수 효과를 발생시키는 연산이고 - 한 번만 사용할 수 있다는 특징이 있다.(2번 이상 사용 시 IllegalStateException 발생) - - 중간 연산은 지연 실행이 되어 실제로 최종 연산이 호출되기 전까지는 대기하고 있다가 최종 연산이 호출되면 그때 중간 연산들이 한 번에 실행이 된다. - - 중간 연산: filter, map, sorted, limit, skip, distinct 등 - - 최종 연산: collect, forEach, count, findFirst, anyMatch, reduce, toList() 등 - -- 객체 그래프 탐색 - - 객체에서 참조를 사용해 연관된 객체를 찾는 것 - - ``` - String storeName = review.getUserMission() - .getMission() - .getStore() - .getStoreName(); - ``` - - 이는 관계형 데이터베이스도 객체지향적으로 탐색이 가능하게 만들고 지연 로딩을 통해 필요한 순간에만 데이터를 로딩할 수 있다는 특징이 있다. - - 객체 그래프 탐색은 JPA의 양날의 검인데, 개발 생산성과 코드 가독성이 매우 높아지는 장점이 있지만, - N+1 문제, 그래프가 복잡해질수록 관리가 어렵다는 단점이 있다. - - 이를 해결하기 위해 여러가지 방법들이 있다. - - 1. Fetch Join(N+1 문제를 해결할 수 있지만 JPQL 직접 작성/페이징과 함께 사용 불가능 같은 단점이 있다.) - 2. @EntityGraph(깔끔한 JPQL 유지 및 동적인 그래프 관리가 가능하지만 Fetch Join만큼 세밀한 제어가 어렵고, 그래프가 복잡해질수록 급격하게 attributePaths가 길어진다는 단점이 있다.) - 3. @BatchSize(간단한 설정 및 기존 지연로딩 코드를 거의 수정할 필요가 없지만 N+1 문제를 해결하지는 못한다.) - 4. DTO 직접 조회(페이징과 완벽하게 호환이 되고 성능이 좋다는 장점이 있지만 엔티티가 아닌 DTO를 쓴다는 점에서 영속성 컨텍스트 관리가 불가능하고 객체 그래프를 그대로 재사용하기 어렵다는 단점이 있다.) -- @Valid vs @Validated - - @Vaild: Jakarta Bean Validation의 표준 어노테이션 - - 중첩된 객체 검증에 주로 사용 - - Controller에서 RequestBody, PathVariable, RequestParam 등에 붙임 - - @Vaildated: Spring에서 제공하는 어노테이션 - - Validation Group을 사용할 때 필수 - - 클래스 레벨이나 메서드 파라미터에 주로 사용 - - Validation Group? - - 같은 DTO에서 상황에 따라 다른 필드를 검증하고 싶을 때 사용하는 기능 \ No newline at end of file diff --git a/Chanyeol/keyword-summary/ch08.md b/Chanyeol/keyword-summary/ch08.md new file mode 100644 index 00000000..282a2ebf --- /dev/null +++ b/Chanyeol/keyword-summary/ch08.md @@ -0,0 +1,66 @@ +- Spring Security가 무엇인가? + + ## 스프링 기반의 애플리케이션의 보안을 담당하는 프레임워크 + + Spring Security는 필터 기반으로 동작한다. + + ❗️필터: 요청이 Dispatcher Servlet으로 가기 전에 위치해있으며, 클라이언트와 자원 사이에서 요청과 응답 정보를 이용해 다양한 처리를 하는 컴포넌트 + + ### 동작흐름 + + HTTP 요청 + ↓ + Filter Chain (여러 보안 필터 순차 실행) + ↓ + 인증 처리 (AuthenticationManager) + ↓ + 인가 처리 (AccessDecisionManager) + ↓ + 컨트롤러 도달 (or 403/401 반환) + + Spring Security는 `SecurityFilterChain`을 통해 요청마다 보안 처리를 수행한다. + + ![SecurityFilterChain](attachment:f16ac6dc-fafe-4c80-b041-8b6b695c9047:스크린샷_2026-05-18_오후_1.41.47.png) + + SecurityFilterChain + + ![Spring Security 인증 처리 과정](attachment:27a6f073-a478-47f1-8f2c-50703ef6fd13:스크린샷_2026-05-18_오후_1.43.36.png) + + Spring Security 인증 처리 과정 + + ### 주요 특징 + + | 특징 | 설명 | + | --- | --- | + | **다양한 인증 방식** | Form 로그인, HTTP Basic, JWT, OAuth2/OIDC, SAML 등 | + | **CSRF 보호** | Cross-Site Request Forgery 공격 방어 | + | **세션 관리** | 세션 고정 공격 방지, 동시 세션 제어 | + | **비밀번호 암호화** | BCrypt, Argon2 등 강력한 해시 알고리즘 | + | **Spring 통합** | Spring MVC, Spring Boot와 완벽하게 통합 | + + ### 자주 사용되는 시나리오 + + - **JWT 기반 인증** — REST API + 모바일/SPA 앱 + - **OAuth2 소셜 로그인** — 카카오, 구글, 네이버 로그인 + - **세션 기반 인증** — 전통적인 MVC 웹 앱 + - **메서드 레벨 보안** — `@PreAuthorize`, `@Secured` 어노테이션 +- 인증(Authentication)vs 인가(Authorization) + + ![스크린샷 2026-05-18 오후 1.55.14.png](attachment:bb447443-1268-43fd-8edb-041dc7f1dfe0:스크린샷_2026-05-18_오후_1.55.14.png) + + - 인증(Authentication) : 본인이누구인지확인 (로그인) + - 승인(Authorization) : 특정 리소스에권한이 있는지확인 (등급 권한) + + | | 인증(Authentication) | 인가(Authorization) | + | --- | --- | --- | + | 기능 | 자격 증명 확인 | 권한 허기/거부 | + | 진행 방식 | 비밀번호, 생체인식, 일회용 핀 또는 앱 | 보안 팀에서 관리하는 설정 사용 | + | 사용자 확인 가능 여부 | 가능 | 불가능 | + | 사용자 변경 가능 여부 | 부분적 가능 | 불가능 | + | 데이터 전송 | ID 토큰 사용 | 액세스 토큰 사용 | +- Stateful vs Stateless + - Stateful: ****서버가 클라이언트 상태를 **기억** + - Stateless**:** 서버가 클라이언트 상태를 **기억하지 않음** + + Stateful → 서버가 기억 → 세션/쿠키 → 전통적 웹 + Stateless → 서버가 무상태 → JWT 토큰 → REST API / 모바일 \ No newline at end of file diff --git "a/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.41.47.png" "b/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.41.47.png" new file mode 100644 index 00000000..c45ab4c4 Binary files /dev/null and "b/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.41.47.png" differ diff --git "a/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.43.36.png" "b/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.43.36.png" new file mode 100644 index 00000000..e910d6fe Binary files /dev/null and "b/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.43.36.png" differ diff --git "a/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.55.14.png" "b/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.55.14.png" new file mode 100644 index 00000000..03bcf83e Binary files /dev/null and "b/Chanyeol/keyword-summary/\354\212\244\355\201\254\353\246\260\354\203\267 2026-05-18 \354\230\244\355\233\204 1.55.14.png" differ diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/controller/AskController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/controller/AskController.java index 990f6dcc..15b3020c 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/controller/AskController.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/controller/AskController.java @@ -5,8 +5,10 @@ import com.example.umc10thweek4.domain.ask.exception.code.AskSuccessCode; import com.example.umc10thweek4.domain.ask.service.AskService; import com.example.umc10thweek4.global.apiPayload.ApiResponse; +import com.example.umc10thweek4.global.security.util.SecurityUtil; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,28 +24,28 @@ public class AskController { * 문의 등록 */ @PostMapping("/v1/asks") - public ApiResponse createAsk(@RequestBody @Valid AskReqDTO.Create request) { - Long currentMemberId = 1L; // TODO: SecurityContext에서 가져오기 + public ResponseEntity> createAsk(@RequestBody @Valid AskReqDTO.Create request) { + Long currentMemberId = SecurityUtil.getCurrentMemberId(); AskResDTO.Create response = askService.createAsk(currentMemberId, request); - return ApiResponse.onSuccess(AskSuccessCode.OK, response); + return ApiResponse.onSuccessResponse(AskSuccessCode.CREATE_SUCCESS, response); } /** * 내가 작성한 문의 목록 */ @GetMapping("/v1/users/{userId}/asks") - public ApiResponse> getMyAsks(@PathVariable Long userId) { + public ResponseEntity>> getMyAsks(@PathVariable Long userId) { List response = askService.getMyAsks(userId); - return ApiResponse.onSuccess(AskSuccessCode.OK, response); + return ApiResponse.onSuccessResponse(AskSuccessCode.LIST_SUCCESS, response); } /** * 문의 상세 조회 */ @GetMapping("/v1/asks/{askId}") - public ApiResponse getAskDetail(@PathVariable Long askId) { + public ResponseEntity> getAskDetail(@PathVariable Long askId) { AskResDTO.GetDetail response = askService.getAskDetail(askId); - return ApiResponse.onSuccess(AskSuccessCode.OK, response); + return ApiResponse.onSuccessResponse(AskSuccessCode.DETAIL_SUCCESS, response); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/exception/code/AskSuccessCode.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/exception/code/AskSuccessCode.java index 77d3e0bb..23f34c11 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/exception/code/AskSuccessCode.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/ask/exception/code/AskSuccessCode.java @@ -11,10 +11,10 @@ public enum AskSuccessCode implements BaseSuccessCode { OK(HttpStatus.OK, "ASK200_1", "요청 성공"), CREATE_SUCCESS(HttpStatus.CREATED, "ASK201_1", "문의 등록 성공"), - LIST_SUCCESS(HttpStatus.OK, "ASK202_1", "문의 목록 조회 성공"), - DETAIL_SUCCESS(HttpStatus.OK, "ASK203_1", "문의 상세 조회 성공"); + LIST_SUCCESS(HttpStatus.OK, "ASK200_2", "문의 목록 조회 성공"), + DETAIL_SUCCESS(HttpStatus.OK, "ASK200_3", "문의 상세 조회 성공"); private final HttpStatus status; private final String code; private final String message; -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/controller/MemberController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/controller/MemberController.java index a99cd4f0..f807819e 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/controller/MemberController.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/controller/MemberController.java @@ -1,12 +1,11 @@ package com.example.umc10thweek4.domain.member.controller; -import com.example.umc10thweek4.domain.member.dto.MemberReqDTO; import com.example.umc10thweek4.domain.member.dto.MemberResDTO; import com.example.umc10thweek4.domain.member.exception.code.MemberSuccessCode; import com.example.umc10thweek4.domain.member.service.MemberService; import com.example.umc10thweek4.global.apiPayload.ApiResponse; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -16,13 +15,8 @@ public class MemberController { private final MemberService memberService; - @PostMapping("/v1/users") - public ApiResponse signUp(@RequestBody @Valid MemberReqDTO.SignUp request) { - return ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP_SUCCESS, memberService.signUp(request)); - } - @GetMapping("/v1/users/{userId}") - public ApiResponse getMyPage(@PathVariable Long userId) { - return ApiResponse.onSuccess(MemberSuccessCode.MY_PAGE_SUCCESS, memberService.getMyPage(userId)); + public ResponseEntity> getMyPage(@PathVariable Long userId) { + return ApiResponse.onSuccessResponse(MemberSuccessCode.MY_PAGE_SUCCESS, memberService.getMyPage(userId)); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/entity/Member.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/entity/Member.java index 2b8f65c9..c4f77979 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/entity/Member.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/entity/Member.java @@ -34,18 +34,20 @@ public class Member extends BaseEntity { @Column(nullable = false) private Gender gender; - @Column(nullable = false) + @Column(nullable = false, unique = true) private String email; - @Column(name = "phone_number", nullable = false) + @Column(name = "phone_number") private String phoneNumber; @Column(nullable = false) private String password; + @Builder.Default @Column(name = "total_points") private Long totalPoints = 0L; + @Builder.Default @Column(name = "complete_num") private Integer completeNum = 0; diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberErrorCode.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberErrorCode.java index 85a57074..97346a12 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberErrorCode.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberErrorCode.java @@ -9,6 +9,8 @@ @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { + INVALID_BIRTHDAY(HttpStatus.BAD_REQUEST, "MEMBER400_1", "생년월일은 yyyy-MM-dd 형식이어야 합니다."), + INVALID_GENDER(HttpStatus.BAD_REQUEST, "MEMBER400_2", "성별은 MALE, FEMALE, NONE 중 하나여야 합니다."), DUPLICATE_EMAIL(HttpStatus.CONFLICT, "MEMBER409_1", "이미 사용 중인 이메일입니다."), DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "MEMBER409_2", "이미 사용 중인 닉네임입니다."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "존재하지 않는 회원입니다."), diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberSuccessCode.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberSuccessCode.java index af5835f4..9a00b12c 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberSuccessCode.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/exception/code/MemberSuccessCode.java @@ -11,9 +11,9 @@ public enum MemberSuccessCode implements BaseSuccessCode { OK(HttpStatus.OK, "MEMBER200_1", "요청 성공"), SIGN_UP_SUCCESS(HttpStatus.CREATED, "MEMBER201_1", "회원가입 성공"), - MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER202_1", "마이페이지 조회 성공"); + MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER200_2", "마이페이지 조회 성공"); private final HttpStatus status; private final String code; private final String message; -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/service/MemberService.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/service/MemberService.java index 7ad8f3e3..d990f39f 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/service/MemberService.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/member/service/MemberService.java @@ -13,11 +13,13 @@ import com.example.umc10thweek4.domain.member.repository.MemberNoticeSettingRepository; import com.example.umc10thweek4.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; @Service @@ -28,6 +30,7 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberNoticeSettingRepository noticeSettingRepository; private final MemberFoodPreferenceRepository foodPreferenceRepository; + private final PasswordEncoder passwordEncoder; /** * 회원가입 @@ -43,15 +46,17 @@ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) { throw new MemberException(MemberErrorCode.DUPLICATE_NICKNAME); } + LocalDate birthday = parseBirthday(request.birthday()); + Gender gender = parseGender(request.gender()); + Member member = memberRepository.save( Member.builder() .name(request.name()) .nickname(request.nickname()) .email(request.email()) - .password(request.password()) // TODO: 비밀번호 암호화 - .birth(LocalDate.parse(request.birthday(), - DateTimeFormatter.ofPattern("yyyy-MM-dd"))) - .gender(Gender.valueOf(request.gender().toUpperCase())) + .password(passwordEncoder.encode(request.password())) + .birth(birthday) + .gender(gender) .phoneNumber(request.phoneNum()) .build() ); @@ -80,6 +85,22 @@ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) { return MemberConverter.toSignUpRes(member); } + private LocalDate parseBirthday(String birthday) { + try { + return LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } catch (DateTimeParseException e) { + throw new MemberException(MemberErrorCode.INVALID_BIRTHDAY); + } + } + + private Gender parseGender(String gender) { + try { + return Gender.valueOf(gender.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.INVALID_GENDER); + } + } + /** * 마이페이지 조회 */ @@ -93,4 +114,4 @@ public MemberResDTO.GetInfo getMyPage(Long userId) { return MemberConverter.toGetInfoRes(member, ns); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/controller/MissionController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/controller/MissionController.java index ce310c7e..7f94ebc3 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/controller/MissionController.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/controller/MissionController.java @@ -8,7 +8,7 @@ import com.example.umc10thweek4.global.apiPayload.code.BaseSuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -21,41 +21,41 @@ public class MissionController { private final MissionService missionService; @GetMapping("/v1/home") - public ApiResponse getHome( + public ResponseEntity> getHome( @RequestParam(required = false) Long regionId, @RequestParam Long memberId ) { - return ApiResponse.onSuccess(MissionSuccessCode.HOME_SUCCESS, + return ApiResponse.onSuccessResponse(MissionSuccessCode.HOME_SUCCESS, missionService.getHomeMissions(regionId, memberId)); } @GetMapping("/v1/users/{userId}/missions") - public ApiResponse> getMyMissions( + public ResponseEntity>> getMyMissions( @PathVariable Long userId, @RequestParam(defaultValue = "0") Integer pageNumber, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String sort) { - return ApiResponse.onSuccess(MissionSuccessCode.MY_MISSION_SUCCESS, + return ApiResponse.onSuccessResponse(MissionSuccessCode.MY_MISSION_SUCCESS, missionService.getMyMissions(userId, pageSize, pageNumber, sort)); } @GetMapping("/v1/missions/{missionId}") - public ApiResponse getMissionDetail(@PathVariable Long missionId) { - return ApiResponse.onSuccess(MissionSuccessCode.OK, null); + public ResponseEntity> getMissionDetail(@PathVariable Long missionId) { + return ApiResponse.onSuccessResponse(MissionSuccessCode.MISSION_DETAIL_SUCCESS, null); } // 가게 미션 설정 @PostMapping("/v1/stores/{storeId}/missions") - public ApiResponse createStoreMission(@PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission missionReqDTO) { + public ResponseEntity> createStoreMission(@PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission missionReqDTO) { BaseSuccessCode code = MissionSuccessCode.CREATED; - return ApiResponse.onSuccess(code, missionService.createMission(storeId, missionReqDTO)); + return ApiResponse.onSuccessResponse(code, missionService.createMission(storeId, missionReqDTO)); } // 가게 내 미션들 조회 @GetMapping("/v1/stores/{storeId}/missions") - public ApiResponse> getStoreMissions(@PathVariable Long storeId) { + public ResponseEntity>> getStoreMissions(@PathVariable Long storeId) { BaseSuccessCode code = MissionSuccessCode.STORE_MISSION_SUCCESS; - return ApiResponse.onSuccess(code, missionService.getStoreMissions(storeId)); + return ApiResponse.onSuccessResponse(code, missionService.getStoreMissions(storeId)); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/exception/code/MissionSuccessCode.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/exception/code/MissionSuccessCode.java index 5233c842..b8b93347 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/exception/code/MissionSuccessCode.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/mission/exception/code/MissionSuccessCode.java @@ -9,15 +9,15 @@ @RequiredArgsConstructor public enum MissionSuccessCode implements BaseSuccessCode { - CREATED(HttpStatus.OK, "MISSION201_1", "미션 생성 성공"), + CREATED(HttpStatus.CREATED, "MISSION201_1", "미션 생성 성공"), OK(HttpStatus.OK, "MISSION200_1", "요청 성공"), HOME_SUCCESS(HttpStatus.OK, "MISSION200_2", "홈 화면 조회 성공"), MY_MISSION_SUCCESS(HttpStatus.OK, "MISSION200_3", "내 미션 목록 조회 성공"), STORE_MISSION_SUCCESS(HttpStatus.OK, "MISSION200_4", "가게 미션 목록 조회 성공"), MISSION_DETAIL_SUCCESS(HttpStatus.OK, "MISSION200_5", "미션 상세 조회 성공"), - MISSION_COMPLETE_SUCCESS(HttpStatus.OK, "MISSION204_1", "미션 완료 처리 성공"); + MISSION_COMPLETE_SUCCESS(HttpStatus.OK, "MISSION200_6", "미션 완료 처리 성공"); private final HttpStatus status; private final String code; private final String message; -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/region/controller/RegionController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/region/controller/RegionController.java index d86a0017..5e749e5b 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/region/controller/RegionController.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/region/controller/RegionController.java @@ -5,6 +5,7 @@ import com.example.umc10thweek4.domain.region.service.RegionService; import com.example.umc10thweek4.global.apiPayload.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -15,12 +16,12 @@ public class RegionController { private final RegionService regionService; @GetMapping("/v1/regions") - public ApiResponse getRegions() { - return ApiResponse.onSuccess(RegionSuccessCode.OK, regionService.getAllRegions()); + public ResponseEntity> getRegions() { + return ApiResponse.onSuccessResponse(RegionSuccessCode.OK, regionService.getAllRegions()); } @GetMapping("/v1/regions/{regionId}") - public ApiResponse getRegion(@PathVariable Long regionId) { - return ApiResponse.onSuccess(RegionSuccessCode.OK, regionService.getRegion(regionId)); + public ResponseEntity> getRegion(@PathVariable Long regionId) { + return ApiResponse.onSuccessResponse(RegionSuccessCode.OK, regionService.getRegion(regionId)); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/controller/ReviewController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/controller/ReviewController.java index d03a1a66..703725e6 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/controller/ReviewController.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/controller/ReviewController.java @@ -5,8 +5,10 @@ import com.example.umc10thweek4.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10thweek4.domain.review.service.ReviewService; import com.example.umc10thweek4.global.apiPayload.ApiResponse; +import com.example.umc10thweek4.global.security.util.SecurityUtil; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -17,27 +19,27 @@ public class ReviewController { private final ReviewService reviewService; @PostMapping("/v1/stores/{storeId}/reviews") - public ApiResponse createReview( + public ResponseEntity> createReview( @PathVariable Long storeId, @RequestBody @Valid ReviewReqDTO.Create request) { - Long currentMemberId = 1L; // 임시 값 + Long currentMemberId = SecurityUtil.getCurrentMemberId(); Long userMissionId = 1L; // 임시 값 ReviewResDTO.Create response = reviewService.createReview(currentMemberId, request, userMissionId); - return ApiResponse.onSuccess(ReviewSuccessCode.OK, response); + return ApiResponse.onSuccessResponse(ReviewSuccessCode.CREATE_SUCCESS, response); } @GetMapping("/v1/users/{userId}/reviews") - public ApiResponse> getMyReviews( + public ResponseEntity>> getMyReviews( @PathVariable Long userId, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String cursor, @RequestParam(required = false) ReviewReqDTO.SortType sort) { // cursor = "reviewId:createdAt" 형태 - return ApiResponse.onSuccess(ReviewSuccessCode.OK, + return ApiResponse.onSuccessResponse(ReviewSuccessCode.LIST_SUCCESS, reviewService.getMyReviews(userId, pageSize, cursor, sort)); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/exception/code/ReviewSuccessCode.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/exception/code/ReviewSuccessCode.java index 27e1898f..41f9628a 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/exception/code/ReviewSuccessCode.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/review/exception/code/ReviewSuccessCode.java @@ -11,7 +11,13 @@ public enum ReviewSuccessCode implements BaseSuccessCode { OK(HttpStatus.OK, "REVIEW200_1", - "성공적으로 문의를 조회했습니다."), + "요청 성공"), + CREATE_SUCCESS(HttpStatus.CREATED, + "REVIEW201_1", + "리뷰 등록 성공"), + LIST_SUCCESS(HttpStatus.OK, + "REVIEW200_2", + "리뷰 목록 조회 성공"), ; private final HttpStatus status; diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/controller/StoreController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/controller/StoreController.java index 65c0c175..98d73f81 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/controller/StoreController.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/controller/StoreController.java @@ -5,6 +5,7 @@ import com.example.umc10thweek4.domain.store.service.StoreService; import com.example.umc10thweek4.global.apiPayload.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -15,17 +16,17 @@ public class StoreController { private final StoreService storeService; @GetMapping("/v1/regions/{regionId}/stores") - public ApiResponse getStoresByRegion( + public ResponseEntity> getStoresByRegion( @PathVariable Long regionId, @RequestParam(required = false) String category) { - return ApiResponse.onSuccess(StoreSuccessCode.OK, + return ApiResponse.onSuccessResponse(StoreSuccessCode.OK, storeService.getStoresByRegion(regionId, category)); } @GetMapping("/v1/stores/{storeId}") - public ApiResponse getStoreDetail(@PathVariable Long storeId) { - return ApiResponse.onSuccess(StoreSuccessCode.OK, + public ResponseEntity> getStoreDetail(@PathVariable Long storeId) { + return ApiResponse.onSuccessResponse(StoreSuccessCode.OK, storeService.getStoreDetail(storeId)); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/exception/code/StoreSuccessCode.java b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/exception/code/StoreSuccessCode.java index c745399e..7a59776b 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/exception/code/StoreSuccessCode.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/domain/store/exception/code/StoreSuccessCode.java @@ -10,9 +10,9 @@ public enum StoreSuccessCode implements BaseSuccessCode { OK(HttpStatus.OK, "STORE200", "가게 조회 성공"), - STORE_MISSIONS_SUCCESS(HttpStatus.OK, "STORE201", "가게 미션 목록 조회 성공"); + STORE_MISSIONS_SUCCESS(HttpStatus.OK, "STORE200_2", "가게 미션 목록 조회 성공"); private final HttpStatus status; private final String code; private final String message; -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/ApiResponse.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/ApiResponse.java index 43be093f..0f528552 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/ApiResponse.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/ApiResponse.java @@ -2,13 +2,11 @@ import com.example.umc10thweek4.global.apiPayload.code.BaseErrorCode; import com.example.umc10thweek4.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10thweek4.global.apiPayload.code.GeneralSuccessCode; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.AllArgsConstructor; -import lombok.Getter; +import org.springframework.http.ResponseEntity; -@Getter @AllArgsConstructor @JsonPropertyOrder({"isSuccess", "code", "message", "result"}) public class ApiResponse { @@ -32,4 +30,14 @@ public static ApiResponse onSuccess(BaseSuccessCode successCode, T result public static ApiResponse onFailure(BaseErrorCode errorCode, T result) { return new ApiResponse<>(false, errorCode.getCode(), errorCode.getMessage(), result); } + + public static ResponseEntity> onSuccessResponse(BaseSuccessCode successCode, T result) { + return ResponseEntity.status(successCode.getStatus()) + .body(onSuccess(successCode, result)); + } + + public static ResponseEntity> onFailureResponse(BaseErrorCode errorCode, T result) { + return ResponseEntity.status(errorCode.getStatus()) + .body(onFailure(errorCode, result)); + } } diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/handler/GeneralExceptionAdvice.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/handler/GeneralExceptionAdvice.java index 7bdd28cf..12bde6a9 100644 --- a/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -21,8 +21,7 @@ public ResponseEntity> handleMemberException( ProjectException e ) { BaseErrorCode errorCode = e.getErrorCode(); - return ResponseEntity.status(errorCode.getStatus()) - .body(ApiResponse.onFailure(errorCode, null)); + return ApiResponse.onFailureResponse(errorCode, null); } // 그 외의 정의되지 않은 모든 예외 처리 @@ -32,12 +31,7 @@ public ResponseEntity> handleException( ) { BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR; - return ResponseEntity.status(code.getStatus()) - .body(ApiResponse.onFailure( - code, - ex.getMessage() - ) - ); + return ApiResponse.onFailureResponse(code, ex.getMessage()); } // @Valid 어노테이션 검증 실패 예외 @@ -52,7 +46,6 @@ public ResponseEntity>> handleMethodArgumentNotV }); BaseErrorCode code = GeneralErrorCode.BAD_REQUEST; - return ResponseEntity.status(code.getStatus()) - .body(ApiResponse.onFailure(code, errors)); + return ApiResponse.onFailureResponse(code, errors); } -} \ No newline at end of file +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/config/PasswordEncoderConfig.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/config/PasswordEncoderConfig.java new file mode 100644 index 00000000..281aa134 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/config/PasswordEncoderConfig.java @@ -0,0 +1,15 @@ +package com.example.umc10thweek4.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/config/SecurityConfig.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/config/SecurityConfig.java new file mode 100644 index 00000000..dc83eca9 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/config/SecurityConfig.java @@ -0,0 +1,65 @@ +package com.example.umc10thweek4.global.config; + +import com.example.umc10thweek4.global.security.provider.CustomAuthenticationProvider; +import com.example.umc10thweek4.global.security.handler.CustomAccessDeniedHandler; +import com.example.umc10thweek4.global.security.handler.CustomAuthenticationEntryPoint; +import com.example.umc10thweek4.global.security.handler.CustomAuthenticationFailureHandler; +import com.example.umc10thweek4.global.security.handler.CustomAuthenticationSuccessHandler; +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.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final CustomAuthenticationProvider customAuthenticationProvider; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; + private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler; + + private final String[] publicUris = { + // Swagger 허용 + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + + // 인증 없이 접근 가능한 API + "/auth/signup", + "/auth/login" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authenticationProvider(customAuthenticationProvider) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests -> requests + .requestMatchers(publicUris).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginProcessingUrl("/auth/login") + .usernameParameter("email") + .passwordParameter("password") + .successHandler(customAuthenticationSuccessHandler) + .failureHandler(customAuthenticationFailureHandler) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/auth/logout") + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + ); + + return http.build(); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/controller/AuthController.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/controller/AuthController.java new file mode 100644 index 00000000..dd41957d --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/controller/AuthController.java @@ -0,0 +1,27 @@ +package com.example.umc10thweek4.global.security.controller; + +import com.example.umc10thweek4.domain.member.dto.MemberReqDTO; +import com.example.umc10thweek4.domain.member.dto.MemberResDTO; +import com.example.umc10thweek4.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10thweek4.domain.member.service.MemberService; +import com.example.umc10thweek4.global.apiPayload.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/auth") +public class AuthController { + + private final MemberService memberService; + + @PostMapping("/signup") + public ResponseEntity> signUp(@RequestBody @Valid MemberReqDTO.SignUp request) { + return ApiResponse.onSuccessResponse(MemberSuccessCode.SIGN_UP_SUCCESS, memberService.signUp(request)); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/entity/AuthMember.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/entity/AuthMember.java new file mode 100644 index 00000000..e422f278 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/entity/AuthMember.java @@ -0,0 +1,32 @@ +package com.example.umc10thweek4.global.security.entity; + +import com.example.umc10thweek4.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 getAuthorities() { + return List.of(); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAccessDeniedHandler.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..186ad585 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAccessDeniedHandler.java @@ -0,0 +1,27 @@ +package com.example.umc10thweek4.global.security.handler; + +import com.example.umc10thweek4.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final SecurityResponseWriter securityResponseWriter; + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + securityResponseWriter.writeFailure(response, GeneralErrorCode.FORBIDDEN); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationEntryPoint.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..0a9c89e4 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationEntryPoint.java @@ -0,0 +1,27 @@ +package com.example.umc10thweek4.global.security.handler; + +import com.example.umc10thweek4.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final SecurityResponseWriter securityResponseWriter; + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + securityResponseWriter.writeFailure(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationFailureHandler.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationFailureHandler.java new file mode 100644 index 00000000..6fa9b881 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationFailureHandler.java @@ -0,0 +1,27 @@ +package com.example.umc10thweek4.global.security.handler; + +import com.example.umc10thweek4.global.apiPayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final SecurityResponseWriter securityResponseWriter; + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException { + securityResponseWriter.writeFailure(response, GeneralErrorCode.UNAUTHORIZED); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationSuccessHandler.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationSuccessHandler.java new file mode 100644 index 00000000..c96a9e1d --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,27 @@ +package com.example.umc10thweek4.global.security.handler; + +import com.example.umc10thweek4.global.apiPayload.code.GeneralSuccessCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final SecurityResponseWriter securityResponseWriter; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException { + securityResponseWriter.writeSuccess(response, GeneralSuccessCode.OK); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/SecurityResponseWriter.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/SecurityResponseWriter.java new file mode 100644 index 00000000..6b028c7e --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/handler/SecurityResponseWriter.java @@ -0,0 +1,32 @@ +package com.example.umc10thweek4.global.security.handler; + +import com.example.umc10thweek4.global.apiPayload.ApiResponse; +import com.example.umc10thweek4.global.apiPayload.code.BaseErrorCode; +import com.example.umc10thweek4.global.apiPayload.code.BaseSuccessCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Component +public class SecurityResponseWriter { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public void writeFailure(HttpServletResponse response, BaseErrorCode errorCode) throws IOException { + response.setStatus(errorCode.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + objectMapper.writeValue(response.getWriter(), ApiResponse.onFailure(errorCode, null)); + } + + public void writeSuccess(HttpServletResponse response, BaseSuccessCode successCode) throws IOException { + response.setStatus(successCode.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + objectMapper.writeValue(response.getWriter(), ApiResponse.onSuccess(successCode, null)); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/provider/CustomAuthenticationProvider.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/provider/CustomAuthenticationProvider.java new file mode 100644 index 00000000..8b59ba8a --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/provider/CustomAuthenticationProvider.java @@ -0,0 +1,32 @@ +package com.example.umc10thweek4.global.security.provider; + +import com.example.umc10thweek4.global.security.service.CustomUserDetailsService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class CustomAuthenticationProvider extends DaoAuthenticationProvider { + + public CustomAuthenticationProvider( + CustomUserDetailsService userDetailsService, + PasswordEncoder passwordEncoder + ) { + super(userDetailsService); + setPasswordEncoder(passwordEncoder); + } + + @Override + protected void additionalAuthenticationChecks( + UserDetails userDetails, + UsernamePasswordAuthenticationToken authentication + ) throws AuthenticationException { + super.additionalAuthenticationChecks(userDetails, authentication); + + // TODO: 여기에 추가 검증 로직 작성 + // 예: 탈퇴 회원, 정지 회원, 이메일 인증 여부 등 + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/service/CustomUserDetailsService.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/service/CustomUserDetailsService.java new file mode 100644 index 00000000..d1d41458 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package com.example.umc10thweek4.global.security.service; + +import com.example.umc10thweek4.domain.member.entity.Member; +import com.example.umc10thweek4.domain.member.repository.MemberRepository; +import com.example.umc10thweek4.global.security.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.findActiveByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("회원을 찾을 수 없습니다.")); + return new AuthMember(member); + } +} diff --git a/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/util/SecurityUtil.java b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/util/SecurityUtil.java new file mode 100644 index 00000000..0b533e24 --- /dev/null +++ b/Chanyeol/src/main/java/com/example/umc10thweek4/global/security/util/SecurityUtil.java @@ -0,0 +1,23 @@ +package com.example.umc10thweek4.global.security.util; + +import com.example.umc10thweek4.global.security.entity.AuthMember; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtil { + + public static AuthMember getCurrentAuthMember() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !(authentication.getPrincipal() instanceof AuthMember + authMember)) { + throw new RuntimeException("로그인 정보가 없습니다."); + } + + return authMember; + } + + public static Long getCurrentMemberId() { + return getCurrentAuthMember().getMember().getId(); + } +}