From f58cef6cfaa88ada8af9ac821a0826f6221c8585 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi <79458446+JeongHeumChoi@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:53:08 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20[Deploy]=20-=20Discord=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EB=B0=98=EC=98=81=20(#200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 코드 스타일 수정 (#154) * Chore: 불필요한 부분 삭제 (#157) * 🐛 [Fix] - 작업 목록 API 수정 (#158) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * 🐛 [Fix] - PeerReview 단계 예외처리 (#160) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: discord command 이름 변경 (#163) * Hotfix: 비밀번호 인증 로직 추가 (#165) * 🖊️ [Chore] - README 수정 (#168) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * 🖊️ [Chore] - README 수정 (#169) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * 🐛 [Fix] - 백로그 타임라인 API (#171) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * fix: R&R 정렬 오류 해결 * 🐛 [Fix] - 로그아웃 시 쿠키 삭제 (#174) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * 💅 [Refactor] - 필요 없는 코드 삭제 (#177) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * ✏️ [Chore] - discord command 이름 변경 및 도움말 기능 추가 (#180) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * Chore: 명령어 이름 및 설명란 수정 * Feat: 도움말 명령어 구현 * 🚑 [Hotfix] - 디스코드 명령어 이름 수정에 따른 Discord listener 반영 (#183) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * !HOTFIX: 명령어 이름 수정에 따른 listener 코드 수정 * 🐛 [Fix] - 작업 기록, 질문 목록 sorting 이슈 해결 (#186) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * !HOTFIX: 명령어 이름 수정에 따른 listener 코드 수정 * Fix: 작업 기록 정렬 최신순으로 수정 * Fix: 질문 목록 정렬 최신순으로 수정 * ✨ [Feature] - 디스코드 답변 정보 추가 (#189) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * !HOTFIX: 명령어 이름 수정에 따른 listener 코드 수정 * Feat: 답변 등록에 답변한 사람 정보 추가 및 형식 수정 * 💅 [Refactor] - 디스코드 답변하기 봇 멘트 수정 (#192) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * !HOTFIX: 명령어 이름 수정에 따른 listener 코드 수정 * Refactor: 봇의 답변하기 멘트 수정 * ✏️ [Chore] - DB 정보 수정 (#195) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * !HOTFIX: 명령어 이름 수정에 따른 listener 코드 수정 * Chore: DB 정보 수정 * Fix: 서브모듈 컨플릭트 해결 * Fix: 서브 모듈 컨플릭트 해결 * ✨ [Feature] - Discord 소셜 로그인 구현 (#198) * refactor: 코드 스타일 수정 * fix: Work 생성 날짜 기준으로 내림차순 설정 * fix: PeerReview일 경우 추가 * Chore: 프로젝트 readme 꾸미기 * chore: README 수정 * Fix: 로그아웃 시, 쿠키 삭제 코드 수정 * Refactor: 필요없는 코드 삭제 * !HOTFIX: 명령어 이름 수정에 따른 listener 코드 수정 * Chore: DB 정보 수정 * Fix: JDA 로그 정보 기록 추가 * Chore: Discord 소셜 로그인 Credentials 추가 * Feat: Discord 소셜 로그인 구현 * Feat: Discord 소셜 로그인 구현 * Chore: Discord 소셜 로그인 Credentials 추가 * FIX --------- Co-authored-by: Lim jeong woo --- build.gradle | 1 + .../StartupValleyApplication.java | 5 +- .../team28/startup_valley/domain/User.java | 17 +++-- .../startup_valley/dto/type/EProvider.java | 13 ++++ .../repository/UserRepository.java | 2 +- .../security/config/SecurityConfig.java | 13 ++++ .../handler/login/DefaultSuccessHandler.java | 2 +- .../handler/login/Oauth2FailureHandler.java | 25 ++++++++ .../handler/login/Oauth2SuccessHandler.java | 45 +++++++++++++ .../logout/CustomLogoutProcessHandler.java | 2 +- .../security/info/DiscordOauth2UserInfo.java | 16 +++++ .../security/info/UserPrincipal.java | 26 +++++++- .../security/info/factory/Oauth2UserInfo.java | 13 ++++ .../info/factory/Oauth2UserInfoFactory.java | 20 ++++++ .../CustomOauth2UserDetailService.java | 64 +++++++++++++++++++ 15 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 src/main/java/goormthon/team28/startup_valley/dto/type/EProvider.java create mode 100644 src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2FailureHandler.java create mode 100644 src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2SuccessHandler.java create mode 100644 src/main/java/goormthon/team28/startup_valley/security/info/DiscordOauth2UserInfo.java create mode 100644 src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfo.java create mode 100644 src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfoFactory.java create mode 100644 src/main/java/goormthon/team28/startup_valley/security/service/CustomOauth2UserDetailService.java diff --git a/build.gradle b/build.gradle index a5a7b16..facf6c9 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { // spring security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // spring boot implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/src/main/java/goormthon/team28/startup_valley/StartupValleyApplication.java b/src/main/java/goormthon/team28/startup_valley/StartupValleyApplication.java index abae360..6be46df 100644 --- a/src/main/java/goormthon/team28/startup_valley/StartupValleyApplication.java +++ b/src/main/java/goormthon/team28/startup_valley/StartupValleyApplication.java @@ -4,6 +4,7 @@ import goormthon.team28.startup_valley.discord.listener.DiscordListener; import goormthon.team28.startup_valley.service.*; import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Activity; @@ -18,6 +19,7 @@ import java.util.TimeZone; +@Slf4j @SpringBootApplication public class StartupValleyApplication { @PostConstruct @@ -27,6 +29,7 @@ public void init() { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(StartupValleyApplication.class, args); + log.info("Initializing JDA"); JDA jda = JDABuilder.createDefault(context.getBean(DiscordBotToken.class).getToken()) .enableIntents(GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MEMBERS) .setChunkingFilter(ChunkingFilter.ALL) @@ -45,6 +48,7 @@ public static void main(String[] args) { ) ) .build(); + log.info("Finished Initializing JDA"); jda.updateCommands().addCommands( Commands.slash("1-팀원업데이트", "해당 프로젝트의 팀원들을 모두 웹으로 연동해요."), @@ -74,5 +78,4 @@ public static void main(String[] args) { Commands.slash("도움말", "Startup Valley 프로덕트를 사용하시기 시작한 여러분들을 위한 안내서입니다. 📖🍀") ).queue(); } - } diff --git a/src/main/java/goormthon/team28/startup_valley/domain/User.java b/src/main/java/goormthon/team28/startup_valley/domain/User.java index e112e1a..8c87e5a 100644 --- a/src/main/java/goormthon/team28/startup_valley/domain/User.java +++ b/src/main/java/goormthon/team28/startup_valley/domain/User.java @@ -1,6 +1,7 @@ package goormthon.team28.startup_valley.domain; import goormthon.team28.startup_valley.dto.type.EProfileImage; +import goormthon.team28.startup_valley.dto.type.EProvider; import goormthon.team28.startup_valley.dto.type.ERole; import jakarta.persistence.*; import lombok.AccessLevel; @@ -31,6 +32,9 @@ public class User { @Column(name = "role", nullable = false) @Enumerated(EnumType.STRING) private ERole role; + @Column(name = "provider", nullable = false) + @Enumerated(EnumType.STRING) + private EProvider provider; /* 사용자 이용 정보 */ @Column(name = "nickname", nullable = false) @@ -42,20 +46,25 @@ public class User { private String refreshToken; @Builder - public User(String serialId, String password, ERole role, String nickname, EProfileImage profileImage) { + public User( + String serialId, + String password, + ERole role, + EProfileImage profileImage, + EProvider provider + ) { this.serialId = serialId; this.password = password; this.role = role; - this.nickname = nickname; this.profileImage = profileImage; + this.provider = provider; } public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } - public void updateUserInfo(String nickname, EProfileImage eProfileImage) { - this.nickname = nickname; + public void updateUserInfo(EProfileImage eProfileImage) { this.profileImage = eProfileImage; } } diff --git a/src/main/java/goormthon/team28/startup_valley/dto/type/EProvider.java b/src/main/java/goormthon/team28/startup_valley/dto/type/EProvider.java new file mode 100644 index 0000000..becdcc8 --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/dto/type/EProvider.java @@ -0,0 +1,13 @@ +package goormthon.team28.startup_valley.dto.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EProvider { + + DISCORD("DISCORD"); + + private final String name; +} diff --git a/src/main/java/goormthon/team28/startup_valley/repository/UserRepository.java b/src/main/java/goormthon/team28/startup_valley/repository/UserRepository.java index b467d9e..68b27cd 100644 --- a/src/main/java/goormthon/team28/startup_valley/repository/UserRepository.java +++ b/src/main/java/goormthon/team28/startup_valley/repository/UserRepository.java @@ -17,7 +17,7 @@ public interface UserRepository extends JpaRepository { Optional findByIdAndRefreshToken(Long id, String refreshToken); @Modifying(clearAutomatically = true) @Query("update User u set u.refreshToken = :refreshToken where u.id = :userId") - void updateRefreshTokenAndLoginStatus(Long userId, String refreshToken); + void updateRefreshToken(Long userId, String refreshToken); Optional findById(Long userId); Optional findBySerialId(String serialId); boolean existsBySerialId(String serialId); diff --git a/src/main/java/goormthon/team28/startup_valley/security/config/SecurityConfig.java b/src/main/java/goormthon/team28/startup_valley/security/config/SecurityConfig.java index b0cef64..f221c4c 100644 --- a/src/main/java/goormthon/team28/startup_valley/security/config/SecurityConfig.java +++ b/src/main/java/goormthon/team28/startup_valley/security/config/SecurityConfig.java @@ -6,9 +6,12 @@ import goormthon.team28.startup_valley.security.handler.exception.CustomAuthenticationEntryPointHandler; import goormthon.team28.startup_valley.security.handler.login.DefaultFailureHandler; import goormthon.team28.startup_valley.security.handler.login.DefaultSuccessHandler; +import goormthon.team28.startup_valley.security.handler.login.Oauth2FailureHandler; +import goormthon.team28.startup_valley.security.handler.login.Oauth2SuccessHandler; import goormthon.team28.startup_valley.security.handler.logout.CustomLogoutProcessHandler; import goormthon.team28.startup_valley.security.handler.logout.CustomLogoutResultHandler; import goormthon.team28.startup_valley.security.provider.JwtAuthenticationManager; +import goormthon.team28.startup_valley.security.service.CustomOauth2UserDetailService; import goormthon.team28.startup_valley.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -24,14 +27,19 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { + private final JwtUtil jwtUtil; private final JwtAuthenticationManager jwtAuthenticationManager; private final DefaultSuccessHandler defaultSuccessHandler; private final DefaultFailureHandler defaultFailureHandler; + private final Oauth2SuccessHandler oauth2SuccessHandler; + private final Oauth2FailureHandler oauth2FailureHandler; + private final CustomOauth2UserDetailService customOauth2UserDetailService; private final CustomLogoutProcessHandler customLogoutProcessHandler; private final CustomLogoutResultHandler customLogoutResultHandler; private final CustomAccessDeniedHandler customAccessDeniedHandler; private final CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http @@ -55,6 +63,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .successHandler(defaultSuccessHandler) .failureHandler(defaultFailureHandler) ) + .oauth2Login(login -> login + .successHandler(oauth2SuccessHandler) + .failureHandler(oauth2FailureHandler) + .userInfoEndpoint(it -> it.userService(customOauth2UserDetailService)) + ) .logout(logout -> logout .logoutUrl("/api/users/sign-out") .addLogoutHandler(customLogoutProcessHandler) diff --git a/src/main/java/goormthon/team28/startup_valley/security/handler/login/DefaultSuccessHandler.java b/src/main/java/goormthon/team28/startup_valley/security/handler/login/DefaultSuccessHandler.java index 376b102..a6aadad 100644 --- a/src/main/java/goormthon/team28/startup_valley/security/handler/login/DefaultSuccessHandler.java +++ b/src/main/java/goormthon/team28/startup_valley/security/handler/login/DefaultSuccessHandler.java @@ -30,7 +30,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); JwtTokenDto jwtTokenDto = jwtUtil.generateTokens(userPrincipal.getUserId(), userPrincipal.getRole()); - userRepository.updateRefreshTokenAndLoginStatus(userPrincipal.getUserId(), jwtTokenDto.refreshToken()); + userRepository.updateRefreshToken(userPrincipal.getUserId(), jwtTokenDto.refreshToken()); AuthenticationResponse.makeLoginSuccessResponse(response, domain, jwtTokenDto, jwtUtil.getRefreshExpiration()); } diff --git a/src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2FailureHandler.java b/src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2FailureHandler.java new file mode 100644 index 0000000..4b8a0ac --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2FailureHandler.java @@ -0,0 +1,25 @@ +package goormthon.team28.startup_valley.security.handler.login; + +import goormthon.team28.startup_valley.exception.ErrorCode; +import goormthon.team28.startup_valley.security.info.AuthenticationResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class Oauth2FailureHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException, ServletException { + AuthenticationResponse.makeFailureResponse(response, ErrorCode.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2SuccessHandler.java b/src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2SuccessHandler.java new file mode 100644 index 0000000..c694639 --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/security/handler/login/Oauth2SuccessHandler.java @@ -0,0 +1,45 @@ +package goormthon.team28.startup_valley.security.handler.login; + +import goormthon.team28.startup_valley.dto.response.JwtTokenDto; +import goormthon.team28.startup_valley.repository.UserRepository; +import goormthon.team28.startup_valley.security.info.AuthenticationResponse; +import goormthon.team28.startup_valley.security.info.UserPrincipal; +import goormthon.team28.startup_valley.util.JwtUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class Oauth2SuccessHandler implements AuthenticationSuccessHandler { + + @Value("${server.domain}") + private String domain; + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Override + @Transactional + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + JwtTokenDto jwtTokenDto = jwtUtil.generateTokens(principal.getUserId(), principal.getRole()); + + userRepository.updateRefreshToken(principal.getUserId(), jwtTokenDto.refreshToken()); + + AuthenticationResponse.makeLoginSuccessResponse(response, domain, jwtTokenDto, jwtUtil.getRefreshExpiration()); + + response.sendRedirect("https://" + domain); + } +} diff --git a/src/main/java/goormthon/team28/startup_valley/security/handler/logout/CustomLogoutProcessHandler.java b/src/main/java/goormthon/team28/startup_valley/security/handler/logout/CustomLogoutProcessHandler.java index 6001d9a..1d23d86 100644 --- a/src/main/java/goormthon/team28/startup_valley/security/handler/logout/CustomLogoutProcessHandler.java +++ b/src/main/java/goormthon/team28/startup_valley/security/handler/logout/CustomLogoutProcessHandler.java @@ -24,6 +24,6 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut throw new CommonException(ErrorCode.INVALID_TOKEN_ERROR); } UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); - userRepository.updateRefreshTokenAndLoginStatus(userPrincipal.getUserId(), null); + userRepository.updateRefreshToken(userPrincipal.getUserId(), null); } } diff --git a/src/main/java/goormthon/team28/startup_valley/security/info/DiscordOauth2UserInfo.java b/src/main/java/goormthon/team28/startup_valley/security/info/DiscordOauth2UserInfo.java new file mode 100644 index 0000000..5f83a13 --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/security/info/DiscordOauth2UserInfo.java @@ -0,0 +1,16 @@ +package goormthon.team28.startup_valley.security.info; + +import goormthon.team28.startup_valley.security.info.factory.Oauth2UserInfo; + +import java.util.Map; + +public class DiscordOauth2UserInfo extends Oauth2UserInfo { + public DiscordOauth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return this.attributes.get("id").toString(); + } +} diff --git a/src/main/java/goormthon/team28/startup_valley/security/info/UserPrincipal.java b/src/main/java/goormthon/team28/startup_valley/security/info/UserPrincipal.java index 477b72f..2f9b6e3 100644 --- a/src/main/java/goormthon/team28/startup_valley/security/info/UserPrincipal.java +++ b/src/main/java/goormthon/team28/startup_valley/security/info/UserPrincipal.java @@ -8,18 +8,22 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.Collection; import java.util.Collections; +import java.util.Map; @Getter @Builder @RequiredArgsConstructor -public class UserPrincipal implements UserDetails { +public class UserPrincipal implements UserDetails, OAuth2User { private final Long userId; private final String password; private final ERole role; + private final Map attributes; private final Collection authorities; + public static UserPrincipal create(UserRepository.UserSecurityForm securityForm){ return UserPrincipal.builder() .userId(securityForm.getId()) @@ -29,6 +33,16 @@ public static UserPrincipal create(UserRepository.UserSecurityForm securityForm) .build(); } + public static UserPrincipal create(UserRepository.UserSecurityForm securityForm, Map attributes) { + return UserPrincipal.builder() + .userId(securityForm.getId()) + .password(securityForm.getPassword()) + .role(securityForm.getRole()) + .attributes(attributes) + .authorities(Collections.singleton(new SimpleGrantedAuthority(securityForm.getRole().getSecurityRole()))) + .build(); + } + @Override public Collection getAuthorities() { return this.authorities; @@ -63,4 +77,14 @@ public boolean isCredentialsNonExpired() { public boolean isEnabled() { return true; } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return userId.toString(); + } } diff --git a/src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfo.java b/src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfo.java new file mode 100644 index 0000000..be10233 --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfo.java @@ -0,0 +1,13 @@ +package goormthon.team28.startup_valley.security.info.factory; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public abstract class Oauth2UserInfo { + protected final Map attributes; + public abstract String getId(); +} diff --git a/src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfoFactory.java b/src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfoFactory.java new file mode 100644 index 0000000..228f75e --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/security/info/factory/Oauth2UserInfoFactory.java @@ -0,0 +1,20 @@ +package goormthon.team28.startup_valley.security.info.factory; + +import goormthon.team28.startup_valley.dto.type.EProvider; +import goormthon.team28.startup_valley.security.info.DiscordOauth2UserInfo; + +import java.util.Map; + +public class Oauth2UserInfoFactory { + public static Oauth2UserInfo getOauth2UserInfo( + EProvider provider, + Map attributes + ){ + Oauth2UserInfo ret; + switch (provider) { + case DISCORD -> ret = new DiscordOauth2UserInfo(attributes); + default -> throw new IllegalAccessError("잘못된 제공자 입니다."); + } + return ret; + } +} diff --git a/src/main/java/goormthon/team28/startup_valley/security/service/CustomOauth2UserDetailService.java b/src/main/java/goormthon/team28/startup_valley/security/service/CustomOauth2UserDetailService.java new file mode 100644 index 0000000..1ac506c --- /dev/null +++ b/src/main/java/goormthon/team28/startup_valley/security/service/CustomOauth2UserDetailService.java @@ -0,0 +1,64 @@ +package goormthon.team28.startup_valley.security.service; + +import goormthon.team28.startup_valley.domain.User; +import goormthon.team28.startup_valley.dto.type.EProvider; +import goormthon.team28.startup_valley.dto.type.ERole; +import goormthon.team28.startup_valley.repository.UserRepository; +import goormthon.team28.startup_valley.security.info.UserPrincipal; +import goormthon.team28.startup_valley.security.info.factory.Oauth2UserInfo; +import goormthon.team28.startup_valley.security.info.factory.Oauth2UserInfoFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOauth2UserDetailService extends DefaultOAuth2UserService { + + private final UserRepository userRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // provider 가져오기 + EProvider provider = EProvider.valueOf( + userRequest.getClientRegistration().getRegistrationId().toUpperCase() + ); + log.info("oauth 제공자 정보 가져오기 성공, 제공자 = {}", provider); + // 사용자 정보 가져오기 + Oauth2UserInfo oauth2UserInfo = Oauth2UserInfoFactory + .getOauth2UserInfo(provider, super.loadUser(userRequest).getAttributes()); + log.info("oauth 사용자 정보 가져오기 성공"); + log.info("attributes = {}", oauth2UserInfo.getAttributes().toString()); + + UserRepository.UserSecurityForm securityForm = userRepository + .findUserSecurityFromBySerialId(oauth2UserInfo.getId()) + .orElseGet(() -> { + log.info("새로운 사용자 접근, 저장 로직 진입"); + User newUser = userRepository.save( + User.builder() + .serialId(oauth2UserInfo.getId()) + .password( + bCryptPasswordEncoder + .encode(UUID.randomUUID().toString()) + ) + .provider(provider) + .role(ERole.USER) + .build() + ); + return UserRepository.UserSecurityForm.invoke(newUser); + }); + log.info("oauth2 사용자 조회 성공"); + return UserPrincipal.create(securityForm, oauth2UserInfo.getAttributes()); + } +} \ No newline at end of file