Skip to content

Commit

Permalink
Merge branch 'main' into feat/#431
Browse files Browse the repository at this point in the history
  • Loading branch information
cruelladevil authored Sep 21, 2023
2 parents 8cdbe12 + 15c6dbd commit f2444d9
Show file tree
Hide file tree
Showing 89 changed files with 2,366 additions and 883 deletions.
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies {

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testImplementation 'org.awaitility:awaitility:4.2.0'

//log to slack
implementation 'com.github.maricn:logback-slack-appender:1.4.0'
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/java/shook/shook/ShookApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class ShookApplication {

Expand Down
31 changes: 15 additions & 16 deletions backend/src/main/java/shook/shook/auth/application/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,46 @@
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import shook.shook.auth.application.dto.GoogleAccessTokenResponse;
import shook.shook.auth.application.dto.GoogleMemberInfoResponse;
import shook.shook.auth.application.dto.ReissueAccessTokenResponse;
import shook.shook.auth.application.dto.TokenPair;
import shook.shook.auth.repository.InMemoryTokenPairRepository;
import shook.shook.member.application.MemberService;
import shook.shook.member.domain.Member;
import shook.shook.member.domain.Nickname;

@RequiredArgsConstructor
@Service
public class AuthService {

private final MemberService memberService;
private final GoogleInfoProvider googleInfoProvider;
private final OAuthProviderFinder oauthProviderFinder;
private final TokenProvider tokenProvider;
private final InMemoryTokenPairRepository inMemoryTokenPairRepository;

public TokenPair login(final String authorizationCode) {
final GoogleAccessTokenResponse accessTokenResponse =
googleInfoProvider.getAccessToken(authorizationCode);
final GoogleMemberInfoResponse memberInfo = googleInfoProvider
.getMemberInfo(accessTokenResponse.getAccessToken());
public TokenPair oAuthLogin(final String oauthType, final String authorizationCode) {
final OAuthInfoProvider oAuthInfoProvider = oauthProviderFinder.getOAuthInfoProvider(oauthType);

final String userEmail = memberInfo.getEmail();
final String accessTokenResponse = oAuthInfoProvider.getAccessToken(authorizationCode);
final String memberInfo = oAuthInfoProvider.getMemberInfo(accessTokenResponse);

final Member member = memberService.findByEmail(userEmail)
.orElseGet(() -> memberService.register(userEmail));
final Member member = memberService.findByEmail(memberInfo)
.orElseGet(() -> memberService.register(memberInfo));

final Long memberId = member.getId();
final String nickname = member.getNickname();
final String accessToken = tokenProvider.createAccessToken(memberId, nickname);
final String refreshToken = tokenProvider.createRefreshToken(memberId, nickname);
inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, accessToken);
return new TokenPair(accessToken, refreshToken);
}

public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken) {
public ReissueAccessTokenResponse reissueAccessTokenByRefreshToken(final String refreshToken, final String accessToken) {
final Claims claims = tokenProvider.parseClaims(refreshToken);
final Long memberId = claims.get("memberId", Long.class);
final String nickname = claims.get("nickname", String.class);
memberService.findByIdAndNicknameThrowIfNotExist(memberId, new Nickname(nickname));

final String accessToken = tokenProvider.createAccessToken(memberId, nickname);
return new ReissueAccessTokenResponse(accessToken);
inMemoryTokenPairRepository.validateTokenPair(refreshToken, accessToken);
final String reissuedAccessToken = tokenProvider.createAccessToken(memberId, nickname);
inMemoryTokenPairRepository.addOrUpdateTokenPair(refreshToken, reissuedAccessToken);
return new ReissueAccessTokenResponse(reissuedAccessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@

@RequiredArgsConstructor
@Component
public class GoogleInfoProvider {
public class GoogleInfoProvider implements OAuthInfoProvider {

private static final String TOKEN_PREFIX = "Bearer ";
private static final String GRANT_TYPE = "authorization_code";
private static final String AUTHORIZATION_HEADER = "Authorization";

@Value("${oauth2.google.access-token-url}")
private String GOOGLE_ACCESS_TOKEN_URL;
Expand All @@ -41,10 +40,11 @@ public class GoogleInfoProvider {

private final RestTemplate restTemplate;

public GoogleMemberInfoResponse getMemberInfo(final String accessToken) {
@Override
public String getMemberInfo(final String accessToken) {
try {
final HttpHeaders headers = new HttpHeaders();
headers.set(AUTHORIZATION_HEADER, TOKEN_PREFIX + accessToken);
headers.set(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken);
final HttpEntity<Object> request = new HttpEntity<>(headers);

final ResponseEntity<GoogleMemberInfoResponse> response = restTemplate.exchange(
Expand All @@ -54,15 +54,16 @@ public GoogleMemberInfoResponse getMemberInfo(final String accessToken) {
GoogleMemberInfoResponse.class
);

return response.getBody();
return Objects.requireNonNull(response.getBody()).getEmail();
} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAccessTokenException();
} catch (HttpServerErrorException e) {
} catch (HttpServerErrorException | NullPointerException e) {
throw new OAuthException.GoogleServerException();
}
}

public GoogleAccessTokenResponse getAccessToken(final String authorizationCode) {
@Override
public String getAccessToken(final String authorizationCode) {
try {
final HashMap<String, String> params = new HashMap<>();
params.put("code", authorizationCode);
Expand All @@ -76,11 +77,11 @@ public GoogleAccessTokenResponse getAccessToken(final String authorizationCode)
params,
GoogleAccessTokenResponse.class);

return Objects.requireNonNull(response.getBody());
return Objects.requireNonNull(response.getBody()).getAccessToken();

} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAuthorizationCodeException();
} catch (HttpServerErrorException e) {
} catch (HttpServerErrorException | NullPointerException e) {
throw new OAuthException.GoogleServerException();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package shook.shook.auth.application;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
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.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import shook.shook.auth.application.dto.KakaoAccessTokenResponse;
import shook.shook.auth.application.dto.KakaoMemberInfoResponse;
import shook.shook.auth.exception.OAuthException;

@RequiredArgsConstructor
@Component
public class KakaoInfoProvider implements OAuthInfoProvider {

private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded;charset=utf-8";
private static final String TOKEN_PREFIX = "Bearer ";
private static final String GRANT_TYPE = "authorization_code";


@Value("${oauth2.kakao.access-token-url}")
private String KAKAO_ACCESS_TOKEN_URL;

@Value("${oauth2.kakao.member-info-url}")
private String KAKAO_MEMBER_INFO_URL;

@Value("${oauth2.kakao.client-id}")
private String KAKAO_CLIENT_ID;

@Value("${oauth2.kakao.redirect-url}")
private String LOGIN_REDIRECT_URL;

private final RestTemplate restTemplate;

public String getMemberInfo(final String accessToken) {
try {
final HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, TOKEN_PREFIX + accessToken);
final HttpEntity<Object> request = new HttpEntity<>(headers);
final ResponseEntity<KakaoMemberInfoResponse> response = restTemplate.exchange(
KAKAO_MEMBER_INFO_URL,
HttpMethod.GET,
request,
KakaoMemberInfoResponse.class
);

return String.valueOf(Objects.requireNonNull(response.getBody()).getId());
} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAccessTokenException();
} catch (HttpServerErrorException | NullPointerException e) {
throw new OAuthException.KakaoServerException();
}
}

public String getAccessToken(final String authorizationCode) {
try {
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", GRANT_TYPE);
params.add("client_id", KAKAO_CLIENT_ID);
params.add("redirect_uri", LOGIN_REDIRECT_URL);
params.add("code", authorizationCode);

HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED);

final HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

final ResponseEntity<KakaoAccessTokenResponse> response = restTemplate.postForEntity(
KAKAO_ACCESS_TOKEN_URL,
request,
KakaoAccessTokenResponse.class);

return Objects.requireNonNull(response.getBody()).getAccessToken();
} catch (HttpClientErrorException e) {
throw new OAuthException.InvalidAuthorizationCodeException();
} catch (HttpServerErrorException | NullPointerException e) {
throw new OAuthException.KakaoServerException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package shook.shook.auth.application;

public interface OAuthInfoProvider {

String getMemberInfo(final String accessToken);

String getAccessToken(final String authorizationCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package shook.shook.auth.application;

import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class OAuthProviderFinder {

private final Map<OAuthType, OAuthInfoProvider> oauthExecution = new HashMap<>();
private final KakaoInfoProvider kakaoInfoProvider;
private final GoogleInfoProvider googleInfoProvider;

@PostConstruct
public void init() {
oauthExecution.put(OAuthType.GOOGLE, googleInfoProvider);
oauthExecution.put(OAuthType.KAKAO, kakaoInfoProvider);
}

public OAuthInfoProvider getOAuthInfoProvider(final String oauthType) {
final OAuthType key = OAuthType.find(oauthType);
return oauthExecution.get(key);
}
}
16 changes: 16 additions & 0 deletions backend/src/main/java/shook/shook/auth/application/OAuthType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package shook.shook.auth.application;

import java.util.Arrays;
import shook.shook.auth.exception.OAuthException;

public enum OAuthType {

GOOGLE, KAKAO;

public static OAuthType find(final String oauthType) {
return Arrays.stream(OAuthType.values())
.filter(type -> type.name().equals(oauthType.toUpperCase()))
.findFirst()
.orElseThrow(OAuthException.OauthTypeNotFoundException::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package shook.shook.auth.application;

import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import shook.shook.auth.exception.TokenException;
import shook.shook.auth.repository.InMemoryTokenPairRepository;

@RequiredArgsConstructor
@Component
public class TokenPairScheduler {

private final TokenProvider tokenProvider;
private final InMemoryTokenPairRepository inMemoryTokenPairRepository;

@Scheduled(cron = "${schedules.in-memory-token.cron}")
public void removeExpiredTokenPair() {
final Set<String> refreshTokens = inMemoryTokenPairRepository.getTokenPairs().keySet();
for (String refreshToken : refreshTokens) {
try {
tokenProvider.parseClaims(refreshToken);
} catch (TokenException.ExpiredTokenException e) {
inMemoryTokenPairRepository.delete(refreshToken);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package shook.shook.auth.application.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class KakaoAccessTokenResponse {

@JsonProperty("access_token")
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package shook.shook.auth.application.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class KakaoMemberInfoResponse {

private Long id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.NoArgsConstructor;

@Schema(description = "액세스 토큰 재발급 응답")
@AllArgsConstructor
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class ReissueAccessTokenResponse {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@ public GoogleServerException() {
super(ErrorCode.GOOGLE_SERVER_EXCEPTION);
}
}

public static class KakaoServerException extends OAuthException {

public KakaoServerException() {
super(ErrorCode.GOOGLE_SERVER_EXCEPTION);
}
}

public static class OauthTypeNotFoundException extends OAuthException {

public OauthTypeNotFoundException() {
super(ErrorCode.NOT_FOUND_OAUTH);
}
}
}
Loading

0 comments on commit f2444d9

Please sign in to comment.