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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.ject.studytrip.auth.application.facade;

import com.ject.studytrip.auth.application.service.KakaoLoginService;
import com.ject.studytrip.auth.infra.dto.KakaoUserInfoResponse;
import com.ject.studytrip.auth.presentation.dto.request.KakaoLoginRequest;
import com.ject.studytrip.auth.presentation.dto.request.KakaoSignupRequest;
import com.ject.studytrip.auth.presentation.dto.response.TokenResponse;
import com.ject.studytrip.member.application.service.MemberService;
import com.ject.studytrip.member.domain.entity.Member;
import com.ject.studytrip.member.domain.entity.SocialProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AuthFacade {
private final KakaoLoginService kakaoLoginService;
private final MemberService memberService;

public TokenResponse kakaoLogin(KakaoLoginRequest request) {
KakaoUserInfoResponse response = kakaoLoginService.getKakaoUserInfo(request.code());
Member member =
memberService.getMemberBySocialProviderAndSocialId(
SocialProvider.KAKAO, response.kakaoId());
return kakaoLoginService.getTokens(member.getId().toString(), member.getRole().name());
}

public TokenResponse kakaoSignup(KakaoSignupRequest request) {
KakaoUserInfoResponse response = kakaoLoginService.getKakaoUserInfo(request.code());
Member member =
memberService.createMemberFromKakao(
response.kakaoId(),
response.getEmail(),
response.getProfileImage(),
request.category(),
request.nickname());
return kakaoLoginService.getTokens(member.getId().toString(), member.getRole().name());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ject.studytrip.auth.application.service;

import com.ject.studytrip.auth.infra.dto.KakaoTokenResponse;
import com.ject.studytrip.auth.infra.dto.KakaoUserInfoResponse;
import com.ject.studytrip.auth.infra.provider.KakaoOauthProvider;
import com.ject.studytrip.auth.infra.provider.TokenProvider;
import com.ject.studytrip.auth.presentation.dto.response.TokenResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class KakaoLoginService {
private final KakaoOauthProvider kakaoOauthProvider;
private final TokenProvider tokenProvider;

public KakaoUserInfoResponse getKakaoUserInfo(String code) {
KakaoTokenResponse response = kakaoOauthProvider.getKakaoTokens(code);
return kakaoOauthProvider.getKakaoUserInfo(response.accessToken());
}

public TokenResponse getTokens(String memberId, String memberRole) {
String accessToken = tokenProvider.createAccessToken(memberId, memberRole);
String refreshToken = tokenProvider.createRefreshToken(memberId, memberRole);
return TokenResponse.of(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ject.studytrip.auth.domain.error;

import com.ject.studytrip.global.exception.error.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@RequiredArgsConstructor
public enum AuthErrorCode implements ErrorCode {
UNAUTHENTICATED(HttpStatus.UNAUTHORIZED, "인증되지 않은 요청입니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "접근 권한이 부족합니다."),

// 카카오 로그인 관련 에러
KAKAO_TOKEN_FETCH_FAILED(HttpStatus.UNAUTHORIZED, "카카오 토큰을 가져오는 데 실패했습니다."),
KAKAO_USER_INFO_FETCH_FAILED(HttpStatus.UNAUTHORIZED, "카카오 사용자 정보를 가져오는 데 실패했습니다."),
INVALID_KAKAO_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 카카오 액세스 토큰입니다."),
INVALID_KAKAO_AUTHORIZATION_CODE(HttpStatus.BAD_REQUEST, "잘못된 카카오 인가 코드입니다."),
KAKAO_SERVER_ERROR(HttpStatus.BAD_GATEWAY, "카카오 서버에서 오류가 발생했습니다."),

// 인증 관련 예외
INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 JWT 토큰입니다."),
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 리프레시 토큰입니다."),
TOKEN_IS_BLACKLISTED(HttpStatus.UNAUTHORIZED, "블랙리스트된 엑세스 토큰입니다."),
;

private final HttpStatus status;
private final String message;

@Override
public String getName() {
return this.name();
}

@Override
public HttpStatus getStatus() {
return this.status;
}

@Override
public String getMessage() {
return this.message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.ject.studytrip.auth.infra.client;

import com.ject.studytrip.auth.domain.error.AuthErrorCode;
import com.ject.studytrip.auth.infra.dto.KakaoTokenResponse;
import com.ject.studytrip.auth.infra.dto.KakaoUserInfoResponse;
import com.ject.studytrip.global.exception.CustomException;
import com.ject.studytrip.global.exception.error.ErrorCode;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Component
@RequiredArgsConstructor
public class KakaoOauthClient {

private final WebClient webClient;

public Mono<KakaoTokenResponse> fetchKakaoTokens(
String tokenUri, BodyInserters.FormInserter<String> formData) {
return webClient
.post()
.uri(tokenUri)
.body(formData)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
handleError(AuthErrorCode.KAKAO_TOKEN_FETCH_FAILED))
.onStatus(
HttpStatusCode::is5xxServerError,
handleError(AuthErrorCode.KAKAO_SERVER_ERROR))
.bodyToMono(KakaoTokenResponse.class);
}

public Mono<KakaoUserInfoResponse> fetchKakaoUserInfo(String userInfoUri, String accessToken) {
return webClient
.get()
.uri(userInfoUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
handleError(AuthErrorCode.KAKAO_USER_INFO_FETCH_FAILED))
.onStatus(
HttpStatusCode::is5xxServerError,
handleError(AuthErrorCode.KAKAO_SERVER_ERROR))
.bodyToMono(KakaoUserInfoResponse.class);
}

private Function<ClientResponse, Mono<? extends Throwable>> handleError(ErrorCode errorCode) {
return response -> Mono.error(new CustomException(errorCode));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ject.studytrip.auth.infra.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

public record KakaoAccount(
@Schema(description = "카카오 프로필") @JsonProperty("profile") KakaoProfile profile,
@Schema(description = "카카오 이메일") @JsonProperty("email") String email) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ject.studytrip.auth.infra.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

public record KakaoProfile(
@Schema(description = "카카오 프로필 이미지") @JsonProperty("profile_image_url")
String profileImage) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ject.studytrip.auth.infra.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

public record KakaoTokenResponse(
@Schema(description = "카카오 토큰 타입") @JsonProperty("token_type") String tokenType,
@Schema(description = "카카오 엑세스 토큰") @JsonProperty("access_token") String accessToken,
@Schema(description = "카카오 엑세스 토큰 만료 시간") @JsonProperty("expires_in")
Integer AccessExpiresIn,
@Schema(description = "카카오 리프레시 토큰") @JsonProperty("refresh_token") String refreshToken,
@Schema(description = "카카오 리프레시 토큰 만료 시간") @JsonProperty("refresh_token_expires_in")
Integer RefreshExpiresIn,
@Schema(description = "카카오 스코프") @JsonProperty("scope") String scope) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ject.studytrip.auth.infra.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

public record KakaoUserInfoResponse(
@Schema(description = "카카오 ID") @JsonProperty("id") String kakaoId,
@Schema(description = "카카오 계정") @JsonProperty("kakao_account") KakaoAccount account) {
public String getProfileImage() {
return account.profile().profileImage();
}

public String getEmail() {
return account.email();
}
}
58 changes: 58 additions & 0 deletions src/main/java/com/ject/studytrip/auth/infra/filter/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.ject.studytrip.auth.infra.filter;

import com.ject.studytrip.auth.infra.provider.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
String token = extractToken(authorizationHeader);
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
setAuthentication(token);
}
filterChain.doFilter(request, response);
}

private String extractToken(String header) {
return (StringUtils.hasText(header) && header.startsWith("Bearer "))
? header.substring(7)
: null;
}

private void setAuthentication(String token) {
String memberId = tokenProvider.extractMemberIdFromToken(token);
String memberRole = tokenProvider.extractMemberRoleFromToken(token);
UsernamePasswordAuthenticationToken authentication =
getAuthentication(memberId, memberRole);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

private UsernamePasswordAuthenticationToken getAuthentication(
String memberId, String memberRole) {
var authorities = List.of(new SimpleGrantedAuthority(memberRole));
return new UsernamePasswordAuthenticationToken(memberId, null, authorities);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.ject.studytrip.auth.infra.provider;

import static org.springframework.util.StringUtils.hasText;

import com.ject.studytrip.auth.domain.error.AuthErrorCode;
import com.ject.studytrip.auth.infra.client.KakaoOauthClient;
import com.ject.studytrip.auth.infra.dto.KakaoTokenResponse;
import com.ject.studytrip.auth.infra.dto.KakaoUserInfoResponse;
import com.ject.studytrip.global.config.properties.KakaoOauthProperties;
import com.ject.studytrip.global.exception.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;

@Component
@RequiredArgsConstructor
public class KakaoOauthProvider {
private final KakaoOauthClient kakaoOauthClient;
private final KakaoOauthProperties kakaoOauthProperties;

public KakaoTokenResponse getKakaoTokens(String code) {
validateKakaoAuthorizationCode(code);
return kakaoOauthClient
.fetchKakaoTokens(kakaoOauthProperties.tokenUri(), createFormData(code))
.block();
}

public KakaoUserInfoResponse getKakaoUserInfo(String accessToken) {
validateKakaoToken(accessToken);
return kakaoOauthClient
.fetchKakaoUserInfo(kakaoOauthProperties.userInfoUri(), accessToken)
.block();
}

private BodyInserters.FormInserter<String> createFormData(String code) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", "authorization_code");
formData.add("client_id", kakaoOauthProperties.clientId());
formData.add("client_secret", kakaoOauthProperties.clientSecret());
formData.add("redirect_uri", kakaoOauthProperties.redirectUri());
formData.add("code", code);
return BodyInserters.fromFormData(formData);
}

private void validateKakaoAuthorizationCode(String code) {
if (!hasText(code)) {
throw new CustomException(AuthErrorCode.INVALID_KAKAO_AUTHORIZATION_CODE);
}
}

private void validateKakaoToken(String accessToken) {
if (!hasText(accessToken)) {
throw new CustomException(AuthErrorCode.INVALID_KAKAO_TOKEN);
}
}
}
Loading