diff --git a/build.gradle b/build.gradle index 3fbd42cb..e9805311 100644 --- a/build.gradle +++ b/build.gradle @@ -100,7 +100,7 @@ dependencies { annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1' - + // mongodb implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' @@ -113,6 +113,9 @@ dependencies { // S3 SDK implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + //AES 암호화 + implementation 'javax.xml.bind:jaxb-api:2.3.1' } tasks.named('test') { diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GithubOAuth2Response.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GithubOAuth2Response.java new file mode 100644 index 00000000..bf188fd4 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GithubOAuth2Response.java @@ -0,0 +1,54 @@ +package org.ezcode.codetest.application.usermanagement.user.dto.response; + +import java.util.Map; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Github OAuth2 응답 처리 클래스") +public class GithubOAuth2Response implements OAuth2Response{ + private final Map attributes; + + public GithubOAuth2Response(Map attributes) { + this.attributes = attributes; + } + + @Override + @Schema(description = "OAuth 제공자 이름", example = "github") + public String getProvider() { + return "github"; + } + + @Override + @Schema(description = "제공자 내부 식별자", example = "109000123456789012345") + public String getProviderId() { + return attributes.get("id").toString(); + } + + @Override + @Schema(description = "사용자 이메일", example = "user@gmail.com") + public String getEmail() { + if (attributes.get("email") == null && getProvider().equals("github")) { + return getGithubId()+"@github.com"; + } else { + return (String) attributes.get("email"); + } + } + + @Override + @Schema(description = "사용자 이름", example = "홍길동") + public String getName() { + return attributes.get("name").toString(); + } + + @Override + @Schema(description = "Github Id", example = "1345932") + public String getGithubId() { + return attributes.get("id").toString(); + } + + @Override + @Schema(description = "Github URL", example = "https://github.com/id") + public String getGithubUrl(){ + return attributes.get("html_url").toString(); + } +} diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GoogleOAuth2Response.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GoogleOAuth2Response.java index 6cc6884f..dc61936b 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GoogleOAuth2Response.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/GoogleOAuth2Response.java @@ -38,4 +38,15 @@ public String getName() { return attributes.get("name").toString(); } + @Override + public String getGithubId() { + return ""; + } + + @Override + public String getGithubUrl() { + return ""; + } + + } diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/OAuth2Response.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/OAuth2Response.java index b97ed7d3..619b895b 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/OAuth2Response.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/dto/response/OAuth2Response.java @@ -12,4 +12,10 @@ public interface OAuth2Response { //사용자 이름 String getName(); + + //깃헙 아이디 + String getGithubId(); + + //깃헙 링크 + String getGithubUrl(); } diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java index c073235c..a0a9010d 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java @@ -35,7 +35,6 @@ public class UserService { private final UserDomainService userDomainService; private final SubmissionDomainService submissionDomainService; private final RedisTemplate redisTemplate; - private final MailService mailService; @Transactional(readOnly = true) public UserInfoResponse getUserInfo(AuthUser authUser) { diff --git a/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java b/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java index 7338eea9..924e4a0e 100644 --- a/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java +++ b/src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java @@ -55,9 +55,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .httpBasic(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) .oauth2Login((outh2)-> outh2 - .userInfoEndpoint((userInfoEndpointConfig -> - userInfoEndpointConfig.userService(customOAuth2UserService))) + .userInfoEndpoint((userInfo -> userInfo + .userService(customOAuth2UserService))) .successHandler(customSuccessHandler)) + // JWT 사용을 위해 세션을 STATELESS로 설정 (세션 정보 저장 x) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) diff --git a/src/main/java/org/ezcode/codetest/common/security/hander/CustomSuccessHandler.java b/src/main/java/org/ezcode/codetest/common/security/hander/CustomSuccessHandler.java index ff3f0080..3f95a101 100644 --- a/src/main/java/org/ezcode/codetest/common/security/hander/CustomSuccessHandler.java +++ b/src/main/java/org/ezcode/codetest/common/security/hander/CustomSuccessHandler.java @@ -4,14 +4,21 @@ import java.util.concurrent.TimeUnit; import org.ezcode.codetest.application.usermanagement.auth.dto.response.OAuthResponse; +import org.ezcode.codetest.common.security.util.AESUtil; +import org.ezcode.codetest.domain.user.exception.AuthException; +import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode; import org.ezcode.codetest.domain.user.model.entity.CustomOAuth2User; import org.ezcode.codetest.domain.user.model.entity.User; import org.ezcode.codetest.domain.user.service.UserDomainService; import org.ezcode.codetest.common.security.util.JwtUtil; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,14 +34,19 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler private final UserDomainService userDomainService; private final RedisTemplate redisTemplate; private final ObjectMapper objectMapper; //json직렬화 + private final OAuth2AuthorizedClientService authorizedClientService; + private final AESUtil aesUtil; public CustomSuccessHandler(JwtUtil jwtUtil, UserDomainService userDomainService, - RedisTemplate redisTemplate, ObjectMapper objectMapper) { + RedisTemplate redisTemplate, ObjectMapper objectMapper, + OAuth2AuthorizedClientService authorizedClientService, AESUtil aesUtil) { this.jwtUtil = jwtUtil; this.userDomainService = userDomainService; this.redisTemplate = redisTemplate; this.objectMapper = objectMapper; - } + this.authorizedClientService = authorizedClientService; + this.aesUtil = aesUtil; + } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws @@ -43,8 +55,27 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo //OAuth2User CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal(); - User loginUser = userDomainService.getUserByEmail(customUserDetails.getEmail()); - log.info("loginUser: {}", loginUser); + User loginUser= userDomainService.getUserByEmail(customUserDetails.getEmail()); + log.info("loginUser Name: {}", loginUser.getUsername()); + + if (customUserDetails.getProvider().equalsIgnoreCase("github")) { + //깃허브 access-token 가져오기 + OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; + OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient( + oauthToken.getAuthorizedClientRegistrationId(), + oauthToken.getName() + ); + + //AES 암호화 + try { + String encodedGithubToken = aesUtil.encrypt(client.getAccessToken().getTokenValue()); + loginUser.setGithubAccessToken(encodedGithubToken); + } catch (Exception e) { + log.error(e.getMessage()); + throw new AuthException(AuthExceptionCode.TOKEN_ENCODE_FAIL); + } + userDomainService.updateUserGithubAccessToken(loginUser); + } String accessToken = jwtUtil.createAccessToken( loginUser.getId(), @@ -64,14 +95,27 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo refreshToken, jwtUtil.getExpiration(refreshToken), TimeUnit.MILLISECONDS); - log.info("레디스에 저장완료"); + + String redirectUri = (String) request.getSession().getAttribute("redirect_uri"); + + if (redirectUri == null) { + throw new AuthException(AuthExceptionCode.REDIRECT_URI_NOT_FOUND); + } + + request.getSession().removeAttribute("redirect_uri"); // uri 사용 후 제거하기 + + String targetUri = UriComponentsBuilder.fromUriString(redirectUri) + .queryParam("accessToken", accessToken) + .queryParam("refreshToken", refreshToken) + .build().toUriString(); //JSON 문자열로 바꿔서 클라이언트에게 응답 본문으로 전달 OAuthResponse oAuthResponse = new OAuthResponse(accessToken, refreshToken); + response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(oAuthResponse)); - log.info("------------- accessToken : {}, refreshToken : {} ------------", accessToken, refreshToken); - } + // response.sendRedirect(targetUri); + } } \ No newline at end of file diff --git a/src/main/java/org/ezcode/codetest/common/security/util/AESUtil.java b/src/main/java/org/ezcode/codetest/common/security/util/AESUtil.java new file mode 100644 index 00000000..dea09a6d --- /dev/null +++ b/src/main/java/org/ezcode/codetest/common/security/util/AESUtil.java @@ -0,0 +1,34 @@ +package org.ezcode.codetest.common.security.util; + +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class AESUtil { + + private static final String ALGORITHM = "AES"; + @Value("${aes.secret.key}") + private String SECRET_KEY; // 반드시 16, 24, 또는 32바이트 + + public String encrypt(String input) throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, keySpec); + byte[] encryptedBytes = cipher.doFinal(input.getBytes()); + return Base64.getEncoder().encodeToString(encryptedBytes); + } + + public String decrypt(String encryptedInput) throws Exception { + SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, keySpec); + byte[] encryptedBytes = Base64.getDecoder().decode(encryptedInput); + byte[] decryptedBytes = cipher.doFinal(encryptedBytes); + return new String(decryptedBytes); + } +} diff --git a/src/main/java/org/ezcode/codetest/common/security/util/JwtFilter.java b/src/main/java/org/ezcode/codetest/common/security/util/JwtFilter.java index 349a2264..736b1034 100644 --- a/src/main/java/org/ezcode/codetest/common/security/util/JwtFilter.java +++ b/src/main/java/org/ezcode/codetest/common/security/util/JwtFilter.java @@ -36,13 +36,6 @@ public class JwtFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - // String requestURI = request.getRequestURI(); - // - // //whiteList 등록 - // if (shouldNotFilter(requestURI)) { - // filterChain.doFilter(request, response); - // return; - // } String bearerToken = request.getHeader("Authorization"); diff --git a/src/main/java/org/ezcode/codetest/common/security/util/SecurityPath.java b/src/main/java/org/ezcode/codetest/common/security/util/SecurityPath.java index 4698ee2b..c4e413f3 100644 --- a/src/main/java/org/ezcode/codetest/common/security/util/SecurityPath.java +++ b/src/main/java/org/ezcode/codetest/common/security/util/SecurityPath.java @@ -5,13 +5,14 @@ @Component public class SecurityPath { public static final String[] PUBLIC_PATH = { + "/login/oauth2/**", //OAuth로그인 접근 "/api/auth/**", + "/api/oauth2/**", "/login", "/ezlogin", "/login/**", "/oauth2/**", "/login/oauth", - "/login/oauth2/**", //OAuth로그인 접근 "/actuator/**", "/chatting", "/submit-test", diff --git a/src/main/java/org/ezcode/codetest/domain/user/exception/code/AuthExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/user/exception/code/AuthExceptionCode.java index bb3f7d42..e58718ef 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/exception/code/AuthExceptionCode.java +++ b/src/main/java/org/ezcode/codetest/domain/user/exception/code/AuthExceptionCode.java @@ -18,9 +18,9 @@ public enum AuthExceptionCode implements ResponseCode { ALREADY_EXIST_USER(false, HttpStatus.BAD_REQUEST, "이미 가입된 유저입니다."), NOT_EMAIL_USER(false, HttpStatus.BAD_REQUEST, "소셜 로그인 회원은 비밀번호 변경을 할 수 없습니다."), PASSWORD_IS_SAME(false, HttpStatus.BAD_REQUEST, "기존 비밀번호와 같습니다. 새로운 비밀번호는 기존 비밀번호와 달라야합니다."), - ALREADY_WITHDRAW_USER(false, HttpStatus.NOT_FOUND, "탈퇴된 회원입니다.") - - ; + ALREADY_WITHDRAW_USER(false, HttpStatus.NOT_FOUND, "탈퇴된 회원입니다."), + TOKEN_ENCODE_FAIL(false, HttpStatus.BAD_REQUEST, "토큰 인코딩에 실패했습니다."), + REDIRECT_URI_NOT_FOUND(false, HttpStatus.BAD_REQUEST, "redirect_uri를 찾을 수 없습니다"); private final boolean success; private final HttpStatus status; diff --git a/src/main/java/org/ezcode/codetest/domain/user/model/entity/CustomOAuth2User.java b/src/main/java/org/ezcode/codetest/domain/user/model/entity/CustomOAuth2User.java index 472380cd..854e37c1 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/model/entity/CustomOAuth2User.java +++ b/src/main/java/org/ezcode/codetest/domain/user/model/entity/CustomOAuth2User.java @@ -30,7 +30,11 @@ public String getName() { } public String getEmail() { - return oAuth2Response.getEmail(); + if (oAuth2Response.getEmail() == null) { + return oAuth2Response.getGithubId()+"@github.com"; + } else { + return oAuth2Response.getEmail(); + } } public String getProvider(){ @@ -40,4 +44,5 @@ public String getProvider(){ public String getUsername() { return oAuth2Response.getProvider()+" "+oAuth2Response.getProviderId(); } + } diff --git a/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java b/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java index 104ebbeb..ba4ec3c2 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java +++ b/src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java @@ -34,7 +34,6 @@ public class User extends BaseEntity { @Column(nullable = false) private String username; - @Column(nullable = false) private String email; @Column(nullable = false) @@ -73,6 +72,7 @@ public class User extends BaseEntity { private boolean verified; //이메일 인증 여부 + private String githubAccessToken; //깃허브 access_token 값 /* @@ -96,7 +96,7 @@ public static User emailUser(String email, String password, String username, Str /* OAuth2로 로그인한 유저 저장 - 구글 & 깃허브 모두 하나의 소셜 유저로 입력 받기 -> AuthType 테이블에서만 구분됨 (GOOGLE, GITHUB) + 구글 이외의 다른 소셜 로그인 확장 가능성을 고려해 socialUser 이름 유지 */ public static User socialUser(String email, String username, String nickname, String password){ return User.builder() @@ -111,10 +111,25 @@ public static User socialUser(String email, String username, String nickname, St .build(); } + //깃허브 아이디와 url을 함께 저장하기 위해 따로 저장 + public static User githubUser(String email, String username, String nickname, String password, String githubUrl){ + return User.builder() + .email(email) + .username(username) + .role(UserRole.USER) + .tier(Tier.NEWBIE) + .nickname(nickname) //닉네임 자동 생성 + .password(password) + .isDeleted(false) + .verified(false) + .githubUrl(githubUrl) + .build(); + } + @Builder public User(String email, String password, String username, String nickname, - Integer age, Tier tier, UserRole role, boolean isDeleted, boolean verified) { + Integer age, Tier tier, UserRole role, boolean isDeleted, boolean verified, String githubUrl) { this.email = email; this.password = password; this.username = username; @@ -124,6 +139,7 @@ public User(String email, String password, String username, String nickname, this.role = role; this.isDeleted = isDeleted; this.verified = verified; + this.githubUrl = githubUrl; } /* @@ -157,6 +173,18 @@ public void setVerified(){ this.verified = true; } + public void setReviewToken(int reviewToken){ + this.reviewToken = reviewToken; + } + + public void setGithubUrl(String githubUrl){ + this.githubUrl = githubUrl; + } + + public void setGithubAccessToken(String githubAccessToken){ + this.githubAccessToken = githubAccessToken; + } + public void decreaseReviewToken() { this.reviewToken -= 1; } diff --git a/src/main/java/org/ezcode/codetest/domain/user/model/entity/UserFactory.java b/src/main/java/org/ezcode/codetest/domain/user/model/entity/UserFactory.java new file mode 100644 index 00000000..68d84c48 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/user/model/entity/UserFactory.java @@ -0,0 +1,31 @@ +package org.ezcode.codetest.domain.user.model.entity; + +import java.util.UUID; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.OAuth2Response; + +public class UserFactory { + public static User createSocialUser( + OAuth2Response response, + String nickname, + String provider + ) { + //나중에 확장성 고려해서 switch문 사용 + return switch (provider.toLowerCase()) { + case "github" -> User.githubUser( + response.getEmail(), + response.getName(), + nickname, + UUID.randomUUID().toString(), + response.getGithubUrl() + ); + default -> User.socialUser( + response.getEmail(), + response.getName(), + nickname, + UUID.randomUUID().toString() + ); + }; + } +} + diff --git a/src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java b/src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java index 99f1905d..590629ca 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java @@ -19,4 +19,6 @@ public interface UserRepository { void decreaseReviewToken(User user); void updateReviewTokens(List ids , int newToken); + + void updateUserGithubAccessToken(User loginUser); } diff --git a/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java b/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java index bec3abf3..5825d785 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java +++ b/src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java @@ -1,13 +1,14 @@ package org.ezcode.codetest.domain.user.service; -import java.util.List; -import java.util.UUID; +import java.util.Map; +import org.ezcode.codetest.application.usermanagement.user.dto.response.GithubOAuth2Response; import org.ezcode.codetest.application.usermanagement.user.dto.response.GoogleOAuth2Response; import org.ezcode.codetest.application.usermanagement.user.dto.response.OAuth2Response; import org.ezcode.codetest.domain.user.model.entity.CustomOAuth2User; import org.ezcode.codetest.domain.user.model.entity.User; import org.ezcode.codetest.domain.user.model.entity.UserAuthType; +import org.ezcode.codetest.domain.user.model.entity.UserFactory; import org.ezcode.codetest.domain.user.model.enums.AuthType; import org.ezcode.codetest.domain.user.repository.UserAuthTypeRepository; import org.ezcode.codetest.domain.user.repository.UserRepository; @@ -35,54 +36,68 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService { public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { //부모 클래스에 존재하는 loadUser 메서드에 userRequest인자를 넣어 유저 정보를 가져온다 OAuth2User oAuth2User = super.loadUser(userRequest); - log.info("loadUser: {}", oAuth2User); + + log.info("loadUser : {}", oAuth2User); //가져온 인증 정보가 구글인지, 깃헙인지 알아내기 위한 변수 String registrationId = userRequest.getClientRegistration().getRegistrationId(); - OAuth2Response oAuth2Response = null; + OAuth2Response oAuth2Response = createResponse(registrationId, oAuth2User.getAttributes()); - //인증 정보를 제공하는 플랫폼마다 형식이 다르기 때문에 이에 맞게 가져와야한다. - if (registrationId.equals("google")) { - //인증 정보가 구글일 때 - //구글 dto구현체에서 정보 가져오기 - oAuth2Response = new GoogleOAuth2Response(oAuth2User.getAttributes()); - } else { - // if (registrationId.equals("github")) { - // //인증 정보가 github 일 때 - // } - return null; - } + String resolvedEmail = oAuth2Response.getEmail(); - String username = oAuth2Response.getName(); - - User findUser = userRepository.getUserByEmail(oAuth2Response.getEmail()); - - if (findUser == null) { - String nickname = userDomainService.generateUniqueNickname(); - //아예 처음 가입을 소셜로하는 회원이면, 비번을 UUID로 설정 - User newUser = User.socialUser(oAuth2Response.getEmail(), username, nickname, UUID.randomUUID().toString()); - log.info("newUser: {} 새로운 유저", newUser); - try { - userRepository.createUser(newUser); - UserAuthType userAuthType = new UserAuthType(newUser, AuthType.GOOGLE); - userAuthTypeRepository.createUserAuthType(userAuthType); - } catch (IllegalStateException e) { - throw new OAuth2AuthenticationException("닉네임 생성 실패입니다"); - } catch (Exception e) { - log.error("OAuth 사용자 생성 실패 : {}", e.getMessage()); - throw new OAuth2AuthenticationException("사용자 생성 실패입니다"); - } + User findUser = userDomainService.getUserByEmail(resolvedEmail); + AuthType authType = AuthType.from(oAuth2Response.getProvider().toUpperCase()); + + processUser(findUser, oAuth2Response, authType, registrationId); + return new CustomOAuth2User(oAuth2Response); + + } + + private OAuth2Response createResponse(String provider, Map attributes) { + return switch (provider.toLowerCase()) { + case "google" -> new GoogleOAuth2Response(attributes); + case "github" -> new GithubOAuth2Response(attributes); + default -> throw new OAuth2AuthenticationException("Unsupported provider"); + }; + } + //신규인지 존재하는 유저인지 + private void processUser( + User user, + OAuth2Response response, + AuthType authType, + String provider + ) { + if (user == null) { + createNewUser(response, authType, provider); } else { - if (!userDomainService.getUserAuthTypes(findUser).contains(AuthType.GOOGLE)) { - UserAuthType userAuthType = new UserAuthType(findUser, AuthType.GOOGLE); - userAuthTypeRepository.createUserAuthType(userAuthType); - } else { - log.info("이메일, 소셜 모두 가입 유저"); - } + updateExistingUser(user, response, authType, provider); } + } + private void createNewUser(OAuth2Response response, AuthType authType, String provider) { + String nickname = userDomainService.generateUniqueNickname(); + User newUser = UserFactory.createSocialUser(response, nickname, provider); + newUser.setVerified(); + newUser.setReviewToken(20); + userRepository.createUser(newUser); + userAuthTypeRepository.createUserAuthType(new UserAuthType(newUser, authType)); + } - return new CustomOAuth2User(oAuth2Response); + private void updateExistingUser(User user, OAuth2Response response, AuthType authType, String provider) { + if (!userDomainService.getUserAuthTypes(user).contains(authType)) { + if (!user.isVerified()) { + user.setVerified(); + user.setReviewToken(20); + } + userAuthTypeRepository.createUserAuthType(new UserAuthType(user, authType)); + updateGithubUrl(user, response, provider); + } + } + private void updateGithubUrl(User user, OAuth2Response response, String provider) { + if ("github".equals(provider) && user.getGithubUrl()==null) { + user.setGithubUrl(response.getGithubUrl()); + } } + } diff --git a/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java b/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java index f66fe95c..95698f78 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java @@ -127,4 +127,9 @@ private static String generateRandomNickname() { public User getUserByEmail(String email) { return userRepository.getUserByEmail(email); } + + public void updateUserGithubAccessToken(User loginUser) { + log.info("Updating github access token for user: {}", loginUser); + userRepository.updateUserGithubAccessToken(loginUser); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserRepositoryImpl.java index 2ce8b74d..8976fd69 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserRepositoryImpl.java @@ -47,4 +47,9 @@ public void updateReviewTokens(List ids, int newToken) { userJpaRepository.updateReviewTokens(ids, newToken); } + @Override + public void updateUserGithubAccessToken(User loginUser) { + userJpaRepository.save(loginUser); + } + } diff --git a/src/main/java/org/ezcode/codetest/presentation/usermanagement/OAuth2Controller.java b/src/main/java/org/ezcode/codetest/presentation/usermanagement/OAuth2Controller.java new file mode 100644 index 00000000..b89c8361 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/presentation/usermanagement/OAuth2Controller.java @@ -0,0 +1,48 @@ +package org.ezcode.codetest.presentation.usermanagement; + +import java.io.IOException; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/oauth2") +@Tag(name = "OAuth2", description = "OAuth2 인증 관련 API") +public class OAuth2Controller { + + @Operation(summary = "GitHub & Google OAuth2 로그인 API", + description = """ + 프론트엔드에서 GitHub 로그인 버튼 클릭 시 이 API를 먼저 호출 + redirect_uri는 로그인 완료 후 accessToken과 refreshToken을 전달받을 프론트의 콜백 URL + + 예시: GET /api/oauth2/authorize/github?redirect_uri=https://ezcode.my/oauth/callback + """) + @Parameters({ + @Parameter(name = "redirect_uri", description = "프론트 콜백 URI", required = true, example = "https://ezcode.my/oauth/callback (이 uri는 예시이니 편하신걸로 바꾸심 됩니당)") + }) + @GetMapping("/authorize/{provider}") + public void redirectToProvider( + @PathVariable String provider, + HttpServletRequest request, + HttpServletResponse response, + @RequestParam(required = false) String redirect_uri + ) throws IOException { + if (redirect_uri != null) { + request.getSession().setAttribute("redirect_uri", redirect_uri); + } + + response.sendRedirect("/oauth2/authorization/" + provider); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a86530ae..04adfe72 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -78,9 +78,9 @@ openai.api.key=${OPEN_API_KEY} # OAuth GOOGLE # ======================== spring.security.oauth2.client.registration.google.client-name=google -spring.security.oauth2.client.registration.google.client-id=${CLIENT_ID} -spring.security.oauth2.client.registration.google.client-secret=${CLIENT_SECRET} -spring.security.oauth2.client.registration.google.redirect-uri=${REDIRECT_URI} +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} +spring.security.oauth2.client.registration.google.redirect-uri=${GOOGLE_REDIRECT_URI} spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.google.scope=profile,email @@ -116,6 +116,22 @@ spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.timeout=5000 spring.mail.properties.mail.smtp.starttls.enable=true +# ======================== +# OAuth GITHUB +# ======================== +spring.security.oauth2.client.registration.github.client-name=github +spring.security.oauth2.client.registration.github.client-id=${CLIENT_ID_GITHUB} +spring.security.oauth2.client.registration.github.client-secret=${CLIENT_SECRET_GITHUB} +spring.security.oauth2.client.registration.github.redirect-uri=${REDIRECT_URI_GITHUB} +spring.security.oauth2.client.registration.github.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.github.scope=user,repo + +#logging.level.org.springframework.security=DEBUG +#logging.level.org.springframework.security.oauth2=DEBUG +#logging.level.org.springframework.security.oauth2.client=TRACE +#logging.level.org.springframework.web.client.RestTemplate=TRACE + + # ======================== # S3 # ======================== @@ -126,7 +142,7 @@ cloud.aws.s3.bucket=ezcode-s3 cloud.aws.region.static=ap-northeast-2 cloud.aws.stack.auto=false -#logging.level.org.springframework.security=DEBUG -#logging.level.org.springframework.security.oauth2=DEBUG -#logging.level.org.springframework.security.oauth2.client=TRACE -#logging.level.org.springframework.web.client.RestTemplate=TRACE +# ======================== +# AES +# ======================== +aes.secret.key=${AES_SECRET_KEY} \ No newline at end of file