Node.JS 서버에 구글 소셜 로그인 기능 넣기 2/2 - 코딩

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

git clone https://github.com/a-mean-blogger/google-oauth.git
cd google-oauth
git reset --hard 514441a
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/google-oauth/tree/514441a77e60d669e462ea429ec52c40f8d78704


Google OAuth client ID와 secret를 사용하여 구글 소셜 로그인 기능을 구현해 봅시다. Google OAuth client ID와 secret가 없다면 이전 글을 읽고 준비를 하시기 바랍니다.

이 강의는 MEAN Stack 강의 시리즈의 게시판 만들기/게시판 Login 기능 추가까지를 읽은 분들을 대상으로 합니다. 즉, 기본적인 node.js Express 사이트 제작 지식과 passport 패키지에 대한 기본적인 지식을 알고 있는 사람들을 대상으로 합니다. 만약 해당 내용을 모르신다면 MEAN Stack 강의 시리즈를 먼저 읽고 진행해 주시기 바랍니다.

이번에 만들 사이트는 main 페이지 그리고 login 페이지의 2 페이지 밖에 없습니다. 처음 사이트에 접속하면 아래와 같이 main 페이지가 표시됩니다.

로그인 링크를 누르면 auth 페이지로 이동하고, 구글 login 링크가 있습니다.

google login 링크를 누르면 구글을 통해 로그인한 후 다시 main 페이지로 돌아옵니다.

처음과 같은 main 페이지지만, 로그인이 되었기 때문에 구글에서 읽어온 정보로 'Welcome {name}'을 표시하고, logout 링크를 표시합니다.

환경 변수 설정

코딩에 앞서 Google OAuth client ID를 GOOGLE_CLIENT_ID, secret을 GOOGLE_SECRET로 환경 변수에 저장합니다.

환경 변수 없이 테스트하고 싶으신 분들은 아래 코드에서 process.env.GOOGLE_CLIENT_ID와 process.env.GOOGLE_SECRET를 각각 client ID와 secret으로 변경하시면 되겠습니다.

프로젝트 생성 및 Package 설치

프로젝트 폴더를 생성합니다. 해당 폴더에서 command line(cmd, git bash 등)에 아래 명령어 입력하여 node.js 프로젝트를 생성합니다.

$ npm init --yes

프로젝트에 필요한 package들도 설치해줍니다.

$ npm install express express-session ejs passport passport-google-oauth2 --save

passport-google-oauth2가 이번 강의의 핵심 패키지입니다. 나머지 패키지들은 이미 MEAN Stack 강의 시리즈에서 설명을 하였습니다.

폴더 구조

코드

// index.js

var express   = require('express');
var app       = express();
var passport  = require('passport');
var session   = require('express-session');

app.set('view engine', 'ejs');
app.use(session({secret:'MySecret', resave: false, saveUninitialized:true}));

// Passport setting
app.use(passport.initialize());
app.use(passport.session());

// Routes
app.use('/', require('./routes/main'));
app.use('/auth', require('./routes/auth'));

// Port setting
var port = 3000;
app.listen(port, function(){
  console.log('server on! http://localhost:'+port);
});

특별히 설명할 것은 따로 없습니다. 다만, express session 설정이 반드시 passport session 위에 있어야 합니다. 아래에 있으면 로그인이 되지 않습니다.

// config/passport.js

var passport         = require('passport');
var GoogleStrategy   = require('passport-google-oauth2').Strategy;

passport.serializeUser(function(user, done) {
  done(null, user);
});
passport.deserializeUser(function(user, done) {
  done(null, user);
});

passport.use(new GoogleStrategy(
  {
    clientID      : process.env.GOOGLE_CLIENT_ID,
    clientSecret  : process.env.GOOGLE_SECRET,
    callbackURL   : '/auth/google/callback',
    passReqToCallback   : true
  }, function(request, accessToken, refreshToken, profile, done){
    console.log('profile: ', profile);
    var user = profile;

    done(null, user);
  }
));

module.exports = passport;

Passport를 설정하는 부분입니다. 자세한 설정법은 passport-google-oauth2 패키지 github 페이지(https://github.com/mstade/passport-google-oauth2)에서 확인 할 수 있습니다.

callbackURL 부분은 구글에 로그인이 이루어 진 후 구글이 다시 사이트로 돌려보내는 주소를 설정하는 부분입니다. 그러므로 사이트는 해당 route를 가지고 있어야 합니다. 다음 파일에서 해당부분을 설명합니다.

간략히 코드의 진행 순서를 설명드리면,

  1. 구글 로그인이 성공.
  2. new GoogleStrategy의 callback function이 실행. 여기서 done(null, user)를 통해 user를 passport.serializeUser에 전달
  3. passport.serializeUser에서 user를 session에 저장.
  4. 이 후 서버에 request가 오는 경우 매번 passport.deserializeUser를 실행하여 session에서 user를 꺼내 user를 복원.

즉 1-3번은 로그인 이후 단 한번만 실행되고, 4번은 이후 request마다 실행됩니다. 위 코드에서는 별도의 user 구조 없이 구글에서 보낸 profile을 그대로 user 모델로 사용하고 있습니다.

만약 사이트의 정해진 user 모델이 있고, DB에 저장하는 경우, new GoogleStrategy의 callback function에서 DB의 user를 찾거나 생성해 주고, 자신의 용도에 맞게 session에 user를 저장할지(위 코드는 session에 user의 전체 정보를 저장합니다), 아니면 매번 DB에서 읽어 올지를 결정하면 됩니다(이 경우 passport.serializeUser에서는 user의 db id만 사용하고, passport.deserializeUser에서 저장된 db id값으로 DB에서 user를 읽어오게 코드를 수정합니다).

console.log('profile: ', profile)를 통해 구글이 어떤 값을 전달하는지 확인해 볼 수 있게 하였습니다. 아래와 같은 정보가 전달됩니다.

provider는 'google'로 이 정보가 구글에서 왔다는 것을 알려줍니다. subid는 google에서 유저를 특정할 수 있는 고유 아이디입니다.

만약 user를 DB에 저장하여 관리한다면 이 두값을 이용하여 DB에 해당 값을 가진 user가 없다면 user를 생성하고, 해당 값을 가진 user가 있다면 그 user를 가져와서 사이트에 로그인시키면 되겠습니다.

// routes/auth.js

var express  = require('express');
var router   = express.Router();
var passport = require('../config/passport.js');

router.get('/login', function(req,res){
  res.render('auth/login');
});

router.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/');
});

router.get('/google',
  passport.authenticate('google', { scope: ['profile'] })
);

router.get('/google/callback',
  passport.authenticate('google'), authSuccess
);

function authSuccess(req, res) {
  res.redirect('/');
}

module.exports = router;

중요하게 살펴봐야 할 곳은 /google route과 /google/callback route입니다.

/google route에서 passport.authenticate('google', { scope: ['profile'] })를 실행하면 구글 로그인페이지로 이동하여 로그인이 이루어지게 됩니다. 로그인이 성공하면 config/passport.js에서의 callbackURL 설정에 따라 /google/callback 페이지로 이동하게 되고, 여기서 authSuccess callback function이 호출됩니다.

// routes/main.js

var express  = require('express');
var router   = express.Router();

router.get('/', function(req,res){
  res.render('main', {user: req.user});
});

module.exports = router;

main page는 req.user를 user로 view에 전달하는데, 로그인이 되어 있다면 req.user에는 user의 정보가 들어가고, 로그인이 되어 있지 않다면 req.user는 아무런 값이 없습니다.

<!-- views/main.ejs -->

<h1>Main Page</h1>

<% if(!user){ %>
  <a href="auth/login">Login</a>
<% } else { %>
  <p>Welcome <%= user.displayName %></p>
    <a href="auth/logout">Logout</a>
<% } %>

user의 값의 유무를 판단하여 로그인이 되어 있으면 displayName을 표시하고 logout 링크를 보여줍니다. 아니라면 login 페이지 링크를 보여줍니다.

<!-- views/auth/login.ejs -->

<h1>Auth Page</h1>

<a href="google">Google Login</a>

로그인 페이지에는 /auth/google 페이지로 이동할 수 있는 링크가 있습니다.

댓글

P
Peter Kim 2020.09.07
와우 업데이트 된거 뭐없나 2주에 한번씩 눈팅하러 오는 사람입니다! 회원가입자한테는 새글알림 메일도 보내주시면 좋겠어요 ㅠㅠ 이번글도 너무 반갑네요! 빨리 따라해봐야지!
혹시 node.js 로 메일링서버 만들기 나 기타 재밌는 강의 부탁드립니다! 개처럼열심히 홍보하고 있어요!!
I
Ian H 2020.09.08
@Peter Kim,
안녕하세요. 주기적으로 방문해주셔서 감사합니다^^ 새글 알림 기능은 구현했었다가.. 무료 SMTP 서비스를 찾지 못해서 숨겨져 있습니다 ㅠㅠ 
요즘 업데이트가 거의 없어서 죄송합니다. 반성하고 좀더 자주 업데이트할 수 있도록 하겠습니다 ㅠㅠ
빡경 2020.09.21
로그아웃하고 다시 구글로그인폼으로 들어가서 다른아이디로 로그인하고싶은데  뭐가남아있는지 계속 그전에 로그인했던 아이디로만 되고 그게 안되네요 억지로 크롬에서 로그아웃하면 되긴하는데 IE로 들어가서 할경우에는 그것도 불가능해서요
I
Ian H 2020.09.21
@빡경,
브라우저에 로그인된 구글계정이 한개면 최조 로그인 이후로는 구글 로그인버튼을 누르면 자동으로 로그인됩니다.
다른 아이디로 로그인하려면 google.com으로 가서 완전히 로그아웃하시거나, 다른 로그인을 추가하시면됩니다.
위 부분은 google.com에 의해 처리되므로 바꿀 수 없고, 모든 사이트의 구글로그인은 동일한 방식으로 작동합니다.
이 답변이 이해가 잘 안되시면 댓글남겨주세요. 더 상세히 설명드릴게요.
빡경 2020.09.21
@Ian H,
한번 실습해보시면 connect.sid 세션아이디가 로그아웃햇음에도 잘사라지지않는데 오류인가요!? 저만의 오류는 아닌것같아서요!
I
Ian H 2020.09.22
@빡경,
connect.sid는 로그인할때 생기고 로그아웃할때 사라지는 것이 아니라,  app.use(session(...))을 사용하면 사이트에 접속시 생성되고 계속유지됩니다.
로그아웃상태에서 connect.sid를 강제로 지운 후에 페이지를 새로고침해보세요. 로그인이 되지도 않았는데 connect.sid는 이미 존재합니다. 로그인, 로그아웃과는 무관합니다^^;
빡경 2020.09.23
@Ian H,
local login 때처럼 currentUser 를 통해 <이메일(로그인한 사용자)> logout  이렇게 nav를 만들고 싶은데 구글에서 넘겨주는 profile에는 이메일정보가 없는데 어떻게해야하나요?
빡경 2020.09.23
@Ian H,
두개만 더 여쭤보고 싶습니다!  1. mongoose에서 find했을떄 결과값이 없으면 []가 출력되는데 ex) user.find({name:"IanH"}, fucntion(err, result) {               if문을 통해서 result의 결과값이 없으면 user 새로만들어주고 있으면 그냥 지나가게 하고싶은데               []는 if(result), if(result == null)가 전부 통과가 됩니다 어떻게 조건을 설정해주어야 하나요?
2. 버튼을 클릭할때마다 schema의 배열 속성(like : [{type:string}])에, 클릭한 속성을 한개 한개씩 수정하고 싶은데      $set? 으로는 불가능한것 같은데 어떻게해야할지 잘 모르겠습니다
I
Ian H 2020.09.23
@빡경,
이메일을 받으려면 routes/auth.js 파일에서 passport.authenticate('google', { scope: ['profile','email'] })와 같이 email을 scope에 추가해주면 됩니다.
1. if(result.length === 0)을 사용하면 결과값이 []인 경우를 잡을 수 있습니다. 혹은 User.find 함수 대신 User.findOne 함수를 사용하면 조건에 맞는 결과값 하나만 찾기 때문에 if(result == null)를 사용할 수 있습니다. 
2. $push를 사용할 수 있습니다. https://docs.mongodb.com/manual/reference/operator/update/push/ 참고해주세요
빡경 2020.09.23
@Ian H,
와 정말 명확하시네요.. 감명받았습니다.  자주 댓글로 여쭤봐도 되나요..? 질문이 많을것같아서요 ㅠㅠ
I
Ian H 2020.09.25
@빡경,
언제든지 질문하셔도 됩니다^^ 제 블로그 보시면 알겠지만 전 질문에 답해주는 것을 좋아하는 편이거든요 ㅋ
다만 본인의 발전을 위해선 영어로 구글링하는 것이 더 좋습니다. 1번같은 경우에 how to check array is empty, 2번은 how to add a value in array in mongoose 으로 구글링 하시면 정말 쉽게 찾을 수 있는 답변들입니다.
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP