정화 코딩

[Node.js] Google OAuth 2.0으로 로그인/로그아웃 구현 본문

Web Development

[Node.js] Google OAuth 2.0으로 로그인/로그아웃 구현

jungh150c 2024. 8. 2. 02:30

구글 개발자 도구 웹페이지에서 구글 클라이언트 생성

https://console.cloud.google.com/apis/dashboard

index.js

import * as dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import session from 'express-session';
import FileStore from 'session-file-store';
import passport from 'passport';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const app = express();

// console.log(`The current environment is: ${process.env.NODE_ENV}`);


app.use(express.json());

import authRouter from './routes/authRoutes.js';
import userRouter from './routes/userRoutes.js';
import placeRouter from './routes/placeRoutes.js';
import travelRouter from './routes/travelRoutes.js';
import reviewRouter from './routes/reviewRoutes.js';

// 라우트 설정
app.use('/auth', authRouter);
app.use('/users', userRouter);
app.use('/places', placeRouter);
app.use('/travels', travelRouter);
app.use('/reviews', reviewRouter);

const fileStore = FileStore(session);
const sessionSecret = process.env.SESSION_SECRET;

// 미들웨어 설정
app.use(session({
  secret: sessionSecret,
  resave: false,
  saveUninitialized: false,
  store: new fileStore({ path: './sessions' }) // 세션 파일 저장 경로 설정
}));

// Passport 미들웨어 초기화
app.use(passport.initialize());
app.use(passport.session());

// Passport의 직렬화 및 역직렬화 설정
// serializeUser : 로그인 / 회원가입 후 1회 실행
// deserializeUser : 페이지 전환시 마다 실행 
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

// 홈페이지 생성 (req.user는 passport의 직렬화된 사용자 정보가 저장됨)
app.get('/', (req, res) => {
  const temp = getPage('Welcome', 'Welcome to visit...', getBtn(req.user));
  res.send(temp);
});

// 로그인/로그아웃 상태에 따른 버튼 생성
const getBtn = (user) => {
  return user !== undefined ? `${user.name} | <a href="/auth/logout">logout</a>` : `<a href="/auth/google">Google Login</a>`;
}

// 페이지 생성 함수
const getPage = (title, description, auth) => {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>${title}</title>
    </head>
    <body>
      ${auth}
      <h1>${title}</h1>
      <p>${description}</p>
    </body>
    </html>
    `;
}

// 서버 시작
app.listen(3000, () => console.log('http://localhost:3000'));

authRoutes.js

import * as dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const authRouter = express.Router();
authRouter.use(express.json());

import session from 'express-session';
import FileStore from 'session-file-store';
import passport from 'passport';
import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth';

// Google Client 관련 정보
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;

const fileStore = FileStore(session);
const sessionSecret = process.env.SESSION_SECRET;

// 미들웨어 설정
authRouter.use(session({
  secret: sessionSecret,
  resave: false,
  saveUninitialized: false,
  store: new fileStore({ path: './sessions' }) // 세션 파일 저장 경로 설정
}));

authRouter.use(passport.initialize());
authRouter.use(passport.session());

passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

// 구글 API ID, Secret 정보 저장 (구글 개발자 웹사이트에서 발급받은 클라이언트 ID와 시크릿 입력)
const googleCredentials = {
  "web": {
    "client_id": googleClientId,
    "client_secret": googleClientSecret,
    "redirect_uris": [
      "http://localhost:3000/auth/google/callback"
    ]
  }
}

// Passport (Google) - 구글 로그인시 정보 GET
passport.use(new GoogleStrategy({
  clientID: googleCredentials.web.client_id,
  clientSecret: googleCredentials.web.client_secret,
  callbackURL: googleCredentials.web.redirect_uris[0]
},
  async (accessToken, refreshToken, profile, done) => {
    try {
      // 사용자 정보가 이미 데이터베이스에 있는지 확인
      let user = await prisma.user.findUnique({
        where: { email: profile.emails[0].value },
      });

      if (user) {
        // 사용자 정보 업데이트
        user = await prisma.user.update({
          where: { id: user.id },
          data: {
            provider: profile.provider,
            providerId: profile.id,
            token: accessToken,
            name: profile.displayName,
          },
        });
      } else {
        // 새로운 사용자 정보 추가
        user = await prisma.user.create({
          data: {
            provider: profile.provider,
            providerId: profile.id,
            token: accessToken,
            name: profile.displayName,
            email: profile.emails[0].value,
          },
        });
      }
      return done(null, user);
    } catch (error) {
      return done(error, null);
    }
  }
));

// 구글 로그인 버튼 클릭 시 구글 인증 페이지로 이동
authRouter.get('/google',
  passport.authenticate('google', { scope: ['email', 'profile'] }));

// 구글 로그인 후 콜백 URL (원래 웹사이트로 돌아옴)
authRouter.get('/google/callback',
  passport.authenticate('google', { failureRedirect: '/auth/login' }),
  (req, res) => res.redirect('/'));

// 로그아웃 페이지: 로그아웃 처리, 세션 삭제 및 쿠키 삭제 후 홈으로 리다이렉션
// passport 패키지 내 req.logout()으로 로그아웃 기능 구현
authRouter.get('/logout', (req, res, next) => {
  req.logout((err) => {
    if (err) {
      return next(err); // 에러가 발생하면 next(err)로 에러 핸들러로 전달
    }
    req.session.destroy((err) => {
      if (err) {
        return next(err); // 에러가 발생하면 next(err)로 에러 핸들러로 전달
      }
      res.clearCookie('connect.sid');
      res.redirect('/');
    });
  });
});

// 에러 핸들러
authRouter.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err); // 이미 헤더가 전송된 경우, 다음 에러 핸들러로 전달
  }
  res.status(500).send(err.message || 'Internal Server Error');
});

export default authRouter;

 


 

[참고] https://goodmemory.tistory.com/97

 

Comments