Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ dependencies {
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'org.springframework.boot:spring-boot-starter-webflux'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Expand Down
25 changes: 24 additions & 1 deletion src/docs/asciidoc/auth-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,27 @@ include::{snippetsDir}/loginUser/1/http-response.adoc[]
include::{snippetsDir}/loginUser/1/response-fields.adoc[]

==== 실패 Response
include::{snippetsDir}/loginUser/2/http-response.adoc[]
include::{snippetsDir}/loginUser/2/http-response.adoc[]

---

=== **2. 카카오 로그인**

카카오 로그인 api 입니다.

==== Request
include::{snippetsDir}/kakaoLogin/1/http-request.adoc[]

==== 성공 Response
성공 1. 카카오 로그인 성공, 서비스에 가입된 유저인 경우
include::{snippetsDir}/kakaoLogin/1/http-response.adoc[]
include::{snippetsDir}/kakaoLogin/1/response-fields.adoc[]

성공 2. 카카오 로그인은 성공했지만, 서비스에 가입된 유저가 아닌 경우 (세션에 카카오 유저 정보를 임시로 등록하고 응답 쿠키에 SESSION 등록, 만료시간 5분)
include::{snippetsDir}/kakaoLogin/2/http-response.adoc[]

==== 실패 Response
실패 1.
include::{snippetsDir}/kakaoLogin/3/http-response.adoc[]
실패 2.
include::{snippetsDir}/kakaoLogin/4/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ftm.server.application.dto.command;

import com.ftm.server.web.dto.request.KakaoLoginRequest;
import lombok.Getter;

@Getter
public class KakaoAuthCommand extends SocialAuthCommand {

private KakaoAuthCommand(String authorizationCode) {
super(authorizationCode);
}

public static KakaoAuthCommand from(KakaoLoginRequest request) {
return new KakaoAuthCommand(request.getAuthorizationCode());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.application.dto.command;

import lombok.Getter;

@Getter
public abstract class SocialAuthCommand {

private final String authorizationCode;

protected SocialAuthCommand(String authorizationCode) {
this.authorizationCode = authorizationCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ftm.server.application.dto.query;

import com.ftm.server.domain.enums.SocialProvider;
import lombok.Getter;

@Getter
public class FindSocialUserQuery {

private final SocialProvider socialProvider;
private final String socialId;

private FindSocialUserQuery(SocialProvider socialProvider, String socialId) {
this.socialProvider = socialProvider;
this.socialId = socialId;
}

public static FindSocialUserQuery of(SocialProvider socialProvider, String socialId) {
return new FindSocialUserQuery(socialProvider, socialId);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ftm.server.application.port;

import com.ftm.server.application.dto.command.UserLoginCommand;
import com.ftm.server.domain.entity.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
Expand All @@ -11,6 +12,9 @@ public interface AuthenticationPort {
// 일반 유저 인증 객체 생성
Authentication createAuthenticationFromCredentials(UserLoginCommand command);

// 소셜 유저 인증 객체 생성
Authentication createAuthenticationFromSocial(User user);

// 인증 세션 등록 (시큐리티 컨텍스트 저장)
void saveAuthenticatedSession(
Authentication authentication,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ftm.server.application.port;

import com.ftm.server.application.dto.command.SocialAuthCommand;
import com.ftm.server.infrastructure.oauth.SocialAuthUser;

public interface SocialAuthClientPort<T extends SocialAuthCommand, R extends SocialAuthUser> {

// OAuthClient 인증 (소셜 인증)
R authenticate(T command);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ftm.server.application.port.repository;

import com.ftm.server.domain.entity.User;
import com.ftm.server.domain.enums.SocialProvider;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

Expand All @@ -9,4 +10,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Boolean existsByEmail(String email);

Optional<User> findByEmail(String email);

Optional<User> findBySocialProviderAndSocialId(SocialProvider socialProvider, String socialId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import com.ftm.server.application.dto.command.GeneralUserCreationCommand;
import com.ftm.server.application.dto.query.FindByEmailQuery;
import com.ftm.server.application.dto.query.FindByIdQuery;
import com.ftm.server.application.dto.query.FindSocialUserQuery;
import com.ftm.server.application.port.repository.UserRepository;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.domain.entity.User;
import com.ftm.server.domain.vo.EmailDuplicationVo;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -28,6 +30,11 @@ public User queryUser(FindByIdQuery query) {
.orElseThrow(() -> CustomException.USER_NOT_FOUND);
}

public Optional<User> querySocialUser(FindSocialUserQuery query) {
return userRepository.findBySocialProviderAndSocialId(
query.getSocialProvider(), query.getSocialId());
}

public User createGeneralUser(GeneralUserCreationCommand command) {
User user = User.createGeneralUser(command);
userRepository.save(user);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.ftm.server.application.usecase.auth;

import com.ftm.server.application.dto.command.KakaoAuthCommand;
import com.ftm.server.application.dto.query.FindByUserIdQuery;
import com.ftm.server.application.dto.query.FindSocialUserQuery;
import com.ftm.server.application.port.SocialAuthClientPort;
import com.ftm.server.application.service.UserImageService;
import com.ftm.server.application.service.UserService;
import com.ftm.server.common.annotation.UseCase;
import com.ftm.server.domain.entity.User;
import com.ftm.server.domain.entity.UserImage;
import com.ftm.server.domain.vo.PendingSocialUserVo;
import com.ftm.server.domain.vo.SocialLoginOutcomeVo;
import com.ftm.server.domain.vo.SocialLoginSuccessVo;
import com.ftm.server.domain.vo.UserSummaryVo;
import com.ftm.server.infrastructure.oauth.kakao.KakaoAuthUser;
import java.util.Optional;
import lombok.RequiredArgsConstructor;

@UseCase
@RequiredArgsConstructor
public class KakaoLoginUseCase {

private final SocialAuthClientPort<KakaoAuthCommand, KakaoAuthUser> oAuthClientPort;
private final UserService userService;
private final UserImageService userImageService;

public SocialLoginOutcomeVo kakaoLogin(KakaoAuthCommand command) {
// 카카오 인증 수행
KakaoAuthUser kakaoUser = oAuthClientPort.authenticate(command);

Optional<User> saved =
userService.querySocialUser(
FindSocialUserQuery.of(
kakaoUser.getSocialProvider(), kakaoUser.getSocialId()));

// 가입된 회원인 경우
if (saved.isPresent()) {
User user = saved.get();
UserImage image =
userImageService.queryUserImageByUserId(FindByUserIdQuery.of(user.getId()));

return SocialLoginSuccessVo.from(user, UserSummaryVo.of(user, image));
}

// 가입된 회원이 아닌 경우
return PendingSocialUserVo.from(kakaoUser.getSocialProvider(), kakaoUser.getSocialId());
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/ftm/server/common/config/WebClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ftm.server.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.defaultHeader(
HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.build();
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/ftm/server/common/consts/StaticConsts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ftm.server.common.consts;

public class StaticConsts {

public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
public static final String AUTHORIZATION_GRANT_TYPE = "authorization_code";
public static final String PENDING_SOCIAL_USER_SESSION_KEY = "PENDING_SOCIAL_USER_INFO";
public static final int PENDING_SOCIAL_USER_SESSION_TTL = 300; // 5분
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ public enum ErrorResponseCode {

// 500번
UNKNOWN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E500_001", "알 수 없는 서버 에러가 발생했습니다."),
FAIL_TO_SEND_EMAIL(HttpStatus.INTERNAL_SERVER_ERROR, "E500_002", "서버 내부 문제로 메일 전송에 실패했습니다.");
FAIL_TO_SEND_EMAIL(HttpStatus.INTERNAL_SERVER_ERROR, "E500_002", "서버 내부 문제로 메일 전송에 실패했습니다."),

// 502번 (외부 서비스에서 문제 발생)
KAKAO_AUTH_TOKEN_EXCHANGE_FAILED(HttpStatus.BAD_GATEWAY, "E502_001", "카카오 인증 토큰 요청에 실패했습니다."),
KAKAO_USER_PROFILE_FETCH_FAILED(HttpStatus.BAD_GATEWAY, "E502_002", "카카오 사용자 정보 요청에 실패했습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/ftm/server/domain/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,15 @@ public static User createGeneralUser(GeneralUserCreationCommand command) {
.role(UserRole.USER)
.build();
}

public static User createTestKakaoUser() {
return User.builder()
.nickname("test")
.socialProvider(SocialProvider.KAKAO)
.socialId("test_kakao_id")
.groomingScore(0)
.isDeleted(false)
.role(UserRole.USER)
.build();
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/ftm/server/domain/vo/PendingSocialUserVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ftm.server.domain.vo;

import com.ftm.server.domain.enums.SocialProvider;
import lombok.Getter;

/** 가입이 필요한 소셜 유저인 경우 VO */
@Getter
public class PendingSocialUserVo extends SocialLoginOutcomeVo {

private final SocialProvider socialProvider;
private final String socialId;

private PendingSocialUserVo(SocialProvider socialProvider, String socialId) {
super(false);
this.socialProvider = socialProvider;
this.socialId = socialId;
}

public static PendingSocialUserVo from(SocialProvider socialProvider, String socialId) {
return new PendingSocialUserVo(socialProvider, socialId);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/ftm/server/domain/vo/SocialLoginOutcomeVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ftm.server.domain.vo;

import java.io.Serializable;
import lombok.Getter;

/** 소셜 로그인 결과 VO */
@Getter
public abstract class SocialLoginOutcomeVo implements Serializable {

private final boolean registered;

protected SocialLoginOutcomeVo(boolean registered) {
this.registered = registered;
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/ftm/server/domain/vo/SocialLoginSuccessVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ftm.server.domain.vo;

import com.ftm.server.domain.entity.User;
import lombok.Getter;

/** 가입된 유저라 로그인에 성공한 경우 VO */
@Getter
public class SocialLoginSuccessVo extends SocialLoginOutcomeVo {

private final User user;
private final UserSummaryVo userSummaryVo;

private SocialLoginSuccessVo(User user, UserSummaryVo userSummaryVo) {
super(true);
this.user = user;
this.userSummaryVo = userSummaryVo;
}

public static SocialLoginSuccessVo from(User user, UserSummaryVo userSummaryVo) {
return new SocialLoginSuccessVo(user, userSummaryVo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ftm.server.infrastructure.oauth;

import com.ftm.server.domain.enums.SocialProvider;
import lombok.Getter;

@Getter
public abstract class SocialAuthUser {

private final SocialProvider socialProvider;
private final String socialId;

protected SocialAuthUser(SocialProvider socialProvider, String socialId) {
this.socialProvider = socialProvider;
this.socialId = socialId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ftm.server.infrastructure.oauth.kakao;

import com.ftm.server.domain.enums.SocialProvider;
import com.ftm.server.infrastructure.oauth.SocialAuthUser;
import lombok.Getter;

@Getter
public class KakaoAuthUser extends SocialAuthUser {

private KakaoAuthUser(String socialId) {
super(SocialProvider.KAKAO, socialId);
}

public static KakaoAuthUser from(String socialId) {
return new KakaoAuthUser(socialId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ftm.server.infrastructure.oauth.kakao;

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

@Getter
@NoArgsConstructor
public class KakaoErrorResponse {

private String error;

@JsonProperty("error_description")
private String errorDescription;
}
Loading