Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 구글 로그인 구현 #27

Merged
merged 19 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;
import com.evenly.took.global.config.properties.jwt.AuthProperties;
import com.evenly.took.global.config.properties.auth.TokenProperties;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
Expand All @@ -24,12 +24,12 @@
@RequiredArgsConstructor
public class JwtTokenProvider {

private final AuthProperties authProperties;
private final TokenProperties tokenProperties;

public String generateAccessToken(String userId) {
Claims claims = generateClaims(userId);
Date now = new Date();
Date expiredAt = new Date(now.getTime() + authProperties.accessTokenExpirationMilliTime());
Date expiredAt = new Date(now.getTime() + tokenProperties.accessTokenExpirationMilliTime());
return buildAccessToken(claims, now, expiredAt);
}

Expand Down Expand Up @@ -76,7 +76,7 @@ private Jws<Claims> parseClaims(String token) {
}

private Key getSigningKey() {
String secret = authProperties.accessTokenSecret();
String secret = tokenProperties.accessTokenSecret();
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;
import com.evenly.took.global.config.properties.jwt.AuthProperties;
import com.evenly.took.global.config.properties.auth.TokenProperties;
import com.evenly.took.global.redis.RedisService;

import lombok.RequiredArgsConstructor;
Expand All @@ -16,12 +16,12 @@
@RequiredArgsConstructor
public class UuidTokenProvider {

private final AuthProperties authProperties;
private final TokenProperties tokenProperties;
private final RedisService redisService;

public String generateRefreshToken(String userId) {
String refreshToken = UUID.randomUUID().toString();
Duration expiration = Duration.ofSeconds(authProperties.refreshTokenExpirationTime());
Duration expiration = Duration.ofSeconds(tokenProperties.refreshTokenExpirationTime());
redisService.setValueWithTTL(refreshToken, userId, expiration);
return refreshToken;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.evenly.took.feature.auth.client.google;

import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import com.evenly.took.feature.auth.client.AuthCodeRequestUrlProvider;
import com.evenly.took.feature.auth.domain.OAuthType;
import com.evenly.took.global.config.properties.auth.GoogleProperties;
import com.evenly.took.global.config.properties.auth.GoogleUrlProperties;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class GoogleAuthCodeRequestUrlProvider implements AuthCodeRequestUrlProvider {

private final GoogleProperties googleProperties;
private final GoogleUrlProperties googleUrlProperties;

@Override
public OAuthType supportType() {
return OAuthType.GOOGLE;
}

@Override
public String provide() {
return UriComponentsBuilder.fromUriString(googleUrlProperties.authorizationUri())
.queryParam("client_id", googleProperties.clientId())
.queryParam("redirect_uri", googleProperties.redirectUri())
.queryParam("response_type", "code")
.queryParam("scope", googleProperties.scope())
.queryParam("access_type", "offline")
.build(true)
.toUriString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.evenly.took.feature.auth.client.google;

import java.nio.charset.StandardCharsets;

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;

import com.evenly.took.feature.auth.client.google.dto.GoogleTokenResponse;
import com.evenly.took.global.config.properties.auth.GoogleProperties;
import com.evenly.took.global.config.properties.auth.GoogleUrlProperties;

@Component
public class GoogleTokenProvider {

private final RestClient restClient;
private final GoogleProperties googleProperties;
private final GoogleUrlProperties googleUrlProperties;

public GoogleTokenProvider(GoogleProperties googleProperties,
GoogleUrlProperties googleUrlProperties,
RestClient.Builder restClientBuilder,
GoogleTokenProviderErrorHandler errorHandler) {

this.googleProperties = googleProperties;
this.googleUrlProperties = googleUrlProperties;
this.restClient = restClientBuilder
.defaultStatusHandler(errorHandler)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE,
StandardCharsets.UTF_8.name())
.build();
}

public GoogleTokenResponse fetchAccessToken(String authCode) {
return restClient.post()
.uri(googleUrlProperties.tokenUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(getTokenRequestParams(authCode))
.retrieve()
.body(GoogleTokenResponse.class);
}

private MultiValueMap<String, String> getTokenRequestParams(String authCode) {
MultiValueMap<String, String> tokenRequestParams = new LinkedMultiValueMap<>();
tokenRequestParams.add("code", authCode);
tokenRequestParams.add("client_id", googleProperties.clientId());
tokenRequestParams.add("client_secret", googleProperties.clientSecret());
tokenRequestParams.add("redirect_uri", googleProperties.redirectUri());
tokenRequestParams.add("grant_type", "authorization_code");
return tokenRequestParams;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.evenly.took.feature.auth.client.google;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.ResponseErrorHandler;

import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class GoogleTokenProviderErrorHandler implements ResponseErrorHandler {

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().isError();
}

@Override
public void handleError(URI uri, HttpMethod method, ClientHttpResponse response) throws IOException {
String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);

if (response.getStatusCode().is4xxClientError()) {
throw new TookException(AuthErrorCode.INVALID_GOOGLE_TOKEN_REQUEST);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 인증 코드 유효하지 않은 부분은 툭 클라이언트에서 잘못 전달했기 때문에 BAD_REQUEST 반환이 맞겠네요!! 저도 경호님 방식 따라야겠어요

}

log.error("Google 승인토큰 오류: {}", responseBody);
throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.evenly.took.feature.auth.client.google;

import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;

import com.evenly.took.feature.auth.client.UserClient;
import com.evenly.took.feature.auth.client.google.dto.GoogleTokenResponse;
import com.evenly.took.feature.auth.client.google.dto.GoogleUserInfoResponse;
import com.evenly.took.feature.auth.domain.OAuthIdentifier;
import com.evenly.took.feature.auth.domain.OAuthType;
import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;
import com.evenly.took.feature.user.domain.User;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class GoogleUserClient implements UserClient {

private final GoogleTokenProvider googleTokenProvider;
private final GoogleUserInfoProvider googleUserInfoProvider;

@Override
public OAuthType supportType() {
return OAuthType.GOOGLE;
}

@Override
public User fetch(String authCode) {
try {
GoogleTokenResponse tokenResponse = googleTokenProvider.fetchAccessToken(authCode);
GoogleUserInfoResponse userInfoResponse = googleUserInfoProvider.fetchUserInfo(tokenResponse.accessToken());

return generateUser(userInfoResponse.sub(), userInfoResponse.name());
} catch (HttpClientErrorException ex) {
throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION);
}
}
Comment on lines +31 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 각 GoogleXXXProvider에서 에러 핸들러를 통해 HttpClientErrorException을 잡고 있어도 잡히지 않는 예외가 있었나요?? 혹시 있었다면 저도 처리해야할 것 같아서 여쭤봐요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이건 제가 handler 적용 전에 했던건데 놓쳤네요!! 괜찮을거 같습니다!


private User generateUser(String oauthId, String name) {
OAuthIdentifier oauthIdentifier = OAuthIdentifier.builder()
.oauthId(oauthId)
.oauthType(OAuthType.GOOGLE)
.build();

return User.builder()
.oauthIdentifier(oauthIdentifier)
.name(name)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.evenly.took.feature.auth.client.google;

import java.nio.charset.StandardCharsets;

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import com.evenly.took.feature.auth.client.google.dto.GoogleUserInfoResponse;
import com.evenly.took.global.config.properties.auth.GoogleUrlProperties;

@Component
public class GoogleUserInfoProvider {

private final RestClient restClient;
private final GoogleUrlProperties googleUrlProperties;

public GoogleUserInfoProvider(GoogleUrlProperties googleUrlProperties,
RestClient.Builder restClientBuilder,
GoogleUserInfoProviderErrorHandler errorHandler) {

this.googleUrlProperties = googleUrlProperties;
this.restClient = restClientBuilder
.defaultStatusHandler(errorHandler)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE,
StandardCharsets.UTF_8.name())
.build();
}

public GoogleUserInfoResponse fetchUserInfo(String accessToken) {
return restClient.get()
.uri(googleUrlProperties.userInfoUrl())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.retrieve()
.body(GoogleUserInfoResponse.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.evenly.took.feature.auth.client.google;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.ResponseErrorHandler;

import com.evenly.took.feature.auth.exception.AuthErrorCode;
import com.evenly.took.feature.common.exception.TookException;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class GoogleUserInfoProviderErrorHandler implements ResponseErrorHandler {

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().isError();
}

@Override
public void handleError(URI uri, HttpMethod method, ClientHttpResponse response) throws IOException {
String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);

if (response.getStatusCode().is4xxClientError()) {
throw new TookException(AuthErrorCode.INVALID_GOOGLE_USER_REQUEST);
}

if (response.getStatusCode().is5xxServerError()) {
throw new TookException(AuthErrorCode.INVALID_GOOGLE_SERVER_ERROR);
}

log.error("Google 사용자정보 알 수 없는 오류: {}", responseBody);
throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.evenly.took.feature.auth.client.google.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record GoogleTokenResponse(

@JsonProperty("access_token")
String accessToken,

@JsonProperty("expires_in")
Integer expiresIn,

@JsonProperty("scope")
String scope,

@JsonProperty("token_type")
String tokenType,

@JsonProperty("id_token")
String idToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.evenly.took.feature.auth.client.google.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record GoogleUserInfoResponse(

@JsonProperty("sub")
String sub,

@JsonProperty("name")
String name
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ public enum AuthErrorCode implements ErrorCode {
JWT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "JWT를 찾을 수 없습니다."),
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "refresh token이 만료되었습니다."),
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "access token이 유효하지 않습니다."),
INVALID_GOOGLE_TOKEN_REQUEST(HttpStatus.BAD_REQUEST, "Google 승인코드 요청 중 잘못된 요청으로 오류가 발생했습니다."),
INVALID_GOOGLE_USER_REQUEST(HttpStatus.BAD_REQUEST,
"Google OAuth V2 정보 요청 중 잘못된 요청으로 오류가 발생했습니다."),
INVALID_GOOGLE_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,
"Google OAuth V2 정보 요청 중 만료된 토큰 또는 승인코드로 오류가 발생했습니다."),
INVALID_GOOGLE_CONNECTION(HttpStatus.INTERNAL_SERVER_ERROR,
"Google OAuth 통신 오류: 구글 로그인 서버와의 연결 과정 중 문제가 발생했습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import com.evenly.took.global.exception.ErrorCode;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TookException extends RuntimeException {

public TookException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

private final ErrorCode errorCode;
}
Loading