+ 00 00 0000

Have any Questions?

Social Login Sample

Social Login Sample

📃 요약

구글 로그인 API 를 이용해서 구글 로그인 샘플을 진행해 보도록 하겠습니다.
구글 클라우드에 API 신청 및 clientId, clientSecret 로 springboot 에 설정합니다.

아울러 구글 소셜 로그인할 때 DB 에 소셜로그인 사용자를 저장해 보겠습니다.
소셜로그인은 카카오, 네이버 등 다양하게 로그인 할 수 있지만
소셜 로그인 예제 이해에 집중하기 위해 구글 로그인만 진행해 보겠습니다.
글에 언급되지 않는 일반 로그인 소스가(Spring Security) 포함되어 있습니다.

구글 로그인 등록은 생략하겠습니다.
구글 로그인 등록에 관한 것은 아래 주소를 참고하세요
( 참조 블로그 : https://deeplify.dev/back-end/spring/oauth2-social-login )

프론트는 Vue 를 사용하고, 벡엔드는 spring boot 를 사용합니다.

DB 는 오라클 도커 이미지를 사용하고 계정은 scott ( 암호 : !Ds1234567890 ) 개발자 계정을 생성하고 사용합니다.
DB 개발자 계정 및 설치하는 방법은 생략합니다.

요소 기술 :

– 프론트엔드 : Vue

– 벡엔드 : Spring Boot & JPA

– DB : Oracle 18xe(Docker)
( Oracle 18xe 도커 이미지 주소 : https://hub.docker.com/r/kangtaegyung/oraclexe-18c )

결과 화면 :

  • 로그인 화면
  • 구글 계정 선택 화면 #1
  • 구글 계정 선택 화면 #2
  • Home 화면 이동
  • 구글 로그인 완료 후 새로운 구글 사용자 테이블에 저장됨

프로젝트 탐색기 : Vue

프로젝트 탐색기 : String Boot

Front : 소셜 로그인 url 정보

메소드URL설명
GEThttp://localhost:8000/oauth2/code/googleLoginView.vue
구글 로그인 버튼 클릭시 실행될 주소
구글 클라우드에서 등록된 승인된 리다이렉트 URL 주소
GET/auth-redirectSocialRedirectView.vue
벡엔드에서 보내준 웹토큰, 유저정보를
로컬스토리지에 저장하고 동시에 Home 으로 강제 이동함

Backend : 소셜 로그인 URL 정보

메소드URL설명
GEThttp://localhost:8080/auth-redirectSocialLoginSuccess.java
소셜로그인 성공후 Vue 리다이렉트 주소로 웹토큰, 유저정보 전송할 URL 정보

구글 로그인 간단 절차

  • 1) 로그인 버튼 클릭 : 소셜 로그인 사이트에서 제공된 URL 입니다.
    • 구글 : http://벡엔드주소/oauth2/authorization/google
  • 2) 로그인 자동 인증 : 스프링 OAuth 패키지가 소셜 로그인 공급자에게 받은 인가코드로 인증 토큰을 자동적으로 받아옵니다.
  • 3) 인증 토큰을 받은 후 추가 로직을 개발자가 구현합니다.
    • DB 에 새 사용자 저장
    • Vue 로 웹토큰(JWT) , 사용자 부가 정보 전송

📃 기술 구현

스펙 :

- Vue 3.x
- 구글 API ( 참조 : https://console.cloud.google.com/apis/dashboard?project=simpledms-371707 )
- jdk 17
- spring boot 3.x
- intellij IDEA & gradle
- logging tool : logback 

테이블 설계

DROP TABLE TB_MEMBER CASCADE CONSTRAINTS;

CREATE TABLE TB_MEMBER
(
    EMAIL       VARCHAR2(1000) NOT NULL PRIMARY KEY, -- id (email)
    PASSWORD    VARCHAR2(1000),                                         -- 암호
    NAME        VARCHAR2(1000),                                         -- 유저명
    CODE_NAME   VARCHAR2(1000),                                         -- 권한코드명(ROLE_USER, ROLE_ADMIN)
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

소셜 로그인 구현을 위한 테이블 설계입니다.

데이터베이스를 오라클(Docker)을 사용하여 구현해 보겠습니다.

Spring build.gradle : dependencies 블럭 내 추가

dependencies {
    //    OAUTH2 라이브러리 추가 : 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
        ... 생략
}

Spring Security 설정 클래스

-WebSecurityConfig.java

package org.example.simpledms.config;

import jakarta.servlet.DispatcherType;
import lombok.RequiredArgsConstructor;
import org.example.simpledms.security.jwt.JwtUtils;
import org.example.simpledms.security.oauth.SocialLoginSuccess;
import org.example.simpledms.security.oauth.SocialLoginServiceCustom;
import org.example.simpledms.security.jwt.AuthTokenFilter;
import org.example.simpledms.security.services.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @fileName : WebSecurityConfig
 * @author : GGG
 * @since  : 2024-04-15
 * description :
 *  1) DB 인증을 위한 함수    : passwordEncoder()
 *  2) 패스워드 암호화 함수     : 필수 정의
 *    @Bean : IOC (스프링이 객체를 생성해주는 것), 함수의 리턴객체를 생성함
 *      => (참고) 용어 : 스프링 생성한 객체 == 빈(Bean==콩)
 *  3) JWT 웹토큰 자동인증 함수 : authenticationJwtTokenFilter()
 *  4) img, css, js 등 인증 무시 설정 함수 : webSecurityCustomizer()
 *      => 사용법 : (web) -> web.ignoring().requestMatchers("경로", "경로2"...)
 *  5) 스프링 시큐리티 규칙 정의 함수(***) : filterChain(HttpSecurity http)
 *    5-1) cors 사용
 *    5-2) csrf 해킹 보안 비활성화(쿠키/세션 사용않함)
 *    5-3) 쿠키/세션 안함(비활성화) -> 로컬스토리지/웹토큰
 *    5-4) form 태그 action 을 이용한 로그인 사용않함 -> axios 통신함
 *    5-5) /api/auth/**  : 이 url 은 모든 사용자 접근 허용, ** (하위 url 모두 포함)
 *    5-8) / : 이 url 은 모든 사용자 접근 허용
 *    5-9) TODO : 웹토큰 클래스를 스프링시큐리티 설정에 끼워넣기 : 모든 게시판 조회(CRUD)에서 아래 인증을 실행함
 *
 *  6) 소셜 로그인
 *    6-1) 소셜 로그인 성공후 처리할 리다이렉션해서 구글 인가코드 받음
 *    6-2) 구글 인가코드 확인 후에 DB 인증, 웹토큰 발행, 프론트로 전송
 */
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final SocialLoginSuccess socialLoginSuccess;

    private final SocialLoginServiceCustom socialLoginServiceCustom ;

    private final AuthTokenFilter authTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

//  img, css, js 등 인증 무시 설정 함수
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(
                "/img/**",
                "/css/**",
                "/js/**"
        );
    }

    //    TODO: 스프링 시큐리티 규칙 정의 함수(***)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.cors(Customizer.withDefaults());                         // 5-1)
        http.csrf((csrf) -> csrf.disable());                          // 5-2)
        http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // 5-3)
        http.formLogin(req -> req.disable());                         // 5-4)

        http.authorizeHttpRequests(req -> req
                .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
                .requestMatchers("/api/auth/**").permitAll()        // 5-5)
                .requestMatchers("/").permitAll()                   // 5-8)
                .anyRequest()
                .authenticated());

//        TODO: 소셜 로그인 설정 부분
        http.oauth2Login(req -> req
                .successHandler(socialLoginSuccess)                                    // 6-1)
                .userInfoEndpoint(arg -> arg.userService(socialLoginServiceCustom))    // 6-2)
        );

//        5-9)
        http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
  • TODO: 소셜 로그인 설정 부분이 Spring Security 에서 설정할 부분입니다.
  • socialLoginSuccess : 성공후에 실행될 클래스이고, Vue 로 웹토큰 , 유저정보를 전송합니다.
  • socialLoginServiceCustom : 구글 인증토큰을 받으면 실행될 클래스 이고, DB 에 새로운 소셜 사용자를 등록합니다.

소셜 로그인 클래스 :  구글 API 인가코드를 받아 인증 처리 하는 클래스

-SocialLoginServiceCustom.java

package org.example.simpledms.security.oauth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.simpledms.model.entity.auth.Member;
import org.example.simpledms.repository.auth.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Map;

/**
 * @fileName : SocialLoginServiceCustom
 * @author : kangtaegyung
 * @since  : 2022/12/16
 * description :
 *  알고리즘
 *      1) OAuth2UserService : 유저정보 있는 클래스
 *      2) OAuth2 로그인 진행시 키가 되는 필드값(PK 와 같음)
 *      3) 소셜 기본정보 DB 저장, 유저가 있으면 무시
 *      4) 소셜유저 생성 및 내보내기
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SocialLoginServiceCustom implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final MemberRepository memberRepository;

    private PasswordEncoder encoder = new BCryptPasswordEncoder();

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2UserService<OAuth2UserRequest, OAuth2User>  socialLoginService = new DefaultOAuth2UserService();        // 1)
        OAuth2User socialLogin = socialLoginService.loadUser(userRequest);
        Map<String, Object> socialUser = socialLogin.getAttributes();

        String googleKey = userRequest.getClientRegistration().getProviderDetails()                                   // 2)
                .getUserInfoEndpoint().getUserNameAttributeName();

        saveSocialIdOrSkip(socialUser);                                                                               // 3)

        return new DefaultOAuth2User(                                                                                  // 4)
                Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                socialUser,
                googleKey);
    }

//    소셜 기본정보 DB 저장 : DB 에 없으면 저장, 있으면 무시하는 함수
    private void saveSocialIdOrSkip(Map<String, Object> socialUser) {
        try {
            if(memberRepository.existsById((String) socialUser.get("email")) == false) {
                memberRepository.save(getDefaultUser(socialUser));
            }
        } catch (Exception e) {
            log.debug("saveOrUpdate 에러" ,e.getMessage());
        }
    }

    //  소셜유저를 기본정보로 생성하는 함수
    private Member getDefaultUser(Map<String, Object> socialUser) {
        return new Member( (String) socialUser.get("email"),                  // 로그인 ID
                encoder.encode("123456"),
                (String) socialUser.get("email"),                             // 이름
                "ROLE_USER"
        );
    }
}
  • 구글 API 인증토큰을 받으면 벡엔드에서 새로운 사용자로 DB 에 저장합니다.
    • 구글 인가코드 및 인증 토큰은 OAuth 스프링 라이브러리가 자동적으로 처리합니다.
    • 개발자는 구글 로그인이 성공하면 인증토큰을 자동적으로 전달받는다고 가정하고 추가적인 로직만 작성하면 됩니다.
    • 여기서는 DB 에 새로운 사용자를 저장합니다.
  • DB 에 저장시 기본정보로 저장합니다.

소셜 로그인 성공후 실행될 클래스 : 웹토큰 발급 및 프론트로 유저 정보 전송

– SocialLoginSuccess.java

package org.example.simpledms.security.oauth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.simpledms.security.jwt.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @fileName : SocialLoginSuccess
 * @author : kangtaegyung
 * @since  : 2022/12/16
 * description : 소셜 로그인 성공 후 처리할 클래스
 *    알고리즘
 *      1) 인증된 객체를 홀더에 저장
 *      2) 인증된 유저 정보를 oAuth2User(소셜로그인 클래스) 에 저장, 소셜로그인은 oAuth2User 사용
 *      3) 권한 정보 가져오기
 *      4) 토큰 발행
 *      5) 리다이렉션 페이지로 이동 : 여기서 구글 인가코드를 받을 수 있음
 *
 *   참고) 함수
 *      - UriComponentsBuilder.fromUriString("기본url")
 *                           .queryParam("키", 값)    // 쿼리스트링 변수명, 값
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class SocialLoginSuccess extends SimpleUrlAuthenticationSuccessHandler {
    private final JwtUtils jwtUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException {
        SecurityContextHolder.getContext().setAuthentication(authentication);                        // 1)
        OAuth2User socialLogin = (OAuth2User)authentication.getPrincipal();                          // 2)
        Map<String, Object> socialUser = socialLogin.getAttributes();

        List<GrantedAuthority> authorities = new ArrayList(authentication.getAuthorities());
        String codeName = authorities.get(0).toString();                                             // 3) 권한

        String jwt = jwtUtils.generateJwtToken((String)socialUser.get("email"));                     // 4)

        String targetUrl = UriComponentsBuilder.fromUriString("http://localhost:8080/auth-redirect")  // 5)
                .queryParam("accessToken", jwt)
                .queryParam("email", socialUser.get("email"))
                .queryParam("codeName", codeName)
                .build().toUriString();
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}
  • 소셜 로그인 성공 후 DB 에 새사용자가 저장되면 웹토큰을 발급해서 프론트로 유저정보와 함께 전송합니다.
  • 프론트 전송 주소는 http://localhost:8080/auth-redirect 이며 웹토큰, email, codeName(권한) 정보를 쿼리스트링 방식으로 전송합니다.

Vue 페이지

– Vue 패키지 추가 :

npm i axios

1) 공통 js

– utils/axiosDefaultConfig.js : axios 기본 설정 파일

import axios from "axios";

// axios 기본 설정
export default axios.create({
  baseURL: "http://localhost:8000/api",
  headers: {
    "Content-Type": "application/json"
  }
});

– router/index.js

import { createRouter, createWebHistory } from "vue-router";

const routes = [
  {
    path: "/",
    component: () => import("../views/HomeView.vue"),
  },
  // 로그인
  {
    path: "/login",
    component: () => import("../views/auth/LoginView.vue"),
  },
  // 회원가입
  {
    path: "/register",
    component: () => import("../views/auth/RegisterView.vue"),
  },
  // 소셜 로그인
  {
    path: "/auth-redirect",
    component: () => import("../views/auth/SocialRedirectView.vue"),
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

Vue 페이지

로그인 결과 화면 :

– views/auth/LoginView.vue

<!-- 사용법 : @submit.prevent="함수" -->
<!--       prevent : submit 의 기본 속성을 막기(다른 곳으로 이동하려는 특징)  -->

<template>
  <div>
    <div class="row justify-content-center">
      <div class="col-xl-10 col-lg-12 col-md-9">
        <div class="card mt-5">
          <div class="card-body p-0">
            <!-- {/* Nested Row within Card Body */} -->
            <div class="row">
              <div class="col-lg-6 bg-login-image"></div>
              <div class="col-lg-6">
                <div class="p-5">
                  <div class="text-center">
                    <h1 class="h4 mb-4">Welcome Back!</h1>
                  </div>
                  <form class="user" @submit.prevent="login">
                    <div class="form-group">
                      <input
                        type="email"
                        class="form-control form-control-user mb-3"
                        placeholder="이메일을 넣기"
                        name="email"
                        v-model="user.email"
                      />
                    </div>
                    <div class="form-group">
                      <input
                        type="password"
                        class="form-control form-control-user mb-3"
                        placeholder="패스워드 넣기"
                        name="password"
                        v-model="user.password"
                      />
                    </div>

                    <button class="btn btn-primary btn-user w-100 mb-3">
                      Login
                    </button>
                    <hr />
                    <a
                      href="http://localhost:8000/oauth2/authorization/google"
                      class="btn btn-google btn-user w-100 mb-2"
                    >
                      <i class="fab fa-google fa-fw"></i>&nbsp;Login with Google
                    </a>
                    <a href="/" class="btn btn-naver btn-user w-100 mb-2">
                      <i class="fa-solid fa-n"></i>&nbsp;Login with Naver
                    </a>
                    <a href="/" class="btn btn-kakao btn-user w-100 mb-3">
                      <i class="fa-solid fa-k"></i>&nbsp;Login with Kakao
                    </a>
                  </form>
                  <hr />
                  <div class="text-center">
                    <a class="small" href="/forgot-password">
                      Forgot Password?
                    </a>
                  </div>
                  <div class="text-center">
                    <a class="small" href="/register"> Create an Account! </a>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
// TODO: 1) spring 보내준 user 객체(웹토큰있음)를 로컬스토리지에 저장
// TODO:   사용법 :  localStorage.setItem(키, 값);
// TODO:     => 단, 값은 문자열만 저장됨
// TODO:   사용법 : JSON.stringify(객체) => 문자열로 바뀐 객체가 리턴됨

// TODO: 2) 공유저장소의 state / mutations 함수 접근법
// TODO:   mutations 사용법 : this.$store.commit("함수명", 저장할객체)
// TODO:     => 로그인성공 공유함수(loginSuccess(state, 유저객체)) 실행
// TODO:   state 사용법 : this.$store.state.공유속성명
// TODO:     => 공유저장소의 공유속성 접근법

// TODO: 3) 뷰의 라이프사이클
// TODO:   - mounted() : 화면이 뜰때 자동 실행 (생명주기 함수)
// TODO:   - created() : 뷰가 생성될대 자동 실행
// TODO:   - created()(1번, 뷰만 생성되면 실행) -> mounted()(2번, html 태그까지 모두 뜰때)
// TODO:     예) destoryed() : 뷰가 삭제될때 실행 (거의 사용 않함)

import AuthService from "@/services/auth/AuthService";
export default {
  data() {
    return {
      user: {
        email: "", // 로그인ID
        password: "",
      },
    };
  },
  methods: {
    async login() {
      try {
        let response = await AuthService.login(this.user);
        console.log(response.data);

        localStorage.setItem("user", JSON.stringify(response.data)); // 1)

        this.$store.commit("loginSuccess", response.data); // 2)

        this.$router.push("/");
      } catch (e) {
        this.$store.commit("loginFailure");

        console.log(e);
      }
    },
  },
  // 화면이 뜰때 실행되는 함수
  created() {
    if (this.$store.state.loggedIn == true) {
      // 로그인 상태이면 로그인 불필요
      this.$router.push("/");
    }
  },
};
</script>
<style>
@import "@/assets/css/login.css";
</style>
  • 로그인 버튼 url : 구글 API 주소 (http://localhost:8000/oauth2/code/google)
  • 상기 주소를 구글 로그인 API 신청 시 클라우드 사이트에 등록해야 합니다.

Vue 리다이렉트 페이지

– views/auth/SocialRedirectView.vue

<!-- 소셜로그인 리다이렉트 페이지 -->
<template>
  <div >
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {},
    };
  },
  mounted() {
    let url = new URL(window.location.href);
    console.log(url);

    const urlParams = url.searchParams;                // uri 정보 가져오기
    const accessToken = urlParams.get("accessToken");
    const email = urlParams.get("email");
    const codeName = urlParams.get("codeName");

    this.user = {
      accessToken: accessToken,
      email: email,
      codeName: codeName,
    };

    console.log("social user", this.user);
    
    localStorage.setItem("user", JSON.stringify(this.user));
    this.$store.commit('loginSuccess', this.user);

    this.$router.push("/");
  },
};
</script>
  • 벡엔드에서 소셜 로그인 성공하면 Vue. 로 웹토큰, 유저정보를 보내줍니다.
  • 로컬스토리지에 유저정보를 저장하고, Home 으로 강제 페이지 이동 시킵니다.

📃 결론

구글 로그인 API 를 이용해서 소셜 로그인을 진행하는 예제를 살펴보았습니다.
구글 로그인 버튼을 클릭하면 구글 API url 로 이동되고,
구글 인증이 완료되면 인가코드를 springboot 으로 전송합니다.
새 사용자를 DB 에 생성하고 웹토큰을 vue 전송해서 소셜 로그인 인증이 완료됩니다.

Spring Boot 는 @RestController 어노테이션을 이용해 구현했으며, 결과는 JSON 데이터로 리턴됩니다.
Vue 는 axios 라이브러리를 사용해 벡엔드와 통신합니다.

DB 프레임워크는 JPA 를 이용해서 sql 문을 직접 제작하지 않고 자동화기능을 이용해 구현했습니다.

구글 로그인 API 에 관심 있으시다면 Source 는 아래에서 찾을 수 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다