~☆~ 우하하!!~ 개발블로그

[SpringBoot][소셜로그인] Naver 로그인 구현 (1차 시도) 본문

SpringBoot

[SpringBoot][소셜로그인] Naver 로그인 구현 (1차 시도)

iwoohaha 2024. 11. 19. 10:58
반응형

일단, 제목에 1차 시도라고 굳이 밝히고 있는 이유는 Naver 계정을 이용한 로그인 구현이 완료될 수 없는 지경임을 확인했기 때문이다.

이번 포스트에서는 Naver 계정으로 로그인하는 기본 절차만 확인하고, 그 결과에 따라 회원정보 테이블 구조를 변경하는 작업을 수행한 후에 완료지을 예정이다.

네이버 계정 로그인을 위한 네이버 개발자센터 설정내용은 https://iwoohaha.tistory.com/336 을 참고하자.

로그인 폼에 네이버 계정 로그인을 위한 버튼을 추가한다. 별다른 설명없이 로그인폼에 배치한 구글 로그인 버튼의 스타일을 변경해봤는데, https://iwoohaha.tistory.com/333 에서 설명하는 css scanner 확장 프로그램의 도움을 받았다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.tyhmeleaf.org">
<head>
    <meta charset="UTF-8">

    <!-- 모바일에서의 적절한 반응형 동작을 위해 -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- -->


    <title>Diary</title>

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
          rel="stylesheet"
          integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
          crossorigin="anonymous"
    >
    <!-- -->

    <!-- head 태그에 다음의 코드를 삽입 -->
    <style type="text/css">

        html,
        body {
            height: 100%;
        }

        .form-signin {
            max-width: 330px;
            padding: 1rem;
        }

        .form-signin .form-floating:focus-within {
            z-index: 2;
        }

        .form-signin input[type="email"] {
            margin-bottom: -1px;
            border-bottom-right-radius: 0;
            border-bottom-left-radius: 0;
        }

        .form-signin input[type="password"] {
            margin-bottom: 10px;
            border-top-left-radius: 0;
            border-top-right-radius: 0;
        }

div {
     margin: 0px;
     padding: 0px;
}

.css-1n7nx3r {
     margin-top: 24px;
}

.css-1n7nx3r::before {
     color: rgb(94, 108, 132);
     content: attr(data-i18n-continue);
     display: block;
     font-size: 14px;
     line-height: 16px;
     margin-bottom: 16px;
     font-weight: 600;
     text-align: center;
}

.css-1vymulm:not(:last-child) {
     margin-bottom: 8px;
}

button {
     font-family: inherit;
}

.css-1bthe7p {
     -webkit-box-align: baseline;
     align-items: baseline;
     box-sizing: border-box;
     display: inline-flex;
     font-size: inherit;
     font-style: normal;
     font-family: inherit;
     max-width: 100%;
     position: relative;
     text-align: center;
     text-decoration: none;
     transition: background 0.1s ease-out, box-shadow 0.15s cubic-bezier(0.47, 0.03, 0.49, 1.38);
     white-space: nowrap;
     cursor: pointer;
     padding: 0px 10px;
     vertical-align: middle;
     width: 100%;
     -webkit-box-pack: center;
     justify-content: center;
     box-shadow: none;
     font-weight: bold;
     border: 1px solid rgb(193, 199, 208);
     border-radius: 3px;
     color: var(--ds-text, #42526E) !important;
     height: 40px !important;
     line-height: 40px !important;
     background: rgb(255, 255, 255) !important;
}

.css-1bthe7p:visited {
     background: var(--ds-background-neutral, rgba(9, 30, 66, 0.04));
     color: var(--ds-text, #42526E) !important;
}

.css-1bthe7p:hover {
     background: var(--ds-background-neutral-hovered, rgba(9, 30, 66, 0.08));
     text-decoration: inherit;
     transition-duration: 0s, 0.15s;
     color: var(--ds-text, #42526E) !important;
}

.css-1bthe7p:active {
     background: var(--ds-background-neutral-pressed, rgba(179, 212, 255, 0.6));
     transition-duration: 0s, 0s;
     color: var(--ds-text, #0052CC) !important;
}

.css-1bthe7p:focus {
     outline: 2px solid var(--ds-border-focused, #2684FF);
     outline-offset: 2px;
}

.css-1bthe7p:focus-visible {
     outline: 2px solid var(--ds-border-focused, #2684FF);
     outline-offset: 2px;
}

@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
    .css-1bthe7p:focus-visible {
         outline: solid 1px;
    }
}
 .css-1bthe7p span {
     -webkit-box-pack: center;
     justify-content: center;
     display: flex !important;
}

.css-1bthe7p span {
     -webkit-box-flex: unset;
     flex-grow: unset;
}

.css-1ti50tg {
     opacity: 1;
     transition: opacity 0.3s;
     display: flex;
     margin: 0px 2px;
     -webkit-box-flex: 0;
     flex-grow: 0;
     flex-shrink: 0;
     align-self: center;
     font-size: 0px;
     line-height: 0;
     user-select: none;
     margin-inline-start: var(--ds-space-negative-025, -2px);
}

 img {
     margin: 0px;
     padding: 0px;
}

img {
     border: 0px;
}

.css-1bthe7p img {
     height: 24px;
     width: 24px;
     margin-right: 6px;
}

.css-178ag6o {
     opacity: 1;
     transition: opacity 0.3s;
     margin: 0px 2px;
     -webkit-box-flex: 1;
     flex-grow: 1;
     flex-shrink: 1;
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
}

@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
    .css-1bthe7p:focus-visible {
         outline: solid 1px;
    }
}
@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
    .css-1bthe7p:focus-visible {
         outline: solid 1px;
    }
}
@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
    .css-1bthe7p:focus-visible {
         outline: solid 1px;
    }
}
    </style>

</head>
<body>

<main class="form-signin w-100 m-auto">
    <th:block th:if="${error}">
        <div th:if="${message}" class="alert alert-danger" role="alert" th:text="${message}"></div>
    </th:block>
<!--    <form th:action="@{/process_login}" method="POST">-->
    <form th:action="@{/signin}" method="POST">
        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>

        <div class="form-floating">
            <input type="email" class="form-control" id="floatingInput" name="email" placeholder="name@example.com">
            <label for="floatingInput">Email address</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" id="floatingPassword" name="password" placeholder="Password">
            <label for="floatingPassword">Password</label>
        </div>

<!--        <div class="form-check text-start my-3">-->
<!--            <input class="form-check-input" type="checkbox" value="yes" name="remember-me" id="flexCheckDefault">-->
<!--            <label class="form-check-label" for="flexCheckDefault">-->
<!--                Remember me-->
<!--            </label>-->
<!--        </div>-->

        <div class="d-grid gap-2">
            <button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
            <a href="/signup">
                <button class="btn btn-success w-100 py-2" type="button">Sign up</button>
            </a>
            <div data-i18n-or="또는" data-i18n-continue="또는 다음을 사용하여 계속하기" class="google-login social-login css-1n7nx3r" data-testid="social-login-wrapper" style="">
                <div data-testid="social-login-button-row" class="css-1vymulm">
                    <div class="css-1vymulm">
                        <a href="/oauth2/authorization/google">
                        <button id="google-auth-button" class="css-1bthe7p" tabindex="0" type="button" style="">
                            <span class="css-1ti50tg">
                                <img src="https://id-frontend.prod-east.frontend.public.atl-paas.net/assets/google-logo.5867462c.svg" alt="">
                            </span>
                            <span class="css-178ag6o">Google</span>
                        </button>
                        </a>
                    </div>
                    <div class="css-1vymulm">
                        <a href="/oauth2/authorization/naver">
                            <button id="naver-auth-button" class="css-1bthe7p" tabindex="0" type="button">
                                <span class="css-1ti50tg">
                                <img src="/images/naverlogo.png" alt="Naver Logo">
                                </span>
                                <span class="css-178ag6o">Naver</span>
                            </button>
                        </a>
                    </div>
                </div>
            </div>
        </div>
        <p class="mt-5 mb-3 text-body-secondary">© 2017–2024</p>
    </form>
</main>

<!-- Popper -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
        integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
        crossorigin="anonymous"></script>

<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
        integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
        crossorigin="anonymous"></script>

</body>
</html>

위 코드로 구현된 로그인폼 화면의 모습은 아래와 같다.

네이버 로그인 버튼은 구글 로그인 버튼의 스타일을 최대한 따랐고, 네이버 로고 이미지는 내가 직접 작업해서 /src/main/resources/static/images 경로에 넣어두었다.

위 경로에 저장된 이미지 파일의 액세스를 위해서 SecurityConfig.java 의 filterChain 함수에 허용 디렉토리 목록에 /images/** 를 추가한 것도 밝혀둔다.

새로 추가한 네이버 로그인 버튼의 URL 은 구글 버튼의 URL 과 비슷하게 /oauth2/authorization/naver 이다.

이번에는 application.yml 에 네이버 로그인 기능에 필요한 설정값을 추가한다.

spring:
  security:
    oauth2:
      client:
        registration:
          naver:
            client-id: NFgdRo9T7HjogFEdUqSu
            client-secret: X7EDZmWMCx
            scope:
              - name
              - email
            client-name: Naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
        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

          google:
            client-id: 528898101025-895po5vpn68pdkmuqj4aq314h2h0eob4.apps.googleusercontent.com
            client-secret: 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
            scope:
              - profile
              - email
#              - openid
...

google 로그인 설정과는 다르게 provider 관련 설정값도 추가해야 한다는 것을 잊지 말자.

여기까지 완료하였으면 CustomOAuth2UserService 의 loadUser 함수에 BreakPoint 를 걸어놓고 디버그로 실행시켜보자.

네이버에 로그인된 상태가 아니라면 네이버 로그인 버튼을 클릭했을 때 네이버 로그인 화면이 표시된다.

로그인을 하면 아래와 같이 동의 화면이 표시된다.

전체 동의하기에 체크를 하고 동의하기 버튼을 클릭하면 loadUser 에 걸어두었던 BreakPoint 에 걸리게 된다.

굳이 여기에 BreakPoint 를 걸어둔 이유는 구글 로그인의 경우와는 다른 데이터 구조를 확인하기 위함이다.

oAuthUser 는 정보 제공자인 naver 가 제공해준 정보인데, 아래에서 볼 수 있는 것처럼 response KEY 를 통해서 id, profile_image, name 등의 값을 구할 수 있는 구조이다.

구글 로그인시 구글이 제공하는 정보 구조와 네이버 로그인시 네이버가 제공하는 정보 구조가 다르므로 provider 값에 따른 분기처리가 필요한 상황이다.

그런데, diary 프로그램의 회원구조는 email 주소가 PK 로 정의되어 있는데, 네이버는 네이버 계정 이메일 주소를 제공해주지 않으므로 회원정보 관리 구조에 대한 대대적인 개편이 필요하게 되었다.

반응형