From ae57a6f843c5e4be33b5463b10f2914daf8817ae Mon Sep 17 00:00:00 2001 From: junyong Date: Sun, 25 May 2025 22:33:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=2010=EC=A3=BC=EC=B0=A8=20-=20?= =?UTF-8?q?=EB=AF=B8=EC=85=981=20Session=20=EB=B0=A9=EC=8B=9D=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- junyong/.DS_Store | Bin 6148 -> 6148 bytes junyong/build.gradle | 9 ++ .../member/converter/MemberConverter.java | 11 ++- .../umc/spring/domain/member/data/Member.java | 13 ++- .../spring/domain/member/data/enums/Role.java | 5 ++ .../repository/member/MemberRepository.java | 4 +- .../service/MemberCommandServiceImpl.java | 8 +- .../validator/CategoriesExistValidator.java | 6 +- .../web/controller/MemberController.java | 11 +-- .../web/controller/MemberViewController.java | 61 ++++++++++++++ .../member/web/dto/MemberRequestDTO.java | 15 +++- .../security/CustomUserDetailsService.java | 31 +++++++ .../config/security/SecurityConfig.java | 41 ++++++++++ .../src/main/resources/templates/admin.html | 11 +++ .../src/main/resources/templates/home.html | 21 +++++ .../src/main/resources/templates/login.html | 27 ++++++ .../src/main/resources/templates/signup.html | 77 ++++++++++++++++++ 17 files changed, 334 insertions(+), 17 deletions(-) create mode 100644 junyong/src/main/java/umc/spring/domain/member/data/enums/Role.java create mode 100644 junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/CustomUserDetailsService.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java create mode 100644 junyong/src/main/resources/templates/admin.html create mode 100644 junyong/src/main/resources/templates/home.html create mode 100644 junyong/src/main/resources/templates/login.html create mode 100644 junyong/src/main/resources/templates/signup.html diff --git a/junyong/.DS_Store b/junyong/.DS_Store index d24210fbc444db215a82b6e05716fb34ab7ab89a..3378a78e5557b8009ddef0a98f2c19b6c30d9ec7 100644 GIT binary patch delta 74 zcmZoMXfc=|#>B`mu~2NHo+2*`0|Nsi1A_nqLk>gg#HjU*ESr0ns#!NSG%#*v=iui6 as@$x|{GE9+zlbFVP%TI?)8+t?EzAI3TMv`~ delta 202 zcmZoMXfc=|#>B)qu~2NHo+38~0|Nsi1A_nqL(0UM^_w>^GBMVJ#8?@U7)lv38FCm> za?%Zhlk;;6fT9cxTvI2Y%I4;~xFqG|Cjmt{R^2VAWO;bZ5nV+J0Tl%qNHz#EJz&_( l!&uF_nVo~51L)Gt3mLyNPv#e~()) .memberMissionList(new ArrayList<>()) .memberPreferList(new ArrayList<>()) + .role(request.getRole()) .build(); } diff --git a/junyong/src/main/java/umc/spring/domain/member/data/Member.java b/junyong/src/main/java/umc/spring/domain/member/data/Member.java index 6d9ff79..6961b41 100644 --- a/junyong/src/main/java/umc/spring/domain/member/data/Member.java +++ b/junyong/src/main/java/umc/spring/domain/member/data/Member.java @@ -5,6 +5,7 @@ import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; +import umc.spring.domain.member.data.enums.Role; import umc.spring.domain.member.data.mapping.MemberMission; import umc.spring.domain.member.data.mapping.MemberPrefer; import umc.spring.domain.notice.data.EventNotice; @@ -55,7 +56,7 @@ public class Member extends BaseEntity { @Column(nullable = false, length = 10) private String birth; - @Column(nullable = true, length = 50) + @Column(nullable = false, length = 50, unique = true) private String email; @ColumnDefault("0") // 점수 기본값 0 @@ -70,6 +71,12 @@ public class Member extends BaseEntity { @Column(nullable = false) private Boolean marketingAgree; + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + // 매핑 @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List memberPreferList = new ArrayList<>(); @@ -86,4 +93,8 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List memberMissionList = new ArrayList<>(); + public void encodePassword(String password) { + this.password = password; + } + } \ No newline at end of file diff --git a/junyong/src/main/java/umc/spring/domain/member/data/enums/Role.java b/junyong/src/main/java/umc/spring/domain/member/data/enums/Role.java new file mode 100644 index 0000000..6906dca --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/member/data/enums/Role.java @@ -0,0 +1,5 @@ +package umc.spring.domain.member.data.enums; + +public enum Role { + ADMIN, USER +} diff --git a/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java b/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java index 8e431fe..f39dd00 100644 --- a/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java +++ b/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java @@ -3,8 +3,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import umc.spring.domain.member.data.Member; -public interface MemberRepository extends JpaRepository { +import java.util.Optional; +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); } \ No newline at end of file diff --git a/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java b/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java index 32f71ce..bc9b3f7 100644 --- a/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java +++ b/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java @@ -1,6 +1,8 @@ package umc.spring.domain.member.service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import umc.spring.domain.member.converter.MemberConverter; @@ -23,6 +25,7 @@ import java.time.LocalDate; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class MemberCommandServiceImpl implements MemberCommandService { @@ -30,19 +33,22 @@ public class MemberCommandServiceImpl implements MemberCommandService { private final MemberRepository memberRepository; private final FoodCategoryRepository foodCategoryRepository; private final MemberMissionRepository memberMissionRepository; + private final PasswordEncoder passwordEncoder; @Override @Transactional public Member joinMember(MemberRequestDTO.JoinDto request) { Member member = MemberConverter.toMember(request); + member.encodePassword(passwordEncoder.encode(request.getPassword())); List foodCategoryList = request.getPreferCategory().stream() .map(category -> { - return foodCategoryRepository.findById(category).orElseThrow(() -> new FoodCategoryHandler(ErrorStatus.FOOD_CATEGORY_NOT_FOUND)); + return foodCategoryRepository.findById(Long.parseLong(category)).orElseThrow(() -> new FoodCategoryHandler(ErrorStatus.FOOD_CATEGORY_NOT_FOUND)); }).toList(); List memberPreferList = MemberPreferConverter.toMemberPreferList(foodCategoryList); memberPreferList.forEach( memberPrefer -> memberPrefer.setMember(member)); + log.info("member save"); return memberRepository.save(member); // 멤버 반환 } diff --git a/junyong/src/main/java/umc/spring/domain/member/valildation/validator/CategoriesExistValidator.java b/junyong/src/main/java/umc/spring/domain/member/valildation/validator/CategoriesExistValidator.java index 3b40dbd..45942b9 100644 --- a/junyong/src/main/java/umc/spring/domain/member/valildation/validator/CategoriesExistValidator.java +++ b/junyong/src/main/java/umc/spring/domain/member/valildation/validator/CategoriesExistValidator.java @@ -12,7 +12,7 @@ @Component @RequiredArgsConstructor -public class CategoriesExistValidator implements ConstraintValidator> { +public class CategoriesExistValidator implements ConstraintValidator> { private final FoodCategoryQueryService foodCategoryQueryService; @@ -22,9 +22,9 @@ public void initialize(ExistCategories constraintAnnotation) { } @Override - public boolean isValid(List values, ConstraintValidatorContext context) { + public boolean isValid(List values, ConstraintValidatorContext context) { boolean isValid = values.stream() // 올바른 카테고리인지 조회 - .allMatch(foodCategoryQueryService::isExistCategory); + .allMatch(categoryId -> foodCategoryQueryService.isExistCategory(Long.parseLong(categoryId))); if(!isValid) { context.disableDefaultConstraintViolation(); diff --git a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java index 7f51d01..6ddecab 100644 --- a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java +++ b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java @@ -28,11 +28,12 @@ public class MemberController { private final MemberCommandService memberCommandService; private final MemberQueryService memberQueryService; - @PostMapping("/") - public ApiResponse join(@RequestBody @Valid MemberRequestDTO.JoinDto request) { - Member member = memberCommandService.joinMember(request); - return ApiResponse.onSuccess(MemberConverter.toJoinResultDto(member)); - } + // 10주차 하느라 주석처리 +// @PostMapping("/") +// public ApiResponse join(@RequestBody @Valid MemberRequestDTO.JoinDto request) { +// Member member = memberCommandService.joinMember(request); +// return ApiResponse.onSuccess(MemberConverter.toJoinResultDto(member)); +// } @GetMapping("/missions") @Operation(summary = "내가 도전중인 미션 목록 조회하기 API", description = "내가 도전 중/도전 완료한 미션을 조회합니다. page와 status가 필요합니다") diff --git a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java new file mode 100644 index 0000000..9fc894b --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java @@ -0,0 +1,61 @@ +package umc.spring.domain.member.web.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import umc.spring.domain.member.service.MemberCommandService; +import umc.spring.domain.member.web.dto.MemberRequestDTO; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class MemberViewController { + + private final MemberCommandService memberCommandService; + + @PostMapping("/members/signup") + public String joinMember(@ModelAttribute("memberJoinDto") MemberRequestDTO.JoinDto request, + BindingResult bindingResult, Model model) { + + if(bindingResult.hasErrors()) { + return "signup"; + } + + try { + log.info("role = {}", request.getRole()); + memberCommandService.joinMember(request); + return "redirect:/login"; + }catch (Exception e) { + model.addAttribute("error", e.getMessage()); + return "signup"; + } + + } + + @GetMapping("/login") + public String loginPage(){ + return "login"; + } + + @GetMapping("/signup") + public String signupPage(Model model){ + model.addAttribute("memberJoinDto", new MemberRequestDTO.JoinDto()); + return "signup"; + } + + @GetMapping("/home") + public String home(){ + return "home"; + } + + @GetMapping("/admin") + public String admin() { + return "admin"; + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberRequestDTO.java b/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberRequestDTO.java index 4b356b4..985ebcd 100644 --- a/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberRequestDTO.java +++ b/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberRequestDTO.java @@ -1,9 +1,12 @@ package umc.spring.domain.member.web.dto; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Getter; +import lombok.Setter; +import umc.spring.domain.member.data.enums.Role; import umc.spring.domain.member.valildation.annotation.ExistCategories; import java.util.List; @@ -11,11 +14,17 @@ public class MemberRequestDTO { @Getter + @Setter public static class JoinDto{ @NotBlank String name; + @NotBlank + @Email + String email; + @NotBlank + String password; @NotNull - Integer gender; + String gender; @NotNull String birth; @NotNull @@ -25,7 +34,9 @@ public static class JoinDto{ @Size(min = 5, max = 12) String specAddress; @ExistCategories - List preferCategory; + List preferCategory; + @NotNull + Role role; } diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/CustomUserDetailsService.java b/junyong/src/main/java/umc/spring/global/common/config/security/CustomUserDetailsService.java new file mode 100644 index 0000000..7324a72 --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package umc.spring.global.common.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +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; +import umc.spring.domain.member.data.Member; +import umc.spring.domain.member.repository.member.MemberRepository; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("해당 이메일을 가진 유저가 존재하지 않습니다: " + username)); + + return User + .withUsername(member.getEmail()) + .password(member.getPassword()) + .roles(member.getRole().name()) + .build(); + + } +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java b/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java new file mode 100644 index 0000000..2761260 --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java @@ -0,0 +1,41 @@ +package umc.spring.global.common.config.security; + +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/", "/home", "/signup", "/members/signup", "/css/**").permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .formLogin((form) -> form. + loginPage("/login") + .defaultSuccessUrl("/home", true) + .permitAll() + ) + .logout((logout) -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/junyong/src/main/resources/templates/admin.html b/junyong/src/main/resources/templates/admin.html new file mode 100644 index 0000000..a2141de --- /dev/null +++ b/junyong/src/main/resources/templates/admin.html @@ -0,0 +1,11 @@ + + + + + Admin Page + + +

Admin Page

+

관리자만 접근할 수 있는 페이지입니다.

+ + \ No newline at end of file diff --git a/junyong/src/main/resources/templates/home.html b/junyong/src/main/resources/templates/home.html new file mode 100644 index 0000000..fb08416 --- /dev/null +++ b/junyong/src/main/resources/templates/home.html @@ -0,0 +1,21 @@ + + + + + Home + + +

Welcome to Home Page!

+ +

+ + + + + +
+ +
+ \ No newline at end of file diff --git a/junyong/src/main/resources/templates/login.html b/junyong/src/main/resources/templates/login.html new file mode 100644 index 0000000..9f0276a --- /dev/null +++ b/junyong/src/main/resources/templates/login.html @@ -0,0 +1,27 @@ + + + + + Login + + +

Login

+
+
+ + +
+
+ + +
+ +
+ +

사용자 이름 또는 비밀번호가 잘못되었습니다.

+

로그아웃되었습니다.

+ + +

계정이 없나요? Sign up

+ + \ No newline at end of file diff --git a/junyong/src/main/resources/templates/signup.html b/junyong/src/main/resources/templates/signup.html new file mode 100644 index 0000000..c5e99c1 --- /dev/null +++ b/junyong/src/main/resources/templates/signup.html @@ -0,0 +1,77 @@ + + + + + 회원가입 + + + +

회원가입

+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + + + + +
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+ +
+ + \ No newline at end of file From 1a3f3625252b0cf28b3da85bcbf7b307163b7c6d Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 3 Jun 2025 16:57:49 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat=20:=2010=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=982,=20=EC=8B=9C=EB=8B=88=EC=96=B4=EB=AF=B8=EC=85=98=201?= =?UTF-8?q?,2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- junyong/.gitignore | 3 +- junyong/build.gradle | 8 ++ .../member/converter/LoginConverter.java | 14 ++ .../member/converter/MemberConverter.java | 7 + .../umc/spring/domain/member/data/Member.java | 5 + .../repository/member/MemberRepository.java | 1 + .../domain/member/service/LoginService.java | 20 +++ .../member/service/MemberCommandService.java | 3 + .../service/MemberCommandServiceImpl.java | 29 ++++ .../member/service/MemberQueryService.java | 2 + .../service/MemberQueryServiceImpl.java | 17 +++ .../web/controller/LoginController.java | 22 +++ .../web/controller/MemberController.java | 26 ++++ .../web/controller/MemberViewController.java | 80 +++++------ .../domain/member/web/dto/LoginDto.java | 29 ++++ .../member/web/dto/MemberResponseDTO.java | 8 ++ .../domain/token/data/RefreshToken.java | 29 ++++ .../repository/RefreshTokenRepository.java | 11 ++ .../token/web/controller/TokenController.java | 58 ++++++++ .../domain/token/web/dto/TokenRequestDTO.java | 14 ++ .../apiPayload/code/status/ErrorStatus.java | 6 + .../global/common/config/WebConfig.java | 9 +- .../config/security/SecurityConfig.java | 109 +++++++++++++-- .../common/config/security/jwt/Constants.java | 8 ++ .../security/jwt/JwtAuthenticationFilter.java | 107 +++++++++++++++ .../config/security/jwt/JwtProperties.java | 25 ++++ .../config/security/jwt/JwtTokenProvider.java | 129 ++++++++++++++++++ .../social/CustomOAuth2UserService.java | 63 +++++++++ .../security/social/OAuth2SuccessHandler.java | 69 ++++++++++ .../security/social/OAuthAttributes.java | 82 +++++++++++ junyong/src/main/resources/application.yml | 38 ++++++ 31 files changed, 979 insertions(+), 52 deletions(-) create mode 100644 junyong/src/main/java/umc/spring/domain/member/converter/LoginConverter.java create mode 100644 junyong/src/main/java/umc/spring/domain/member/service/LoginService.java create mode 100644 junyong/src/main/java/umc/spring/domain/member/web/controller/LoginController.java create mode 100644 junyong/src/main/java/umc/spring/domain/member/web/dto/LoginDto.java create mode 100644 junyong/src/main/java/umc/spring/domain/token/data/RefreshToken.java create mode 100644 junyong/src/main/java/umc/spring/domain/token/repository/RefreshTokenRepository.java create mode 100644 junyong/src/main/java/umc/spring/domain/token/web/controller/TokenController.java create mode 100644 junyong/src/main/java/umc/spring/domain/token/web/dto/TokenRequestDTO.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/jwt/Constants.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtAuthenticationFilter.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtProperties.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtTokenProvider.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/social/CustomOAuth2UserService.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/social/OAuth2SuccessHandler.java create mode 100644 junyong/src/main/java/umc/spring/global/common/config/security/social/OAuthAttributes.java diff --git a/junyong/.gitignore b/junyong/.gitignore index 2e52df8..0f70cd3 100644 --- a/junyong/.gitignore +++ b/junyong/.gitignore @@ -10,4 +10,5 @@ out/ ._.DS_Store **/.DS_Store **/._.DS_Store -src/main/generated \ No newline at end of file +src/main/generated +application.yml \ No newline at end of file diff --git a/junyong/build.gradle b/junyong/build.gradle index b4c6f80..a693ad0 100644 --- a/junyong/build.gradle +++ b/junyong/build.gradle @@ -59,6 +59,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //oauth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + // thymleaf implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' diff --git a/junyong/src/main/java/umc/spring/domain/member/converter/LoginConverter.java b/junyong/src/main/java/umc/spring/domain/member/converter/LoginConverter.java new file mode 100644 index 0000000..c973d61 --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/member/converter/LoginConverter.java @@ -0,0 +1,14 @@ +package umc.spring.domain.member.converter; + +import umc.spring.domain.member.web.dto.LoginDto; + +public class LoginConverter { + + public static LoginDto.LoginResultDto toLoginResultDto(Long memberId, String accessToken) { + return LoginDto.LoginResultDto.builder() + .memberId(memberId) + .accessToken(accessToken) + .build(); + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/member/converter/MemberConverter.java b/junyong/src/main/java/umc/spring/domain/member/converter/MemberConverter.java index 80b5214..fe080e7 100644 --- a/junyong/src/main/java/umc/spring/domain/member/converter/MemberConverter.java +++ b/junyong/src/main/java/umc/spring/domain/member/converter/MemberConverter.java @@ -53,5 +53,12 @@ public static Member toMember(MemberRequestDTO.JoinDto request) { .build(); } + public static MemberResponseDTO.MemberInfoDto toMemberInfoDto(Member member) { + return MemberResponseDTO.MemberInfoDto.builder() + .name(member.getName()) + .email(member.getEmail()) + .gender(member.getGender().name()) + .build(); + } } diff --git a/junyong/src/main/java/umc/spring/domain/member/data/Member.java b/junyong/src/main/java/umc/spring/domain/member/data/Member.java index 6961b41..96851d0 100644 --- a/junyong/src/main/java/umc/spring/domain/member/data/Member.java +++ b/junyong/src/main/java/umc/spring/domain/member/data/Member.java @@ -97,4 +97,9 @@ public void encodePassword(String password) { this.password = password; } + public Member update(String name) { + this.name = name; + return this; + } + } \ No newline at end of file diff --git a/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java b/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java index f39dd00..b88ddc2 100644 --- a/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java +++ b/junyong/src/main/java/umc/spring/domain/member/repository/member/MemberRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import umc.spring.domain.member.data.Member; +import java.util.List; import java.util.Optional; public interface MemberRepository extends JpaRepository { diff --git a/junyong/src/main/java/umc/spring/domain/member/service/LoginService.java b/junyong/src/main/java/umc/spring/domain/member/service/LoginService.java new file mode 100644 index 0000000..281b98f --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/member/service/LoginService.java @@ -0,0 +1,20 @@ +package umc.spring.domain.member.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import umc.spring.global.common.config.security.jwt.JwtTokenProvider; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LoginService { + + private final JwtTokenProvider jwtTokenProvider; + + public void socialLogin(String code, String registrationId) { + System.out.println("code = " + code); + System.out.println("registrationId = " + registrationId); + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandService.java b/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandService.java index 9fa53a3..ebf4602 100644 --- a/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandService.java +++ b/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandService.java @@ -1,6 +1,7 @@ package umc.spring.domain.member.service; import umc.spring.domain.member.data.Member; +import umc.spring.domain.member.web.dto.LoginDto; import umc.spring.domain.member.web.dto.MemberRequestDTO; import umc.spring.domain.member.web.dto.MemberResponseDTO; @@ -10,4 +11,6 @@ public interface MemberCommandService { MemberResponseDTO.CompleteDto completeMission(Long memberMissionId, String confirmNumber); + LoginDto.LoginResultDto loginMember(LoginDto.LoginRequestDto request); + } diff --git a/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java b/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java index bc9b3f7..f09b132 100644 --- a/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java +++ b/junyong/src/main/java/umc/spring/domain/member/service/MemberCommandServiceImpl.java @@ -2,9 +2,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import umc.spring.domain.member.converter.LoginConverter; import umc.spring.domain.member.converter.MemberConverter; import umc.spring.domain.member.converter.MemberMissionConverter; import umc.spring.domain.member.converter.MemberPreferConverter; @@ -15,14 +18,17 @@ import umc.spring.domain.member.repository.foodCategory.FoodCategoryRepository; import umc.spring.domain.member.repository.member.MemberRepository; import umc.spring.domain.member.repository.memberMission.MemberMissionRepository; +import umc.spring.domain.member.web.dto.LoginDto; import umc.spring.domain.member.web.dto.MemberRequestDTO; import umc.spring.domain.member.web.dto.MemberResponseDTO; import umc.spring.domain.mission.data.enums.MissionStatus; import umc.spring.global.common.apiPayload.code.status.ErrorStatus; import umc.spring.global.common.apiPayload.exception.handler.ErrorHandler; import umc.spring.global.common.apiPayload.exception.handler.FoodCategoryHandler; +import umc.spring.global.common.config.security.jwt.JwtTokenProvider; import java.time.LocalDate; +import java.util.Collections; import java.util.List; @Slf4j @@ -34,6 +40,7 @@ public class MemberCommandServiceImpl implements MemberCommandService { private final FoodCategoryRepository foodCategoryRepository; private final MemberMissionRepository memberMissionRepository; private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; @Override @Transactional @@ -78,4 +85,26 @@ public MemberResponseDTO.CompleteDto completeMission(Long memberMissionId, Strin return MemberMissionConverter.toCompleteDto(memberMission); } + @Override + public LoginDto.LoginResultDto loginMember(LoginDto.LoginRequestDto request) { + Member member = memberRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + if(!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new ErrorHandler(ErrorStatus.INVALID_PASSWORD); + } + + Authentication authenticationToken = new UsernamePasswordAuthenticationToken( + member.getEmail(), null, + Collections.singleton(() -> member.getRole().name()) + ); + + String accessToken = jwtTokenProvider.generateToken(authenticationToken); + + return LoginConverter.toLoginResultDto( + member.getId(), + accessToken + ); + } + } diff --git a/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryService.java b/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryService.java index 262936f..7f8292c 100644 --- a/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryService.java +++ b/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryService.java @@ -1,8 +1,10 @@ package umc.spring.domain.member.service; +import jakarta.servlet.http.HttpServletRequest; import umc.spring.domain.member.web.dto.MemberResponseDTO; import umc.spring.domain.mission.data.enums.MissionStatus; public interface MemberQueryService { MemberResponseDTO.MissionListDto getMissions(MissionStatus status, Integer page); + MemberResponseDTO.MemberInfoDto getMemberInfo(HttpServletRequest request); } diff --git a/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryServiceImpl.java b/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryServiceImpl.java index d2d94d4..d068f58 100644 --- a/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryServiceImpl.java +++ b/junyong/src/main/java/umc/spring/domain/member/service/MemberQueryServiceImpl.java @@ -1,9 +1,13 @@ package umc.spring.domain.member.service; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.spring.domain.member.converter.MemberConverter; import umc.spring.domain.member.converter.MemberMissionConverter; import umc.spring.domain.member.data.Member; import umc.spring.domain.member.data.mapping.MemberMission; @@ -13,6 +17,7 @@ import umc.spring.domain.mission.data.enums.MissionStatus; import umc.spring.global.common.apiPayload.code.status.ErrorStatus; import umc.spring.global.common.apiPayload.exception.handler.ErrorHandler; +import umc.spring.global.common.config.security.jwt.JwtTokenProvider; @Service @@ -21,6 +26,7 @@ public class MemberQueryServiceImpl implements MemberQueryService { private final MemberRepository memberRepository; private final MemberMissionRepository memberMissionRepository; + private final JwtTokenProvider jwtTokenProvider; @Override public MemberResponseDTO.MissionListDto getMissions(MissionStatus status, Integer page) { @@ -33,4 +39,15 @@ public MemberResponseDTO.MissionListDto getMissions(MissionStatus status, Intege } return MemberMissionConverter.toMissionListDto(memberMissionList); } + + @Override + @Transactional(readOnly = true) + public MemberResponseDTO.MemberInfoDto getMemberInfo(HttpServletRequest request) { + Authentication authentication = jwtTokenProvider.extractAuthentication(request); + String email = authentication.getName(); + + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND)); + return MemberConverter.toMemberInfoDto(member); + } } diff --git a/junyong/src/main/java/umc/spring/domain/member/web/controller/LoginController.java b/junyong/src/main/java/umc/spring/domain/member/web/controller/LoginController.java new file mode 100644 index 0000000..093a34c --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/member/web/controller/LoginController.java @@ -0,0 +1,22 @@ +package umc.spring.domain.member.web.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import umc.spring.domain.member.service.LoginService; + +@Slf4j +@RestController +@RequestMapping("/login") +@RequiredArgsConstructor +public class LoginController { + + private final LoginService loginService; + + @GetMapping("/oauth/code/{registrationId}") + public void googleLogin(@RequestParam String code, @PathVariable String registrationId) { + log.info("hello1"); + loginService.socialLogin(code, registrationId); + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java index 6ddecab..3bca9d2 100644 --- a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java +++ b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberController.java @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; @@ -12,6 +14,7 @@ import umc.spring.domain.member.data.Member; import umc.spring.domain.member.service.MemberCommandService; import umc.spring.domain.member.service.MemberQueryService; +import umc.spring.domain.member.web.dto.LoginDto; import umc.spring.domain.member.web.dto.MemberRequestDTO; import umc.spring.domain.member.web.dto.MemberResponseDTO; import umc.spring.domain.mission.data.enums.MissionStatus; @@ -63,4 +66,27 @@ public ApiResponse completeMission(@ExistMemberMi return ApiResponse.onSuccess(completeDto); } + @PostMapping("/login") + @Operation(summary = "유저 로그인 API", description = "유저가 로그인하는 API입니다") + public ApiResponse login(@RequestBody @Valid LoginDto.LoginRequestDto request) { + return ApiResponse.onSuccess(memberCommandService.loginMember(request)); + } + + @GetMapping("/info") + @Operation(summary = "유저 내 정보 조회 API - 인증 필요", + description = "유저가 내 정보를 조회하는 API입니다", + security = {@SecurityRequirement(name = "JWT TOKEN")} + ) + public ApiResponse getMyInfo(HttpServletRequest request) { + return ApiResponse.onSuccess(memberQueryService.getMemberInfo(request)); + } + + @PostMapping("/join") + @Operation(summary = "유저 회원가입 API", + description = "유저가 회원가입하는 API입니다") + public ApiResponse join(@RequestBody @Valid MemberRequestDTO.JoinDto request) { + Member member = memberCommandService.joinMember(request); + return ApiResponse.onSuccess(MemberConverter.toJoinResultDto(member)); + } + } diff --git a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java index 9fc894b..3c35c88 100644 --- a/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java +++ b/junyong/src/main/java/umc/spring/domain/member/web/controller/MemberViewController.java @@ -1,5 +1,6 @@ package umc.spring.domain.member.web.controller; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; @@ -15,47 +16,48 @@ @Controller @RequiredArgsConstructor public class MemberViewController { - - private final MemberCommandService memberCommandService; - - @PostMapping("/members/signup") - public String joinMember(@ModelAttribute("memberJoinDto") MemberRequestDTO.JoinDto request, - BindingResult bindingResult, Model model) { - - if(bindingResult.hasErrors()) { - return "signup"; - } - - try { - log.info("role = {}", request.getRole()); - memberCommandService.joinMember(request); - return "redirect:/login"; - }catch (Exception e) { - model.addAttribute("error", e.getMessage()); - return "signup"; - } - - } - - @GetMapping("/login") - public String loginPage(){ - return "login"; - } - - @GetMapping("/signup") - public String signupPage(Model model){ - model.addAttribute("memberJoinDto", new MemberRequestDTO.JoinDto()); - return "signup"; - } - +// +// private final MemberCommandService memberCommandService; +// +// @PostMapping("/members/signup") +// public String joinMember(@ModelAttribute("memberJoinDto") MemberRequestDTO.JoinDto request, +// BindingResult bindingResult, Model model) { +// +// if(bindingResult.hasErrors()) { +// return "signup"; +// } +// +// try { +// log.info("role = {}", request.getRole()); +// memberCommandService.joinMember(request); +// return "redirect:/login"; +// }catch (Exception e) { +// model.addAttribute("error", e.getMessage()); +// return "signup"; +// } +// +// } +// +// @GetMapping("/login") +// public String loginPage(){ +// return "login"; +// } +// +// @GetMapping("/signup") +// public String signupPage(Model model){ +// model.addAttribute("memberJoinDto", new MemberRequestDTO.JoinDto()); +// return "signup"; +// } +// @GetMapping("/home") - public String home(){ + public String home(Model model) { + model.addAttribute("member", new MemberRequestDTO()); return "home"; } - - @GetMapping("/admin") - public String admin() { - return "admin"; - } +// +// @GetMapping("/admin") +// public String admin() { +// return "admin"; +// } } diff --git a/junyong/src/main/java/umc/spring/domain/member/web/dto/LoginDto.java b/junyong/src/main/java/umc/spring/domain/member/web/dto/LoginDto.java new file mode 100644 index 0000000..10f48a4 --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/member/web/dto/LoginDto.java @@ -0,0 +1,29 @@ +package umc.spring.domain.member.web.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +public class LoginDto { + + @Getter + @Setter + public static class LoginRequestDto{ + @NotBlank(message = "이메일은 필수입니다") + @Email(message = "올바른 이메일 형식이 아닙니다") + private String email; + + @NotBlank(message = "패스워드는 필수입니다") + private String password; + + + } + + @Getter + @Builder + public static class LoginResultDto{ + Long memberId; + String accessToken; + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberResponseDTO.java b/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberResponseDTO.java index 38259e2..27a86ce 100644 --- a/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberResponseDTO.java +++ b/junyong/src/main/java/umc/spring/domain/member/web/dto/MemberResponseDTO.java @@ -59,5 +59,13 @@ public static class CompleteDto{ LocalDateTime completedAt; } + @Builder + @Getter + public static class MemberInfoDto{ + String name; + String email; + String gender; + } + } diff --git a/junyong/src/main/java/umc/spring/domain/token/data/RefreshToken.java b/junyong/src/main/java/umc/spring/domain/token/data/RefreshToken.java new file mode 100644 index 0000000..bfde155 --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/token/data/RefreshToken.java @@ -0,0 +1,29 @@ +package umc.spring.domain.token.data; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.*; + +@Entity +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String refreshToken; + + @Column(nullable = false) + private String email; + + public RefreshToken updateToken(String token) { + this.refreshToken = token; + return this; + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/token/repository/RefreshTokenRepository.java b/junyong/src/main/java/umc/spring/domain/token/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..c02f3aa --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/token/repository/RefreshTokenRepository.java @@ -0,0 +1,11 @@ +package umc.spring.domain.token.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import umc.spring.domain.token.data.RefreshToken; + +import java.util.Optional; + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByRefreshToken(String token); +} diff --git a/junyong/src/main/java/umc/spring/domain/token/web/controller/TokenController.java b/junyong/src/main/java/umc/spring/domain/token/web/controller/TokenController.java new file mode 100644 index 0000000..8bfef40 --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/token/web/controller/TokenController.java @@ -0,0 +1,58 @@ +package umc.spring.domain.token.web.controller; + +import io.jsonwebtoken.Jwts; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import umc.spring.domain.token.data.RefreshToken; +import umc.spring.domain.token.repository.RefreshTokenRepository; +import umc.spring.domain.token.web.dto.TokenRequestDTO; +import umc.spring.global.common.config.security.jwt.JwtTokenProvider; + +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/token") +@RequiredArgsConstructor +public class TokenController { + + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenRepository refreshTokenRepository; + + @PostMapping + @Operation(summary = "액세스 토큰 재발급 API", description = "RefreshToken이 필요합니다") + public ResponseEntity refreshAccessToken(@RequestBody TokenRequestDTO.RefreshDto request) { + String refreshToken = request.getRefreshToken(); + + if(!jwtTokenProvider.validateToken(refreshToken)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token"); + } + + RefreshToken savedRefreshToken = refreshTokenRepository.findByRefreshToken(refreshToken) + .orElseThrow(() -> new RuntimeException("Refresh token not found")); + + String email = Jwts.parserBuilder() + .setSigningKey(jwtTokenProvider.getSigningKey()) + .build() + .parseClaimsJws(refreshToken) + .getBody() + .getSubject(); + + if(!savedRefreshToken.getEmail().equals(email)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token email mismatch"); + } + + +// 새로운 액세스 토큰 발급 + String newAccessToken = jwtTokenProvider.generateToken(email, "USER"); + return ResponseEntity.ok(Map.of("accessToken", newAccessToken)); + } + +} diff --git a/junyong/src/main/java/umc/spring/domain/token/web/dto/TokenRequestDTO.java b/junyong/src/main/java/umc/spring/domain/token/web/dto/TokenRequestDTO.java new file mode 100644 index 0000000..21e9cec --- /dev/null +++ b/junyong/src/main/java/umc/spring/domain/token/web/dto/TokenRequestDTO.java @@ -0,0 +1,14 @@ +package umc.spring.domain.token.web.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +public class TokenRequestDTO { + + @Getter + public static class RefreshDto { + @NotNull + String refreshToken; + } + +} diff --git a/junyong/src/main/java/umc/spring/global/common/apiPayload/code/status/ErrorStatus.java b/junyong/src/main/java/umc/spring/global/common/apiPayload/code/status/ErrorStatus.java index 535dc49..e878d86 100644 --- a/junyong/src/main/java/umc/spring/global/common/apiPayload/code/status/ErrorStatus.java +++ b/junyong/src/main/java/umc/spring/global/common/apiPayload/code/status/ErrorStatus.java @@ -19,6 +19,12 @@ public enum ErrorStatus implements BaseErrorCode { // 멤버 관련 에러 MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자 없음"), NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수입니다"), + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "MEMBER4004", "올바르지 않은 토큰입니다"), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER4005", "올바르지 않은 비밀번호 입니다."), + FAIL_AUTHENTICATION(HttpStatus.FORBIDDEN, "MEMBER4006", "인증 실패"), + TOKEN_EXPIRED(HttpStatus.FORBIDDEN, "MEMBER4007", "토큰 만료"), + + INVALID_REGISTRATION_ID(HttpStatus.BAD_REQUEST, "MEMBER4008", "잘못된 소셜 로그인입니다"), // 음식 카테고리 관련 에러 FOOD_CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "FOOD4001", "존재하지 않는 음식 카테고리입니다."), diff --git a/junyong/src/main/java/umc/spring/global/common/config/WebConfig.java b/junyong/src/main/java/umc/spring/global/common/config/WebConfig.java index 5779575..b9ca1a4 100644 --- a/junyong/src/main/java/umc/spring/global/common/config/WebConfig.java +++ b/junyong/src/main/java/umc/spring/global/common/config/WebConfig.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import umc.spring.domain.region.validation.resolver.RegionArgumentResolver; import umc.spring.global.common.validation.resolver.PageArgumentResolver; @@ -22,6 +23,12 @@ public void addArgumentResolvers(List resolvers) resolvers.add(pageArgumentResolver); } - + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") // 모든 경로에 대해 + .allowedOrigins("*") // 모든 Origin 허용 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*"); + } } diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java b/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java index 2761260..beaa34b 100644 --- a/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java +++ b/junyong/src/main/java/umc/spring/global/common/config/security/SecurityConfig.java @@ -1,35 +1,122 @@ package umc.spring.global.common.config.security; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import umc.spring.global.common.apiPayload.ApiResponse; +import umc.spring.global.common.apiPayload.code.status.ErrorStatus; +import umc.spring.global.common.config.security.jwt.JwtAuthenticationFilter; +import umc.spring.global.common.config.security.jwt.JwtTokenProvider; +import umc.spring.global.common.config.security.social.CustomOAuth2UserService; +import umc.spring.global.common.config.security.social.OAuth2SuccessHandler; +import java.util.List; + +@Slf4j @EnableWebSecurity @Configuration public class SecurityConfig { @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtTokenProvider jwtTokenProvider, ObjectMapper objectMapper, CustomOAuth2UserService customOAuth2UserService, OAuth2SuccessHandler oAuth2SuccessHandler) throws Exception { http + .csrf(AbstractHttpConfigurer::disable) + .cors( cors -> + cors.configurationSource(request -> { + var config = new org.springframework.web.cors.CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:8080")); // Swagger UI origin + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + return config; + }) + ) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) .authorizeHttpRequests((requests) -> requests - .requestMatchers("/", "/home", "/signup", "/members/signup", "/css/**").permitAll() + .requestMatchers("/", "/home", "/signup", "/css/**").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() + .requestMatchers("/api/token/**").permitAll() + .requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() + ) - .formLogin((form) -> form. - loginPage("/login") - .defaultSuccessUrl("/home", true) - .permitAll() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, objectMapper), UsernamePasswordAuthenticationFilter.class) + // 인증 실패 + .exceptionHandling((exception) -> { + exception.authenticationEntryPoint((request, response, authException) -> { + log.info("request = {}", request); + log.info("exception = {}", authException.getMessage()); + log.info("1"); + Object ex = request.getAttribute("exception"); + log.info("request.getAttribute = {}", ex); + + if (ex == ErrorStatus.TOKEN_EXPIRED) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue( + response.getOutputStream(), + ApiResponse.onFailure(ErrorStatus.TOKEN_EXPIRED.getCode(), ErrorStatus.TOKEN_EXPIRED.getMessage(), null) + ); + return; + } + + log.info("2"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue( + response.getOutputStream(), + ApiResponse.onFailure(ErrorStatus.FAIL_AUTHENTICATION.getCode(), ErrorStatus.FAIL_AUTHENTICATION.getMessage(), null) + ); + }); + }) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) // ★ 이거 꼭 필요 + ) + .successHandler(oAuth2SuccessHandler) // 별도 핸들러 등록 ) - .logout((logout) -> logout - .logoutUrl("/logout") - .logoutSuccessUrl("/login?logout") - .permitAll() - ); + +// .oauth2Login(oauth2 -> oauth2 +// .userInfoEndpoint(userInfo -> userInfo +// .userService(customOAuth2UserService) +// ) +// .successHandler((request, response, authentication) -> { +// +// +// // OAuth2User에서 필요한 사용자 정보 추출 +// OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); +// +// String name = oAuth2User.getAttribute("name"); +// String email = oAuth2User.getAttribute("email"); +// +// // JWT 토큰 생성 +// String jwt = jwtTokenProvider.generateToken(name, email, "USER"); +// +// // JWT 토큰을 헤더에 추가 +//// response.setHeader("Authorization", "Bearer " + jwt); +// +// // 리디렉션 +// response.sendRedirect("/home?token=" + jwt); +// }) +// ) + ; + return http.build(); } diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/jwt/Constants.java b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/Constants.java new file mode 100644 index 0000000..d033d9a --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/Constants.java @@ -0,0 +1,8 @@ +package umc.spring.global.common.config.security.jwt; + +public final class Constants { + + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; + +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtAuthenticationFilter.java b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..eb10af4 --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,107 @@ +package umc.spring.global.common.config.security.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import umc.spring.global.common.apiPayload.ApiResponse; +import umc.spring.global.common.apiPayload.code.status.ErrorStatus; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter{ + + private final JwtTokenProvider jwtTokenProvider; + private final ObjectMapper objectMapper; + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + log.info("JwtAuthenticationFilter"); + String uri = request.getRequestURI(); + + log.info(" permit 검사"); + // permitAll 경로는 토큰 검사하지 않음 + if (uri.startsWith("/login") || uri.startsWith("/signup") || uri.startsWith("/members/join") || + uri.startsWith("/members/login") || uri.startsWith("/swagger-ui") || uri.startsWith("/v3/api-docs") || + uri.equals("/") || uri.equals("/home") || + uri.startsWith("/oauth2/") || uri.startsWith("/login/oauth2/") || uri.startsWith("/api/token")) { + filterChain.doFilter(request, response); + return; + } + + String token = resolveToken(request); + + log.info("토큰 있는지 검사"); + if (!StringUtils.hasText(token)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getOutputStream(), + ApiResponse.onFailure(ErrorStatus.INVALID_TOKEN.getCode(), ErrorStatus.INVALID_TOKEN.getMessage(), null)); + return; // 필터 체인 진행 중단 + } + + ErrorStatus errorCode = null; + + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } else { + errorCode = ErrorStatus.INVALID_TOKEN; + } + } catch (ExpiredJwtException e) { + errorCode = ErrorStatus.TOKEN_EXPIRED; + } catch (Exception e) { + errorCode = ErrorStatus.FAIL_AUTHENTICATION; + } + + if (errorCode != null) { + request.setAttribute("exception", errorCode); + } + + + filterChain.doFilter(request, response); + + +// ErrorStatus errorCode = jwtTokenProvider.validateToken(token) ? null : ErrorStatus.INVALID_TOKEN; +// log.info("errorCode = {}", errorCode); +// +// if (errorCode == null) { +// try { +// Authentication authentication = jwtTokenProvider.getAuthentication(token); +// SecurityContextHolder.getContext().setAuthentication(authentication); +// } catch (Exception e) { +// response.setStatus(HttpStatus.UNAUTHORIZED.value()); +// response.setContentType(MediaType.APPLICATION_JSON_VALUE); +// objectMapper.writeValue( +// response.getOutputStream(), +// ApiResponse.onFailure(ErrorStatus.FAIL_AUTHENTICATION.getCode(), ErrorStatus.FAIL_AUTHENTICATION.getMessage(), null) +// ); +// return; +// } +// } else { +// request.setAttribute("exception", errorCode); +// } +// +// filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtProperties.java b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtProperties.java new file mode 100644 index 0000000..8d97ebb --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtProperties.java @@ -0,0 +1,25 @@ +package umc.spring.global.common.config.security.jwt; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@ConfigurationProperties("jwt.token") +public class JwtProperties { + + private String secretKey = ""; + private Expiration expiration; + + @Getter + @Setter + public static class Expiration{ + private Long access; + + // TODO : refresh token + } + +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtTokenProvider.java b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..dd74fa4 --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/jwt/JwtTokenProvider.java @@ -0,0 +1,129 @@ +package umc.spring.global.common.config.security.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import umc.spring.global.common.apiPayload.code.status.ErrorStatus; +import umc.spring.global.common.apiPayload.exception.handler.ErrorHandler; + +import java.security.Key; +import java.util.Collections; +import java.util.Date; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtProperties jwtProperties; + + public Key getSigningKey() { + return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes()); + } + + // Jwt Token 생성 및 반환 + public String generateToken(Authentication authentication) { + String email = authentication.getName(); + return Jwts.builder() + .setSubject(email) + .claim("role", authentication.getAuthorities().iterator().next().getAuthority()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getAccess())) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String generateToken(String email, String role) { + Claims claims = Jwts.claims().setSubject(email); + claims.put("role", role); + + return Jwts.builder().setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getAccess())) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + + } + + public String generateRefreshToken(String email) { + return Jwts.builder() + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration().getAccess())) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + // 토큰 유효성 검사 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.info("validateToken() 내부 e = {}", e.getMessage()); + return false; + } + } + + // 토큰 유효기간 검사 + public boolean isTokenExpired(String token) { + try { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return true; + } catch (Exception e) { + return true; + } + } + + // JWT 토큰에서 인증 정보 추출 -> Authentication 객체 반환 + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + String email = claims.getSubject(); + String role = claims.get("role", String.class); + + User principal = new User(email, "", Collections.singleton(() -> role)); + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } + + // 순수 토큰 반환 + public static String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(Constants.AUTH_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(Constants.TOKEN_PREFIX)) { + return bearerToken.substring(Constants.TOKEN_PREFIX.length()); + } + return null; + } + + public Authentication extractAuthentication(HttpServletRequest request) { + String accessToken = resolveToken(request); + if(accessToken == null || !validateToken(accessToken)) { + throw new ErrorHandler(ErrorStatus.INVALID_TOKEN); + } + return getAuthentication(accessToken); + } + + + +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/social/CustomOAuth2UserService.java b/junyong/src/main/java/umc/spring/global/common/config/security/social/CustomOAuth2UserService.java new file mode 100644 index 0000000..7bc3617 --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/social/CustomOAuth2UserService.java @@ -0,0 +1,63 @@ +package umc.spring.global.common.config.security.social; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import umc.spring.domain.member.data.Member; +import umc.spring.domain.member.repository.member.MemberRepository; + +import java.util.Collections; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + log.info("thank you"); + + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + // 서비스 구분 + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + + // 로그인 진행 시 키가 되는 필드값 + // 구글은 sub, + // 네이버 카카오 로그인 시 필요 + String userNameAttributeName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + // OAuth2UserService로 가져온 Oauth2User의 attribute를 담은 클래스 + OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); + + // 서비스에 회원가입이나 기존 회원의 정보를 업데이트 한다 + Member member = saveOrUpdate(attributes); + + return new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority(member.getRole().name())), + attributes.getAttributes(), attributes.getNameAttributeKey() + ); + + } + + private Member saveOrUpdate(OAuthAttributes attributes) { + Member member = memberRepository.findByEmail(attributes.getEmail()) + .map(entity -> entity.update(attributes.getName())) + .orElse(attributes.toEntity()); + + return memberRepository.save(member); + } + +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/social/OAuth2SuccessHandler.java b/junyong/src/main/java/umc/spring/global/common/config/security/social/OAuth2SuccessHandler.java new file mode 100644 index 0000000..7b5a3e7 --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/social/OAuth2SuccessHandler.java @@ -0,0 +1,69 @@ +package umc.spring.global.common.config.security.social; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import umc.spring.domain.token.data.RefreshToken; +import umc.spring.domain.token.repository.RefreshTokenRepository; +import umc.spring.global.common.config.security.jwt.JwtTokenProvider; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +@Component +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenRepository refreshTokenRepository; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + log.info("handler"); + + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + log.info("attributes = {}", oAuth2User.getAttributes()); + + String email = oAuth2User.getAttribute("email"); + log.info("email = {}", email); + + String accessToken = jwtTokenProvider.generateToken(email, "USER"); + String refreshToken = jwtTokenProvider.generateRefreshToken(email); + log.info("accessToken = {}", accessToken); + log.info("refreshToken = {}", refreshToken); + + addRefresh(email, refreshToken); + +// String targetUrl = UriComponentsBuilder.fromUriString("/home") +// .queryParam("token", accessToken) +// .build().toUriString(); +// +// getRedirectStrategy().sendRedirect(request, response, targetUrl); + + // JWT 토큰을 헤더에 추가 + response.setHeader("Authorization", "Bearer " + accessToken); + + // 리디렉션 + response.sendRedirect("/home?token=" + accessToken); + + } + + protected void addRefresh(String email, String refresh) { + RefreshToken refreshToken = refreshTokenRepository.findByEmail(email) + .map(token -> token.updateToken(refresh)) + .orElse( + RefreshToken.builder(). + email(email). + refreshToken(refresh). + build() + ); + refreshTokenRepository.save(refreshToken); + } + +} diff --git a/junyong/src/main/java/umc/spring/global/common/config/security/social/OAuthAttributes.java b/junyong/src/main/java/umc/spring/global/common/config/security/social/OAuthAttributes.java new file mode 100644 index 0000000..0b91a1b --- /dev/null +++ b/junyong/src/main/java/umc/spring/global/common/config/security/social/OAuthAttributes.java @@ -0,0 +1,82 @@ +package umc.spring.global.common.config.security.social; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import umc.spring.domain.member.data.Member; +import umc.spring.domain.member.data.enums.Gender; +import umc.spring.domain.member.data.enums.Role; +import umc.spring.global.common.apiPayload.code.status.ErrorStatus; +import umc.spring.global.common.apiPayload.exception.handler.ErrorHandler; + +import java.util.ArrayList; +import java.util.Map; + +@Slf4j +@Getter +public class OAuthAttributes { + + private Map attributes; + private String nameAttributeKey; + private String name; + private String email; + private String pictureURL; + + @Builder + public OAuthAttributes(Map attributes, String nameAttributeKey, + String name, String email, String pictureURL) { + this.attributes = attributes; + this.nameAttributeKey = nameAttributeKey; + this.name = name; + this.email = email; + this.pictureURL = pictureURL; + } + + public static OAuthAttributes of(String registrationId, + String userNameAttributeName, + Map attributes) { + switch (registrationId) { + case "google": + ofGoogle(userNameAttributeName, attributes); + break; + case "kakao": + break; + default: + throw new ErrorHandler(ErrorStatus.INVALID_REGISTRATION_ID); + } + return ofGoogle(userNameAttributeName, attributes); + } + + private static OAuthAttributes ofGoogle(String userNameAttributeName, + Map attributes) { + return OAuthAttributes.builder() + .name((String) attributes.get("name")) + .email((String) attributes.get("email")) + .pictureURL((String) attributes.get("picture")) + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + public Member toEntity() { + return Member.builder() + .name(name) + .email(email) + .role(Role.USER) + .address("") + .specAddress("") + .gender(Gender.NONE) + .birth("") + .phone("") + .locationAgree(false) + .marketingAgree(false) + .password("") + .memberPreferList(new ArrayList<>()) + .eventNoticeList(new ArrayList<>()) + .inquiryNoticeList(new ArrayList<>()) + .reviewNoticeList(new ArrayList<>()) + .memberMissionList(new ArrayList<>()) + .build(); + } + +} diff --git a/junyong/src/main/resources/application.yml b/junyong/src/main/resources/application.yml index cdba2a7..c11dde1 100644 --- a/junyong/src/main/resources/application.yml +++ b/junyong/src/main/resources/application.yml @@ -17,7 +17,45 @@ spring: hbm2ddl: auto: update default_batch_fetch_size: 1000 + security: + oauth2: + client: + registration: + google: + client-id: { CLIENT_ID } + client-secret: { CLIENT_SECRET } + redirect-uri: "http://localhost:8080/login/oauth2/code/google" + scope: + - email + - profile + provider: + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://oauth2.googleapis.com/token + user-info-uri: https://openidconnect.googleapis.com/v1/userinfo + discord: name: discord-feign-client webhook-url: https://discord.com/api/webhooks/1369411881374187633/OSmD5a7C760NaYXpQ0V5b8AGcU6T9viSTt1qdklzBoy7yZeNEg8wAF_f-bieOcxiX_ry + +jwt: + token: + secretKey: { JWT_KEY } + expiration: + access: 3600000 + refresh: 86400000 + +logging: + level: + root: INFO + org.springframework.security: DEBUG + umc.spring: DEBUG + +springdoc: + swagger-ui: + path: /swagger-ui.html + api-docs: + path: /v3/api-docs + default-produces-media-type: application/json + default-consumes-media-type: application/json