티스토리 뷰

728x90
반응형

목차

     

     

    OAuth 2.0

     

    OAuth(Open Authorization)란 사용자의 비밀번호 없이도 접근 권한을 받을 수 있는 개방형 표준이다.

     

    구체적으로는 보안을 위한 인증을 다른 업체에게 맡겨버리고 접근 권한만 획득하는 방식,

     

    혹은 그 방식에 대한 표준이 OAuth라고 생각하면 된다.

     

    다시 강조하지만 OAuth는 인증이 아닌 권한 부여에 대한 표준이다.

     

    나머지 특징과 Workflow에 대해서는 이전에 정리한 글로 대체한다.

     

    [Network]OAuth 2.0

     

    [Network]OAuth 2.0

    OAuth(Open Authorization)는 사용자의 비밀번호 없이도 접근 권한을 받을 수 있는 개방형 표준이다. 구체적으로는 사용자가 사용하는 앱(여기서는 클라이언트라 부른다)이 보안을 위해 인증을 다른 업

    gnidinger.tistory.com

    [Network]OAuth 2.0 Workflow

     

    [Network]OAuth 2.0 Workflow

    지난 글에서 OAuth 2.0에 대한 소개와 대략적인 일처리 흐름 및 용어에 대한 정리를 마쳤다. 2022.09.28 - [Development/Network] - [Network]OAuth 2.0 [Network]OAuth2.0 OAuth(Open Authorization)는 사용자의 비밀번호 없이

    gnidinger.tistory.com

     

    Basic Settings

     

    먼저, 이후에 필요한 라이브러리 및 패키지를 설치해야 한다.

    npm install passport passport-google-oauth20 passport-kakao passport-naver-v2 passport-jwt cors
    npm install @types/passport @types/passport-google-oauth20 @types/passport-kakao @types/passport-jwt @types/cors --save-dev

    각 패키지의 이름과 간단한 설명은 다음과 같다.

     

    • Passport: 인증을 도와주는 미들웨어
    • Passport-google-oauth20: Google OAuth 2.0 인증 전략을 위한 패키지
    • Passport-kakao: Kakao OAuth 인증 전략을 위한 패키지
    • Passport-naver-v2: Naver OAuth 인증 전략을 위한 패키지
    • Passport-jwt: JWT를 이용한 인증 전략을 위한 미들웨어
    • Cors: Cross-Origin Resource Sharing 설정을 위한 패키지

    계속해서 각 서비스의 프로젝트(앱) 생성과 클라이언트 아이디, 시크릿을 받아와야 한다.

     

    해당 과정은 스크린샷의 개수 때문에 글이 길어져 따로 분리했다. 아래로 들어가면 아주 세세하게 모든 단계가 적혀 있다.

     

    [OAuth 2.0]구글/카카오/네이버 클라이언트 아이디/시크릿 발급

     

    [OAuth 2.0]구글/카카오/네이버 클라이언트 아이디/시크릿 발급

    목차 이 글은 2023년 7월 9일 기준, OAuth 2.0 구현을 위한 클라이언트 아이디/시크릿 발급 방법을 정리한 것이다. 지금 진행 중인 노드/타입스크립트/Express로 게시판 만들기에서 길어져 분리된 글이

    gnidinger.tistory.com

    계속해서 오늘의 코드를 전부 구현하고 나면 보게 될 폴더 및 모듈 배치이다.

     

     

    /src/models/User.ts

     

    가장 먼저 변동이 적은 유저 모델부터 보자.

     

    OAuth를 구현하는 전략에는 몇 가지 선택지가 있지만,

     

    여기서 나는 한 계정에 구글, 카카오, 네이버 정보를 모두 등록하고 로그인할 수 있도록 하겠다.

     

    이를 위해 인터페이스와 스키마를 아래와 같이 고친다.

    interface IUser extends Document {
      userSeq: number;
      id: string;
      googleId?: string;
      kakaoId?: string;
      naverId?: string;
      password: string;
      name: string;
      matchPassword: (enteredPassword: string) => Promise<boolean>;
    }
    
    const userSchema: Schema<IUser> = new Schema(
      {
        userSeq: { type: Number, unique: true, required: true, default: 1 },
        id: {
          type: String,
          unique: true,
          required: true,
          match: [/^([\w-.]+@([\w-]+\.)+[\w-]{2,4})?$/, '올바른 이메일 형식을 입력해 주세요.'],
        },
        googleId: String,
        kakaoId: String,
        naverId: String,
        password: {
          type: String,
          required: true,
          validate: {
            validator: function (v: string) {
              return /^(?=.*[A-Za-z])(?=.*\d).{6,}$/.test(v);
            },
            message: (props) => `${props.value} 는 문자, 숫자를 포함해 6글자 이상이어야 합니다.`,
          },
        },
        name: { type: String, required: true },
      },
      {
        timestamps: true,
        toJSON: {
          transform: function (doc, ret) {
            delete ret.password;
            return ret;
          },
        },
      }
    );

    여기서 googleId?: string;에 붙은 물음표(?)는 해당 필드의 값이 있을 수도 있고 없을 수도 있으나,

     

    있다면 반드시 문자열 타입이어야 한다는 것을 의미한다.

     

    /src/utils/generateRandomPassword.ts

     

    import bcrypt from 'bcryptjs';
    
    async function generateRandomPassword() {
      const rawPassword = Math.random().toString(36).slice(-10);
      const salt = await bcrypt.genSalt(10);
      const hashedPassword = await bcrypt.hash(rawPassword, salt);
      return hashedPassword;
    }
    
    export default generateRandomPassword;

    이 모듈은 뒤에서 사용하게 될 랜덤 비밀번호 생성 함수를 정의하고 있다.

     

    먼저 해싱을 위해 bcrypt를 가져오고, 이를 이용해 generateRandomPassword라는 이름의 비동기 함수를 선언한다.

      const rawPassword = Math.random().toString(36).slice(-10);

    먼저 랜덤한 문자열을 생성한다. Math.random()은 0 이상 1 미만의 실수를 반환하고

     

    toString(36)은 이 숫자를 36진수(0-9, a-z)로 변환한다.

     

    이어서 slice(-10)은 해당 문자열의 마지막 10글자를 잘라내는데, 

     

    이 값이 해싱 전의 원본 비밀번호 역할을 한다.

      const salt = await bcrypt.genSalt(10);
      const hashedPassword = await bcrypt.hash(rawPassword, salt);
      return hashedPassword;

    이어서 해싱에 사용할 솔트를 생성한다. bcrypt.genSalt(10)은 10자리의 솔트를 생성하는 역할을 한다.

     

    다음으로 원본 비밀번호와 솔트를 이용해 비밀번호를 해싱한 뒤 결과를 기다려 반환한다.

    export default generateRandomPassword;

    마지막으로 늘 그렇듯 함수를 내보내주고 모듈 작성을 마친다.

     

    /src/config

     

    config 폴더에는 두 가지 모듈이 추가되었다. 이는 각각 CORS 정책과 OAuth 로직을 담고 있다.

     

    /cors.ts

     

    CORS에 관한 설명과 타입스크립트 환경에서의 설정, 그리고 각 필드의 의미에 대해선 아래의 글에 적어두었다.

     

    [Node.js]CORS 설정

     

    [Node.js]CORS 설정

    목차 CORS CORS란 Cross-Origin Resource Sharing, 번역하면 교차 출처 리소스 공유의 줄임말이며, 한 마디로 말하자면 다른 도메인, 프로토콜, 포트에서 실행 중인 웹 페이지의 서버 접근을 제한하는 정책

    gnidinger.tistory.com

     

    여기서는 바로 코드를 보고 설정을 살핀 뒤 넘어간다.

    import cors from 'cors';
    
    const corsOptions: cors.CorsOptions = {
      origin: '*',
      methods: '*',
      allowedHeaders: '*',
      credentials: false,
      maxAge: 7200,
    };
    
    export default cors(corsOptions);

    먼저 앞서 설치한 cors를 불러온다. 이어서 다음과 같이 설정한다.

     

    • origin: '*': 모든 출처(origin)로부터의 요청을 허용한다
    • methods: '*': 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 허용한다
    • allowedHeaders: '*': 모든 헤더를 허용한다
    • credentials: false: 인증 정보(cookies, headers 등)를 포함한 요청을 허용하지 않는다. 이는 보안을 위한 설정이다.
    • maxAge: 7200: CORS 설정이 캐시 되는 시간을 초 단위로 설정한다. 여기서는 7200초(2시간) 동안 캐싱된다.

    마지막으로 방금 생성한 옵션을 적용해 새로운 cors 객체를 생성하고 내보낸다.

     

    이렇게 생성된 미들웨어는 서버로의 모든 요청에 대해 CORS를 적용하게 된다.

     

    /passportConfig.ts

     

    여기서부터가 본 게임이다.

     

    나는 노드에서 OAuth를 구현하는 것이 처음이라 시간이 꽤나 걸렸는데,

     

    막상 완성하고, 설명하려고 보니 생각보다 로직이 초라해서 마음이 아팠다.

     

    구현 순서는 구글 > 카카오 > 네이버 순이다.

     

    조금 길지만 코드 전체를 써넣고 한 토막씩 잘라서 살펴보기로 하자.

    import passport from 'passport';
    import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
    import { Strategy as KakaoStrategy } from 'passport-kakao';
    import { Strategy as NaverStrategy } from 'passport-naver-v2';
    import { NaverProfile } from '../types/NaverProfile';
    import User from '../models/User';
    import generateToken from '../utils/generateToken';
    import generateRandomPassword from '../utils/generateRandomPassword';
    
    passport.use(
      new GoogleStrategy(
        {
          clientID: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
          callbackURL: 'http://localhost:8080/users/auth/google/callback',
        },
        async (_accessToken, _refreshToken, profile, done) => {
          const existingUser = await User.findOne({ googleId: profile.id });
    
          if (existingUser) {
            const token = generateToken(existingUser.userSeq, existingUser.name);
            const userObject = existingUser.toObject();
    
            done(null, { ...userObject, token });
          } else {
            const newUser = new User({
              googleId: profile.id,
              id: profile.emails![0].value,
              name: profile.displayName,
              password: await generateRandomPassword(),
            });
    
            const savedUser = await newUser.save();
    
            if (savedUser) {
              const token = generateToken(savedUser.userSeq, savedUser.name);
              const userObject = savedUser.toObject();
    
              done(null, { ...userObject, token });
            } else {
              done(new Error('사용자 저장에 실패하였습니다.'));
            }
          }
        }
      )
    );
    
    passport.use(
      new KakaoStrategy(
        {
          clientID: process.env.KAKAO_CLIENT_ID!,
          clientSecret: process.env.KAKAO_CLIENT_SECRET!,
          callbackURL: 'http://localhost:8080/users/auth/kakao/callback',
        },
        async (_accessToken, _refreshToken, profile, done) => {
          const existingUser = await User.findOne({ kakaoId: profile.id });
    
          if (existingUser) {
            const token = generateToken(existingUser.userSeq, existingUser.name);
            const userObject = existingUser.toObject();
    
            done(null, { ...userObject, token });
          } else {
            if (!profile._json.kakao_account.email) {
              done(new Error('프로필에 이메일이 존재하지 않습니다.'));
              return;
            }
    
            const newUser = new User({
              kakaoId: profile.id,
              id: profile._json.kakao_account.email,
              name: profile.displayName,
              password: await generateRandomPassword(),
            });
    
            const savedUser = await newUser.save();
    
            if (savedUser) {
              const token = generateToken(savedUser.userSeq, savedUser.name);
              const userObject = savedUser.toObject();
    
              done(null, { ...userObject, token });
            } else {
              done(new Error('사용자 저장에 실패하였습니다.'));
            }
          }
        }
      )
    );
    
    passport.use(
      new NaverStrategy(
        {
          clientID: process.env.NAVER_CLIENT_ID!,
          clientSecret: process.env.NAVER_CLIENT_SECRET!,
          callbackURL: 'http://localhost:8080/users/auth/naver/callback',
        },
        async (
          _accessToken: string,
          _refreshToken: string,
          profile: NaverProfile,
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          done: (error: any, user?: any) => void
        ) => {
          // console.log(profile);
          const existingUser = await User.findOne({ naverId: profile.id });
    
          if (existingUser) {
            const token = generateToken(existingUser.userSeq, existingUser.name);
            const userObject = existingUser.toObject();
    
            done(null, { ...userObject, token });
          } else {
            const newUser = new User({
              naverId: profile.id,
              id: profile._json.response.email,
              name: profile._json.response.name,
              password: await generateRandomPassword(),
            });
    
            const savedUser = await newUser.save();
    
            if (savedUser) {
              const token = generateToken(savedUser.userSeq, savedUser.name);
              const userObject = savedUser.toObject();
    
              done(null, { ...userObject, token });
            } else {
              done(new Error('사용자 저장에 실패하였습니다.'));
            }
          }
        }
      )
    );

    길어 보이긴 해도 잘 살펴보면 그저 비슷한 로직이 세 번 반복되는 것을 확인할 수 있다.

    import passport from 'passport';
    import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
    import { Strategy as KakaoStrategy } from 'passport-kakao';
    import { Strategy as NaverStrategy } from 'passport-naver-v2';
    import { NaverProfile } from '../types/NaverProfile';
    import User from '../models/User';
    import generateToken from '../utils/generateToken';
    import generateRandomPassword from '../utils/generateRandomPassword';

    가장 먼저 passport 미들웨어를 가져온다.

     

    이어서 각 서비스에 대한 인증 전략을 설치한 라이브러리에서 가져오는데,

     

    네이버의 경우만 조금 특별하게 NaverProfile을 추가로 가져온다. 이는 뒤에서 설명하겠다.

     

    이어서 사용자 등록 혹은 로그인을 위해 User 모델을, JWT 생성을 위해 generateToken 함수를,

     

    정해진 비밀번호가 없는 OAuth 사용자의 랜덤한 비밀번호 생성을 위해 generateRandomPassword 함수를 가져온다.

     

    이어서 가져온 GoogleStrategy를 이용해 OAuth 2.0을 설정하고 인증을 처리할 미들웨어를 생성한다.

    passport.use(
      new GoogleStrategy(

    Passport 미들웨어는 passport.use 함수를 이용해 다양한 인증 전략을 설정한다.

     

    여기서는 Google OAuth 2.0 인증 전략을 사용하겠다고 명시한다.

        {
          clientID: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
          callbackURL: 'http://localhost:8080/users/auth/google/callback',
        },

    이어서 위 글에서 발급받은 클라이언트 아이디와 시크릿을 입력한다.

     

    다음으로 입력한 콜백 URL은 구글 인증 후 리다이렉트 될 주소를 적어준다.

     

    이는 구글 클라우드 콘솔에서 설정한 값과 일치해야 하는데, 콘솔에서 주소를 설정하는 법은 생략한다.

        async (_accessToken, _refreshToken, profile, done) => {

    인증이 끝난 후 실행될 콜백 함수를 정의한다. 여기서 accessToken, refreshToken은 쓰이지 않지만,

     

    Strategy 인터페이스의 콜백함수 verify의 시그니처에 포함되어 있기 때문에 반드시 적어주어야 한다.

     

    대신 '_'를 앞에 붙여줌으로써 매개변수가 필요 없음을 명시적으로 표시한다. 이는 일종의 관례다.

     

    profile은 당연히 사용자 프로필이고, done 함수는 인증과정이 끝났을 때 호출될 함수를 가리킨다.

          const existingUser = await User.findOne({ googleId: profile.id });

    이어서 요청한 구글 아이디를 가진 사용자가 있는지 동기적으로 조회한다.

          if (existingUser) {
            const token = generateToken(existingUser.userSeq, existingUser.name);
            const userObject = existingUser.toObject();
    
            done(null, { ...userObject, token });

    만약 사용자가 이미 존재한다면 해당 프로필에서 정보를 가져와 토큰을 생성한다.

     

    이어서 .toObject()메서드를 이용해 mongoose 도큐먼트 객체를 일반 JS 객체로 변환한다.

     

    이는 몽구스 객체를 단순화해서 직렬화나 수정 없이 사용 및 반환하기 위함이다.

     

    마지막으로 done()을 호출해 인증 과정을 종료하고 위에서 변환한 사용자 객체와 토큰을 반환한다.

          } else {
            const newUser = new User({
              googleId: profile.id,
              id: profile.emails![0].value,
              name: profile.displayName,
              password: await generateRandomPassword(),
            });
    
            const savedUser = await newUser.save();

    계속해서 사용자가 존재하지 않는 경우의 로직을 구현한다.

     

    가장 먼저 구글이 제공한 정보와 generateRandomPassword를 이용해 새로운 사용자 객체를 생성하고 저장한다.

            if (savedUser) {
              const token = generateToken(savedUser.userSeq, savedUser.name);
              const userObject = savedUser.toObject();
    
              done(null, { ...userObject, token });

    이어서 생성한 객체가 정상적으로 저장되었는지 확인한 후,

     

    위의 로직과 마찬가지로 토큰과 객체를 생성 및 변환해 done 함수를 통해 반환한다.

            } else {
              done(new Error('사용자 저장에 실패하였습니다.'));
            }
          }
        }
      )
    );

    마지막으로 사용자 정보 저장이 실패한 경우의 에러 처리를 하고 인증 전략 설정을 완료한다.

     

    이어지는 코드는 카카오 인증을 위한 전략 설정인데, 대부분이 구글 전략과 동일하다.

     

    따라서 추가된 코드만 확인하고 넘어가도록 하겠다.

            if (!profile._json.kakao_account.email) {
              done(new Error('프로필에 이메일이 존재하지 않습니다.'));
              return;
            }

    저장된 사용자 정보가 없어서 새로 읽어와야 하는 경우, 이메일이 없을 때를 대비한 로직을 작성한다.

     

    이는 카카오 계정에는 이메일이 없는 경우가 있기 때문이다. 물론 에러만 내는 게 아니라 추가로 로직을 작성할 필요가 있다.

            const newUser = new User({
              kakaoId: profile.id,
              id: profile._json.kakao_account.email,
              name: profile.displayName,
              password: await generateRandomPassword(),
            });

    응답 프로필의 구조가 다르기 때문에 아이디(이메일)를 가져오는 코드가 조금 달라졌다.

     

    이외에는 동일한 로직으로, 딱히 손을 더 볼 필요가 없다.

     

    이어서 네이버 로직이다. 로직 자체만 보면 별다른 게 없는 것 같지만

     

    나는 여기서 벽에 부딪쳤다. 해당 내용은 아래와 같이 해결했다.

     

    /src/types/NaverProfile.ts

     

    내가 검색한 바로는 passport-naver-v2를 위한 타입스크립트 타입 정의가 없다.

     

    따라서 위에 구현한 대로 따라 하면 타입 에러가 발생하는데, 지금부터 보려는 파일은 해당 문제를 해결하기 위해

     

    직접 네이버 프로필 타입을 정의하는 코드로 이루어져 있다.

     

    import { Profile } from 'passport';
    
    export interface NaverProfile extends Omit<Profile, 'emails'> {
      id: string;
      displayName: string;
      email: string;
      _json: {
        response: {
          id: string;
          profile_image: string;
          email: string;
          name: string;
        };
      };
    }
    
    export type VerifyCallback = (
      accessToken: string,
      refreshToken: string,
      profile: NaverProfile,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      done: (error: any, user?: any) => void
    ) => void;

    요약부터 하자면 OAuth 구현에서 필요한 NaverProfile 인터페이스와 콜백함수 VerifyCallback을 정의한다.

    import { Profile } from 'passport';

    먼저 Passport에서 기본으로 제공하는 Profile 객체를 불러온다.

     

    이 객체는 Passport에서 사용하는 표준 사용자 프로필 구조를 정의하는 데 쓰인다.

    export interface NaverProfile extends Omit<Profile, 'emails'> {
      id: string;
      displayName: string;
      email: string;
      _json: {
        response: {
          id: string;
          profile_image: string;
          email: string;
          name: string;
        };
      };
    }

    이어서 NaverProfile 인터페이스를 정의한다. 여기서

    extends Omit<Profile, 'emails'>

    는 해당 인터페이스가 'emails'프로퍼티를 제거(Omit)한 Profile을 상속받는다는 의미이다.

     

    이는 네이버 OAuth 요청 시 반환값이 Profile 구조와 일치하지 않기 때문이다.

     

    이어서 정의된 'id', 'displayname', 'email', '_json'은 네이버의 반환값에 맞춰 작성한 필드이다.

     

    참고로 각 서비스에서 반환하는 프로필의 구조를 확인하려면

    async (accessToken: string, refreshToken: string, profile: NaverProfile, done: (error: any, user?: any) => void) => {
        console.log(profile);
        ...
      }

    와 같이 프로필 자체를 콘솔에 찍어보면 된다.

    export type VerifyCallback = (
      accessToken: string,
      refreshToken: string,
      profile: NaverProfile,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      done: (error: any, user?: any) => void
    ) => void;

    마지막으로 VerifyCallback 타입을 정의해서 내보낸다.

     

    이 타입은 Passport의 전략에서 사용하는 콜백 함수의 타입을 정의한 것이다.

     

    구글이나 카카오는 타입이 정의되어 있지만 네이버는 그렇지 않아 오류가 나던 부분을 직접 정의함으로써 해결한 것이다.

     


    이렇게 타입을 정해주고 나면 나머지는 그대로 사용하면 된다.

     

    다만 done을 매개변수에 넣을 때

          done: (error: any, user?: any) => void

    위와 같이 자료형을 입력해줘야 하고

            const newUser = new User({
              naverId: profile.id,
              id: profile._json.response.email,
              name: profile._json.response.name,
              password: await generateRandomPassword(),
            });

    카카오때와 마찬가지로 정보를 가져오는 구조가 조금 달라졌다는 차이점이 있다.

     

    /src/routes/userRoutes.ts

     

    정말 끝으로 방금 구현한 OAuth가 사용할 라우터를 추가해 준다.

    import express from 'express';
    import {
      registerUser,
      loginUser,
      getUserByUserSeq,
      updateUser,
      updateUserPassword,
      deleteUser,
    } from '../controllers/userController';
    import { authMiddleware } from '../middleware/authentication';
    import passport from 'passport';
    import '../config/passportConfig';
    
    const router = express.Router();
    
    router.route('/register').post(registerUser);
    router.route('/login').post(loginUser);
    router.route('/:userSeq').get(getUserByUserSeq);
    router.route('/:userSeq/edit').patch(authMiddleware, updateUser);
    router.route('/:userSeq/changePassword').patch(authMiddleware, updateUserPassword);
    router.route('/:userSeq/delete').delete(authMiddleware, deleteUser);
    
    router.get('/current_user', authMiddleware, (req, res) => {
      res.send(res.locals.user);
    });
    
    router.get(
      '/auth/google',
      passport.authenticate('google', {
        scope: ['profile', 'email'],
        session: false,
      })
    );
    
    router.get(
      '/auth/google/callback',
      passport.authenticate('google', { failureRedirect: '/login', session: false }),
      (req, res) => {
        res.redirect('/');
      }
    );
    
    router.get(
      '/auth/kakao',
      passport.authenticate('kakao', {
        session: false,
      })
    );
    
    router.get(
      '/auth/kakao/callback',
      passport.authenticate('kakao', { failureRedirect: '/login', session: false }),
      (req, res) => {
        res.redirect('/');
      }
    );
    
    router.get(
      '/auth/naver',
      passport.authenticate('naver', {
        session: false,
      })
    );
    
    router.get(
      '/auth/naver/callback',
      passport.authenticate('naver', { failureRedirect: '/login', session: false }),
      (req, res) => {
        res.redirect('/');
      }
    );
    
    export default router;
    router.get('/current_user', authMiddleware, (req, res) => {
      res.send(res.locals.user);
    });

    이 부분은 현재 유저의 정보를 가져오기 위한 엔드포인트이다.

     

    OAuth와는 직접적인 상관이 없으나, 예를 들어 새로고침 같은 행동을 할 때 이쪽으로 요청을 보내 로그인을 유지할 수 있다.

     

    아래의 코드는 OAuth 요청을 보낼 엔드포인트와 콜백 엔드포인트, 그리고 인증이 성공했을 때의 리다이렉트 주소를 정의한다.

     

    보통 OAuth는 세션을 사용하지만 여기서는 JWT를 사용했기 때문에

        session: false,

    를 이용해 세션을 사용하지 않도록 명시했다.

     

    이와 같이 구현하고 나면 세 서비스의 오어스 구현도 끝이 난다.

     

    다음 글에서는 어떤 구현을 할까 조금 고민이 되지만 뭔가 하긴 할 생각이다.

     

    우선 OAuth 2.0 구현은, 이걸로 끝!

    One more thing - FE Code with React

     

    이렇게 구현까지 했는데 정작 테스트를 못 한다면 구현한 느낌이 안 날 것이다.

     

    물론 테스트는 프론트 코드가 없어도 포스트맨 등으로 할 수 있지만,

     

    그래도 기분을 내기 위해 프론트 코드를 아주아주 간단하게 구성해 보기로 했다.

     

    먼저 폴더를 하나 만들고 거기서 터미널을 열어, 다음과 같이 필요한 모듈을 설치한다.

    npx create-react-app google-auth-test
    cd google-auth-test

    이렇게 해서 들어가면 나름대로 프로젝트가 생성되어 있을 것이다.

     

    여기서 App.js 파일을 아래와 같이 구성한다.

    import React from "react";
    
    const LoginPage = () => {
      const handleLogin = () => {
        window.location.href = "http://localhost:8080/users/auth/google";
      };
    
      return (
        <div>
          <button onClick={handleLogin}>Google로 로그인</button>
        </div>
      );
    };
    
    export default LoginPage;

    정말 리액트만 가지고 URL만 옮겨가도록 페이지를 구성했다.

     

    Axios조차 쓰지 않았으니 이보다 간단할 수는 없을 것 같다.

     

    어쨌거나 이렇게 구성하고 서버를 켠 뒤에 프론트엔드 코드도 켜고

    http://localhost:3000/

    로 접근하면 화면 왼쪽 위에 작고 소중한 버튼이 만들어져 있을 것이다.

     

    이걸로 정말, 끝!

    반응형
    댓글
    공지사항
    최근에 올라온 글
    최근에 달린 댓글
    Total
    Today
    Yesterday
    링크
    «   2024/09   »
    1 2 3 4 5 6 7
    8 9 10 11 12 13 14
    15 16 17 18 19 20 21
    22 23 24 25 26 27 28
    29 30
    글 보관함