본문 바로가기
BackEnd

[Spring] OAuth Client로 네이버 로그인 구현하기

by solanarey 2024. 9. 3.

자동목차

OAuth 2.0

OAuth 2.0은 표준화된 규칙과 절차를 정의한 프로토콜입니다.

애플리케이션 간의 인증 및 권한 부여를 위한 상호 작용을 명확하게 규정하고 클라이언트(사용자가 이용하는 서비스 애플리케이션)가 자원 소유자(사용자)의 자원에 접근할 수 있도록 허용하는 방법을 정의합니다.

주로 간편 로그인을 할 때 사용됩니다.(ex. 카카오 로그인, 네이버 로그인 등)

역할

  • Resource Owner - 자원을 소유하고 있는 사람입니다. 서비스 애플리케이션을 사용하는 실제 사용자입니다.
  • Client - 서비스 애플리케이션 서버입니다. 리소스 서버(구글, 네이버 등)로 Resource Owner의 인증 인가를 요청 하기때문에 리소스 서버의 입장에서 보는 관점으로 클라이언트입니다.
  • Resource Server & Authorization Server - 인증 서버 및 리소스 서버입니다. 인증 서버는 인증 및 인가에 대하여 처리하고 리소스 서버는 저장되어 있는 리소스 오너의 정보를 응답해줍니다.



내부 동작

A. 사용자가 네이버 소셜 로그인을 시도

  • 네이버 소셜 로그인 버튼을 클릭
  • 그 후 네이버 로그인 화면에서 동의항목 체크 후 로그인

B. 사용자는 로그인 성공 후 권한 코드를 클라이언트에게 전달

C. 클라이언트 → 인증 서버로 인증을 요청

  • 클라이언트가 사용자로부터 받은 권한 코드(Authorization code), client_id, redirect_uri, response_type, scope 등을 인증서버에 요청합니다.

https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=%7Bclient-id%7D&scope=name email&state=7iAGmKj9v3O7NZSDNG6L0eaw1tCtktvjRMXLwUVIUb4%3D&redirect\_uri=http://localhost:8080/login/oauth2/code/naver
client_id - 클라이언트 애플리케이션의 고유 식별자입니다.scope - 클라이언트가 요청하는 권한 범위입니다.
redirect_uri - 네이버 인증 서버가 사용자를 인증한 후, 권한 코드를 포함하여 리다이렉션할 URL입니다.
response_type=code - 권한 코드를 요청하겠다는 파라미터입니다.

 

D. 사용자 로그인 완료되면 인증 서버로부터 액세스 토큰을 발급받습니다.

  • 클라이언트는 받은 권한 코드를 사용해 네이버의 인증 서버에 액세스 토큰을 요청합니다.
  • 인증 서버는 요청이 유효하다고 판단되면 액세스 토큰을 발급합니다. 이 토큰은 클라이언트가 리소스 서버로부터 사용자 자원에 접근하는 데 필요한 인증 정보입니다.

E. 클라이언트 → 리소스 서버에 사용자 정보 요청

  • 클라이언트는 발급받은 액세스 토큰을 사용하여 네이버의 리소스 서버에 사용자의 정보를 요청합니다.
  • 리소스 서버는 액세스 토큰이 유효하면 요청된 자원을 반환합니다. (사용자가 동의항목에서 체크한 항목)

F. 사용자 정보 제공

  • 클라이언트는 리소스 서버에서 받은 사용자 정보를 활용하여 로그인한 사용자에게 서비스를 제공합니다.
    • 사용자 이름, 이메일 등을 받아 화면에 표시하거나 사용자 계정을 생성합니다.



OAuth Client

OAuth Client는 스프링에서 제공하는 라이브러리이자 Spring Security 모듈의 일부입니다.

이 라이브러리를 사용하면 OAuth 2.0 프로토콜을 통해 인증 및 인가를 처리하는 로직을 쉽게 구현할 수 있습니다.

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client

시작하기에 앞서 build.gradle에 OAuth Client 의존성을 추가해줍니다.

그리고

https://developers.naver.com/main/ 로 이동하여 개발자 센터 페이지에서 애플리케이션을 생성한 후 redirect uri 를 지정해주고 동의항목에서 무슨 정보를 받아 올 것인지 설정해주어야 합니다.

그 후 client-id, client-secret 값을 application.yml 에 아래와 같이 명시해줍니다.



속성값 정의

# registraion에는 클라이언트 설정을 지정하고 provider 에서는 리소스서버와 인증서버의 엔드포인트를 지정합니다.
spring:
  security:
    oauth2:
      client:
        registration:
          naver:
            client-id: # 네이버 개발자 센터에서 발급받은 클라이언트 ID
            client-secret: # 네이버 개발자 센터에서 발급받은 클라이언트 시크릿
            client-name: naver # 일반적으로 제공자 이름을 적음
            redirect-uri: 
            authorization-grant-type: authorization_code # 인증방식
            scope: # 받아올 사용자 정보 스코프
              - name
              - email

        provider:
          naver:
            authorization-uri: <https://nid.naver.com/oauth2.0/authorize>
            token-uri: <https://nid.naver.com/oauth2.0/token>
            user-info-uri: <https://openapi.naver.com/v1/nid/me>
            user-name-attribute: response

위에서 redirect-uri를 보면 http://도메인/login/oauth2/code/naver 와 같이 지정 되어있는데,

스프링 시큐리티 oauth2 client에서 기본적으로 제공하는 기능을 사용하는 경우에는 `login/oauth2/code/제공자명 을 따라야 합니다. (정해져있는 uri를 사용하기 싫다면 커스텀으로 구현하여 사용할 수 있습니다.)

가령 구글이나 카카오의 경우

/login/oauth2/code/google 혹은 /login/oauth2/code/kakao 와 같이 말입니다.



응답 데이터 클래스 정의

네이버 로그인 개발 문서에 따르면 사용자 정보를 가져오는 데이터 포맷은 위와 같습니다.

그렇기에 아래와 같이 매핑하여 클래스를 작성해줍니다.



OAuth2Response

public class NaverResponse implements OAuth2Response {

    private final static String RESPONSE = "response";
    private final static String ID = "id";
    private final static String NAME = "name";
    private final static String EMAIL = "email";
    private final static String PROVIDER = "naver";
    private final Map<String, Object> attribute;

    public NaverResponse(Map<String, Object> attribute) {
        this.attribute = (Map<String, Object>) attribute.get(RESPONSE);
    }

    @Override
    public String getProvider() {
        return PROVIDER;
    }

    @Override
    public String getProviderId() {
        return Optional.ofNullable(attribute.get(ID))
                .map(Object::toString)
                .orElse("");
    }

    @Override
    public String getEmail() {
        return Optional.ofNullable(attribute.get(EMAIL))
                .map(Object::toString)
                .orElse("");
    }

    @Override
    public String getName() {
        return Optional.ofNullable(attribute.get(NAME))
                .map(Object::toString)
                .orElse("");
    }
}

한 애플리케이션에 네이버, 카카오, 구글 로그인와 같이 소셜 로그인이 다양하게 포함되어 있기에 공통되는 메서드들을 추상화 해놨습니다.

응답 데이터 클래스까지 작성했다면 이제 사용자를 로그인 시키고 받아온 사용자 정보를 토대로 데이터베이스에 저장 하는 로직을 작성해봅시다.



public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

                // provider 이름을 가져와 변수에 저장합니다.
        String registerationId = userRequest.getClientRegistration().getRegistrationId();

                // provider 이름을 토대로 응답데이터 클래스가 생성됩니다.
        OAuth2Response oAuth2Response = whichProvider(oAuth2User, registerationId);

        UserDto userDto = UserDto.builder()
                .email(oAuth2Response.getEmail())
                .nickname(oAuth2Response.getName())
                .role("ROLE_USER")
                .provider(oAuth2Response.getProvider())
                .build();

        return new CustomOAuth2User(userDto);
    }
}

사용자 정보를 로드하는 부분을 커스터마이징 하기위해 DefaultOAuth2UserService를 상속받습니다.

그 후 loadUser 메서드를 오버라이딩합니다.

loadUser 메서드는 인증 과정에서 사용자의 정보를 가져오는 역할을 합니다.

OAuth2User - 인증을 통해 가져온 사용자 정보를 관리하는 데 사용되는 인터페이스입니다.

OAuth2UserRequest - 사용자의 요청 정보입니다. 인가 된 액세스토큰, 제공자, redirect-uri, 클라이언트 시크릿 등 정보를 담고있습니다.

OAuth2User oAuth2User = super.loadUser(userRequest);

해당 코드는 DefaultOAuth2UserService에서 구현되어있는 loadUser 메서드를 사용하여 사용자의 요청 정보를 토대로 OAuth2User 객체를 생성하는 코드입니다.

→ 하지만 이 과정에서 액세스 토큰이 유효하지 않는 등 사용자 정보가 불명확할 수 있기때문에 OAuth2AuthenticationException 예외를 던지게 됩니다.

CustomOAuth2User - 클라이언트에서 사용자에게 응답해줄 데이터를 정의하기위해 OAuth2User 인터페이스를 상속받아 구현한 클래스입니다. (loadUser 메서드의 반환타입이 OAuth2User 이기때문에 OAuth2User를 상속받고 구현해야합니다.)

그리고 해당 서비스 클래스에서 가입 유무를 판별해 회원가입 시키는 DB저장 로직 등을 추가하면 됩니다.



OAuth2User

public class CustomOAuth2User implements OAuth2User {

    private final UserDto userDto;

    @Override
    public Map<String, Object> getAttributes() {
        return Map.of();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userDto.getRole();
            }
        });

        return collection;
    }

    @Override
    public String getName() {
        return userDto.getNickname();
    }

    public String getEmail() {
        return userDto.getEmail();
    }

    public String getProvider() {
        return userDto.getProvider();
    }
}



SecurityConfig

loadUser 메서드를 통해 사용자 정보가 성공적으로 로딩이 되었거나 실패 했을 경우 후처리를 해주기 위해 handler를 빈으로 등록합니다.

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    private final CustomOAuth2UserService customOAuth2UserService;
        private final CustomSuccessHandler customSuccessHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .oauth2Login(oauth2 -> oauth2
                        .userInfoEndpoint(userInfoEndpointConfig ->
                                userInfoEndpointConfig.userService(customOAuth2UserService))
                        .successHandler(customSuccessHandler)
                        .failureHandler(failureHandler()));

        return http.build();
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler failureHandler() {
        return new SimpleUrlAuthenticationFailureHandler("/auth/loginFailure");
    }
}

oauth2Login() - OAuth 2.0 로그인을 설정하는 메서드입니다. 이 메서드를 통해 로그인 프로세스 전반에 걸친 설정을 정의할 수 있습니다

userInfoEndpoint() - 해당 메서드는 로그인 과정에서 사용자 정보를 처리하는 방법을 정의합니다. 이전에 작성했던 customOAuth2UserService를 지정해줍니다.

successHandler() - 로그인 성공 후 호출될 핸들러를 설정합니다.

failureHanlder() - 로그인 실패 후 호출될 핸들러를 설정합니다. (failure Handler는 SimpleUrlAuthenticationFailureHandler 를 통해 간단하게 리다이렉트 될 url만 지정했습니다.)

success handler는 onAuthenticationSuccess 메서드를 오버라이딩 받아 구현해야 하기에 따로 클래스를 작성해주도록 합니다.



CustomSuccessHandler

@Component
public class CustomSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

    }
}

핸들러 클래스에서도 마찬가지로 소셜 로그인에 성공하게 되면 다른 url로 리다이렉트 시킨다든지 후처리를 해주는 코드를 작성해주시면 됩니다.



클라이언트 사이드에서 로그인 URL 지정

const onNaverLogin = () => {
  window.location.href = "<http://localhost:8080/oauth2/authorization/naver>";
}

const onGoogleLogin = () => {
  window.location.href = "<http://localhost:8080/oauth2/authorization/google>";
}

const onKakaoLogin = () => {
  window.location.href = "<http://localhost:8080/oauth2/authorization/kakao>";
}

function App() {
  return (
    <Router>
      <div className="App">
        <header className="App-header">
          <button onClick={onNaverLogin}>NAVER LOGIN</button>
          <button onClick={onGoogleLogin}>GOOGLE LOGIN</button>
          <button onClick={onKakaoLogin}>KAKAO LOGIN</button>
          <Routes>
            <Route path="/register" element={<Register />} />
          </Routes>
        </header>
      </div>
    </Router>
  );
}

export default App;

마지막으로 클라이언트 사이드에서 로그인 요청 Url을 http://도메인명/oauth2/authorization/{provider} 와 같이 일치 시켜주어야 합니다.

도메인명 뒤에 입력된 /oauth2/authorization/{provider} 또한 스프링 시큐리티 OAuth Client 에서 내부적으로 정해져있는 url입니다.

뒤의 provider 이름은 맨 처음에 정의 했었던 application.yml 의 registration의 실제 id값으로 지정해주어야 합니다.

'BackEnd' 카테고리의 다른 글

마이크로 서비스 간 분산추적 with Zipkin  (1) 2024.10.08
Spring Cloud로 MSA 구축해보기  (3) 2024.09.30
API Versioning  (1) 2024.09.02
[Spring] AOP  (0) 2023.08.29
[Java] String클래스의 메소드들  (0) 2023.08.22