diff --git a/build.gradle b/build.gradle index 3f9f173..035059c 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,11 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { diff --git a/src/main/generated/umc/spring/domain/Quser.java b/src/main/generated/umc/spring/domain/Quser.java index 7fdc327..d7394d5 100644 --- a/src/main/generated/umc/spring/domain/Quser.java +++ b/src/main/generated/umc/spring/domain/Quser.java @@ -34,10 +34,14 @@ public class Quser extends EntityPathBase { public final StringPath name = createString("name"); + public final StringPath password = createString("password"); + public final StringPath phone_num = createString("phone_num"); public final NumberPath point = createNumber("point", Long.class); + public final EnumPath role = createEnum("role", umc.spring.domain.enums.Role.class); + public final ListPath user_favoriteList = this.createList("user_favoriteList", umc.spring.domain.mapping.user_favorite.class, umc.spring.domain.mapping.Quser_favorite.class, PathInits.DIRECT2); public final ListPath user_missionList = this.createList("user_missionList", umc.spring.domain.mapping.user_mission.class, umc.spring.domain.mapping.Quser_mission.class, PathInits.DIRECT2); 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..2c2c84a --- /dev/null +++ b/src/main/java/umc/spring/config/security/CustomOAuth2UserService.java @@ -0,0 +1,66 @@ +package umc.spring.config.security; + +import jakarta.validation.constraints.Null; +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.enums.Gender; +import umc.spring.domain.enums.Role; +import umc.spring.domain.user; +import umc.spring.repository.UserRepository; + +import java.lang.reflect.Member; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserRepository 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"; // 임시 이메일 생성 + + // 사용자 정보 저장 또는 업데이트 + user 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 user saveOrUpdateUser(String email, String nickname) { + user member = memberRepository.findByEmail(email) + .orElse(user.builder() + .email(email) + .name(nickname) + .password(passwordEncoder.encode("OAUTH_USER_" + UUID.randomUUID())) + .gender(Gender.NULL) // 기본값 설정 + .address("소셜로그인") // 기본값 설정 + .role(Role.USER) + .build()); + + return memberRepository.save(member); + } +} 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..095895b --- /dev/null +++ b/src/main/java/umc/spring/config/security/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +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.user; +import umc.spring.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + user member = memberRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("해당 이메일을 가진 유저가 존재하지 않습니다: " + username)); + + 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..b8f7cfd --- /dev/null +++ b/src/main/java/umc/spring/config/security/SecurityConfig.java @@ -0,0 +1,45 @@ +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/UserConverter.java b/src/main/java/umc/spring/converter/UserConverter.java new file mode 100644 index 0000000..cd273e3 --- /dev/null +++ b/src/main/java/umc/spring/converter/UserConverter.java @@ -0,0 +1,30 @@ +package umc.spring.converter; + +import org.apache.catalina.User; +import umc.spring.domain.enums.Gender; +import umc.spring.domain.user; +import umc.spring.web.dto.UserRequestDTO; + +import java.lang.reflect.Member; + +public class UserConverter { + public static user toMember(UserRequestDTO.JoinDto request) { + Gender gender = null; + switch (request.getGender()) { + case MALE: gender = Gender.MALE; break; + case FEMALE: gender = Gender.FEMALE; break; + } + System.out.println(request.getPassword()); + + return user.builder() + .name(request.getName()) + .email(request.getEmail()) // 추가된 코드 + .password(request.getPassword()) // 추가된 코드 + .gender(gender) + .birth(request.getBirth()) + .address(request.getAddress()) + .role(request.getRole()) // 추가된 코드 + .build(); + } + // ... 기타 메소드들 ... +} \ No newline at end of file diff --git a/src/main/java/umc/spring/domain/enums/Gender.java b/src/main/java/umc/spring/domain/enums/Gender.java index b74c14e..5c46615 100644 --- a/src/main/java/umc/spring/domain/enums/Gender.java +++ b/src/main/java/umc/spring/domain/enums/Gender.java @@ -1,5 +1,5 @@ package umc.spring.domain.enums; public enum Gender { - MALE, FEMALE + MALE, FEMALE, NULL } 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/user.java b/src/main/java/umc/spring/domain/user.java index dfa2ed9..06ecd06 100644 --- a/src/main/java/umc/spring/domain/user.java +++ b/src/main/java/umc/spring/domain/user.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.*; import umc.spring.domain.enums.Gender; +import umc.spring.domain.enums.Role; import umc.spring.domain.mapping.mission; import umc.spring.domain.mapping.user_favorite; import umc.spring.domain.mapping.user_mission; @@ -36,16 +37,22 @@ public class user { @Column(nullable = false, length = 100) private String address; - @Column(nullable = false, length = 30) + @Column(nullable = false, unique = true) private String email; - @Column(nullable = false, length = 20) + @Column(nullable = false,length = 255) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + + @Column(length = 20) private String phone_num; - @Column(nullable = false) + @Column() private Long mission_count; - @Column(nullable = false) + @Column() private Long point; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) @@ -56,4 +63,8 @@ public class user { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List user_reviewList = new ArrayList<>(); + + public void encodePassword(String password) { + this.password = password; + } } diff --git a/src/main/java/umc/spring/repository/UserRepository.java b/src/main/java/umc/spring/repository/UserRepository.java index 99c25e6..19f3c43 100644 --- a/src/main/java/umc/spring/repository/UserRepository.java +++ b/src/main/java/umc/spring/repository/UserRepository.java @@ -4,5 +4,8 @@ import umc.spring.domain.mapping.user_review; import umc.spring.domain.user; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/umc/spring/service/RestaurantService/MemberCommandService.java b/src/main/java/umc/spring/service/RestaurantService/MemberCommandService.java new file mode 100644 index 0000000..726c0e7 --- /dev/null +++ b/src/main/java/umc/spring/service/RestaurantService/MemberCommandService.java @@ -0,0 +1,8 @@ +package umc.spring.service.RestaurantService; + +import umc.spring.domain.user; +import umc.spring.web.dto.UserRequestDTO; + +public interface MemberCommandService { + public user joinMember(UserRequestDTO.JoinDto request); +} diff --git a/src/main/java/umc/spring/service/RestaurantService/MemberCommandServiceImpl.java b/src/main/java/umc/spring/service/RestaurantService/MemberCommandServiceImpl.java new file mode 100644 index 0000000..240d90b --- /dev/null +++ b/src/main/java/umc/spring/service/RestaurantService/MemberCommandServiceImpl.java @@ -0,0 +1,30 @@ +package umc.spring.service.RestaurantService; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.spring.converter.UserConverter; +import umc.spring.converter.UserConverter; +import umc.spring.domain.user; +import umc.spring.repository.UserRepository; +import umc.spring.web.dto.UserRequestDTO; + +@Service +@RequiredArgsConstructor +public class MemberCommandServiceImpl implements MemberCommandService{ + private final PasswordEncoder passwordEncoder; + + private final UserRepository userRepository; + + @Override + @Transactional + public user joinMember(UserRequestDTO.JoinDto request) { + + user newMember = UserConverter.toMember(request); + + newMember.encodePassword(passwordEncoder.encode(request.getPassword())); + + return userRepository.save(newMember); + } +} 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..d4001c9 --- /dev/null +++ b/src/main/java/umc/spring/web/controller/MemberViewController.java @@ -0,0 +1,60 @@ +package umc.spring.web.controller; + +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.RestaurantService.MemberCommandService; +import umc.spring.web.dto.UserRequestDTO; + +@Controller +public class MemberViewController { + MemberCommandService memberCommandService; + + public MemberViewController(MemberCommandService memberCommandService) { + this.memberCommandService = memberCommandService; + } + + @GetMapping("/login") + public String loginPage() { + return "login"; + } + + @GetMapping("/signup") + public String signupPage(Model model) { + model.addAttribute("memberJoinDto", new UserRequestDTO.JoinDto()); + + return "signup"; + } + + @GetMapping("/home") + public String home() { + return "home"; + } + + @GetMapping("/admin") + public String admin() { + return "admin"; + } + + @PostMapping("/members/signup") + public String joinMember(@ModelAttribute("memberJoinDto") UserRequestDTO.JoinDto request, // 협업시에는 기존 RequestBody 어노테이션을 붙여주시면 됩니다! + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + // 뷰에 데이터 바인딩이 실패할 경우 signup 페이지를 유지합니다. + return "signup"; + } + + try { + memberCommandService.joinMember(request); + return "redirect:/login"; + } catch (Exception e) { + // 회원가입 과정에서 에러가 발생할 경우 에러 메시지를 보내고, signup 페이디를 유지합니다. + model.addAttribute("error", e.getMessage()); + return "signup"; + } + } +} diff --git a/src/main/java/umc/spring/web/dto/UserRequestDTO.java b/src/main/java/umc/spring/web/dto/UserRequestDTO.java new file mode 100644 index 0000000..a763720 --- /dev/null +++ b/src/main/java/umc/spring/web/dto/UserRequestDTO.java @@ -0,0 +1,35 @@ +package umc.spring.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.enums.Gender; +import umc.spring.domain.enums.Role; + +import java.time.LocalDate; +import java.util.List; + +public class UserRequestDTO { + @Getter + @Setter // thymeleaf에서 사용하기 위해 추가 + public static class JoinDto { + @NotBlank + String name; + @NotBlank + @Email + String email; // 이메일 필드 추가 + @NotBlank + String password; // 비밀번호 필드 추가 + @NotNull + Gender gender; + @NotNull + LocalDate birth; + @Size(min = 5, max = 12) + String address; + @NotNull + Role role; // 역할 필드 추가 + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4263e6f..54cff8b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,23 @@ 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: 2b6b6767c7ad029ce1297dd574f9ea11 + client-secret: vk20pzhZqelkz90eHsMHhxPtMDQybrtw + 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..920dd71 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,25 @@ + + + + Login + + +

Login

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

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

+

로그아웃되었습니다.

+ +카카오로 로그인 + + + \ 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..133752c --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,50 @@ + + + + 회원가입 + + + +

회원가입

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