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

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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,34 @@
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 lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class GoogleAuthCodeRequestUrlProvider implements AuthCodeRequestUrlProvider {

private final GoogleProperties googleProperties;

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

@Override
public String provide() {
return UriComponentsBuilder.fromUriString(googleProperties.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,105 @@
package com.evenly.took.feature.auth.client.google;

import java.util.Map;

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.HttpClientErrorException;
import org.springframework.web.client.RestClient;

import com.evenly.took.feature.auth.client.UserClient;
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 com.evenly.took.global.config.properties.auth.GoogleProperties;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class GoogleUserClient implements UserClient {

private final GoogleProperties googleProperties;
private final RestClient.Builder restClientBuilder;

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

@Override
public User fetch(String authCode) {
try {
RestClient restClient = restClientBuilder.build();

MultiValueMap<String, String> tokenRequestParams = getTokenRequestParams(authCode);
Map tokenResponse = requestAccessToken(restClient, tokenRequestParams);

String accessToken = tokenResponse.get("access_token").toString();
Map userInfoResponse = requestUserInfo(restClient, accessToken);

String oauthId = userInfoResponse.get("sub").toString();
String name = userInfoResponse.get("name").toString();
return generateUser(oauthId, name);

} catch (HttpClientErrorException ex) {
throw new TookException(AuthErrorCode.INVALID_GOOGLE_CONNECTION);
}
}

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

private Map requestAccessToken(RestClient restClient, MultiValueMap<String, String> tokenRequestParams) {
Map tokenResponse = restClient.post()
.uri(googleProperties.tokenUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(tokenRequestParams)
.retrieve()
.body(Map.class);

validateClientResponse(tokenResponse, "access_token", AuthErrorCode.INVALID_GOOGLE_TOKEN);
return tokenResponse;
}

private Map requestUserInfo(final RestClient restClient, final String accessToken) {
Map userInfoResponse = restClient.get()
.uri(googleProperties.userInfoUrl())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.retrieve()
.body(Map.class);

validateClientResponse(userInfoResponse, "sub", AuthErrorCode.INVALID_GOOGLE_USER_NOT_FOUND);
return userInfoResponse;
}

private void validateClientResponse(Map response, String key, AuthErrorCode errorCode) {
if (response == null || !response.containsKey(key)) {
throw new TookException(errorCode);
}
}

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
Expand Up @@ -15,6 +15,11 @@ 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(HttpStatus.UNAUTHORIZED, "Google OAuth 토큰 발급 실패: 유효한 토큰 응답을 받지 못했습니다."),
INVALID_GOOGLE_USER_NOT_FOUND(HttpStatus.UNAUTHORIZED,
"Google OAuth 사용자 정보 조회 실패: 응답 상태가 비정상적이거나 사용자 정보를 확인할 수 없습니다."),
INVALID_GOOGLE_CONNECTION(HttpStatus.BAD_GATEWAY,
"Google OAuth 통신 오류: 사용자 정보를 가져오는 중 HTTP 클라이언트 에러가 발생했습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import com.evenly.took.global.config.properties.jwt.AuthProperties;
import com.evenly.took.global.config.properties.auth.TokenProperties;

@EnableConfigurationProperties({
AuthProperties.class,
TokenProperties.class,
})
@Configuration
public class PropertiesConfig {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.evenly.took.global.config.properties.auth;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "oauth.google")
public record GoogleProperties(
String authorizationUri,
String tokenUri,
String redirectUri,
String userInfoUrl,
String clientId,
String clientSecret,
String scope
) {
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.evenly.took.global.config.properties.jwt;
package com.evenly.took.global.config.properties.auth;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "auth")
public record AuthProperties(
public record TokenProperties(
String accessTokenSecret,
Long accessTokenExpirationTime,
Long refreshTokenExpirationTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private void defaultFilterChain(HttpSecurity http) throws Exception {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
Expand Down
17 changes: 14 additions & 3 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ springdoc:
syntax-highlight:
theme: none

jwt:
access-token-secret: ${JWT_ACCESS_TOKEN_SECRET}
access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME}
auth:
access-token-secret: ${JWT_ACCESS_TOKEN_SECRET:secret}
access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200}
refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}

oauth:
google:
authorizationUri: ${GOOGLE_AUTHORIZATION_URL}
tokenUri: ${GOOGLE_OAUTH_TOKEN_URI}
redirectUri: ${GOOGLE_OAUTH_REDIRECT_URI}
userInfoUrl: ${GOOGLE_OAUTH_USER_INFO_URL}
clientId: ${GOOGLE_OAUTH_CLIENT_ID}
clientSecret: ${GOOGLE_OAUTH_CLIENT_SECRET}
scope: ${GOOGLE_OAUTH_SCOPE}
10 changes: 10 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ auth:
access-token-secret: ${JWT_ACCESS_TOKEN_SECRET:secret}
access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200}
refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}

oauth:
google:
authorizationUri: ${GOOGLE_AUTHORIZATION_URL}
tokenUri: ${GOOGLE_OAUTH_TOKEN_URI}
redirectUri: ${GOOGLE_OAUTH_REDIRECT_URI}
userInfoUrl: ${GOOGLE_OAUTH_USER_INFO_URL}
clientId: ${GOOGLE_OAUTH_CLIENT_ID}
clientSecret: ${GOOGLE_OAUTH_CLIENT_SECRET}
scope: ${GOOGLE_OAUTH_SCOPE}
17 changes: 14 additions & 3 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ springdoc:
syntax-highlight:
theme: none

jwt:
access-token-secret: ${JWT_ACCESS_TOKEN_SECRET}
access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME}
auth:
access-token-secret: ${JWT_ACCESS_TOKEN_SECRET:secret}
access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200}
refresh-token-expiration-time: ${REFRESH_TOKEN_EXPIRATION_TIME}

oauth:
google:
authorizationUri: ${GOOGLE_AUTHORIZATION_URL}
tokenUri: ${GOOGLE_OAUTH_TOKEN_URI}
redirectUri: ${GOOGLE_OAUTH_REDIRECT_URI}
userInfoUrl: ${GOOGLE_OAUTH_USER_INFO_URL}
clientId: ${GOOGLE_OAUTH_CLIENT_ID}
clientSecret: ${GOOGLE_OAUTH_CLIENT_SECRET}
scope: ${GOOGLE_OAUTH_SCOPE}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import com.evenly.took.feature.common.exception.TookException;
import com.evenly.took.feature.user.domain.User;
import com.evenly.took.global.config.properties.jwt.AuthProperties;
import com.evenly.took.global.config.properties.auth.TokenProperties;
import com.evenly.took.global.service.MockTest;

class JwtTokenProviderTest extends MockTest {
Expand All @@ -23,8 +23,8 @@ void setUp() {

String accessTokenSecret = "secretKey123secretKey123secretKey123";

AuthProperties authProperties = new AuthProperties(accessTokenSecret, 3600L, 1L);
jwtTokenProvider = new JwtTokenProvider(authProperties);
TokenProperties tokenProperties = new TokenProperties(accessTokenSecret, 3600L, 1L);
jwtTokenProvider = new JwtTokenProvider(tokenProperties);
}

@Test
Expand Down