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

[SpringBoot][소셜로그인] Facebook 으로 로그인/회원가입 본문

SpringBoot

[SpringBoot][소셜로그인] Facebook 으로 로그인/회원가입

iwoohaha 2024. 11. 23. 09:26
반응형

Facebook 계정으로 로그인을 구현하기 위해서 선행되어야 할 작업은 Facebook 개발자 센터에서 앱 설정 작업이다. 이 작업 내용은 https://iwoohaha.tistory.com/342 을 참고하면 된다.

application.yml 에 기록해야 할 설정값의 내용은 Google 로그인의 경우와 비슷하다. 비교를 위해서 Google 의 설정값을 함께 예시로 보여주고 있다.

spring:
  security:
    oauth2:
      client:
        registration:
          facebook:
            client-id: # Facebook 애플리케이션 ID
            client-secret: # Facebook 애플리케이션 Secret
            scope:
              - public_profile
              - email
              
          google:
            client-id: 528898101025-895po5vpn68pdkmuqj4aq314h2h0eob4.apps.googleusercontent.com
            client-secret: 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
            scope:
              - profile
              - email
#              - openid

login.html 페이지에 Facebook 로그인하기 버튼을 추가한다. 앞서 작성한 Google, Naver, Kakao 로그인하기 버튼에 사용된 각 소셜 로그인 로고 이미지를 svg 파일 사용 방식으로 변경하고, 버튼에 사용된 css 스타일도 조금 수정해보았다.

<!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-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;
    }

    .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;
    }

    .css-google-button {
         -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-naver-button {
         -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: #04C75B !important;
    }

    .css-kakao-button {
         -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: #fee500 !important;
    }

    .css-facebook-button {
         -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: #1877F2 !important;
    }
</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-google-button" tabindex="0" type="button">
                                <span class="css-1ti50tg">
                                    <img src="/images/googlelogo.svg" alt="Google Logo" style="width: 24px; height: 24px; margin-right: 10px;">
                                </span>
                                <span>Google</span>
                            </button>

                        </a>
                    </div>
                    <div class="css-1vymulm">
                        <a href="/oauth2/authorization/naver">

                            <button id="naver-auth-button" class="css-naver-button" tabindex="0" type="button">
                                <span class="css-1ti50tg">
                                    <img src="/images/naverlogo.svg" alt="Naver Logo" style="width: 34px; height: 34px; margin-right: 10px;">
                                </span>
                                <span>Naver</span>
                            </button>

                        </a>
                    </div>
                    <div class="css-1vymulm">
                        <a href="/oauth2/authorization/kakao">

                            <button id="kakao-auth-button" class="css-kakao-button" tabindex="0" type="button">
                                <span class="css-1ti50tg">
                                    <img src="/images/kakaologo.svg" alt="Kakao Logo" style="width: 34px; height: 34px; margin-right: 10px;">
                                </span>
                                <span>Kakao</span>
                            </button>

                        </a>
                    </div>

                    <div class="css-1vymulm">
                        <a href="/oauth2/authorization/facebook">

                            <button id="facebook-auth-button" class="css-facebook-button" tabindex="0" type="button">
                                <span class="css-1ti50tg">
                                    <img src="/images/facebooklogo.svg" alt="Facebook Logo" style="width: 24px; height: 24px; margin-right: 10px;">
                                </span>
                                <span style="color: white;">Facebook</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>

외관상으로는 큰 차이가 나지는 않지만, css 전문가가 아니다보니, 주먹구구식으로 가져다가 쓴 코드가 지저분해서 나름 조금 정리를 해본 셈이다.

새로 추가한 Facebook 버튼의 URL 은 /oauth2/authorization/facebook 이다.

여기에서 사용하는 각 소셜 로그인 버튼의 svg 파일은 zip 파일로 묶어서 올려둔다.

sociallogos.zip
0.01MB

 

Facebook 버튼을 클릭했을 때 해당 정보제공자로부터 어떤 형식의 데이터가 전달되는지를 보려면 앞에서 해봤던 것처럼 CustomOAuth2UserService 의 loadUser 함수에 BreakPoint 를 걸고 Debug 모드로 실행시키면 된다.

위 스크린샷에서 볼 수 있는 것처럼 id, name, email 이라는 키값으로 이루어진 attributes 를 참고하면 되는 것으로 확인된다.

이런 구성의 데이터를 가져올 수 있는 FacebookOAuth2UserInfo 클래스를 아래와 같이 구현한다.

package com.woohahaapps.study.diary.domain;

import java.util.HashMap;
import java.util.Map;

public class FacebookOAuth2UserInfo implements OAuth2UserInfo {
    private final Map<String, Object> attributes;

    public FacebookOAuth2UserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    @Override
    public String getProvider() {
        return "facebook";
    }

    @Override
    public String getProviderId() {
        return (String) attributes.get("id");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }

    @Override
    public String getProfileImageUrl() {
        return "";
    }

    @Override
    public Map<String, Object> convertToMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("provider", getProvider());
        map.put("providerid", getProviderId());
        map.put("email", getEmail());
        map.put("name", getName());
        map.put("profileimageurl", getProfileImageUrl());
        return map;
    }
}

loadUser 함수에는 FacebookOAuth2UserInfo 클래스를 사용하는 코드를 추가한다.

...
        OAuth2UserInfo oAuth2UserInfo = null;
        if (provider.equals("google")) {
            oAuth2UserInfo = new GoogleOAuth2UserInfo(oAuth2User.getAttributes());
        } else if (provider.equals("naver")) {
            oAuth2UserInfo = new NaverOAuth2UserInfo(oAuth2User.getAttribute("response"));
        } else if (provider.equals("kakao")) {
            oAuth2UserInfo = new KakaoOAuth2UserInfo(oAuth2User.getAttributes());
        } else if (provider.equals("facebook")) {
            oAuth2UserInfo = new FacebookOAuth2UserInfo(oAuth2User.getAttributes());
        }
...

앞에서 해봤던 경험대로 여기까지만 추가한 뒤 실행시키면 나머지는 자동으로 처리될 것을 안다.

로그인 화면에서 Facebook 버튼을 클릭하면 facebook 로그인 화면으로 이동(Facebook 로그인이 되어 있지 않은 경우에만)되고,

facebook 로그인에 성공하면 로그인한 계정으로 계속할 것인지를 확인한다.

아직 Facebook 에 검수를 받지 못한 상태라서 나타나는 내용도 있는데, 이것은 상용 서버로 프로그램을 업로드하기 전에 해결해야 할 내용이다.

"승우님으로 계속" 버튼을 클릭하면 전달받은 이메일 주소로 회원가입이 필요한 경우 회원가입이 처리되고, 해당 이메일 계정으로 로그인된다.

반응형