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/#18] 소셜 로그인 유즈케이스 구현 #19

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5eb21f9
[FEAT] AuthRequest에 AuthenticateSocialAccount 구현
sung-silver Nov 23, 2024
3c0aa67
[FEAT] AuthApi 인터페이스 정의
sung-silver Nov 23, 2024
3549802
[FEAT] AuthenticateSocialAccountUsecase에서 사용하는 info 객체 변경
sung-silver Nov 23, 2024
5b3f295
[FEAT] UserRepository 인터페이스 정의
sung-silver Nov 23, 2024
54d0cb3
[FEAT] 소셜 계정 정보를 통해 회원을 조회하는 로직 구현
sung-silver Nov 23, 2024
c76c61d
[FEAT] UserRepository 인터페이스의 구현체인 UserRepositoryImpl 구현
sung-silver Nov 23, 2024
f2a1387
[CHORE] 주석 제거
sung-silver Nov 23, 2024
1c8386e
[FEAT] AuthenticatorSocialAccountService에 authenticate 구현
sung-silver Nov 23, 2024
5623edd
[REFACTOR] 상수 클래스 접근제어자 변경
sung-silver Nov 24, 2024
14bb8f6
[FEAT] CookieUtil 구현
sung-silver Nov 24, 2024
a88880f
[FEAT] 로그인 성공 코드 구현
sung-silver Nov 24, 2024
875b1d7
[FEAT] Web/App 소셜 로그인 응답 형식 정의
sung-silver Nov 24, 2024
b6b40b5
[FEAT] 소셜 로그인 web/app 컨트롤러 구현
sung-silver Nov 24, 2024
a29c24e
[FEAT] Jwt Subject에 userId를 넣는 로직 추가 구현
sung-silver Nov 24, 2024
602403f
[FIX] 앱 로그인 경로 수정
sung-silver Nov 25, 2024
105348b
[REFACTOR] 쿼리로 작성한 메서드를 메서드 시그니처를 사용하도록 변경
sung-silver Nov 25, 2024
ac028b4
[CHORE] 와일드카드 제거
sung-silver Nov 25, 2024
21186a8
[REFACTOR] 와일드 카드 제거를 위한 코드 변경
sung-silver Nov 25, 2024
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
@@ -1,6 +1,7 @@
package sopt.makers.authentication.application.auth.api;

import sopt.makers.authentication.application.auth.dto.request.AuthRequest;
import sopt.makers.authentication.application.auth.dto.request.AuthRequest.*;
sung-silver marked this conversation as resolved.
Show resolved Hide resolved
import sopt.makers.authentication.support.common.api.BaseResponse;

import org.springframework.http.ResponseEntity;
Expand All @@ -12,4 +13,10 @@ ResponseEntity<BaseResponse<?>> createPhoneVerification(

ResponseEntity<BaseResponse<?>> verifyPhoneVerification(
AuthRequest.VerifyPhoneVerification phoneVerification);

ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromWeb(
AuthenticateSocialAuthInfo socialAuthInfo);

ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromApp(
AuthRequest.AuthenticateSocialAuthInfo socialAuthInfo);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package sopt.makers.authentication.application.auth.api;

import static sopt.makers.authentication.support.code.domain.success.AuthSuccess.AUTHENTICATE_SOCIAL_ACCOUNT;

import sopt.makers.authentication.application.auth.dto.request.AuthRequest;
import sopt.makers.authentication.application.auth.dto.response.AuthResponse;
import sopt.makers.authentication.support.code.domain.success.AuthSuccess;
import sopt.makers.authentication.support.common.api.BaseResponse;
import sopt.makers.authentication.support.util.CookieUtil;
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase;
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase.AuthenticateTokenInfo;
import sopt.makers.authentication.usecase.auth.port.in.CreatePhoneVerificationUsecase;

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -18,6 +25,8 @@
public class AuthApiController implements AuthApi {

private final CreatePhoneVerificationUsecase createVerificationUsecase;
private final AuthenticateSocialAccountUsecase authenticateSocialAccountUsecase;
private final CookieUtil cookieUtil;

@Override
@PostMapping("/phone")
Expand All @@ -34,4 +43,34 @@ public ResponseEntity<BaseResponse<?>> verifyPhoneVerification(
AuthRequest.VerifyPhoneVerification phoneVerification) {
return null;
}

@Override
@PostMapping("/web/login")
public ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromWeb(
AuthRequest.AuthenticateSocialAuthInfo socialAuthInfo) {
AuthenticateTokenInfo tokenInfo =
authenticateSocialAccountUsecase.authenticate(socialAuthInfo.toCommand());
HttpHeaders headers = cookieUtil.setRefreshToken(tokenInfo.refreshToken());

return ResponseEntity.ok()
.headers(headers)
.body(
BaseResponse.ofSuccess(
AUTHENTICATE_SOCIAL_ACCOUNT,
AuthResponse.AuthenticateSocialAuthInfoForWeb.of(tokenInfo.accessToken())));
}

@Override
@PostMapping("/web/app")
sung-silver marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

p3

사소한 것이긴 한데
web/login / app/login 으로 지정하신 이유가 따로 있는지 궁금합니다!!

저는 보통 path를 정할 때, 패키징을 많이 생각하는데요!
login까지는 공통 관심사이고 플랫폼(web/app)에서 관심사가 나뉘어지는 논리적인 상황에서
플랫폼에 대한 path가 먼저 나오게 된다면 플랫폼이 먼저 구분지어진다고 느껴져요!!

또한 Controller의 정의 규칙도 보통 path 기반으로 관심사를 분리하고 정의하게 되는데
위와 같은 방식으로 진행한다면 앞으로의 플랫폼 구분이 필요하 기능들은 플랫폼 path가 앞에 오게될거에요!!
그로 인해 Controller 분리가 필요해진다면 login 기능만 분리한 AuthLoginController 로 분리되기 보다는 AppController, WebController로 분리될 가능성이 커보이구요!!

물론 정답은 없는 요소이지만 정책을 정하면 좋을 것 같아 얘기해봅니다!!
(cc. @hyunw9 씨의 의견도 궁금해요!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이전에 전달받은 토큰 운용 관련 문서(cc. https://www.notion.so/sopt-makers/API-Token-045310ea8c5749e3b27846e2384b1c5b?pvs=4) 대로 구현을 하긴 했는데요!

다시 여쭤보지 않았던 이유는 이 문서를 전달해주실 때 제가 없는 자리에서 관련된 이야기가 나왔는데 그 내용을 제가 인지하지 못하고 있어서 오빠가 관련 내용을 작성해서 보내주신다고 하셨던거로 기억해요. 그래서 이미 논의가 끝난 사안이라고 생각하여 문서에 적힌 경로 및 Response DTO 및 토큰을 그대로 구현했습니다. 만약 클라이언트와 협의된 내용이 아니고 오빠가 작성해주신 내용이 클라이언트 분들과 경로에 대해 협의가 된 사안이 아니라 변경해도 된다면 저도 변경하는 것이 더 좋다고 생각합니다!

public ResponseEntity<BaseResponse<?>> authenticateSocialAuthInfoFromApp(
AuthRequest.AuthenticateSocialAuthInfo socialAuthInfo) {
AuthenticateTokenInfo tokenInfo =
authenticateSocialAccountUsecase.authenticate(socialAuthInfo.toCommand());

return ResponseEntity.ok(
BaseResponse.ofSuccess(
AUTHENTICATE_SOCIAL_ACCOUNT,
AuthResponse.AuthenticateSocialAuthInfoForApp.of(
tokenInfo.accessToken(), tokenInfo.refreshToken())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import static lombok.AccessLevel.PRIVATE;

import sopt.makers.authentication.domain.auth.AuthPlatform;
import sopt.makers.authentication.domain.auth.PhoneVerificationType;
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase.AuthenticateSocialAccountCommand;
import sopt.makers.authentication.usecase.auth.port.in.CreatePhoneVerificationUsecase.CreateVerificationCommand;

import lombok.RequiredArgsConstructor;
Expand All @@ -18,4 +20,10 @@ public CreateVerificationCommand toCommand() {
}

public record VerifyPhoneVerification(String name, String number, String code) {}

public record AuthenticateSocialAuthInfo(String code, String authPlatform) {
public AuthenticateSocialAccountCommand toCommand() {
return new AuthenticateSocialAccountCommand(AuthPlatform.find(authPlatform), code);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(access = PRIVATE)
public final class AuthResponse {}
public final class AuthResponse {
public record AuthenticateSocialAuthInfoForWeb(String accessToken) {
public static AuthenticateSocialAuthInfoForWeb of(String accessToken) {
return new AuthenticateSocialAuthInfoForWeb(accessToken);
}
}

public record AuthenticateSocialAuthInfoForApp(String accessToken, String refreshToken) {
public static AuthenticateSocialAuthInfoForApp of(String accessToken, String refreshToken) {
return new AuthenticateSocialAuthInfoForApp(accessToken, refreshToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package sopt.makers.authentication.database;

import sopt.makers.authentication.database.rdb.repository.UserRetriever;
import sopt.makers.authentication.domain.auth.SocialAccount;
import sopt.makers.authentication.domain.user.User;
import sopt.makers.authentication.usecase.auth.port.out.UserRepository;

import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;

@Repository
Copy link
Contributor

Choose a reason for hiding this comment

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

P3

해당 어노테이션은 UserJpaRepository <>에 붙이는게 더 적절하다고 생각하는데, 성은님 의견이 궁금합니다!

Copy link
Member

Choose a reason for hiding this comment

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

옹..!! 저는 성은이 방식이 적합하다고 생각했는데!!

본래 @Controller, @Service, @Repository 어노테이션들은 물론 각자마다의 기능이 조금씩 포함되어 있지만
"역할"을 나타내는게 강한 친구라고 생각해요!
(만약 빈 등록만이 목적이라면 @Component를 사용해도 무관하니까요!!)

본 객체는 Usercase In 포트를 구현하는 Service 객체에 주입이 필요하니 빈 등록이 필요함과 동시에 "저장소"라는 의미를 전달할 수 있어야 하기 때문에 @Component 보다는 @Repository가 더 적합해 보이구요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

JpaRepository를 상속받을 경우 스프링부트에서는 자동으로 빈 등록을 해준다고 하더라고요
그래서 중복해서 어노테이션을 붙일 필요는 없다는 입장이었습니다!
다만 in 포트를 구현해야하는 객체인 UserRepositoryImpl은 빈 등록을 해야하기에 고민하다가 Repository를 선택하게 되었습니다! 동규오빠와 같은 의견이었던 것 같아요 ㅎㅎ

@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final UserRetriever userRetriever;

@Override
public User findBySocialAccount(SocialAccount socialAccount) {
return userRetriever.findBySocialAccount(socialAccount);
}

@Override
public Long findIdByUser(User user) {
return userRetriever.findIdByUser(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package sopt.makers.authentication.database.rdb.repository;

import sopt.makers.authentication.database.rdb.entity.UserEntity;
import sopt.makers.authentication.domain.auth.AuthPlatform;

import java.util.Optional;

import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.*;

public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByAuthPlatformTypeAndAuthPlatformId(
AuthPlatform authPlatformType, String authPlatformId);

@Query(
"SELECT u.id FROM UserEntity u WHERE u.authPlatformType = :authPlatformType AND u.authPlatformId = :authPlatformId")
sung-silver marked this conversation as resolved.
Show resolved Hide resolved
Optional<Long> findIdByAuthPlatformTypeAndAuthPlatformId(
@Param("authPlatformType") AuthPlatform authPlatformType,
@Param("authPlatformId") String authPlatformId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package sopt.makers.authentication.database.rdb.repository;

import static sopt.makers.authentication.support.code.domain.failure.AuthFailure.NOT_FOUND_USER_WITH_SOCIAL_ACCOUNT;

import sopt.makers.authentication.database.rdb.entity.UserEntity;
import sopt.makers.authentication.domain.auth.*;
import sopt.makers.authentication.domain.user.User;
import sopt.makers.authentication.support.exception.domain.AuthException;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserRetriever {
private final UserJpaRepository userJpaRepository;

public User findBySocialAccount(SocialAccount socialAccount) {
UserEntity userEntity =
userJpaRepository
.findByAuthPlatformTypeAndAuthPlatformId(
socialAccount.authPlatformType(), socialAccount.authPlatformId())
.orElseThrow(() -> new AuthException(NOT_FOUND_USER_WITH_SOCIAL_ACCOUNT));
return userEntity.toDomain();
}

public Long findIdByUser(User user) {
AuthPlatform authPlatformType = user.getSocialAccount().authPlatformType();
String authPlatformId = user.getSocialAccount().authPlatformId();
return userJpaRepository
.findIdByAuthPlatformTypeAndAuthPlatformId(authPlatformType, authPlatformId)
.orElseThrow(() -> new AuthException(NOT_FOUND_USER_WITH_SOCIAL_ACCOUNT));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@RequiredArgsConstructor(access = PRIVATE)
public enum AuthFailure implements FailureCode {
INVALID_SOCIAL_PLATFORM(HttpStatus.BAD_REQUEST, "지원하지 않는 소셜 플랫폼입니다"),
;
NOT_FOUND_USER_WITH_SOCIAL_ACCOUNT(HttpStatus.BAD_REQUEST, "소셜 계정 정보와 일치하는 회원이 없습니다");
private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@RequiredArgsConstructor(access = PRIVATE)
public enum AuthSuccess implements SuccessCode {
CREATE_PHONE_VERIFICATION(HttpStatus.CREATED, "번호 인증 생성에 성공했습니다."),
;
AUTHENTICATE_SOCIAL_ACCOUNT(HttpStatus.OK, "소셜 로그인에 성공했습니다.");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
public class JwtConstant {

public static final String TOKEN_HEADER = "Bearer ";
public static final String REFRESH_TOKEN_HEADER = "refresh-token";
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package sopt.makers.authentication.support.constant;

public abstract class OAuthConstant {
import static lombok.AccessLevel.PRIVATE;

import lombok.*;
sung-silver marked this conversation as resolved.
Show resolved Hide resolved

@RequiredArgsConstructor(access = PRIVATE)
public final class OAuthConstant {
public static final String CLIENT_ID = "client_id";
public static final String CLIENT_SECRET = "client_secret";
public static final String CODE = "code";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ private SystemConstant() {}
public static final String PATTERN_ERROR_PATH = "/error";
public static final String PATTERN_AUTH = API_DEFAULT_PREFIX + "/auth" + PATTERN_ALL;
public static final String PATTERN_TEST = API_DEFAULT_PREFIX + "/test" + PATTERN_ALL;
public static final String PATTERN_ROOT_PATH = "/";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sopt.makers.authentication.support.util;

import static sopt.makers.authentication.support.constant.JwtConstant.REFRESH_TOKEN_HEADER;
import static sopt.makers.authentication.support.constant.SystemConstant.PATTERN_ROOT_PATH;

import sopt.makers.authentication.support.value.JwtProperty;

import java.time.Duration;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class CookieUtil {
private final JwtProperty jwtProperty;
private static final String SAME_SITE_NONE = "None";

public HttpHeaders setRefreshToken(String refreshToken) {
long durationMillis = jwtProperty.secret().expiration().refreshTokenExpiration();
Duration duration = Duration.ofMillis(durationMillis);
ResponseCookie cookie =
ResponseCookie.from(REFRESH_TOKEN_HEADER, refreshToken)
.httpOnly(true)
.secure(true)
.sameSite(SAME_SITE_NONE)
.path(PATTERN_ROOT_PATH)
.maxAge(duration)
.build();
HttpHeaders headers = new HttpHeaders();

headers.add(HttpHeaders.SET_COOKIE, cookie.toString());
return headers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import sopt.makers.authentication.domain.auth.AuthPlatform;

public interface AuthenticateSocialAccountUsecase {
SocialAccountInfo authenticate(AuthenticateSocialAccountCommand command);
AuthenticateTokenInfo authenticate(AuthenticateSocialAccountCommand command);

record SocialAccountInfo(String authPlatformId, String authPlatformType) {
public static SocialAccountInfo of(String authPlatformId, String authPlatformType) {
return new SocialAccountInfo(authPlatformId, authPlatformType);
record AuthenticateTokenInfo(String accessToken, String refreshToken) {
public static AuthenticateTokenInfo of(String accessToken, String refreshToken) {
return new AuthenticateTokenInfo(accessToken, refreshToken);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package sopt.makers.authentication.usecase.auth.port.out;

import sopt.makers.authentication.domain.auth.SocialAccount;
import sopt.makers.authentication.domain.user.User;

public interface UserRepository {
User findBySocialAccount(SocialAccount socialAccount);

Long findIdByUser(User user);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package sopt.makers.authentication.usecase.auth.service;

import sopt.makers.authentication.domain.auth.SocialAccount;
import sopt.makers.authentication.domain.user.Role;
import sopt.makers.authentication.domain.user.User;
import sopt.makers.authentication.support.jwt.provider.JwtAuthAccessTokenProvider;
import sopt.makers.authentication.support.jwt.provider.JwtAuthRefreshTokenProvider;
import sopt.makers.authentication.support.security.authentication.CustomAuthentication;
import sopt.makers.authentication.usecase.auth.port.in.AuthenticateSocialAccountUsecase;
import sopt.makers.authentication.usecase.auth.port.out.OAuthAuthenticator;
import sopt.makers.authentication.usecase.auth.port.out.UserRepository;

import java.util.List;

import org.springframework.stereotype.Service;

Expand All @@ -11,12 +20,23 @@
@RequiredArgsConstructor
public class AuthenticateSocialAccountService implements AuthenticateSocialAccountUsecase {
private final OAuthAuthenticator oAuthAuthenticator;
private final UserRepository userRepository;
private final JwtAuthAccessTokenProvider jwtAuthAccessTokenProvider;
private final JwtAuthRefreshTokenProvider jwtAuthRefreshTokenProvider;

@Override
public SocialAccountInfo authenticate(AuthenticateSocialAccountCommand command) {
public AuthenticateTokenInfo authenticate(AuthenticateSocialAccountCommand command) {
String authPlatformId =
oAuthAuthenticator.getAuthPlatformId(command.authPlatform(), command.code());
User user =
userRepository.findBySocialAccount(
SocialAccount.of(authPlatformId, command.authPlatform().name()));
List<Role> roles = List.of(user.getActivities().getLastActivity().role());
Long userId = userRepository.findIdByUser(user);
CustomAuthentication customAuthentication = new CustomAuthentication(userId, roles);
String accessToken = jwtAuthAccessTokenProvider.generate(customAuthentication);
String refreshToken = jwtAuthRefreshTokenProvider.generate(accessToken);

return SocialAccountInfo.of(authPlatformId, command.authPlatform().name());
return AuthenticateTokenInfo.of(accessToken, refreshToken);
}
}