본문 바로가기
프로젝트/Kimstagram

[내멋대로 만드는 Kimstagram] 12. OAuth2를 활용한 페이스북 로그인

by kim-dev 2024. 1. 29.
반응형

이제 슬슬 외부에서 제공하는 API를 활용하는 법을 알아야 할 것 같아서 페이스북 로그인을 구현해보려고 한다.
사실 조금 찾아보니까 OAuth2를 활용하면 굉장히 간단하게 구현할 수 있는 것 같은데... ㅋㅋㅋㅋ 크게 도움이 될 진 모르겠지만....


https://developers.facebook.com/

 

Meta for Developers

개발자를 위한 필요한 조치 대시보드 도입 이번 달부터 developers.facebook.com에서 앱을 관리하는 몇몇 앱 관리자를 대상으로 필요한 조치 대시보드를 공개합니다. 데이터 사용 확인을 위한 데이터

developers.facebook.com

우선 위 링크로 들어가면 Meta(페이스북)의 개발자 센터가 나온다.
빠르게 로그인해준 후, 앱을 만들어 주었다.

 

그리고 이제 내 앱으로 들어가서 기본 설정으로 들어가면, 나의 앱ID와 시크릿 코드를 확인할 수 있다.

 

그럼 이제 이 앱ID와 시크릿 코드를 application.properties에 입력해준다.

spring.security.oauth2.client.registration.facebook.client-id=(앱ID)
spring.security.oauth2.client.registration.facebook.client-secret=(시크릿 코드)
spring.security.oauth2.client.registration.facebook.scope=public_profile, email

 

SecurityConfig를 수정하여 Spring Security에게 oauth2를 사용하고 있음을 알려주자.

http.authorizeRequests()
                .antMatchers("/checkToken").access(("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')"))
                .antMatchers("/**", "/index", "/auth/**", "/login", "/join", "/js/**", "/dynamicImage/**", "/image/**", "/favicon.ico").permitAll()
                .anyRequest().authenticated()
                .and().oauth2Login()
                .and().exceptionHandling().authenticationEntryPoint(((request, response, authException) -> response.sendRedirect("/")));

 

그리고 페이스북 로그인 버튼의 url을 "/oauth2/authorization/facebook"로 설정해준다.

<!-- 페이스북 로그인 버튼 -->
<a href="/oauth2/authorization/facebook"><img src="/image/login/log_fc.png" id="facebookButton" onmousedown="index.facebookClick()"/></a>

 

됐다. 이제 저 버튼을 클릭하면 바로 페이스북 로그인 페이지로 이어져서 로그인할 수 있게 된다.
그렇다면 로그인 후 응답이 어디로 오는 걸까? 그걸 서버에서 받아야 유저 정보를 DB에 등록하던가 하는데...

 

바로 DefaultOAuth2UserService 의 loadUser() 메서드이다!
이걸 오버라이드해서 구현해주면, 페이스북 로그인 응답으로 오는 데이터를 다룰 수 있게 된다.

package com.kimdev.kimstagram.service;

import com.kimdev.kimstagram.Repository.AccountRepository;
import com.kimdev.kimstagram.controller.api.auth.PrincipalDetail;
import com.kimdev.kimstagram.model.Account;
import com.kimdev.kimstagram.model.RoleType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class OAuth2Service extends DefaultOAuth2UserService {

    @Autowired
    AccountRepository accountRepository;

    @Autowired
    private BCryptPasswordEncoder encoder;

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

        String provider = userRequest.getClientRegistration().getRegistrationId(); // facebook
        String providerId = oAuth2User.getAttribute("id"); // Facebook의 고유 id를 가져옴


        Account account = accountRepository.findByProviderId(providerId);
        if(account == null) { // 해당 유저가 없을 경우
            String username = provider+"_"+providerId; // facebook_12345678 양식
            String password = encoder.encode(UUID.randomUUID().toString());
            String email = oAuth2User.getAttribute("email");
            String name = oAuth2User.getAttribute("name");

            account = new Account();
            account.setUsername(username);
            account.setPassword(password);
            account.setEmail(email);
            account.setName(name);
            account.setUse_profile_img(0);
            account.setRole(RoleType.ROLE_USER);

            accountRepository.save(account);
        }

        return new PrincipalDetail(account, oAuth2User.getAttributes());
    }
}

기존의 유저도 PrincipalDetail을 반환했으니, 페이스북으로 로그인하는 유저도 PrincipalDetail을 반환하도록 했다.
그래서 PrincipalDetail 역시 아래와 같이 수정해 주었다.

package com.kimdev.kimstagram.controller.api.auth;

import com.kimdev.kimstagram.model.Account;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

@Getter
public class PrincipalDetail implements UserDetails, OAuth2User {

    private Account account; // 로그인을 시도하는 Account
    private Map<String, Object> attributes; // 페이스북 로그인이라면 이것도 들어옴

    public PrincipalDetail(Account account) {
        this.account = account;
    }

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

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

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

        return collectors;
    };

    @Override
    public String getPassword() {
        return account.getPassword();
    }

    @Override
    public String getUsername() {
        return account.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String getName() {
        return null;
    }
}

 

여튼 이러면 페이스북 로그인 자체는 끝난다.
그런데... 아마 로그인을 해도 /index로 넘어가지 않을 것이다.
왜냐? 기존에는 로그인 성공 시 Jwt 토큰을 받아서 js에서 /index로 리다이렉트 해주었는데,
페이스북 로그인에서는 우리가 적용한 이 js의 통제를 받지 않으니 토큰도 못 받고 인덱스로 리다이렉팅도 안 되는 것...

 

그럼 즉, OAuth2의 로그인이 성공했을 경우를 처리해주는 메서드를 구현해서 적용해주면 된다.
SpringConfig에서 아래와 같이 oauth2Login()이 성공하면 OAuth2Service의 loginSuccess()를 타게 해준다.

http.authorizeRequests()
    .antMatchers("/checkToken").access(("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')"))
    .antMatchers("/**", "/index", "/auth/**", "/login", "/join", "/js/**", "/dynamicImage/**", "/image/**", "/favicon.ico").permitAll()
    .anyRequest().authenticated()
    .and().oauth2Login()
        .successHandler(((request, response, authentication) -> {
        oAuth2Service.loginSuccess(request, response, authentication);
    }))
    ...

 

loginSuccess()는 다음과 같이 만들었다.

public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
    PrincipalDetail principalDetail = (PrincipalDetail) authentication.getPrincipal();

    Account principal = principalDetail.getAccount();
    int principalId = principal.getId();
    String username = principal.getUsername();

    if (username.startsWith("facebook")) {
        response.sendRedirect("/facebook/facebookSetUsername");
    } else {
        String AccessToken = makeAccessToken(principalId, username);
        String RefreshToken = makeRefreshToken(principalId, username);
        response.sendRedirect("/facebookAuth?acesstoken="+AccessToken+"&refreshtoken="+RefreshToken);
    }

}

public String makeAccessToken(int principalId, String username) {
    // Hash 암호 방식으로 JWT토큰 생성 (RSA방식은 아님)
    String jwtToken = JWT.create()
            .withSubject("JWT_TOKEN")
            .withExpiresAt(new Date(System.currentTimeMillis() + (60000*10))) // 토큰의 만료 시간 (10분)
            .withClaim("id", principalId) // 비공개 클레임인데 그냥 값 암거나 넣으면 될 듯?
            .withClaim("username", username)
            .sign(Algorithm.HMAC512("kimdevAuth")); // 토큰에 붙일 고유한 Secret 값

    return "Bearer " + jwtToken;
}

public String makeRefreshToken(int principalId, String username) {
    Account account = accountRepository.findById(principalId).get();

    // 리프레시 토큰 생성
    String refreshToken = JWT.create()
            .withSubject("JWT_TOKEN")
            .withExpiresAt(new Date(System.currentTimeMillis() + (60000*60 * 24))) // 24시간 동안 유효
            .withClaim("refresh", "refresh")
            .sign(Algorithm.HMAC512("kimdevRefresh"));

    securityService.saveRefreshToken(refreshToken, account);

    return "Bearer " + refreshToken;
}

받은 principalId와 username으로 토큰들을 만들어서 쿼리스트링으로 반환한다.

 

그 전에, username이 "facebook"으로 시작한다면 그 유저네임은 "facebook_1234567" 이런 형식이라는 건데, 이건 보기 흉하므로 수정할 수 있도록 "/facebookSetUsername"으로 이동하게 해 주었다.

여기서는 아래와 같이 자신의 Username을 수정할 수 있다.

facebookSetUsername.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<head>
    <%@ include file="../../layout/indexHeader.jsp"%>

    <style>
        #button {
            width: 100px;
            height: 30px;
            text-align: center;
            padding-top: 1px;
            background-color: #0095f6;
            border: 1px solid #ededed;
            color: white;
            cursor: pointer;
        }
        #button:hover {
            background-color: #1877f2;
        }
        #button:active {
            background-color: #4cb5f9;
        }
    </style>
</head>

<body>
    <div style="width: 100%; height: 50%; display: flex; flex-direction: column; justify-content: center; align-items: center">
        <div style="font-size: 26px; font-weight: bold">
            아이디 설정
        </div>
        <div style="margin-bottom: 20px">
            kimstagram에서 사용할 아이디를 입력해 주세요.
        </div>
        <input id="input" style="width: 300px; margin-bottom: 10px">
        <div id="button" onclick="setUsername('${oriusername}')">
            확인
        </div>

        <div id="using" style="display: none; color: #ed4956; margin-top: 30px">
            이미 사용 중인 아이디입니다.
        </div>
    </div>
</body>


<script src="/js/facebookSetUsername.js"></script>

 

facebookSetUsername.js

function setUsername(oriusername) {
    let data = {
        oriusername: oriusername,
        newusername: document.getElementById('input').value
    };

    $.ajax({
        type: "GET",
        url: "/setUsername",
        data: data,
        contentType: "application/json; charset=utf-8"
    }).done(function(resp) {
        if (resp === 0) { // 이미 해당 유저네임이 존재
            document.getElementById('input').value = "";
            document.getElementById('using').style.display = "block";
        } else {
            alert("아이디 설정이 완료되었습니다.\r\n로그인을 다시 진행해 주세요.");
            location.href = "/";
        }
    });
}

뭐 이렇게 간단하게 작성했다.

 

그러면 진짜 로그인이 완료되고, 유저네임까지 제대로 설정했다면?!
액세스 토큰과 리프레시 토큰을 들고 /facebookAuth로 이동해서, LocalStorage에 토큰들을 저장한 후 /index로 이동하면 된다!

@GetMapping("/facebook/facebookAuth")
public String facebookAuth(Model model, @RequestParam String acesstoken, @RequestParam String refreshtoken) {
    model.addAttribute("acess", acesstoken);
    model.addAttribute("refresh", refreshtoken);

    return "facebook/facebookAuth";
}

이렇게 컨트롤러에서 토큰들을 받아서 Model에 담아 facebookAuth.jsp로 보낸 뒤

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<head>
    <%@ include file="../../layout/indexHeader.jsp"%>
</head>

<body>
    <div style="width: 100%; height: 50%; display: flex; flex-direction: column; justify-content: center; align-items: center">
        <div style="font-size: 26px">
            잠시만 기다려 주세요...
        </div>
        <script>
            localStorage.setItem('Authorization', '${access}')
            localStorage.setItem('Refresh-Token', '${refresh}')
            location.href = "/index";
        </script>
    </div>
</body>


<script src="/js/facebookSetUsername.js"></script>

facebookAuth.jsp에서는 이렇게 Model에 담긴 토큰을 받아 LocalStorage에 저장한 후 /index로 리다이렉팅 해주면 된다.

 

이렇게 짜면

이런 폼이 뜨면서 LocalStorage에 토큰들을 저장하게 되고...

저장이 완료된다면?!!

이렇게 인덱스 페이지로 리다이렉팅 된다 ㅎㅎㅎ

 

 

어찌저찌... 힘들게 구현 완료한 것 같다...
어떻게 OAuth2를 쓰고도 이렇게 힘들게 할 수 있지?? ㅋㅋㅋ
Jwt토큰 발급하는 과정이 항상 힘든 것 같다. 특히 서버에서 만든 토큰을 클라이언트로 넘기는 방법을 고안하는 게 제일 힘든 듯... ㅋㅋㅋㅋ

 

뭐 여튼 성공했으니~~ 이만 쉬러 가야지 ㅎㅎ

 


 

 

OAuth 2.0 + JWT + Spring Security로 회원 기능 개발하기 - 앱등록과 OAuth 2.0 기능구현

OAuth 2.0 로그인 기능을 구현하기 위해서는 꽤나 많은 작업이 필요합니다.이번 파트에서는 네이버, 카카오, 구글에 앱을 등록하고 OAuth 2.0을 본격적으로 사용할 수 있는 준비를 해보겠습니다.구글

velog.io

 

 

[Spring Security] 6. OAuth2 Google Login ②

Spring security 공부

velog.io

 

 

[Spring Boot] OAuth 2.0 로그인 (카카오, 네이버, 페이스북 로그인)

[Spring Boot] OAuth 2.0 로그인 (구글 로그인)에서 구글 로그인과 회원가입에 대해 정리했었음 구글 뿐 아니라 카카오, 네이버, 페이스북 로그인에 대해서 정리 과정이 거의 비슷하긴 하지만 조금씩

chb2005.tistory.com