들어가기 전 ✔
기록을 통한 리마인드를 위해 남기는 글입니다.
기존 프로젝트에 구현하는 것이기 때문에 모든 코드보다는 추가되는 코드 위주로 다루겠습니다.
🤔 Spring Security 없이 소셜 로그인을 구현하는 이유
처음에는 시큐리티에 내장된 OAuth 2.0을 통해 구현하려고 했습니다.
하지만 구현을 하다 보니 커스터마이징에 한계가 존재했고, 스프링 시큐리티에 대한 의존성이 높았습니다.
그래서 직접 소셜 서버와 통신해서 코드와 토큰을 발급받는 로직을 구현하는 것으로 결정했습니다.
🐘 build.gradle
// (1)
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// (2)
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
// (3)
testImplementation 'io.projectreactor:reactor-test'
(1) : Http Interface를 사용해서 비동기로 http를 처리하기 위해 적용한다.
(2) : yml 등에 정의된 외부 구성 속성을 자바 클래스에 바인딩하기 위해 적용한다.
(3) : webflux와 함께 사용되는 반응형 라이브러리로, 비동기 및 이벤트 기반 애플리케이션을 구축하고 테스트를 작성하기 위해 적용한다.
백엔드 위주의 작업 순서
- 사용자의 로그인 요청을 받은 인증 서버는 Auth Code를 포함 시켜 프론트단으로 Redirect 한다.
- 프론트단은 Redirect 과정에서 코드를 꺼내서 서버에 인증 코드와 함께 POST 요청을 보낸다.
- 백단은 받은 인증 코드로 인증 서버에 Access Token을 발급 받는다.
- Access Token으로 사용자 정보를 조회하고 로그인한다.
- 로그인을 진행한 뒤, Access Token을 담아서 프론트단한테 보낸다.
대충 큰 흐름은 이렇습니다.
자세한 사항은 로직을 구현하며 살펴보겠습니다.
🔥 해당 소셜 로그인 화면으로 리다이렉트
💫 OauthType
package com.jisungin.domain.oauth;
import java.util.Locale;
public enum OauthType {
KAKAO;
public static OauthType fromName(String name) {
return OauthType.valueOf(name.toUpperCase(Locale.ENGLISH));
}
}
인증 서버를 식별하기 위한 클래스입니다.
요청 데이터는 문자열이기 때문에 열거 타입으로 변환하는 메서드를 추가했습니다.
💫 AuthCodeRequestUrlProvider
package com.jisungin.domain.oauth.authcode;
import com.jisungin.domain.oauth.OauthType;
public interface AuthCodeRequestUrlProvider {
// 인증 서버의 타입
OauthType supportType();
// 인증 코드 요청 주소
String provide();
}
💫 KakaoAuthCodeRequestUrlProvider
package com.jisungin.infra.oauth.authcode;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.domain.oauth.authcode.AuthCodeRequestUrlProvider;
import com.jisungin.infra.oauth.kakao.KakaoOauthConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
@Component
@RequiredArgsConstructor
public class KakaoAuthCodeRequestUrlProvider implements AuthCodeRequestUrlProvider {
private final KakaoOauthConfig kakaoOauthConfig;
@Override
public OauthType supportType() {
return OauthType.KAKAO;
}
@Override
public String provide() {
return UriComponentsBuilder
.fromUriString("https://kauth.kakao.com/oauth/authorize")
.queryParam("client_id", kakaoOauthConfig.clientId())
.queryParam("redirect_uri", kakaoOauthConfig.redirectUri())
.queryParam("response_type", "code")
.queryParam("scope", String.join(",", kakaoOauthConfig.scope()))
.toUriString();
}
}
(https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code)
해당 링크를 참고하여 인가 코드를 제공하는 로직을 작성했습니다.
💫 KakaoOauthConfig
package com.jisungin.infra.oauth.kakao;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "oauth.kakao")
public record KakaoOauthConfig(
String clientId,
String clientSecret,
String redirectUri,
String[] scope
) {
}
Kakao에 대한 정보는 application-oauth.yml에 등록해놓고 가져옵니다.
@ConfigurationProperties를 정상적으로 사용하려면 실행 애플리케이션에 @ConfigurationPropertiesScan을 추가해야 합니다.
💫 AuthCodeRequestUrlProviderComposite
package com.jisungin.domain.oauth.authcode;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.exception.BusinessException;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.jisungin.exception.ErrorCode.OAUTH_TYPE_NOT_FOUND;
@Component
public class AuthCodeRequestUrlProviderComposite {
private final Map<OauthType, AuthCodeRequestUrlProvider> mapping;
public AuthCodeRequestUrlProviderComposite(Set<AuthCodeRequestUrlProvider> providers) {
mapping = providers.stream()
.collect(Collectors.toMap(
AuthCodeRequestUrlProvider::supportType,
Function.identity()
));
}
public String provide(OauthType oauthType) {
return getProvider(oauthType).provide();
}
public AuthCodeRequestUrlProvider getProvider(OauthType oauthType) {
return Optional.ofNullable(mapping.get(oauthType))
.orElseThrow(() -> new BusinessException(OAUTH_TYPE_NOT_FOUND));
}
}
mapping에는 서버 타입에 해당하는 AuthCodeRequestUrlProvider 자체가 저장됩니다.
요청한 서버 타입이 존재하지 않으면 예외 처리를 합니다.
provide 메서드를 통해 서버에 대한 정보를 확인할 수 있습니다.
💫 OauthService
package com.jisungin.application.oauth;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.domain.oauth.authcode.AuthCodeRequestUrlProviderComposite;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OauthService {
private final AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite;
public String getAuthCodeRequestUrl(OauthType oauthType) {
return authCodeRequestUrlProviderComposite.provide(oauthType);
}
}
💫 OauthController
package com.jisungin.api.oauth;
import com.jisungin.api.ApiResponse;
import com.jisungin.application.oauth.OauthService;
import com.jisungin.domain.oauth.OauthType;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/oauth")
public class OauthController {
private final OauthService oauthService;
@SneakyThrows
@GetMapping("/{oauthType}")
public ApiResponse<Void> redirectAuthRequestUrl(
@PathVariable OauthType oauthType,
HttpServletResponse response
) {
String redirectUrl = oauthService.getAuthCodeRequestUrl(oauthType);
response.sendRedirect(redirectUrl);
return ApiResponse.ok(null);
}
}
@SneakyTrows로 response에 대해 비검사 예외로 처리하여 추후에 예외 처리를 하도록 코드를 간결하게 작성했습니다.
이후 실행되는 메서드에 올바른 로그인 서버가 아닌 경우 예외를 처리하도록 하여 문제가 없다고 판단했습니다.
그리고 response.sendRedirect()를 통해 리다이렉트를 수행했습니다.
💫 OauthTypeConverter
package com.jisungin.api.oauth;
import com.jisungin.domain.oauth.OauthType;
import org.springframework.core.convert.converter.Converter;
public class OauthTypeConverter implements Converter<String, OauthType> {
@Override
public OauthType convert(String source) {
return OauthType.fromName(source);
}
}
요청은 문자열로 오기 때문에 이전에 만든 열거 타입 변환 메서드로 convert 로직을 수행합니다.
💫 WebConfig
package com.jisungin.config;
import com.jisungin.api.oauth.OauthTypeConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.PATCH.name(),
HttpMethod.DELETE.name()
)
.allowCredentials(true)
.exposedHeaders("*");
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new OauthTypeConverter());
}
}
기본적인 CORS 설정과 위에 작성한 컨버터를 WebConfig에 등록했습니다.
🔥 Auth Code로 Access Token을 가져오기
(https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token)
Kakao Access Token에 대한 정보입니다.
💫 KakaoToken
package com.jisungin.infra.oauth.kakao.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
@JsonNaming(SnakeCaseStrategy.class)
public record KakaoToken(
String tokenType,
String accessToken,
String idToken,
Integer expiresIn,
String refreshToken,
Integer refreshTokenExpiresIn,
String scope
) {
}
@JsonNaming(SnakeCaseStrategy.class)을 적용하여 변수를 Camel Case로 받아올 수 있습니다.
💫 KakaoApiClient
package com.jisungin.infra.oauth.kakao.client;
import com.jisungin.infra.oauth.kakao.dto.KakaoToken;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.PostExchange;
import static org.springframework.http.MediaType.*;
public interface KakaoApiClient {
@PostExchange(url = "https://kauth.kakao.com/oauth/token", contentType = APPLICATION_FORM_URLENCODED_VALUE)
KakaoToken fetchToken(@RequestParam MultiValueMap<String, String> params);
}
Http Interface Client를 통해 설정한 URL에서 Access Token을 가져옵니다.
결과 데이터에 적합한 MultiValueMap을 적용하여 여러 개의 값을 저장할 수 있도록 했습니다.
저장된 값은 KakaoToken으로 매핑해서 반환합니다.
💫 HttpInterfaceConfig
package com.jisungin.infra.oauth.config;
import com.jisungin.infra.oauth.kakao.client.KakaoApiClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Configuration
public class HttpInterfaceConfig {
@Bean
public KakaoApiClient kakaoApiClient() {
return createHttpInterface(KakaoApiClient.class);
}
private <T> T createHttpInterface(Class<T> tClass) {
WebClient webClient = WebClient.create();
HttpServiceProxyFactory build = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(webClient)).build();
return build.createClient(tClass);
}
}
KakoApiClient를 사용하려면 구현체를 @Bean으로 등록해야 합니다.
createHttpInterface는 Http 클라이언트를 생성하는 역할을 합니다.
일단 비동기 Http 클라이언트인 WebClient를 생성하고 HttpServiceProxyFactory로 WebClient를 프록시 인스턴스로 설정합니다.
이렇게 프록시로 구현된 인터페이스는 그 자체로 로직 수행이 가능합니다.
구현 없이 사용할 수 있어서 간결성과 유지보수성이 좋아집니다.
또한 의존성을 해당 클라이언트가 직접 해결하지 않고 외부에서 주입 받기 때문에 DI(의존성 주입)를 적용할 수 있습니다.
🔥 Access Token으로 사용자 정보 조회하기
(https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info)
Kakao 사용자 정보 조회에 대한 정보입니다.
💫 KakaoUserResponse
package com.jisungin.infra.oauth.kakao.dto;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.jisungin.domain.oauth.OauthId;
import com.jisungin.domain.user.User;
import java.time.LocalDateTime;
import static com.jisungin.domain.oauth.OauthType.*;
@JsonNaming(SnakeCaseStrategy.class)
public record KakaoUserResponse(
Long id,
boolean hasSignedUp,
LocalDateTime connectedAt,
KakaoAccount kakaoAccount
) {
public User toEntity() {
return User.builder()
.oauthId(OauthId.builder()
.oauthId(String.valueOf(id))
.oauthType(KAKAO)
.build()
)
.name(kakaoAccount.profile.nickname)
.profileImage(kakaoAccount.profile.profileImageUrl)
.build();
}
@JsonNaming(SnakeCaseStrategy.class)
public record KakaoAccount(
boolean profileNeedsAgreement,
boolean profileNicknameNeedsAgreement,
boolean profileImageNeedsAgreement,
Profile profile,
boolean nameNeedsAgreement,
String name,
boolean emailNeedsAgreement,
boolean isEmailValid,
boolean isEmailVerified,
String email,
boolean ageRangeNeedsAgreement,
String ageRange,
boolean birthyearNeedsAgreement,
String birthyear,
boolean birthdayNeedsAgreement,
String birthday,
String birthdayType,
boolean genderNeedsAgreement,
String gender,
boolean phoneNumberNeedsAgreement,
String phoneNumber,
boolean ciNeedsAgreement,
String ci,
LocalDateTime ciAuthenticatedAt
) {
}
@JsonNaming(SnakeCaseStrategy.class)
public record Profile(
String nickname,
String thumbnailImageUrl,
String profileImageUrl,
boolean isDefaultImage
) {
}
}
가져올 수 있는 모든 사용자 정보를 정의했습니다.
💫 OauthId
package com.jisungin.domain.oauth;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class OauthId {
@Column(nullable = false, name = "oauth_id")
private String oauthId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, name = "oauth_type")
private OauthType oauthType;
@Builder
public OauthId(String oauthId, OauthType oauthType) {
this.oauthId = oauthId;
this.oauthType = oauthType;
}
}
OuathId 객체에서 oauthId는 특정 인증 서버의 식별자 값이 저장됩니다.
oauthType에는 해당 인증 서버 타입이 저장됩니다.
이 객체는 서로 다른 로그인 서비스간의 식별자 중복을 방지하기 위해 사용됩니다.
💫 User
package com.jisungin.domain.user;
import com.jisungin.domain.BaseEntity;
import com.jisungin.domain.oauth.OauthId;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "users",
uniqueConstraints = {
@UniqueConstraint(
name = "oauth_id_unique",
columnNames = {
"oauth_id",
"oauth_type"
}
),
}
)
@Entity
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Embedded
private OauthId oauthId;
@Column(name = "user_name")
private String name;
@Column(name = "user_profile_image")
private String profileImage;
@Builder
public User(OauthId oauthId, String name, String profileImage) {
this.oauthId = oauthId;
this.name = name;
this.profileImage = profileImage;
}
}
@Embedded를 사용해서 OauthId 객체를 User 엔티티에 포함시켰습니다.
또한 Table에서 uniqueConstraints와 @UniqueConstraint를 사용해서 유니크 제약 조건을 생성했습니다.
oauth_id와 oauth_type의 칼럼 그룹은 테이블 내에서 유일함을 보장합니다.
💫 UserRepository
package com.jisungin.domain.user.repository;
import com.jisungin.domain.oauth.OauthId;
import com.jisungin.domain.user.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByOauthId(OauthId oauthId);
}
DB에서 User를 OauthId로 찾는 메서드를 추가했습니다.
💫 KakaoApiClient
package com.jisungin.infra.oauth.kakao.client;
import com.jisungin.infra.oauth.kakao.dto.KakaoToken;
import com.jisungin.infra.oauth.kakao.dto.KakaoUserResponse;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.PostExchange;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.MediaType.*;
public interface KakaoApiClient {
@PostExchange(url = "https://kauth.kakao.com/oauth/token", contentType = APPLICATION_FORM_URLENCODED_VALUE)
KakaoToken fetchToken(@RequestParam MultiValueMap<String, String> params);
// 추가
@GetExchange(url = "https://kapi.kakao.com/v2/user/me")
KakaoUserResponse fetchUser (@RequestHeader(name = AUTHORIZATION) String bearerToken);
}
사용자 정보 URL에 Access Token으로 사용자 정보를 요청합니다.
결과는 KakoUserResponse로 반환됩니다.
💫 UserClient
package com.jisungin.domain.oauth.client;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.domain.user.User;
public interface UserClient {
OauthType supportType();
User fetch(String authCode);
}
fetch 메서드는 인자로 인증 코드를 받습니다.
해당 인증 코드로 AccessToken을 발급하고 사용자 정보 조회 로직을 수행합니다.
💫 UserClientComposite
package com.jisungin.domain.oauth.client;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.domain.user.User;
import com.jisungin.exception.BusinessException;
import com.jisungin.exception.ErrorCode;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class UserClientComposite {
private final Map<OauthType, UserClient> mapping;
public UserClientComposite(Set<UserClient> clients) {
this.mapping = clients.stream()
.collect(Collectors.toMap(
UserClient::supportType,
Function.identity()
));
}
public User fetch(OauthType oauthType, String authCode) {
return getClient(oauthType).fetch(authCode);
}
private UserClient getClient(OauthType oauthType) {
return Optional.ofNullable(mapping.get(oauthType))
.orElseThrow(() -> new BusinessException(ErrorCode.OAUTH_TYPE_NOT_FOUND));
}
}
해당 클래스를 통해 기존 코드의 변경 없이 각 서비스의 해당하는 사용자 정보 조회 구현체를 등록할 수 있습니다.
💫 KakaoUserClient
package com.jisungin.infra.oauth.kakao;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.domain.oauth.client.UserClient;
import com.jisungin.domain.user.User;
import com.jisungin.infra.oauth.kakao.client.KakaoApiClient;
import com.jisungin.infra.oauth.kakao.dto.KakaoToken;
import com.jisungin.infra.oauth.kakao.dto.KakaoUserResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@Component
@RequiredArgsConstructor
public class KakaoUserClient implements UserClient {
private final KakaoApiClient kakaoApiClient;
private final KakaoOauthConfig kakaoOauthConfig;
@Override
public OauthType supportType() {
return OauthType.KAKAO;
}
@Override
public User fetch(String authCode) {
KakaoToken tokenInfo = kakaoApiClient.fetchToken(tokenRequestParams(authCode));
KakaoUserResponse kakaoUserResponse = kakaoApiClient.fetchUser("Bearer " + tokenInfo.accessToken());
return kakaoUserResponse.toEntity();
}
private MultiValueMap<String, String> tokenRequestParams(String authCode) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", kakaoOauthConfig.clientId());
params.add("redirect_uri", kakaoOauthConfig.redirectUri());
params.add("code", authCode);
params.add("client_secret", kakaoOauthConfig.clientSecret());
return params;
}
}
fetch() 메서드에 동작은 다음과 같습니다.
- Auth Code를 통해서 AccessToken을 조회합니다.
- AccessToken으로 사용자 정보를 조회합니다.
- 회원 정보를 User 객체로 변환해서 반환합니다.
💫 OauthService
package com.jisungin.application.oauth;
import com.jisungin.domain.oauth.OauthType;
import com.jisungin.domain.oauth.authcode.AuthCodeRequestUrlProviderComposite;
import com.jisungin.domain.oauth.client.UserClientComposite;
import com.jisungin.domain.user.User;
import com.jisungin.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OauthService {
private final AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite;
private final UserClientComposite userClientComposite;
private final UserRepository userRepository;
public String getAuthCodeRequestUrl(OauthType oauthType) {
return authCodeRequestUrlProviderComposite.provide(oauthType);
}
// 추가
public Long login(OauthType oauthType, String authCode) {
User user = userClientComposite.fetch(oauthType, authCode);
User savedUser = userRepository.findByOauthId(user.getOauthId())
.orElseGet(() -> userRepository.save(user));
return savedUser.getId();
}
}
로그인 로직의 순서는 아래와 같습니다.
- 로그인하려는 oauthType에 해당하는 회원 인증 코드를 조회합니다.
- 유일한 식별자인 oauthId로 사용자 객체를 가져옵니다.
- 만약 가입이 되어있지 않다면 회원가입도 진행합니다.
- 가입이 되어있다면 로그인만 진행합니다.
- 현재는 사용자 객체의 id를 반환하지만 추후에 JWT를 적용할 예정입니다.
💫 OauthController
package com.jisungin.api.oauth;
import com.jisungin.api.ApiResponse;
import com.jisungin.application.oauth.OauthService;
import com.jisungin.domain.oauth.OauthType;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/oauth")
public class OauthController {
private final OauthService oauthService;
@SneakyThrows
@GetMapping("/{oauthType}")
public ApiResponse<Void> redirectAuthRequestUrl(
@PathVariable OauthType oauthType,
HttpServletResponse response
) {
String redirectUrl = oauthService.getAuthCodeRequestUrl(oauthType);
response.sendRedirect(redirectUrl);
return ApiResponse.ok(null);
}
// 추가
@GetMapping("/login/{oauthType}")
public ApiResponse<Long> login(
@PathVariable OauthType oauthType,
@RequestParam("code") String code
) {
Long login = oauthService.login(oauthType, code);
return ApiResponse.ok(login);
}
}
사용자가 로그인 + 정보 제공 동의를 진행하면 프론트단으로 Redirect 됩니다.
이때 AuthCode를 받아서 /auth/login/{oauthType}에 요청합니다.
❌ 적용을 하며 발생했던 오류
직접 구현하며 겪었던 오류를 아래 정리하겠습니다.
⭕ 설정 파일 탐색 문제
제 프로젝트 파일은 아래와 같이 application.yml에서 로컬, 개발, 운영 환경을 나눠서 관리합니다.
spring:
profiles:
active:
local
group:
local-env:
- local
dev-env:
- dev
prod-env:
- prod
include:
oauth
oauth 설정 파일을 include에 추가하지 않아서 @ConfigurationProperties(prefix = "oauth.kakao")가 동작하지 않았습니다.
완전히 휴먼 에러지만 그래도 @ConfigurationProperties의 동작을 리마인드 할 수 있어서 좋은(?) 경험이었습니다.
⭕ ARM 네이티브 라이브러리 호환성 문제
스프링에서 Http Interface를 사용하려면 webflux 의존성을 추가합니다.
Netty는 성능상 이점을 얻기 위해 네이티브 코드를 사용해 시스템의 DNS 리졸버와 연동합니다.
그런데 ARM 기반 아키텍처에는 필요한 라이브러리가 없어서 에러가 발생합니다.
아래 의존성을 직접 추가하면 해결할 수 있습니다.
// ARM Native Library Compatibility
runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.104.Final:osx-aarch_64'
💡 참고 내용
https://ttl-blog.tistory.com/1434
[Spring] 쉬운 확장이 가능한 OAuth2.0 로그인 구현(카카오, 네이버, 구글 등) (Security 사용 X)
(전 순정 백엔드 개발자기 때문에 React는 못합니다. React 코드는 Chat GPT 시켜서 구현하였고, 대신 백엔드에 온 진심을 담하서 구현하였으니, 프론트 코드는 정말 그냥 테스트용으로만 참고해 주시
ttl-blog.tistory.com
'Spring' 카테고리의 다른 글
STOMP로 소켓 방식 채팅 구현 + Rate Limiter, Token Bucket으로 API 처리율 제한하기 (0) | 2024.06.21 |
---|---|
Redis Sorted Sets으로 인기 검색어 구현하기 (0) | 2024.04.09 |
리뷰 좋아요 조회 쿼리 N + 1 문제 해결하기 (0) | 2024.04.05 |
[Spring] could not initialize proxy [...] - no Session 오류 (0) | 2024.04.02 |
QueryDSL 동적 정렬 쿼리 OrderSpecifier 구현하기 (0) | 2024.03.28 |