diff --git a/week10/week10_mission/java/umc/spring/config/security/CustomOAuth2UserService.java b/week10/week10_mission/java/umc/spring/config/security/CustomOAuth2UserService.java new file mode 100644 index 0000000..ba36cac --- /dev/null +++ b/week10/week10_mission/java/umc/spring/config/security/CustomOAuth2UserService.java @@ -0,0 +1,61 @@ +package umc.spring.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import umc.spring.domain.enums.Role; + +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/week10/week10_mission/java/umc/spring/config/security/SecurityConfig.java b/week10/week10_mission/java/umc/spring/config/security/SecurityConfig.java new file mode 100644 index 0000000..62387d0 --- /dev/null +++ b/week10/week10_mission/java/umc/spring/config/security/SecurityConfig.java @@ -0,0 +1,41 @@ +package umc.spring.config.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/", "/home", "/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(); + } +} \ No newline at end of file diff --git a/week10/week10_mission/java/umc/spring/web/controller/MemberViewController.java b/week10/week10_mission/java/umc/spring/web/controller/MemberViewController.java new file mode 100644 index 0000000..8f4f5ce --- /dev/null +++ b/week10/week10_mission/java/umc/spring/web/controller/MemberViewController.java @@ -0,0 +1,31 @@ +package umc.spring.web.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import umc.spring.web.dto.MemberRequestDTO; + +@Controller +public class MemberViewController { + + @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/week10/week10_mission/java/umc/spring/web/dto/MemberRequestDTO.java b/week10/week10_mission/java/umc/spring/web/dto/MemberRequestDTO.java new file mode 100644 index 0000000..401e95d --- /dev/null +++ b/week10/week10_mission/java/umc/spring/web/dto/MemberRequestDTO.java @@ -0,0 +1,40 @@ +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.Role; + +import java.util.List; + +public class MemberRequestDTO { + @Getter + @Setter // thymeleaf에서 사용하기 위해 추가 + public static class JoinDto { + @NotBlank + String name; + @NotBlank + @Email + String email; // 이메일 필드 추가 + @NotBlank + String password; // 비밀번호 필드 추가 + @NotNull + Integer gender; + @NotNull + Integer birthYear; + @NotNull + Integer birthMonth; + @NotNull + Integer birthDay; + @Size(min = 5, max = 12) + String address; + @Size(min = 5, max = 12) + String specAddress; + List preferCategory; + @NotNull + Role role; // 역할 필드 추가 + } +} diff --git a/week10/week10_mission/resources/templates/admin.html b/week10/week10_mission/resources/templates/admin.html new file mode 100644 index 0000000..55dbff1 --- /dev/null +++ b/week10/week10_mission/resources/templates/admin.html @@ -0,0 +1,10 @@ + + + + Admin Page + + +

Admin Page

+

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

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

Welcome to Home Page!

+

+ + + +
+ +
+ \ No newline at end of file diff --git a/week10/week10_mission/resources/templates/login.html b/week10/week10_mission/resources/templates/login.html new file mode 100644 index 0000000..118cb26 --- /dev/null +++ b/week10/week10_mission/resources/templates/login.html @@ -0,0 +1,27 @@ + + + + Login + + +

Login

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

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

+

로그아웃되었습니다.

+ +

계정이 없나요? Sign up

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

회원가입

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+ +
+ + \ No newline at end of file diff --git a/week10/week10_workbook.md b/week10/week10_workbook.md new file mode 100644 index 0000000..b633924 --- /dev/null +++ b/week10/week10_workbook.md @@ -0,0 +1,397 @@ +## 🎯 핵심 키워드 + + + +
+ +- **Spring Security** + + : 인증, 권한 관리, 데이터 보호 기능을 포함하여 웹 개발 과정에서 필수적인 **사용자 관리 기능**을 구현하는데 도움을 주는 Spring의 강력한 **프레임워크** + + 보안 기능을 추가할 때 Spring Security 사용하는 이유는 Spring Security가 **Spring의 생태계에서 보안에 필요한 기능들을 제공**하기 때문이다. Spring Security는 개발 구조가 Spring이라는 프레임워크 안에서 활용하기 적합한 구조로 설계되어 있어, 보안 기능을 추가할 때 활용하기 좋다. + + 프레임워크를 사용하지 않고 코드를 직접 작성할 경우 Spring에서 추구하는 IoC/DI 패턴과 같은 확장 패턴을 염두 해서 인증/인가 부분을 직접 개발하기는 쉽지 않은데, Spring Security에서는 이와 같은 기능들을 제공해 주기 때문에 개발 작업 효율을 높일 수 있다. + + **→** Spring을 사용할 경우에는 Spring Security를 활용하여 **보안 기능을 추가** + +
+ + https://www.elancer.co.kr/blog/detail/235 + + https://hello-judy-world.tistory.com/216 + +
+ +- **인증(Authentication)과 인가(Authorization)** + + ### 인증 + + - '증명하다'라는 의미로 사용자의 신원을 검증하는 프로세스 + - 유저 아이디와 비밀번호를 이용하여 로그인하는 과정 + + ### 인가 + + - 인증 이후의 프로세스 + - 인증된 사용자가 어떠한 자원에 접근할 수 있는지를 확인하는 절차 + - '권한부여'나 '허가'와 같은 의미로 사용 + - 어떤 대상이 특정 목적을 실현하도록 허용(Access) 하는 것을 의미 + +
+ + https://dev.gmarket.com/45 + +
+ +## 💪 미션 기록 + + + +- **미션 기록** + - java + - config + + ```java + package umc.spring.config.security; + + import lombok.RequiredArgsConstructor; + import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; + import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; + import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + import org.springframework.security.oauth2.core.user.OAuth2User; + import org.springframework.stereotype.Service; + import umc.spring.domain.enums.Role; + + 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); + } + } + ``` + + ```java + package umc.spring.config.security; + + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @EnableWebSecurity + @Configuration + public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/", "/home", "/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(); + } + } + ``` + + - web + + ```java + package umc.spring.web.controller; + + import org.springframework.stereotype.Controller; + import org.springframework.ui.Model; + import org.springframework.web.bind.annotation.GetMapping; + import umc.spring.web.dto.MemberRequestDTO; + + @Controller + public class MemberViewController { + + @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"; + } + } + + ``` + + ```java + 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.Role; + + import java.util.List; + + public class MemberRequestDTO { + @Getter + @Setter // thymeleaf에서 사용하기 위해 추가 + public static class JoinDto { + @NotBlank + String name; + @NotBlank + @Email + String email; // 이메일 필드 추가 + @NotBlank + String password; // 비밀번호 필드 추가 + @NotNull + Integer gender; + @NotNull + Integer birthYear; + @NotNull + Integer birthMonth; + @NotNull + Integer birthDay; + @Size(min = 5, max = 12) + String address; + @Size(min = 5, max = 12) + String specAddress; + List preferCategory; + @NotNull + Role role; // 역할 필드 추가 + } + } + + ``` + + - resources + + ```html + + + + Admin Page + + +

Admin Page

+

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

+ + + ``` + + ```html + + + + Home + + +

Welcome to Home Page!

+

+ + + +
+ +
+ + ``` + + ```html + + + + Login + + +

Login

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

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

+

로그아웃되었습니다.

+ +

계정이 없나요? Sign up

+ + 카카오로 로그인 + + + + ``` + + ```html + + + + 회원가입 + + + +

회원가입

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+ + +
+ +
+ + + ``` + +
+ +> **GitHub 저장소 주소** +> +> +> https://github.com/seoyeoneel02/SpringBoot/issues/19 +> \ No newline at end of file