From 7b6dd043b5c2769fadba006dc58032d74d5d06a5 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Wed, 13 May 2026 19:33:12 +0900 Subject: [PATCH 01/34] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20status=20READY=EC=83=81=ED=83=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/converter/MissionConverter.java | 2 +- .../com/example/umc10th/domain/mission/dto/MissionReqDTO.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 07017df2..fbab0a4d 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -75,7 +75,7 @@ public static Mission toMission( ){ return Mission.builder() .store(store) - .status(dto.status()) + .status(MissionStatus.READY) .rewardPoint(dto.rewardPoint()) .deadline(dto.deadline()) .content(dto.content()) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 54799596..5ab9e76c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -37,8 +37,7 @@ public record CreateMission( @NotNull(message = "마감기한은 필수입니다.") LocalDate deadline, @NotNull(message = "미션 성공 포인트는 필수입니다.") - Integer rewardPoint, - MissionStatus status + Integer rewardPoint ) { } } From 591549dc917245a57922163d028c58384f94d01a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 15 May 2026 12:48:38 +0900 Subject: [PATCH 02/34] =?UTF-8?q?feat:=20ResponseEntity=EB=A1=9C=20HTTP=20?= =?UTF-8?q?status=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 8 +++--- .../mission/controller/MissionController.java | 27 ++++++++++++------- .../review/controller/ReviewController.java | 15 ++++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index eaa23a2c..7c422bad 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -7,6 +7,7 @@ import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; 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; @@ -21,12 +22,13 @@ public class MemberController { // 마이페이지 @PostMapping("/v1/members/me") - public ApiResponse getInfo( + public ResponseEntity> getInfo( // 받은 JSON 데이터를 자바 객체(dto)로 변환해서 씀 @RequestBody MemberReqDTO.GetInfo dto ){ - BaseSuccessCode code= MemberSuccessCode.OK; - return ApiResponse.onSuccess(code,memberService.getInfo(dto)); + return ResponseEntity + .status(MemberSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(dto))); } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 408bcd54..634e55ff 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -13,6 +13,7 @@ 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; @@ -25,41 +26,47 @@ public class MissionController { // 내가 진행중이거나 완료한 미션 목록 조회(페이징 포함) @PostMapping("/members/missions") - public ApiResponse getMemberMissions( + public ResponseEntity> getMemberMissions( @RequestBody MissionReqDTO.MissionListReqDTO dto ){ // 서비스에서 데이터 가져오기 - return ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(dto)); + return ResponseEntity + .status(MissionSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(dto))); } // 현재 선택된 지역에서 도전 가능한 미션 목록(페이징 포함) @GetMapping("/regions/{regionId}/missions") - public ApiResponsegetRegionMissions( + public ResponseEntity>getRegionMissions( @PathVariable Long regionId, @ModelAttribute MissionReqDTO.MyMissionReqDTO dto ){ - return ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getRegionMissionList(regionId,dto)); + return ResponseEntity + .status(MissionSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getRegionMissionList(regionId,dto))); } // 가게 미션 생성 @PostMapping("/stores/{storeId}/missions") - public ApiResponse createMission( + public ResponseEntity> createMission( @PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission dto // Valid 검증 어노테이션떄매 사용 ){ - BaseSuccessCode code=MissionSuccessCode.CREATED; - return ApiResponse.onSuccess(code,missionService.createMission(storeId,dto)); + return ResponseEntity + .status(MissionSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.CREATED,missionService.createMission(storeId,dto))); } // 가게 내 미션들 조회 @GetMapping("/stores/{storeId}/missions") - public ApiResponse>getMissions( + public ResponseEntity>>getMissions( @PathVariable Long storeId, @RequestParam Integer pageSize, // 한 페이지에 몇개 보여줄지 @RequestParam String cursor, @RequestParam String query ){ - BaseSuccessCode code=MissionSuccessCode.OK; - return ApiResponse.onSuccess(code,missionService.getMissions(storeId,pageSize,cursor,query)); + return ResponseEntity + .status(MissionSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getMissions(storeId,pageSize,cursor,query))); } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 9759217c..f4aa6be7 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -10,6 +10,7 @@ import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,24 +23,26 @@ public class ReviewController { // 특정 가게에 리뷰 등록 @PostMapping("/stores/{storeId}/reviews") - public ApiResponse createReview( + public ResponseEntity> createReview( @PathVariable Long storeId, // {storeId}에 적힌 숫자를 storeId라는 변수에 담음 @RequestBody ReviewReqDTO.CreateReview dto // 사용자가 작성한 리뷰 정보 가져옴 ) { - BaseSuccessCode code=ReviewSuccessCode.CREATED; - return ApiResponse.onSuccess(code, reviewService.createReviewResult(storeId,dto)); + return ResponseEntity + .status(ReviewSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(ReviewSuccessCode.CREATED, reviewService.createReviewResult(storeId,dto))); } // 내가 작성한 리뷰들 조회 @GetMapping("/members/{memberId}/reviews") - public ApiResponse> getReviews( + public ResponseEntity>> getReviews( @PathVariable Long memberId, @RequestParam Integer pageSize, @RequestParam String cursor, @RequestParam String query ){ - BaseSuccessCode code= ReviewSuccessCode.OK; - return ApiResponse.onSuccess(code,reviewService.getReviews(memberId,pageSize,cursor,query)); + return ResponseEntity + .status(ReviewSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(ReviewSuccessCode.OK,reviewService.getReviews(memberId,pageSize,cursor,query))); } } From 90074baf894601264a602a66fe87ffaee3a4d102 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 15 May 2026 12:49:09 +0900 Subject: [PATCH 03/34] =?UTF-8?q?fix:=20MissionSuccessCode=20CREATED=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C=20201=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/exception/code/MissionSuccessCode.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java index 4a2e60d0..e6289484 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java @@ -9,12 +9,12 @@ @RequiredArgsConstructor public enum MissionSuccessCode implements BaseSuccessCode { - CREATED(HttpStatus.OK, - "MISSION200_1", - "성공적으로 미션을 생성했습니다."), OK(HttpStatus.OK, - "MISSION200_2", + "MISSION200_1", "성공적으로 미션이 조회되었습니다."), + CREATED(HttpStatus.CREATED, + "MISSION201_1", + "성공적으로 미션을 생성했습니다.") ; private final HttpStatus status; private final String code; From eae3b62bfe9ce03f94d55206d832a2dc0e59edd5 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:41:27 +0900 Subject: [PATCH 04/34] =?UTF-8?q?feat:=20Spring=20Security=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Hyeonu/build.gradle b/Hyeonu/build.gradle index 4c422380..2b6a1a72 100644 --- a/Hyeonu/build.gradle +++ b/Hyeonu/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') { From bf625a0adc2af48c4ce020f21544ccd9ddf23fc8 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:41:52 +0900 Subject: [PATCH 05/34] =?UTF-8?q?feat:=20password=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/entity/Member.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 063d5939..0f390e54 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -47,6 +47,9 @@ public class Member extends BaseEntity { @Column(name="email", nullable=false) private String email; + @Column(name="password") + private String password; + @Column(name="point", nullable=false) @Builder.Default private int point=0; From d1fdf404b1a25d9a156038440763ccc66e835585 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:42:06 +0900 Subject: [PATCH 06/34] =?UTF-8?q?feat:=20findByEmail=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/repository/MemberRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index aa14436c..8d9fa19f 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -9,4 +9,6 @@ public interface MemberRepository extends JpaRepository { @Query("SELECT m FROM Member m WHERE m.name=:name AND m.deletedAt IS NULL") Optional findActiveMember(String name); + + Optional findByEmail(String username); } From f4e8c49dcacb6d79f9c8a719311fc49f02f799bb Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:42:48 +0900 Subject: [PATCH 07/34] =?UTF-8?q?feat:=20Spring=20Security=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 00000000..11ed4763 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.example.umc10th.global.config; + +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; + +@EnableWebSecurity // Spring Security 활성화 +@Configuration // 스프링 설정 파일임을 선언 +public class SecurityConfig { + + // 허용 URI 목록 + private final String[] allowUris = { + // Swagger 허용 + "/swagger-ui/**", // API 문서 UI + "/swagger-resources/**", // Swagger 리소스 + "/v3/api-docs/**", // Openapi 스펙 문서 + "/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // RestAPI는 보통 CSRF 공격에 덜 취약하므로 비활성화 + .csrf(AbstractHttpConfigurer::disable) + // 요청 권한 설정 + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() // allowUris는 누구나 접근 가능 + .anyRequest().authenticated() // 그 외 모든 요청은 로그인 필요 + ) + // 폼 로그인 설정 + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) // 로그인 성공 시 이동 + .permitAll() // 로그인 페이지는 모든 사용자가 접근 가능 + ) + // 로그아웃 설정 + .logout(logout -> logout + .logoutUrl("/logout") // 이 URL로 POST 요청 시 로그아웃 + .logoutSuccessUrl("/login?logout") // 로그아웃 후 로그인 페이지로 이동 + .permitAll() + ); + + return http.build(); + } + + // PasswordEncoder- 비밀번호 암호화 + // BCrypt 알고리즘으로 해시 암호화(같은 비밀번호도 매번 다른 해시값이 생성되어 암호화) + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} From 34b76bb1f453844e5640baffc05b0a3536d0e7fb Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:42:56 +0900 Subject: [PATCH 08/34] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java new file mode 100644 index 00000000..dc1306d2 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,32 @@ +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.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 // final 필드 생성자 자동 생성 +// Spring Security가 "이 인터페이스를 구현한 클래스로 사용자를 조회해라" +public class CustomUserDetailsService implements UserDetailsService { + + // DB에서 회원을 조회하기 위한 Repository + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + )throws UsernameNotFoundException{ + // 이메일로 DB에서 회원 조회 + Member member=memberRepository.findByEmail(username) + .orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + // AuthMember로 감싸서 반환 + return new AuthMember(member); + } +} From be1f2870aa68511efba5f1c7173a03fbe7e150ce Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:43:02 +0900 Subject: [PATCH 09/34] =?UTF-8?q?feat:=20Spring=20Security=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/entity/AuthMember.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java new file mode 100644 index 00000000..1e3d3da4 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -0,0 +1,36 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter // 모든 필드의 getter 자동 생성 +@RequiredArgsConstructor // final 필드를 받는 생성자 자동 생성 +// Member를 Security용으로 포장한 Wrapper 역할 +public class AuthMember implements UserDetails { + + private final Member member; + + // 자신의 역할 반환(유저, 관리자) + @Override + public Collection getAuthorities(){ + return List.of(); // 권한 구분 미구현 상태(빈 리스트 반환) + } + + @Override + public @Nullable String getPassword(){ // @Nullable은 비밀번호가 없을 수도 있음(소셜 로그인 등) + return member.getPassword(); // Member 엔티티의 비밀번호 반환 + } + + @Override + public String getUsername(){ + return member.getEmail(); // 이메일을 username으로 사용(식별자로) + } +} From 89f89d7bd2e73e3ef8a08df02eef86ea53955f79 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 13:03:08 +0900 Subject: [PATCH 10/34] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=ED=95=A0=20=EB=95=8C=20JSON=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=91=EB=8B=B5=ED=95=B4=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/handler/CustomAccessDenied.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java new file mode 100644 index 00000000..929cdce2 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java @@ -0,0 +1,39 @@ +package com.example.umc10th.global.security.handler; + +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.fasterxml.jackson.databind.ObjectMapper; +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; + +// 권한이 없는 사용자가 접근할 떄 JSON으로 응답해주는 핸들러 +// AccessDeniedHandler는 로그인은 했지만 권한이 없을 때 어떻게 처리할지 +// 401 Unauthorized 로그인 자체를 안함/ 403 Forbidden 로그인은 했지만 권한 없음 (이 핸들러가 처리) +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + )throws IOException { + // Java 객체->JSON문자열로 변환해주는 도구 + ObjectMapper objectMapper=new ObjectMapper(); + BaseErrorCode code= GeneralErrorCode.FORBIDDEN; // 403 Forbidden 에러코드 가져옴 + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); // JSON 형식으로 응답 + response.setStatus(code.getStatus().value()); // HTTP 상태코드 403 설정 + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse=ApiResponse.onFailure(code,null); + + // 실제 Response로 덮어쓰기 (만든 객체를 실제 HTTP 응답 바디에 JSON으로 작성) + objectMapper.writeValue(response.getOutputStream(),errorResponse); + } +} From 272e7a15c710b1eb535b501d71e6b5744e51757a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 13:03:23 +0900 Subject: [PATCH 11/34] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=95=88=ED=96=88=EC=9D=84=EB=96=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=A0=91=EA=B7=BC=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?JSON=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5=ED=95=B4=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/handler/CustomEntryPoint.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java new file mode 100644 index 00000000..bfe138b2 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.handler; + +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.fasterxml.jackson.databind.ObjectMapper; +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 { + ObjectMapper objectMapper=new ObjectMapper(); + BaseErrorCode code= GeneralErrorCode.UNAUTHORIZED; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse= ApiResponse.onFailure(code,null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(),errorResponse); + } +} From 462e5acf234907fb9159417f8ee40673eabf9617 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 13:03:39 +0900 Subject: [PATCH 12/34] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=EC=83=81?= =?UTF-8?q?=ED=99=A9=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 11ed4763..009289f6 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.handler.CustomAccessDenied; +import com.example.umc10th.global.security.handler.CustomEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -42,7 +44,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logoutUrl("/logout") // 이 URL로 POST 요청 시 로그아웃 .logoutSuccessUrl("/login?logout") // 로그아웃 후 로그인 페이지로 이동 .permitAll() - ); + ) + // 예외 상황 핸들러 + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) // 403 발생 + .authenticationEntryPoint(customEntryPoint())) //401 발생 + ; + return http.build(); } @@ -53,4 +61,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + // + @Bean + public CustomAccessDenied customAccessDenied(){ + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint(){ + return new CustomEntryPoint(); + } } From 5941632b42a7ada67b80f868c54ae57f5c6a7c2a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:54:25 +0900 Subject: [PATCH 13/34] =?UTF-8?q?refactor:=20=EC=A0=84=EC=97=AD=20MemberSu?= =?UTF-8?q?ccessCode=EB=A5=BC=20Member=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/code/MemberSuccessCode.java | 21 ++++++++++++++++++- .../apiPayload/code/MemberSuccessCode.java | 19 ----------------- 2 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index f42e80f8..5720f48a 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -1,4 +1,23 @@ package com.example.umc10th.domain.member.exception.code; -public enum MemberSuccessCode { +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + + OK(HttpStatus.OK, + "MEMBER200_1", + "성공적으로 유저를 조회했습니다."), + SIGN_UP(HttpStatus.OK, + "MEMBER200_2", + "회원가입에 성공했습니다."); + + + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java deleted file mode 100644 index 0d337cca..00000000 --- a/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.umc10th.global.apiPayload.code; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@Getter -@RequiredArgsConstructor -public enum MemberSuccessCode implements BaseSuccessCode{ - - OK(HttpStatus.OK, - "MEMBER200_1", - "성공적으로 유저를 조회했습니다."), - ; - - private final HttpStatus status; - private final String code; - private final String message; -} From 23e31fee458cdccaa7db7756232a6d6963ef0fb8 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:55:41 +0900 Subject: [PATCH 14/34] =?UTF-8?q?chore:=20StoreController=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/store/controller/StoreController.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 2561b353..c7906d26 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -1,10 +1,5 @@ package com.example.umc10th.domain.store.controller; -import com.example.umc10th.domain.store.dto.StoreResDTO; -import com.example.umc10th.domain.store.service.StoreService; -import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; From 72b1311cfee8e490028189769f26a1d1c1b5b708 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:56:02 +0900 Subject: [PATCH 15/34] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 009289f6..16900ba4 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -21,7 +21,7 @@ public class SecurityConfig { "/swagger-ui/**", // API 문서 UI "/swagger-resources/**", // Swagger 리소스 "/v3/api-docs/**", // Openapi 스펙 문서 - "/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) + "/api/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) }; @Bean From 1d669cf5454daacbfaff93b8d2aad307ff5d444f Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:56:30 +0900 Subject: [PATCH 16/34] =?UTF-8?q?chore:=20ReviewController=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/review/controller/ReviewController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index f4aa6be7..c12932b5 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -2,19 +2,13 @@ import com.example.umc10th.domain.review.dto.ReviewReqDTO; import com.example.umc10th.domain.review.dto.ReviewResDTO; -import com.example.umc10th.domain.review.entity.Review; import com.example.umc10th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequiredArgsConstructor // final이 붙은거 생성자 대신 작성 @RequestMapping("/api/v1") From b36f63924b42e3074ebaa3d53ec25a822e40bbc5 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:56:37 +0900 Subject: [PATCH 17/34] =?UTF-8?q?chore:=20MissionController=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/controller/MissionController.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 634e55ff..b0b5d9eb 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -2,22 +2,14 @@ import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; -import com.example.umc10th.domain.mission.entity.Mission; -import com.example.umc10th.domain.mission.entity.mapping.MemberMission; -import com.example.umc10th.domain.mission.enums.MissionStatus; import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; 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; - @RestController @RequiredArgsConstructor @RequestMapping("/api/v1") From 2a4f0730ae6b9bc1e7628537ea6854f5834f22e0 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:57:16 +0900 Subject: [PATCH 18/34] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A9=94=EC=84=9C=EB=93=9C=20signUp=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 7c422bad..1a4135b9 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -4,8 +4,7 @@ import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -31,4 +30,13 @@ public ResponseEntity> getInfo( .body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(dto))); } + // 회원가입 + @PostMapping("/auth/sign-up") + public ResponseEntity> signUp( + @RequestBody MemberReqDTO.SignUpReqDTO dto + ){ + return ResponseEntity + .status(MemberSuccessCode.SIGN_UP.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP,memberService.signUp(dto))); + } } From b115f9be8f494ae72c4d3ed8454963b259b79bfd Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:57:44 +0900 Subject: [PATCH 19/34] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20SignUpReqDTO,=20SignUpResDTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/dto/MemberReqDTO.java | 17 +++++++++++++++++ .../umc10th/domain/member/dto/MemberResDTO.java | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index e937272d..d28130cf 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -1,9 +1,26 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.enums.Address; +import com.example.umc10th.domain.member.enums.Gender; + +import java.time.LocalDate; + public class MemberReqDTO { // 마이페이지 public record GetInfo( Long id ){} + + // 회원가입 + public record SignUpReqDTO( + String name, + Gender gender, + LocalDate birth, + Address address, + String detailAddress, + String phoneNumber, + String email, + String password + ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java index e6334334..88bf982d 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.service.MemberService; import lombok.Builder; import java.time.LocalDate; @@ -18,4 +19,10 @@ public record GetInfo( Integer point, Enum status ){} + + // 회원가입 + @Builder + public record SignUpResDTO( + Long id + ){} } From 23936bdd8418917c1c4484b3cde0a8569bf26b18 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:58:06 +0900 Subject: [PATCH 20/34] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20signUp=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 93e9be65..76ea6d08 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -8,12 +8,14 @@ import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; // 마이 페이지 public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { // DTO에서 유저 ID를 추출 @@ -24,4 +26,21 @@ public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { // 컨버터를 이용해서 응답 DTO 생성 & return return MemberConverter.toGetInfo(member); } + + // 회원가입 + public MemberResDTO.SignUpResDTO signUp(MemberReqDTO.SignUpReqDTO dto) { + // 비밀번호 BCrypt 암호화 + String encodedPassword=passwordEncoder.encode(dto.password()); + + // DTO -> Entity 변환 + Member member=MemberConverter.toMember(dto, encodedPassword); + + // DB저장 + Member savedMember=memberRepository.save(member); + + // Entity -> ResponseDTO 변환 + return MemberConverter.toSignUpResDTO(savedMember); + + + } } From d35a75923c3eb9cd595c9af19e4004c09f04dc34 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:58:31 +0900 Subject: [PATCH 21/34] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20toMember,=20toSignUp=20=EC=BB=A8=EB=B2=84=ED=84=B0?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/converter/MemberConverter.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 96452757..25ab43da 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.converter; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; @@ -17,4 +18,22 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) { .status(member.getStatus()) .build(); } + + // DTO -> Entity 변환 + public static Member toMember(MemberReqDTO.SignUpReqDTO dto, String encodedPassword) { + return Member.builder() + .name(dto.name()) + .gender(dto.gender()) + .birth(dto.birth()) + .address(dto.address()) + .detailAddress(dto.detailAddress()) + .phoneNumber(dto.phoneNumber()) + .email(dto.email()) + .password(encodedPassword) // BCrypt 암호화된 비밀번호 + .build(); + } + + public static MemberResDTO.SignUpResDTO toSignUpResDTO(Member member) { + return new MemberResDTO.SignUpResDTO(member.getId()); + } } From fa60ededa6c861c1ffa1b030a555036f5d4ca90c Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:58:43 +0900 Subject: [PATCH 22/34] =?UTF-8?q?docs:=208=EC=A3=BC=EC=B0=A8=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/keyword_summary/ch08.md | 115 +++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Hyeonu/keyword_summary/ch08.md diff --git a/Hyeonu/keyword_summary/ch08.md b/Hyeonu/keyword_summary/ch08.md new file mode 100644 index 00000000..7c18d5eb --- /dev/null +++ b/Hyeonu/keyword_summary/ch08.md @@ -0,0 +1,115 @@ +- Spring Security가 무엇인가? + + 자바 기반 웹 애플리케이션의 보안을 담당하는 프레임워크이다. + + 개발자가 보안 로직을 직접 구현하지않고 몇가지 설정만으로 아래 3가지를 처리할 수 있음 + + - 인증(Authentication)- 이 사용자가 누구인지 식별(로그인) + - 인가(Authorization)- 이 사용자가 해당 리소스에 접근할 권한이 있는지 식별(권한 확인) + - 보안 위협 방어- CSRF, XSS 등 각종 공격으로부터 보호 + + Filter Chain을 통해 HTTP요청을 순차적으로 필터링하며, SecurityConfig를 통해 커스텀 보안 설정을 적용할 수 있다. + + + +- 인증(Authentication)vs 인가(Authorization) + + + + **인증(Authentication)** + + 목적 + + 사용자 신원 확인 + + 실패 시 + + 401 Unauthorized + + 예시 + + 로그인, 회원가입 + + Spring Security + + `AuthenticationEntryPoint` + + + + **인가(Authorization)** + + 목적 + + 리소스 접근 권한 확인 + + 실패 시 + + 403 Forbidden + + 예시 + + 관리자 페이지 접근 + + Spring Security + + `AccessDeniedHandler` + + 인증 먼저 그 다음 인가 + +- Stateful vs Stateless + + + + **Stateful** + + 상태 저장 + + 서버가 저장 + + 방식 + + 세션 + + 동작 + + 로그인 시 서버에 세션 저장 → 요청마다 세션 확인 + + 장점 + + 구현 간단 + 즉시 로그아웃 가능 + + 단점 + + 서버 부하 + 확장성 낮음 + + 예시 + + 폼 로그인 + + + + **Stateless** + + 상태 저장 + + 서버가 저장 안함 + + 방식 + + JWT 토큰 + + 동작 + + 토큰에 사용자 정보 담아서 → 요청마다 토큰 검증 + + 장점 + + 토큰 탈취 시 만료 전까지 막기 어려움 + + 예시 + + JWT + + 이번 주차는 Stateful 방식 \ No newline at end of file From e1140583615d01475e475b1b178cb8152279a8fa Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:36:19 +0900 Subject: [PATCH 23/34] =?UTF-8?q?feat:=20getUserEmail()=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/global/security/entity/AuthMember.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java index 1e3d3da4..d2892ddc 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -30,7 +30,11 @@ public Collection getAuthorities(){ } @Override - public String getUsername(){ + public String getUsername(){ // Spring Security 규칙상 만든것 return member.getEmail(); // 이메일을 username으로 사용(식별자로) } + + public String getUserEmail(){ + return member.getEmail(); + } } From 5f3909f9a5470905397dacb4b1b5ed482d253023 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:37:33 +0900 Subject: [PATCH 24/34] =?UTF-8?q?refactor:=20=EB=A7=A4=EA=B0=9C=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20username->email=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/service/CustomUserDetailsService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java index dc1306d2..b60f3dde 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -21,10 +21,10 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername( - String username + String email )throws UsernameNotFoundException{ // 이메일로 DB에서 회원 조회 - Member member=memberRepository.findByEmail(username) + Member member=memberRepository.findByEmail(email) .orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); // AuthMember로 감싸서 반환 return new AuthMember(member); From d788e2b68d02bf9a28a0d195832968e860b89550 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:38:09 +0900 Subject: [PATCH 25/34] =?UTF-8?q?feat:=20JWT=20authentication=20filter=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/filter/JwtAuthFilter.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java new file mode 100644 index 00000000..eae26a10 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,74 @@ +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.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 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); + } + } +} From 0bd8310fd28488124993b8aaa5e74b23815dbbe9 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:38:28 +0900 Subject: [PATCH 26/34] =?UTF-8?q?feat:=20JWT=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/util/JwtUtil.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java new file mode 100644 index 00000000..9fff3a08 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java @@ -0,0 +1,93 @@ +package com.example.umc10th.global.security.util; + +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; // JWT 서명에 쓸 키 + private final Duration accessExpiration; // 토큰 유효기간 + + public JwtUtil( + @Value("${jwt.token.secretKey}")String secret, // .env의 JWT_SECRET_KEY값 + @Value("${jwt.token.expiration.access}")Long accessExpiration // 1800000(30분) + ){ + //문자열 시크릿키 ->HMAC-SHA 알고리즘용 SecretKey 객체로 변환 + this.secretKey= Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + // 1800000ms -> Duration 객체로 변환 + this.accessExpiration=Duration.ofMillis(accessExpiration); + } + // AccessToken 생성 + public String createAccessToken(AuthMember member){ + return createToken(member,accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * 유저 정보를 추출할 토큰 + * 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token){ + try{ + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + }catch(JwtException e){ + return null; + } + } + + /** 토큰 유효성 확인 + * + * 유효한지 확인할 토큰 + * 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(); // 현재 시각 + + // member가 가진 권한 목록을 콤마로 이어붙임(ROLE_USER) + // 인가 정보 + String authorities=member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUserEmail()) // User 이메일을 Subject로 + .claim("role",authorities) // payload에 role 추가 + .claim("email",member.getUserEmail()) // payload에 email 추가 + .issuedAt(Date.from(now)) // 발급시각 + .expiration(Date.from(now.plus(expiration))) // 만료시각 (지금 +30분) + .signWith(secretKey) // 시크릿 키로 서명 + .compact(); // 최종 JWT 문자열로 직렬화 + } + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException{ + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} From db73402df9601d838624566355e124c1a0b82bb8 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:39:14 +0900 Subject: [PATCH 27/34] =?UTF-8?q?refactor:=20=ED=8F=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20->=20jwt=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 16900ba4..5522e2ef 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,7 +1,11 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.handler.CustomAccessDenied; import com.example.umc10th.global.security.handler.CustomEntryPoint; +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; @@ -10,11 +14,16 @@ 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; @EnableWebSecurity // Spring Security 활성화 @Configuration // 스프링 설정 파일임을 선언 +@RequiredArgsConstructor public class SecurityConfig { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + // 허용 URI 목록 private final String[] allowUris = { // Swagger 허용 @@ -35,10 +44,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .anyRequest().authenticated() // 그 외 모든 요청은 로그인 필요 ) // 폼 로그인 설정 - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) // 로그인 성공 시 이동 - .permitAll() // 로그인 페이지는 모든 사용자가 접근 가능 - ) + .formLogin(AbstractHttpConfigurer::disable) + // 세션 + .sessionManagement(AbstractHttpConfigurer::disable) + // JWT 필터 + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) // 로그아웃 설정 .logout(logout -> logout .logoutUrl("/logout") // 이 URL로 POST 요청 시 로그아웃 @@ -72,4 +82,9 @@ public CustomAccessDenied customAccessDenied(){ public CustomEntryPoint customEntryPoint(){ return new CustomEntryPoint(); } + + @Bean + public JwtAuthFilter jwtAuthFilter(){ + return new JwtAuthFilter(jwtUtil,customUserDetailsService); + } } From cf7a1ab8cb770cd2f1ae712b190ed6242238946a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:39:28 +0900 Subject: [PATCH 28/34] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20api?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 10 +++++++++ .../member/converter/MemberConverter.java | 7 +++++++ .../domain/member/dto/MemberReqDTO.java | 6 ++++++ .../domain/member/dto/MemberResDTO.java | 7 +++++++ .../exception/code/MemberErrorCode.java | 2 +- .../exception/code/MemberSuccessCode.java | 6 +++++- .../domain/member/service/MemberService.java | 21 +++++++++++++++++++ 7 files changed, 57 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 1a4135b9..683f5b62 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -39,4 +39,14 @@ public ResponseEntity> signUp( .status(MemberSuccessCode.SIGN_UP.getStatus()) .body(ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP,memberService.signUp(dto))); } + + // 로그인 + @PostMapping("/auth/login") + public ResponseEntity>login( + @RequestBody MemberReqDTO.LoginReqDTO dto + ){ + return ResponseEntity + .status(MemberSuccessCode.LOGIN.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.LOGIN,memberService.login(dto))); +} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 25ab43da..d8c667a3 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -36,4 +36,11 @@ public static Member toMember(MemberReqDTO.SignUpReqDTO dto, String encodedPassw public static MemberResDTO.SignUpResDTO toSignUpResDTO(Member member) { return new MemberResDTO.SignUpResDTO(member.getId()); } + + // 로그인 + public static MemberResDTO.LoginResDTO toLoginResDTO(String token){ + return MemberResDTO.LoginResDTO.builder() + .accessToken(token) + .build(); + } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index d28130cf..91c40329 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -23,4 +23,10 @@ public record SignUpReqDTO( String email, String password ){} + + // 로그인 + public record LoginReqDTO( + String email, + String password + ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java index 88bf982d..4b1a5388 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java @@ -25,4 +25,11 @@ public record GetInfo( public record SignUpResDTO( Long id ){} + + // 로그인 + + @Builder + public record LoginResDTO( + String accessToken + ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index c2fe99e8..d40c26de 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor public enum MemberErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, " MEMBER404_1","해당 사용자를 찾을 수 없습니다."), - ; + INVALID_PASSWORD(HttpStatus.NOT_FOUND,"MEMBER404_2","비밀번호가 일치하지 않습니다." ); private final HttpStatus status; private final String code; diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index 5720f48a..38283c21 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -14,7 +14,11 @@ public enum MemberSuccessCode implements BaseSuccessCode { "성공적으로 유저를 조회했습니다."), SIGN_UP(HttpStatus.OK, "MEMBER200_2", - "회원가입에 성공했습니다."); + "회원가입에 성공했습니다."), + + LOGIN(HttpStatus.OK, + "MEMBER200_3", + "로그인에 성공했습니다."); private final HttpStatus status; diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 76ea6d08..a4ebeda1 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -7,6 +7,8 @@ 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.entity.AuthMember; +import com.example.umc10th.global.security.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -16,6 +18,7 @@ public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; // 마이 페이지 public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { // DTO에서 유저 ID를 추출 @@ -41,6 +44,24 @@ public MemberResDTO.SignUpResDTO signUp(MemberReqDTO.SignUpReqDTO dto) { // Entity -> ResponseDTO 변환 return MemberConverter.toSignUpResDTO(savedMember); + } + + // 로그인 + public MemberResDTO.LoginResDTO login(MemberReqDTO.LoginReqDTO dto){ + + // 이메일로 유저 찾기 + Member member=memberRepository.findByEmail(dto.email()) + .orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + // 비밀번호 검증 + if(!passwordEncoder.matches(dto.password(),member.getPassword())){ + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + + // 토큰 발급 + AuthMember authMember=new AuthMember(member); + String token=jwtUtil.createAccessToken(authMember); + return MemberConverter.toLoginResDTO(token); } } From 03dbccc709e15fbd7aab4305f67ffba1495b2036 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 15:39:40 +0900 Subject: [PATCH 29/34] =?UTF-8?q?feat:=20jwt=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Hyeonu/build.gradle b/Hyeonu/build.gradle index 2b6a1a72..d6674e9f 100644 --- a/Hyeonu/build.gradle +++ b/Hyeonu/build.gradle @@ -37,6 +37,12 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + + // 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' } tasks.named('test') { From 4af032957e0d083546408e73104d6c07b9073967 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 17:35:16 +0900 Subject: [PATCH 30/34] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/enums/SocialType.java | 5 + .../umc10th/global/config/SecurityConfig.java | 72 ++++++++------ .../umc10th/global/security/dto/KakaoDTO.java | 31 ++++++ .../umc10th/global/security/dto/OAuthDTO.java | 10 ++ .../global/security/entity/OAuthMember.java | 34 +++++++ .../global/security/filter/JwtAuthFilter.java | 57 ++++++----- .../security/handler/OAuthSuccessHandler.java | 56 +++++++++++ .../security/service/CustomOAuthService.java | 66 +++++++++++++ .../service/CustomUserDetailsService.java | 21 +++-- .../umc10th/global/security/util/JwtUtil.java | 94 ++++++++++--------- 10 files changed, 333 insertions(+), 113 deletions(-) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java new file mode 100644 index 00000000..843ecfb8 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/SocialType.java @@ -0,0 +1,5 @@ +package com.example.umc10th.domain.member.enums; + +public enum SocialType { + KAKAO +} diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 5522e2ef..b2dcf3e4 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -3,6 +3,8 @@ import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.handler.CustomAccessDenied; import com.example.umc10th.global.security.handler.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; @@ -16,75 +18,83 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -@EnableWebSecurity // Spring Security 활성화 -@Configuration // 스프링 설정 파일임을 선언 +@EnableWebSecurity +@Configuration @RequiredArgsConstructor public class SecurityConfig { private final JwtUtil jwtUtil; private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuthService customOAuthService; // 추가 - // 허용 URI 목록 private final String[] allowUris = { - // Swagger 허용 - "/swagger-ui/**", // API 문서 UI - "/swagger-resources/**", // Swagger 리소스 - "/v3/api-docs/**", // Openapi 스펙 문서 - "/api/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/api/auth/**", + "/oauth/**" // OAuth 경로 추가 }; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // RestAPI는 보통 CSRF 공격에 덜 취약하므로 비활성화 .csrf(AbstractHttpConfigurer::disable) - // 요청 권한 설정 .authorizeHttpRequests(requests -> requests - .requestMatchers(allowUris).permitAll() // allowUris는 누구나 접근 가능 - .anyRequest().authenticated() // 그 외 모든 요청은 로그인 필요 + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() ) - // 폼 로그인 설정 .formLogin(AbstractHttpConfigurer::disable) - // 세션 .sessionManagement(AbstractHttpConfigurer::disable) - // JWT 필터 .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) - // 로그아웃 설정 .logout(logout -> logout - .logoutUrl("/logout") // 이 URL로 POST 요청 시 로그아웃 - .logoutSuccessUrl("/login?logout") // 로그아웃 후 로그인 페이지로 이동 + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") .permitAll() ) - // 예외 상황 핸들러 + // OAuth 추가 + .oauth2Login(oauth -> oauth + .authorizationEndpoint(auth -> auth + .baseUri("/oauth/authorize") + ) + .redirectionEndpoint(redirect -> redirect + .baseUri("/oauth/callback/**") + ) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuthService) + ) + .successHandler(oAuthSuccessHandler()) + ) .exceptionHandling(exception -> exception - .accessDeniedHandler(customAccessDenied()) // 403 발생 - .authenticationEntryPoint(customEntryPoint())) //401 발생 - ; - + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) + ); return http.build(); } - // PasswordEncoder- 비밀번호 암호화 - // BCrypt 알고리즘으로 해시 암호화(같은 비밀번호도 매번 다른 해시값이 생성되어 암호화) @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - // @Bean - public CustomAccessDenied customAccessDenied(){ + public CustomAccessDenied customAccessDenied() { return new CustomAccessDenied(); } @Bean - public CustomEntryPoint customEntryPoint(){ + public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } @Bean - public JwtAuthFilter jwtAuthFilter(){ - return new JwtAuthFilter(jwtUtil,customUserDetailsService); + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + + // 추가 + @Bean + public OAuthSuccessHandler oAuthSuccessHandler() { + return new OAuthSuccessHandler(jwtUtil); } -} +} \ No newline at end of file diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java new file mode 100644 index 00000000..64fbe2d4 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java @@ -0,0 +1,31 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.domain.member.enums.SocialType; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class KakaoDTO implements OAuthDTO{ + + private final String id; + private final String email; + private final String name; + + @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; + } +} \ No newline at end of file diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java new file mode 100644 index 00000000..144f4c53 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java @@ -0,0 +1,10 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.domain.member.enums.SocialType; + +public interface OAuthDTO { + SocialType getSocialType(); + String getSocialUid(); + String getSocialEmail(); + String getName(); +} diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java new file mode 100644 index 00000000..90eb679f --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java @@ -0,0 +1,34 @@ +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; + +@RequiredArgsConstructor +public class OAuthMember implements OAuth2User { + + @Getter + private final Member member; + private final Map attributes; + + @Override + public Map getAttributes(){ + return attributes; + } + + @Override + public Collection getAuthorities(){ + return List.of(); + } + + @Override + public String getName(){ + return member.getSocialUid(); + } +} diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java index eae26a10..b5e73529 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java @@ -1,5 +1,6 @@ package com.example.umc10th.global.security.filter; +import com.example.umc10th.domain.member.enums.SocialType; import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseErrorCode; import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; @@ -32,43 +33,39 @@ 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 ")){ + ) throws ServletException, IOException { + try { + String token = request.getHeader("Authorization"); + 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() + token = token.replace("Bearer ", ""); + if (jwtUtil.isValid(token)) { + String uid = jwtUtil.getUid(token); + SocialType socialType = jwtUtil.getSocialType(token); + + UserDetails user; + if (socialType != null) { + user = customUserDetailsService.loadUserByUidAndSocialType(socialType, uid); + } else { + String email = jwtUtil.getEmail(token); + 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; - + 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); + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + mapper.writeValue(response.getOutputStream(), errorResponse); } } -} +} \ No newline at end of file diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java new file mode 100644 index 00000000..9bc5f4c6 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java @@ -0,0 +1,56 @@ +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.toLoginResDTO(accessToken) + ); + + // 응답 출력 + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java new file mode 100644 index 00000000..ba9678b5 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java @@ -0,0 +1,66 @@ +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.enums.SocialType; +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.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; + +@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; + Map attributes = oAuthMember.getAttribute("kakao_account"); + Map profile = (Map) attributes.get("profile"); + 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 -> { + String email = attributes.get("email").toString(); + String name = profile.get("nickname").toString(); + dto = new KakaoDTO(socialUid, email, name); + } + 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/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java index b60f3dde..8a791304 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -1,6 +1,7 @@ package com.example.umc10th.global.security.service; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; 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; @@ -16,17 +17,21 @@ // Spring Security가 "이 인터페이스를 구현한 클래스로 사용자를 조회해라" public class CustomUserDetailsService implements UserDetailsService { - // DB에서 회원을 조회하기 위한 Repository private final MemberRepository memberRepository; @Override - public UserDetails loadUserByUsername( - String email - )throws UsernameNotFoundException{ - // 이메일로 DB에서 회원 조회 - Member member=memberRepository.findByEmail(email) - .orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - // AuthMember로 감싸서 반환 + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return new AuthMember(member); + } + + public UserDetails loadUserByUidAndSocialType( + SocialType socialType, + String username + ) throws UsernameNotFoundException { + Member member = memberRepository.findBySocialTypeAndSocialUid(socialType, username) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); return new AuthMember(member); } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java index 9fff3a08..d00580af 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java @@ -1,5 +1,6 @@ package com.example.umc10th.global.security.util; +import com.example.umc10th.domain.member.enums.SocialType; import com.example.umc10th.global.security.entity.AuthMember; import io.jsonwebtoken.Claims; @@ -19,71 +20,76 @@ @Component public class JwtUtil { - private final SecretKey secretKey; // JWT 서명에 쓸 키 - private final Duration accessExpiration; // 토큰 유효기간 + private final SecretKey secretKey; + private final Duration accessExpiration; public JwtUtil( - @Value("${jwt.token.secretKey}")String secret, // .env의 JWT_SECRET_KEY값 - @Value("${jwt.token.expiration.access}")Long accessExpiration // 1800000(30분) - ){ - //문자열 시크릿키 ->HMAC-SHA 알고리즘용 SecretKey 객체로 변환 - this.secretKey= Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - // 1800000ms -> Duration 객체로 변환 - this.accessExpiration=Duration.ofMillis(accessExpiration); + @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); + + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e) { + return null; + } } - /** 토큰에서 이메일 가져오기 - * - * 유저 정보를 추출할 토큰 - * 유저 이메일을 토큰에서 추출합니다 - */ - public String getEmail(String token){ - try{ - return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 - }catch(JwtException e){ + // 추가: UID 가져오기 + public String getUid(String token) { + try { + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e) { return null; } } - /** 토큰 유효성 확인 - * - * 유효한지 확인할 토큰 - * True, False 반환합니다 - */ - public boolean isValid(String token){ - try{ + public SocialType getSocialType(String token) { + try { + Object socialType = getClaims(token).getPayload().get("social_type"); + if (socialType == null) return null; + return SocialType.valueOf(socialType.toString().toUpperCase()); + } catch (Exception e) { + return null; + } + } + + public boolean isValid(String token) { + try { getClaims(token); return true; - }catch(JwtException e){ + } catch (JwtException e) { return false; } } - // 토큰 생성 - private String createToken(AuthMember member,Duration expiration){ - Instant now= Instant.now(); // 현재 시각 + // 변경: subject를 email → uid로, social_type claim 추가 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); - // member가 가진 권한 목록을 콤마로 이어붙임(ROLE_USER) - // 인가 정보 - String authorities=member.getAuthorities().stream() + String authorities = member.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); return Jwts.builder() - .subject(member.getUserEmail()) // User 이메일을 Subject로 - .claim("role",authorities) // payload에 role 추가 - .claim("email",member.getUserEmail()) // payload에 email 추가 - .issuedAt(Date.from(now)) // 발급시각 - .expiration(Date.from(now.plus(expiration))) // 만료시각 (지금 +30분) - .signWith(secretKey) // 시크릿 키로 서명 - .compact(); // 최종 JWT 문자열로 직렬화 + .subject(member.getUsername()) // UID를 Subject로 + .claim("role", authorities) + .claim("social_type", member.getMember().getSocialType()) // social_type 추가 + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(expiration))) + .signWith(secretKey) + .compact(); } - // 토큰 정보 가져오기 - private Jws getClaims(String token) throws JwtException{ + + private Jws getClaims(String token) throws JwtException { return Jwts.parser() .verifyWith(secretKey) .clockSkewSeconds(60) From fcfbf0c14985db957b343b5f72e431b56b9a746d Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 17:35:26 +0900 Subject: [PATCH 31/34] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/entity/AuthMember.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java index d2892ddc..5d345721 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -30,11 +30,9 @@ public Collection getAuthorities(){ } @Override - public String getUsername(){ // Spring Security 규칙상 만든것 - return member.getEmail(); // 이메일을 username으로 사용(식별자로) + public String getUsername() { + // 소셜 로그인이면 socialUid, 일반 로그인이면 email + return member.getSocialUid() != null ? member.getSocialUid() : member.getEmail(); } - public String getUserEmail(){ - return member.getEmail(); - } } From 55929534073eb430eff815f31946eb51492bb32e Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 17:35:46 +0900 Subject: [PATCH 32/34] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20API=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 8 ++++++-- .../domain/member/converter/MemberConverter.java | 10 ++++++++++ .../example/umc10th/domain/member/entity/Member.java | 10 +++++++++- .../domain/member/exception/code/MemberErrorCode.java | 3 ++- .../domain/member/repository/MemberRepository.java | 4 ++++ .../umc10th/domain/member/service/MemberService.java | 9 ++------- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 683f5b62..f9303259 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -5,8 +5,10 @@ import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.security.entity.AuthMember; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,11 +25,13 @@ public class MemberController { @PostMapping("/v1/members/me") public ResponseEntity> getInfo( // 받은 JSON 데이터를 자바 객체(dto)로 변환해서 씀 - @RequestBody MemberReqDTO.GetInfo dto + // @RequestBody MemberReqDTO.GetInfo dto + // 헤더에 담긴 토큰을 가지고 사용자 정보 리턴 + @AuthenticationPrincipal AuthMember member ){ return ResponseEntity .status(MemberSuccessCode.OK.getStatus()) - .body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(dto))); + .body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(member))); } // 회원가입 diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index d8c667a3..eaaac706 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.global.security.dto.OAuthDTO; public class MemberConverter { public static MemberResDTO.GetInfo toGetInfo(Member member) { @@ -43,4 +44,13 @@ public static MemberResDTO.LoginResDTO toLoginResDTO(String token){ .accessToken(token) .build(); } + + public static Member toMember(OAuthDTO dto) { + return Member.builder() + .email(dto.getSocialEmail()) + .name(dto.getName()) + .socialType(dto.getSocialType()) + .socialUid(dto.getSocialUid()) + .build(); + } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 0f390e54..90bcad51 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.member.enums.Address; import com.example.umc10th.domain.member.enums.Gender; import com.example.umc10th.domain.member.enums.MemberStatus; +import com.example.umc10th.domain.member.enums.SocialType; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -27,7 +28,7 @@ public class Member extends BaseEntity { @Column(name="name", nullable=false) private String name; - @Column(name="gender", nullable=false) + @Column(name="gender") @Enumerated(EnumType.STRING) private Gender gender; @@ -59,4 +60,11 @@ public class Member extends BaseEntity { @Builder.Default private MemberStatus status=MemberStatus.ACTIVE; + @Column(name="social_type") + @Enumerated(EnumType.STRING) + private SocialType socialType; + + @Column(name="social_uid") + private String socialUid; + } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index d40c26de..a4f47c8d 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -8,7 +8,8 @@ @RequiredArgsConstructor public enum MemberErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, " MEMBER404_1","해당 사용자를 찾을 수 없습니다."), - INVALID_PASSWORD(HttpStatus.NOT_FOUND,"MEMBER404_2","비밀번호가 일치하지 않습니다." ); + INVALID_PASSWORD(HttpStatus.NOT_FOUND,"MEMBER404_2","비밀번호가 일치하지 않습니다." ), + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.NOT_FOUND,"MEMBER404_3" ,"지원하지 않는 소셜 로그인입니다." ); private final HttpStatus status; private final String code; diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 8d9fa19f..f34d570c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,9 +1,11 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; + import java.util.Optional; public interface MemberRepository extends JpaRepository { @@ -11,4 +13,6 @@ public interface MemberRepository extends JpaRepository { Optional findActiveMember(String name); Optional findByEmail(String username); + + Optional findBySocialTypeAndSocialUid(SocialType socialType, String socialUid); } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index a4ebeda1..9c7bdd16 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -20,14 +20,9 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; // 마이 페이지 - public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { - // DTO에서 유저 ID를 추출 - Long memberId=dto.id(); - // DB에서 해당 유저 ID로 데이터 조회 - Member member=memberRepository.findById(memberId) - .orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + public MemberResDTO.GetInfo getInfo(AuthMember member) { // 컨버터를 이용해서 응답 DTO 생성 & return - return MemberConverter.toGetInfo(member); + return MemberConverter.toGetInfo(member.getMember()); } // 회원가입 From 50c9b0d47c54b8aea4bf68ed0ff23b4c5c4d1127 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 17:35:59 +0900 Subject: [PATCH 33/34] =?UTF-8?q?feat:=20OAuth=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hyeonu/build.gradle b/Hyeonu/build.gradle index d6674e9f..335af929 100644 --- a/Hyeonu/build.gradle +++ b/Hyeonu/build.gradle @@ -43,6 +43,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' implementation 'org.springframework.boot:spring-boot-configuration-processor' + + // OAuth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { From 48a7b66f07fd78c044ea6c7edba3f16e50261bdb Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 25 May 2026 17:36:19 +0900 Subject: [PATCH 34/34] =?UTF-8?q?docs:=20ch09=20=ED=95=B5=EC=8B=AC=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/keyword_summary/ch09.md | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Hyeonu/keyword_summary/ch09.md diff --git a/Hyeonu/keyword_summary/ch09.md b/Hyeonu/keyword_summary/ch09.md new file mode 100644 index 00000000..9dfb4768 --- /dev/null +++ b/Hyeonu/keyword_summary/ch09.md @@ -0,0 +1,58 @@ +- 세션과 토큰의 차이는? + + | 구분 | 세션 기반 인증 | 토큰 기반 인증 | + | --- | --- | --- | + | 인증 정보 저장 위치 | 서버 |클라이언트(브라우저/앱) | + | 상태 유지 | Stateful(상태 유지) | Stateless(상태 없음) | + | 인증 방식 | 서버가 세션 ID 기억 | 토큰 자체로 사용자 인증 | + | 저장 형태 | 쿠키(JSESSIONID) | JWT 등 토큰 | + | 확장성 | 서버 늘어나면 관리 복잡 | 서버 확장 쉬움 | + | 보안 | 상대적으로 안전 | 토큰 탈취 시 위험 | + + **세션은 서버가 로그인 상태를 기억하고, 토큰은 토큰 자체가 인증 정보를 가진다.** + + +- 엑세스 토큰과 리프레시 토큰이란? + + **Access Token** + + 실제 인증에 사용하는 메인 토큰 + + - API 요청 시 사용 + - Authorization 헤더에 담아 전송 + - 유효기간이 짧음 + + **Refresh Token** + + - 로그인 유지 목적 + - Access Token 만료 시 새 토큰 발급 + - 유효기간이 김 (2주~ 1개월) + + 흐름 + + - 로그인 성공 + - Access + Refresh Token 발급 + - Access Token 만료 + - Refresh Token으로 재발급 + - 재로그인 없이 계속 사용 + + +- OAuth 1.0과 OAuth 2.0의 차이는? + + **OAuth** + + 비밀번호를 직접 공유하지 않고 제 3자 서비스가 권한을 위임받는 인증 방식 + + 예) 카카오, 구글, 네이버 로그인 + + | 구분 | OAuth 1.0 | OAuth 2.0 | + | --- | --- | --- | + | 보안 방식 | 복잡한 서명(Signature) | HTTPS 기반 | + | 구현 난이도 | 어려움 | 쉬움 | + | 성능 | 느림 | 빠름 | + | 사용성 | 낮음 | 높음 | + | 현재 사용 | 거의 안 씀 | 대부분 사용 | + + OAuth 1.0은 서명 기반으로 복잡하고, OAuth 2.0은 토큰 기반으로 단순하고 확장성이 좋다. + + OAuth 2.0은 OAuth 1.0의 복잡한 서명 방식을 제거하고 Access Token 기반 인증을 도입하여 구현이 단순해지고 확장성이 향상된 방식이다. \ No newline at end of file