diff --git a/build.gradle b/build.gradle index 95db1c1..ed03004 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,18 @@ dependencies { // Testing testImplementation 'org.springframework.book:spring-boo-starter-test' + + // thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' + + // spring security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // oauth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + } sourceSets { diff --git a/src/main/generated/umc/spring/domain/QMember.java b/src/main/generated/umc/spring/domain/QMember.java index ff8e19e..988dfd0 100644 --- a/src/main/generated/umc/spring/domain/QMember.java +++ b/src/main/generated/umc/spring/domain/QMember.java @@ -45,12 +45,16 @@ public class QMember extends EntityPathBase { public final StringPath name = createString("name"); + public final StringPath password = createString("password"); + public final StringPath phoneNumber = createString("phoneNumber"); public final NumberPath point = createNumber("point", Integer.class); public final ListPath reviewList = this.createList("reviewList", Review.class, QReview.class, PathInits.DIRECT2); + public final EnumPath role = createEnum("role", umc.spring.domain.enums.Role.class); + public final EnumPath socialType = createEnum("socialType", umc.spring.domain.enums.SocialType.class); public final StringPath specAddress = createString("specAddress"); diff --git a/src/main/generated/umc/spring/domain/mapping/QMemberMission.java b/src/main/generated/umc/spring/domain/mapping/QMemberMission.java index 101b761..29da571 100644 --- a/src/main/generated/umc/spring/domain/mapping/QMemberMission.java +++ b/src/main/generated/umc/spring/domain/mapping/QMemberMission.java @@ -33,7 +33,7 @@ public class QMemberMission extends EntityPathBase { public final umc.spring.domain.QMission mission; - public final EnumPath status = createEnum("status", MissionStatus.class); + public final EnumPath status = createEnum("status", umc.spring.domain.enums.MissionStatus.class); //inherited public final DateTimePath updatedAt = _super.updatedAt; diff --git a/src/main/java/umc/spring/config/security/CustomOAuth2UserService.java b/src/main/java/umc/spring/config/security/CustomOAuth2UserService.java new file mode 100644 index 0000000..f993fce --- /dev/null +++ b/src/main/java/umc/spring/config/security/CustomOAuth2UserService.java @@ -0,0 +1,65 @@ +package umc.spring.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +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.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import umc.spring.domain.Member; +import umc.spring.domain.enums.Gender; +import umc.spring.domain.enums.Role; +import umc.spring.repository.MemberRepository.MemberRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + Map attributes = oAuth2User.getAttributes(); + Map properties = (Map) attributes.get("properties"); + + String nickname = (String) properties.get("nickname"); + String email = nickname + "@kakao.com"; // 임시 이메일 생성 + + // 사용자 정보 저장 또는 업데이트 + Member member = saveOrUpdateUser(email, nickname); + + // 이메일을 Principal로 사용하기 위해 attributes 수정 + Map modifiedAttributes = new HashMap<>(attributes); + modifiedAttributes.put("email", email); + + return new DefaultOAuth2User( + oAuth2User.getAuthorities(), + modifiedAttributes, + "email" // email Principal로 설정 + ); + } + + private Member saveOrUpdateUser(String email, String nickname) { + Member member = memberRepository.findByEmail(email) + .orElse(Member.builder() + .email(email) + .name(nickname) + .password(passwordEncoder.encode("OAUTH_USER_" + UUID.randomUUID())) + .gender(Gender.NONE) // 기본값 설정 + .address("소셜로그인") // 기본값 설정 + .specAddress("소셜로그인") // 기본값 설정 + .role(Role.USER) + .build()); + + return memberRepository.save(member); + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/config/security/CustomUserDetailsService.java b/src/main/java/umc/spring/config/security/CustomUserDetailsService.java new file mode 100644 index 0000000..838f11e --- /dev/null +++ b/src/main/java/umc/spring/config/security/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package umc.spring.config.security; + +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; +import umc.spring.domain.Member; +import umc.spring.repository.MemberRepository.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("해당 이메일을 가진 유저가 없습니다.")); + + return org.springframework.security.core.userdetails.User + .withUsername(member.getEmail()) + .password(member.getPassword()) + .roles(member.getRole().name()) + .build(); + + } +} diff --git a/src/main/java/umc/spring/config/security/SecurityConfig.java b/src/main/java/umc/spring/config/security/SecurityConfig.java new file mode 100644 index 0000000..336a8b4 --- /dev/null +++ b/src/main/java/umc/spring/config/security/SecurityConfig.java @@ -0,0 +1,46 @@ +package umc.spring.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() + ) + .oauth2Login(oauth2 -> oauth2 + .loginPage("/login") + .defaultSuccessUrl("/home", true) + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/umc/spring/converter/MemberConverter.java b/src/main/java/umc/spring/converter/MemberConverter.java index 269745a..88bc9e2 100644 --- a/src/main/java/umc/spring/converter/MemberConverter.java +++ b/src/main/java/umc/spring/converter/MemberConverter.java @@ -41,10 +41,13 @@ public static Member toMember(MemberRequestDTO.JoinDTO request) { return Member.builder() .address(request.getAddress()) + .email(request.getEmail()) + .password(request.getPassword()) .specAddress(request.getSpecAddress()) .gender(gender) .name(request.getName()) .memberPreferList(new ArrayList<>()) + .role(request.getRole()) .build(); } diff --git a/src/main/java/umc/spring/converter/MemberMissionConverter.java b/src/main/java/umc/spring/converter/MemberMissionConverter.java index 9788afd..4fc7c97 100644 --- a/src/main/java/umc/spring/converter/MemberMissionConverter.java +++ b/src/main/java/umc/spring/converter/MemberMissionConverter.java @@ -3,7 +3,7 @@ import umc.spring.domain.Member; import umc.spring.domain.Mission; import umc.spring.domain.mapping.MemberMission; -import umc.spring.domain.mapping.MissionStatus; +import umc.spring.domain.enums.MissionStatus; import umc.spring.web.dto.MemberMissionDTO.MemberMissionResponseDTO; import java.time.LocalDateTime; diff --git a/src/main/java/umc/spring/converter/StoreConverter.java b/src/main/java/umc/spring/converter/StoreConverter.java index 5b86e9f..11a5ff6 100644 --- a/src/main/java/umc/spring/converter/StoreConverter.java +++ b/src/main/java/umc/spring/converter/StoreConverter.java @@ -50,6 +50,8 @@ public static MissionResponseDTO.MissionPreviewListDTO missionPreviewListDTO(Pag .totalElements(missionList.getTotalElements()) .listSize(missionPreviewDTOList.size()) .missionList(missionPreviewDTOList) + .build(); + } public static ReviewResponseDTO.ReviewPreviewDTO reviewPreviewDTO(Review review) { return ReviewResponseDTO.ReviewPreviewDTO.builder() .ownerNickname(review.getMember().getName()) diff --git a/src/main/java/umc/spring/domain/Member.java b/src/main/java/umc/spring/domain/Member.java index b56cd6c..0eb59cd 100644 --- a/src/main/java/umc/spring/domain/Member.java +++ b/src/main/java/umc/spring/domain/Member.java @@ -8,6 +8,7 @@ import umc.spring.domain.common.BaseEntity; import umc.spring.domain.enums.Gender; import umc.spring.domain.enums.MemberStatus; +import umc.spring.domain.enums.Role; import umc.spring.domain.enums.SocialType; import umc.spring.domain.mapping.MemberAgree; import umc.spring.domain.mapping.MemberMission; @@ -52,7 +53,7 @@ public class Member extends BaseEntity { private LocalDate inactiveDate; -// @Column(nullable = false, length = 50) + @Column(nullable = false, unique = true, length = 50) private String email; @Column(length = 15) @@ -61,6 +62,12 @@ public class Member extends BaseEntity { @ColumnDefault("0") private Integer point; + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List memberAgreeList = new ArrayList<>(); @@ -73,4 +80,8 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List memberMissionList = new ArrayList<>(); + public void encodePassword(String password) { + this.password = password; + } + } diff --git a/src/main/java/umc/spring/domain/mapping/MissionStatus.java b/src/main/java/umc/spring/domain/enums/MissionStatus.java similarity index 61% rename from src/main/java/umc/spring/domain/mapping/MissionStatus.java rename to src/main/java/umc/spring/domain/enums/MissionStatus.java index f090fb4..e6182ee 100644 --- a/src/main/java/umc/spring/domain/mapping/MissionStatus.java +++ b/src/main/java/umc/spring/domain/enums/MissionStatus.java @@ -1,4 +1,4 @@ -package umc.spring.domain.mapping; +package umc.spring.domain.enums; public enum MissionStatus { CHALLENGING, COMPLETE diff --git a/src/main/java/umc/spring/domain/enums/Role.java b/src/main/java/umc/spring/domain/enums/Role.java new file mode 100644 index 0000000..61d3b50 --- /dev/null +++ b/src/main/java/umc/spring/domain/enums/Role.java @@ -0,0 +1,5 @@ +package umc.spring.domain.enums; + +public enum Role { + ADMIN, USER +} diff --git a/src/main/java/umc/spring/domain/mapping/MemberMission.java b/src/main/java/umc/spring/domain/mapping/MemberMission.java index 9b4587f..80469fe 100644 --- a/src/main/java/umc/spring/domain/mapping/MemberMission.java +++ b/src/main/java/umc/spring/domain/mapping/MemberMission.java @@ -2,10 +2,10 @@ import jakarta.persistence.*; import lombok.*; -import org.hibernate.annotations.Fetch; import umc.spring.domain.Member; import umc.spring.domain.Mission; import umc.spring.domain.common.BaseEntity; +import umc.spring.domain.enums.MissionStatus; @Entity @Getter diff --git a/src/main/java/umc/spring/repository/MemberMissionRepository/MemberMissionRepository.java b/src/main/java/umc/spring/repository/MemberMissionRepository/MemberMissionRepository.java index 2d897f7..e127c3b 100644 --- a/src/main/java/umc/spring/repository/MemberMissionRepository/MemberMissionRepository.java +++ b/src/main/java/umc/spring/repository/MemberMissionRepository/MemberMissionRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.repository.query.Param; import umc.spring.domain.Mission; import umc.spring.domain.mapping.MemberMission; -import umc.spring.domain.mapping.MissionStatus; +import umc.spring.domain.enums.MissionStatus; import java.util.Optional; diff --git a/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java b/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java index aa953ac..3f78a20 100644 --- a/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java +++ b/src/main/java/umc/spring/repository/MemberRepository/MemberRepository.java @@ -3,5 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import umc.spring.domain.Member; +import java.util.Optional; + public interface MemberRepository extends JpaRepository , MemberRepositoryCustom { + Optional findByEmail(String email); } diff --git a/src/main/java/umc/spring/service/MemberMissionService/MemberMissionServiceImpl.java b/src/main/java/umc/spring/service/MemberMissionService/MemberMissionServiceImpl.java index 5fcf3a5..4c5291d 100644 --- a/src/main/java/umc/spring/service/MemberMissionService/MemberMissionServiceImpl.java +++ b/src/main/java/umc/spring/service/MemberMissionService/MemberMissionServiceImpl.java @@ -4,7 +4,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import org.springframework.validation.Errors; import umc.spring.apiPayload.code.status.ErrorStatus; import umc.spring.apiPayload.exception.handler.MemberHandler; import umc.spring.apiPayload.exception.handler.MissionHandler; @@ -12,7 +11,7 @@ import umc.spring.domain.Member; import umc.spring.domain.Mission; import umc.spring.domain.mapping.MemberMission; -import umc.spring.domain.mapping.MissionStatus; +import umc.spring.domain.enums.MissionStatus; import umc.spring.repository.MemberMissionRepository.MemberMissionRepository; import umc.spring.repository.MemberRepository.MemberRepository; import umc.spring.repository.MissionRepository.MissionRepository; diff --git a/src/main/java/umc/spring/service/MemberSevice/MemberServiceImpl.java b/src/main/java/umc/spring/service/MemberSevice/MemberServiceImpl.java index baf13f8..05f93ba 100644 --- a/src/main/java/umc/spring/service/MemberSevice/MemberServiceImpl.java +++ b/src/main/java/umc/spring/service/MemberSevice/MemberServiceImpl.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import umc.spring.apiPayload.code.status.ErrorStatus; import umc.spring.apiPayload.exception.handler.FoodCategoryHandler; @@ -28,12 +29,16 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; private final FoodCategoryRepository foodCategoryRepository; private final ReviewRepository reviewRepository; + private final PasswordEncoder passwordEncoder; @Override @Transactional public Member joinMember(MemberRequestDTO.JoinDTO request) { Member newMember = MemberConverter.toMember(request); + + newMember.encodePassword(passwordEncoder.encode(request.getPassword())); + List foodCategoryList = request.getPreferCategory().stream() .map(category -> { return foodCategoryRepository.findById(category).orElseThrow(() -> new FoodCategoryHandler(ErrorStatus.FOOD_CATEGORY_NOT_FOUND)); diff --git a/src/main/java/umc/spring/web/controller/MemberViewController.java b/src/main/java/umc/spring/web/controller/MemberViewController.java new file mode 100644 index 0000000..c02bf75 --- /dev/null +++ b/src/main/java/umc/spring/web/controller/MemberViewController.java @@ -0,0 +1,59 @@ +package umc.spring.web.controller; + +import lombok.RequiredArgsConstructor; +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.service.MemberSevice.MemberServiceImpl; +import umc.spring.web.dto.MemberDTO.MemberRequestDTO; + +@Controller +@RequiredArgsConstructor +public class MemberViewController { + + private final MemberServiceImpl memberService; + + + @PostMapping("/members/signup") + public String joinMember(@ModelAttribute("memberJoinDto") MemberRequestDTO.JoinDTO request, // 협업시에는 기존 RequestBody 어노테이션을 붙여주시면 됩니다! + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + // 뷰에 데이터 바인딩이 실패할 경우 signup 페이지를 유지합니다. + return "signup"; + } + + try { + memberService.joinMember(request); + return "redirect:/login"; + } catch (Exception e) { + // 회원가입 과정에서 에러가 발생할 경우 에러 메시지를 보내고, signup 페이디를 유지합니다. + 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/src/main/java/umc/spring/web/controller/StoreController.java b/src/main/java/umc/spring/web/controller/StoreController.java index 20d89b9..3d57694 100644 --- a/src/main/java/umc/spring/web/controller/StoreController.java +++ b/src/main/java/umc/spring/web/controller/StoreController.java @@ -55,7 +55,7 @@ public ApiResponse join(@RequestBody @Valid Stor @Parameters({ @Parameter(name = "storeId", description = "가게의 아이디, path variable 입니다!") }) - public ApiResponse getReviewList(@ExistStores @PathVariable(name = "storeId") Long storeId, @CheckPage @RequestParam(name = "page") Integer page){ + public ApiResponse getMissionList(@ExistStores @PathVariable(name = "storeId") Long storeId, @CheckPage @RequestParam(name = "page") Integer page){ Page missionList = storeService.getMissionList(storeId, page-1); return ApiResponse.onSuccess(StoreConverter.missionPreviewListDTO(missionList)); } diff --git a/src/main/java/umc/spring/web/dto/MemberDTO/MemberRequestDTO.java b/src/main/java/umc/spring/web/dto/MemberDTO/MemberRequestDTO.java index d5dcf72..b7de8a3 100644 --- a/src/main/java/umc/spring/web/dto/MemberDTO/MemberRequestDTO.java +++ b/src/main/java/umc/spring/web/dto/MemberDTO/MemberRequestDTO.java @@ -1,9 +1,12 @@ package umc.spring.web.dto.MemberDTO; +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.enums.Role; import umc.spring.validation.annotation.ExistCategories; import java.util.List; @@ -11,9 +14,15 @@ public class MemberRequestDTO { @Getter + @Setter public static class JoinDTO { @NotBlank String name; + @NotBlank + @Email + String email; + @NotBlank + String password; @NotNull Integer gender; @NotNull @@ -28,5 +37,7 @@ public static class JoinDTO { String specAddress; @ExistCategories List preferCategory; + @NotNull + Role role; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 60a1ba9..136ddbe 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,22 @@ spring: use_sql_comments: true hbm2ddl: auto: update - default_batch_fetch_size: 1000 \ No newline at end of file + default_batch_fetch_size: 1000 + security: + oauth2: + client: + registration: + kakao: + client-authentication-method: client_secret_post + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + authorization-grant-type: authorization_code + scope: profile_nickname + client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id \ No newline at end of file diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 0000000..55dbff1 --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,10 @@ + + + + Admin Page + + +

Admin Page

+

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

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

Welcome to Home Page!

+

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

Login

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

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

+

로그아웃되었습니다.

+ +

계정이 없나요? Sign up

+ +카카오로 로그인 + + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..ff2a177 --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,71 @@ + + + + 회원가입 + + + +

회원가입

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+ +
+ + \ No newline at end of file