Skip to content

Commit

Permalink
feat: 구글 로그인 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
GitJIHO committed Nov 26, 2024
1 parent 83d5dba commit 09802eb
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 12 deletions.
3 changes: 2 additions & 1 deletion src/main/java/com/software/ott/OttApplication.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) {
Expand Down
25 changes: 20 additions & 5 deletions src/main/java/com/software/ott/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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제외"))
Expand Down Expand Up @@ -78,6 +77,22 @@ public ResponseEntity<TokenResponse> naverCallback(@RequestParam("code") String
return ResponseEntity.ok().body(loginResponse);
}

@Operation(summary = "Oauth 구글 인증페이지 리다이렉트", description = "구글 로그인 화면으로 이동한다.", security = @SecurityRequirement(name = "JWT제외"))
@GetMapping("/auth/oauth/google")
public ResponseEntity<Void> 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<TokenResponse> 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<byte[]> generateQRCode(@RequestAttribute("memberId") Long memberId, @RequestParam String phoneNumber) {
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/software/ott/auth/dto/GoogleTokenResponse.java
Original file line number Diff line number Diff line change
@@ -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
) {
}
16 changes: 16 additions & 0 deletions src/main/java/com/software/ott/auth/dto/GoogleUserResponse.java
Original file line number Diff line number Diff line change
@@ -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
) {
}
125 changes: 125 additions & 0 deletions src/main/java/com/software/ott/auth/service/GoogleApiService.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<MultiValueMap<String, String>> request = new RequestEntity<>(body, headers, HttpMethod.POST, UriComponentsBuilder.fromUriString("https://oauth2.googleapis.com/token").build().toUri());

ResponseEntity<GoogleTokenResponse> response = restTemplate.exchange(request, GoogleTokenResponse.class);

return response.getBody();
}

public GoogleUserResponse getUserInfo(String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);

HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<GoogleUserResponse> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
29 changes: 23 additions & 6 deletions src/main/java/com/software/ott/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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();

Expand All @@ -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<Member> 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)) {
Expand Down

0 comments on commit 09802eb

Please sign in to comment.