diff --git a/src/main/java/com/software/ott/OttApplication.java b/src/main/java/com/software/ott/OttApplication.java index 17286a3..ed61fcc 100644 --- a/src/main/java/com/software/ott/OttApplication.java +++ b/src/main/java/com/software/ott/OttApplication.java @@ -1,6 +1,7 @@ package com.software.ott; import com.software.ott.common.properties.CommonProperties; +import com.software.ott.common.properties.GoogleProperties; import com.software.ott.common.properties.KakaoProperties; import com.software.ott.common.properties.NaverProperties; import org.springframework.boot.SpringApplication; @@ -10,7 +11,7 @@ @SpringBootApplication @EnableJpaAuditing -@EnableConfigurationProperties({KakaoProperties.class, CommonProperties.class, NaverProperties.class}) +@EnableConfigurationProperties({KakaoProperties.class, CommonProperties.class, NaverProperties.class, GoogleProperties.class}) public class OttApplication { public static void main(String[] args) { diff --git a/src/main/java/com/software/ott/auth/controller/AuthController.java b/src/main/java/com/software/ott/auth/controller/AuthController.java index 500e3fa..754ac50 100644 --- a/src/main/java/com/software/ott/auth/controller/AuthController.java +++ b/src/main/java/com/software/ott/auth/controller/AuthController.java @@ -3,10 +3,7 @@ import com.software.ott.auth.dto.TokenRefreshRequest; import com.software.ott.auth.dto.TokenResponse; -import com.software.ott.auth.service.KakaoApiService; -import com.software.ott.auth.service.NaverApiService; -import com.software.ott.auth.service.PhoneNumberAuthService; -import com.software.ott.auth.service.TokenService; +import com.software.ott.auth.service.*; import com.software.ott.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -30,13 +27,15 @@ public class AuthController { private final MemberService memberService; private final PhoneNumberAuthService phoneNumberAuthService; private final NaverApiService naverApiService; + private final GoogleApiService googleApiService; - public AuthController(TokenService tokenService, KakaoApiService kakaoApiService, MemberService memberService, PhoneNumberAuthService phoneNumberAuthService, NaverApiService naverApiService) { + public AuthController(TokenService tokenService, KakaoApiService kakaoApiService, MemberService memberService, PhoneNumberAuthService phoneNumberAuthService, NaverApiService naverApiService, GoogleApiService googleApiService) { this.tokenService = tokenService; this.kakaoApiService = kakaoApiService; this.memberService = memberService; this.phoneNumberAuthService = phoneNumberAuthService; this.naverApiService = naverApiService; + this.googleApiService = googleApiService; } @Operation(summary = "토큰 재발급", description = "RefreshToken으로 AccessToken과 RefreshToken을 재발급 한다.", security = @SecurityRequirement(name = "JWT제외")) @@ -78,6 +77,22 @@ public ResponseEntity naverCallback(@RequestParam("code") String return ResponseEntity.ok().body(loginResponse); } + @Operation(summary = "Oauth 구글 인증페이지 리다이렉트", description = "구글 로그인 화면으로 이동한다.", security = @SecurityRequirement(name = "JWT제외")) + @GetMapping("/auth/oauth/google") + public ResponseEntity redirectToGoogleAuth(HttpServletRequest httpServletRequest) { + String url = googleApiService.getGoogleLoginUrl(httpServletRequest); + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(URI.create(url)); + return new ResponseEntity<>(headers, HttpStatus.FOUND); + } + + @Operation(summary = "Oauth 구글 로그인 콜백", description = "구글 로그인 이후 발생하는 인가코드를 통해 AccessToken을 발급받고 사용자 정보를 조회한다.", security = @SecurityRequirement(name = "JWT제외")) + @GetMapping("/auth/oauth/google/callback") + public ResponseEntity googleCallback(@RequestParam("code") String code, HttpServletRequest httpServletRequest) { + TokenResponse loginResponse = memberService.googleLogin(code, httpServletRequest); + return ResponseEntity.ok().body(loginResponse); + } + @Operation(summary = "전화번호 인증용 qr 생성", description = "사용자 전화번호 인증용 qr을 생성합니다.") @GetMapping("/qr") public ResponseEntity generateQRCode(@RequestAttribute("memberId") Long memberId, @RequestParam String phoneNumber) { diff --git a/src/main/java/com/software/ott/auth/dto/GoogleTokenResponse.java b/src/main/java/com/software/ott/auth/dto/GoogleTokenResponse.java new file mode 100644 index 0000000..6de1908 --- /dev/null +++ b/src/main/java/com/software/ott/auth/dto/GoogleTokenResponse.java @@ -0,0 +1,15 @@ +package com.software.ott.auth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record GoogleTokenResponse( + String accessToken, + int expiresIn, + String refreshToken, + String scope, + String tokenType, + String idToken +) { +} diff --git a/src/main/java/com/software/ott/auth/dto/GoogleUserResponse.java b/src/main/java/com/software/ott/auth/dto/GoogleUserResponse.java new file mode 100644 index 0000000..e6b81fb --- /dev/null +++ b/src/main/java/com/software/ott/auth/dto/GoogleUserResponse.java @@ -0,0 +1,16 @@ +package com.software.ott.auth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record GoogleUserResponse( + String id, + String email, + String name, + String givenName, + String familyName, + String picture, + String locale +) { +} diff --git a/src/main/java/com/software/ott/auth/service/GoogleApiService.java b/src/main/java/com/software/ott/auth/service/GoogleApiService.java new file mode 100644 index 0000000..a0533ac --- /dev/null +++ b/src/main/java/com/software/ott/auth/service/GoogleApiService.java @@ -0,0 +1,125 @@ +package com.software.ott.auth.service; + +import com.software.ott.auth.dto.GoogleTokenResponse; +import com.software.ott.auth.dto.GoogleUserResponse; +import com.software.ott.common.exception.BadRequestException; +import com.software.ott.common.properties.GoogleProperties; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@RequiredArgsConstructor +public class GoogleApiService { + + private static final String GOOGLE_AUTH_BASE_URL = "https://accounts.google.com/o/oauth2/v2/auth"; + private static final String GOOGLE_API_BASE_URL = "https://www.googleapis.com/oauth2/v2/userinfo"; + private static final String LOCALHOST_URL = "localhost:5173"; + private static final String LOCALHOST_URL_IP = "127.0.0.1:5173"; + private static final String SUB_SERVER_URL = "http://ott.backapi.site/redirection"; + private static final String SUB_SERVER_URL_WITHOUT_HTTP = "ott.backapi.site"; + + private final RestTemplate restTemplate; + private final GoogleProperties googleProperties; + + public String getGoogleLoginUrl(HttpServletRequest httpServletRequest) { + String originHeader = httpServletRequest.getHeader("Origin"); + String refererHeader = httpServletRequest.getHeader("Referer"); + + String redirectUri = getRedirectUriBasedOnRequest(originHeader, refererHeader); + + if (redirectUri == null) { + String hostHeader = httpServletRequest.getHeader("Host"); + redirectUri = getRedirectUriBasedOnRequest(hostHeader, null); + } + + if (redirectUri == null) { + throw new BadRequestException("해당 도메인에서는 구글 로그인이 불가합니다."); + } + + return UriComponentsBuilder.fromUriString(GOOGLE_AUTH_BASE_URL) + .queryParam("client_id", googleProperties.clientId()) + .queryParam("redirect_uri", redirectUri) + .queryParam("response_type", "code") + .queryParam("scope", "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile") + .encode() + .build() + .toUriString(); + } + + private String getRedirectUriBasedOnRequest(String primaryUrl, String secondaryUrl) { + if (isAllowedDomain(primaryUrl) || isAllowedDomain(secondaryUrl)) { + return googleProperties.redirectUri(); + } else if (isLocalDomain(primaryUrl) || isLocalDomain(secondaryUrl)) { + return googleProperties.devRedirectUri(); + } else if (isSubAllowedDomain(primaryUrl) || isSubAllowedDomain(secondaryUrl)) { + return SUB_SERVER_URL; + } + return null; + } + + private boolean isAllowedDomain(String url) { + return url != null && url.contains(googleProperties.frontUriWithoutHttp()); + } + + private boolean isLocalDomain(String url) { + return url != null && (url.contains(LOCALHOST_URL) || url.contains(LOCALHOST_URL_IP)); + } + + private boolean isSubAllowedDomain(String url) { + return url != null && url.contains(SUB_SERVER_URL_WITHOUT_HTTP); + } + + public GoogleTokenResponse getAccessToken(String authorizationCode, HttpServletRequest httpServletRequest) { + String originHeader = httpServletRequest.getHeader("Origin"); + String refererHeader = httpServletRequest.getHeader("Referer"); + + String redirectUri = getRedirectUriBasedOnRequest(originHeader, refererHeader); + + if (redirectUri == null) { + String hostHeader = httpServletRequest.getHeader("Host"); + redirectUri = getRedirectUriBasedOnRequest(hostHeader, null); + } + + if (redirectUri == null) { + throw new BadRequestException("해당 도메인에서는 구글 로그인이 불가합니다."); + } + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", authorizationCode); + body.add("client_id", googleProperties.clientId()); + body.add("client_secret", googleProperties.clientSecret()); + body.add("redirect_uri", redirectUri); + body.add("grant_type", "authorization_code"); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + + RequestEntity> request = new RequestEntity<>(body, headers, HttpMethod.POST, UriComponentsBuilder.fromUriString("https://oauth2.googleapis.com/token").build().toUri()); + + ResponseEntity response = restTemplate.exchange(request, GoogleTokenResponse.class); + + return response.getBody(); + } + + public GoogleUserResponse getUserInfo(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + GOOGLE_API_BASE_URL, HttpMethod.GET, entity, GoogleUserResponse.class); + + if (response.getBody() == null || response.getBody().email() == null) { + throw new BadRequestException("구글 계정으로부터 이메일을 받아올 수 없습니다."); + } + + return response.getBody(); + } +} diff --git a/src/main/java/com/software/ott/common/properties/GoogleProperties.java b/src/main/java/com/software/ott/common/properties/GoogleProperties.java new file mode 100644 index 0000000..01c81da --- /dev/null +++ b/src/main/java/com/software/ott/common/properties/GoogleProperties.java @@ -0,0 +1,13 @@ +package com.software.ott.common.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "google") +public record GoogleProperties( + String clientId, + String redirectUri, + String clientSecret, + String devRedirectUri, + String frontUriWithoutHttp +) { +} diff --git a/src/main/java/com/software/ott/member/service/MemberService.java b/src/main/java/com/software/ott/member/service/MemberService.java index 20f9c50..daef69f 100644 --- a/src/main/java/com/software/ott/member/service/MemberService.java +++ b/src/main/java/com/software/ott/member/service/MemberService.java @@ -2,10 +2,7 @@ import com.software.ott.auth.dto.*; -import com.software.ott.auth.service.KakaoApiService; -import com.software.ott.auth.service.KakaoTokenService; -import com.software.ott.auth.service.NaverApiService; -import com.software.ott.auth.service.TokenService; +import com.software.ott.auth.service.*; import com.software.ott.common.exception.ConflictException; import com.software.ott.common.exception.NotFoundException; import com.software.ott.member.dto.LoginRequest; @@ -28,6 +25,7 @@ public class MemberService { private final KakaoApiService kakaoApiService; private final KakaoTokenService kakaoTokenService; private final NaverApiService naverApiService; + private final GoogleApiService googleApiService; @Transactional public TokenResponse kakaoLogin(String authorizationCode, HttpServletRequest httpServletRequest) { @@ -52,8 +50,8 @@ public TokenResponse kakaoLogin(String authorizationCode, HttpServletRequest htt @Transactional public TokenResponse naverLogin(String code, String state, HttpServletRequest httpServletRequest) { - NaverTokenResponse tokenResponse = naverApiService.getAccessToken(code, state, httpServletRequest); - NaverUserResponse naverUserResponse = naverApiService.getUserInfo(tokenResponse.accessToken()); + NaverTokenResponse naverTokenResponse = naverApiService.getAccessToken(code, state, httpServletRequest); + NaverUserResponse naverUserResponse = naverApiService.getUserInfo(naverTokenResponse.accessToken()); String email = naverUserResponse.response().email(); @@ -69,6 +67,25 @@ public TokenResponse naverLogin(String code, String state, HttpServletRequest ht return new TokenResponse(accessToken, refreshToken); } + @Transactional + public TokenResponse googleLogin(String code, HttpServletRequest httpServletRequest) { + GoogleTokenResponse googleTokenResponse = googleApiService.getAccessToken(code, httpServletRequest); + GoogleUserResponse googleUserResponse = googleApiService.getUserInfo(googleTokenResponse.accessToken()); + + String email = googleUserResponse.email(); + + Optional optionalMember = memberRepository.findByEmail(email); + + if (optionalMember.isEmpty()) { + registerNewMember(googleUserResponse.name(), googleUserResponse.email()); + } + + String accessToken = tokenService.generateAccessToken(email); + String refreshToken = tokenService.generateRefreshToken(email); + + return new TokenResponse(accessToken, refreshToken); + } + public void registerNewMember(String name, String email) { if (memberRepository.existsByEmail(email)) {