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 18 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,46 @@
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.request.GoogleTokenRequest;
import com.evenly.took.feature.auth.client.google.dto.response.GoogleTokenResponse;
import com.evenly.took.feature.auth.client.google.error.GoogleTokenProviderErrorHandler;
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) {
GoogleTokenRequest request = GoogleTokenRequest.of(googleProperties, authCode);
return restClient.post()
.uri(googleUrlProperties.tokenUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(request.toMultiValueMap())
.retrieve()
.body(GoogleTokenResponse.class);
}
}
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.response.GoogleTokenResponse;
import com.evenly.took.feature.auth.client.google.dto.response.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,39 @@
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.response.GoogleUserInfoResponse;
import com.evenly.took.feature.auth.client.google.error.GoogleUserInfoProviderErrorHandler;
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,33 @@
package com.evenly.took.feature.auth.client.google.dto.request;

import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import com.evenly.took.global.config.properties.auth.GoogleProperties;

public record GoogleTokenRequest(
String grantType,
String clientId,
String redirectUri,
String code,
String clientSecret
) {

public static GoogleTokenRequest of(GoogleProperties googleProperties, String code) {
return new GoogleTokenRequest("authorization_code",
googleProperties.clientId(),
googleProperties.redirectUri(),
code,
googleProperties.clientSecret());
}

public MultiValueMap<String, Object> toMultiValueMap() {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("grant_type", grantType);
map.add("client_id", clientId);
map.add("redirect_uri", redirectUri);
map.add("code", code);
map.add("client_secret", clientSecret);
return map;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.evenly.took.feature.auth.client.google.dto.response;

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.response;

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
@@ -0,0 +1,38 @@
package com.evenly.took.feature.auth.client.google.error;

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);
}

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

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);
}
}
Loading