Nodejs

 

생활 코딩 강의 목록 :  https://opentutorials.org/module/3590

인프런 강의 목록 https://www.inflearn.com/course/node-js-express/dashboard

 

 

 

이 수업은 CC 라이센스를 따르고 있으며, 아래 링크 에서도 볼 수 있습니다.
Node.js-Express https://opentutorials.org/course/3370
쿠키와 인증 https://opentutorials.org/course/3387
세션과 인증 https://opentutorials.org/course/3400 passpor.js
https://opentutorials.org/course/3402
다중 사용자 https://opentutorials.org/course/3411 google login https://opentutorials.org/course/3413 facebook login
https://opentutorials.org/course/3414

 

 

WEB2 - Node.js 수업의 예제 코드 바로가기

 

 

소스 :  https://github.com/braverokmc79/Nodejs-master-passpot-google

 

$ git clone https://github.com/passport/todos-express-starter.git google-tutorial

 

 

Node.Js - Express (생활코딩) - 4 Passport.Js Passport Mysql , Passport 비밀번호 암호화, Passport 회원 가입 , Passport 로그인 , Passport 인증

 

 

Google Login

 

 

80. 수업소개

3

 

 

 

 

 

 

81. 프로젝트 등록

 

 

 

 

 

82. Passprot.js 설정

 

 

83.Resource Owner 승인절차

 

 

 

 

 

84.id 획득 절차

 

 

 

85.사용자 추가

 

 

 

86.수업을 마치며

 

 

 

 

 

 

 

 

 

Passport 사이트  공식 문서 구글 연동  : 

1) https://www.passportjs.org/tutorials/google/

 

2) https://www.passportjs.org/packages/passport-google-oauth/

 

 

 

$ npm install passport-google-oauth


$ npm install passport-google-oidc


 

 

앱 등록

이전 섹션 에서 사용자가 "Google로 로그인"을 클릭할 때 Google로 리디렉션하는 경로를 추가했습니다. 이 섹션에서는 Google API를 사용할 수 있도록 앱을 Google에 등록합니다.

Google Cloud Platform 콘솔 로 이동합니다 .

프로젝트 목록에서 프로젝트를 선택하거나 새 프로젝트를 만듭니다.

API 및 서비스 페이지 로 이동하여 자격 증명 을 선택 합니다.

기존 애플리케이션이 있는 경우 OAuth 2.0 클라이언트 ID 아래에 나열됩니다 . OAuth 클라이언트 편집 을 클릭 하여 클라이언트 ID와 비밀을 얻고 전략 구성 을 진행합니다 . 그렇지 않으면 계속하십시오.

아직 구성 하지 않은 경우 OAuth 동의 화면 을 구성 합니다 . Google 계정이 있는 모든 사용자가 애플리케이션을 사용할 수 있도록 하려면 외부 를 선택 합니다. 앱 이름, 지원 이메일 및 개발자 연락처 정보를 입력하여 앱 등록 프로세스를 완료합니다.

자격 증명 만들기 를 클릭 한 다음 OAuth 클라이언트 ID 를 선택 합니다.

응용 프로그램 유형 으로 웹 응용 프로그램 을 선택 합니다 .

승인된 리디렉션 URI 에서 URI 추가 를 클릭 합니다. 를 입력하십시오 .http://localhost:3000/oauth2/redirect/google

만들기 를 클릭 하여 OAuth 클라이언트를 만듭니다. 다음 화면에는 클라이언트 ID와 암호가 표시됩니다. 전략 구성 을 진행합니다 .

 

 

환경변수 설정

$ npm install dotenv

const dotenv = require("dotenv")
dotenv.config();
console.log("환경변수  :", process.env['GOOGLE_CLIENT_ID']);

 

.env

$ touch .env

GOOGLE_CLIENT_ID=__INSERT_CLIENT_ID_HERE__
GOOGLE_CLIENT_SECRET=__INSERT_CLIENT_SECRET_HERE__

 

 

DB 테이블 생성

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(30),
  `email` varchar(30) unique NOT NULL,
  `hashed_password` varchar(100) DEFAULT NULL,
  `salt` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
)ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4;



CREATE TABLE `federated_credentials` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) unique NOT NULL,
  `provider` varchar(100) unique NOT NULL,
  `subject` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`) ,
  FOREIGN KEY (user_id) REFERENCES users(id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;




CREATE TABLE `topics` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
  `description` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `created` datetime DEFAULT current_timestamp(),
  `user_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

 

 

 

 

 

 

 

1)main.js

const express = require('express')
const fs = require('fs');
const bodyParser = require('body-parser')
const compression = require('compression')
const logger = require('morgan');
const session = require('express-session')
const MySQLStore = require('express-mysql-session')(session);
const flash = require('connect-flash');
const app = express()
const port = 3000
const helmet = require('helmet')
const dotenv = require("dotenv")
dotenv.config();

app.use(helmet())
app.use(express.static('public'))
app.use(bodyParser.urlencoded({ extended: false }));
app.use(compression());


// session DB 저장 방식 - session 테이블이 자동 생성되고  세션이 passport의해 저장 된다.
app.use(session({
  secret: '12312dajfj23rj2po4$#%@#',
  resave: false,
  saveUninitialized: true,
  store: new MySQLStore({
    host: 'localhost',
    port: 3306,
    user: 'opentutorials',
    password: '1111',
    database: 'opentutorials'
  })
}));

app.use(flash());
const passport = require("./lib/passport")(app);

app.get('*', (req, res, next) => {
  fs.readdir('./data', function (error, filelist) {
    req.list = filelist;
    next();
  });
});


const indexRouter = require("./routes/index");
const authRouter = require("./routes/auth")(passport);
const topicRouter = require("./routes/topic");

app.use("/", indexRouter);
app.use('/auth', authRouter);
app.use('/topic', topicRouter);



app.use(function (req, res) {
  res.status(400).send("Sorry cant find that!");
});

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

 

 

 

 

2)routes/auth.js

const express = require('express');
const router = express.Router();
const template = require('../lib/template.js');
const auth = require("../lib/auth.js");
const db = require("../lib/db");
global.crypto = require('crypto')

module.exports = function (passport) {

  //로그인 페이지
  router.get("/login", (req, res) => {
    const fmsg = req.flash();
    let feedback = '';
    if (fmsg.error) {
      feedback = fmsg.error[0];
    }
const express = require('express');
const router = express.Router();
const template = require('../lib/template.js');
const auth = require("../lib/auth.js");
const db = require("../lib/db");
global.crypto = require('crypto')

module.exports = function (passport) {

  //로그인 페이지
  router.get("/login", (req, res) => {
    const fmsg = req.flash();
    let feedback = '';
    if (fmsg.error) {
      feedback = fmsg.error[0];
    }

    const title = 'WEB - login';
    const list = template.list(req.list);
    const html = template.HTML(title, list, `
            <div style="color:red">${feedback}</div>
            <form action="/auth/login_process" method="post">
              <p><input type="text" name="email" placeholder="email"></p>  
              <p><input type="password" name="password" placeholder="password"></p>                                 
              <p>
                <input type="submit" value="login">

              </p>
              <p>
                 <a class="button google" style="padding: 8px; text-decoration: none; color: #fff;background: #a22f2f;border-radius: 50px;"
                       href="/auth/login/federated/google">Sign in with Google</a>
              </p>
            </form>
          `, '', auth.statusUI(req, res));
    res.send(html);

  });


  // 로그인 처리 - 1) 성공 및 실패 페이지 설정 및 flash 사용여부 설정하기
  router.post('/login_process', passport.authenticate('local', {
    successRedirect: '/',
    failureRedirect: '/auth/login',
    failureFlash: true,
    successFlash: true
  }));



  //1.구글로그인 처리 - 로그인 버튼 클릭시
  router.get('/login/federated/google', passport.authenticate('google'));


  //2.구글로그인 처리 - 콜백 반환
  router.get('/oauth2/redirect/google', passport.authenticate('google', {
    successRedirect: '/',
    failureRedirect: '/auth/login',
    failureFlash: true,
    successFlash: true
  }));





  //로그 아웃 처리
  router.get('/logout', function (req, res, next) {
    req.logout(function (err) {
      if (err) { return next(err); }

      req.session.destroy(function (err) {
        res.redirect("/");
      })
    });
  });

  //회원가입페이지
  router.get('/signup', function (req, res, next) {
    const fmsg = req.flash();
    let feedback = '';
    console.log("회원 가입페이지 : ", fmsg);
    if (fmsg.error) {
      feedback = fmsg.error[0];
    }

    const title = 'WEB - login';
    const list = template.list(req.list);
    const html = template.HTML(title, list, `
      <div style="color:red">${feedback}</div>
           <h1>Sign up</h1>
      <form action="/auth/signup_process" method="post">
          <section>
              <label for="username">이름</label>
              <input id="username" name="username" type="text" placeholder="이름" required>
          </section>
         <section>
              <label for="email">이메일</label>
              <input id="email" name="email" type="email" placeholder="이메일" required>
          </section>          
          <section>
              <label for="password">비밀번호</label>
              <input id="password" name="password" type="password" autocomplete="new-password" placeholder="비밀번호" required>
          </section>
          <section>
              <label for="password">비밀번호확인</label>
              <input id="password" name="pw2" type="password" autocomplete="new-password" placeholder="비밀번호확인" required>
          </section>          
          <button type="submit">Sign up</button>
      </form>
          `, '', auth.statusUI(req, res));
    res.send(html);
  });


  //회원가입처리
  router.post('/signup_process', function (req, res, next) {
    if (req.body.password !== req.body.pw2) {
      req.flash("error", "비밀번호가 일치 하지 않습니다.");
      return res.redirect("/auth/signup");
    }
    crypto.randomBytes(16, (error, buf) => {
      const salt = buf.toString("base64");

      crypto.pbkdf2(req.body.password.trim(), salt, 310000, 32, 'sha256', function (err, hashedPassword) {
        console.log("회원가입 데이터 : ", req.body.password, salt, hashedPassword.toString('hex'));

        if (err) { return next(err); }
        db.query('INSERT INTO users (username,email, hashed_password, salt) VALUES (?, ?, ?, ?)', [
          req.body.username,
          req.body.email,
          hashedPassword.toString('hex'),
          salt
        ], function (err, results, fields) {

          if (err) {
            req.flash("error", err.toString());
            return res.redirect('/auth/signup');
          }

          var user = {
            id: results.insertId,
            username: req.body.username
          };

          console.log("등록한 insertId :", results.insertId);

          req.login(user, function (err) {
            if (err) { return next(err); }
            req.flash("success", "회원가입을 축하합니다.");
            res.redirect('/');
          });


        });
      });


    });
  });


  return router;
}


    const title = 'WEB - login';
    const list = template.list(req.list);
    const html = template.HTML(title, list, `
            <div style="color:red">${feedback}</div>
            <form action="/auth/login_process" method="post">
              <p><input type="text" name="username" placeholder="username"></p>  
              <p><input type="password" name="password" placeholder="password"></p>                                 
              <p>
                <input type="submit" value="login">

              </p>
              <p>
              <a class="button google" href="/auth/login/federated/google">Sign in with Google</a>
              </p>
            </form>
          `, '', auth.statusUI(req, res));
    res.send(html);

  });


  //1.구글로그인 처리 - 로그인 버튼 클릭시
  router.get('/login/federated/google', passport.authenticate('google'));


  //2.구글로그인 처리 - 콜백 반환
  router.get('/oauth2/redirect/google', passport.authenticate('google', {
    successRedirect: '/',
    failureRedirect: '/auth/login'
  }));





  //로그 아웃 처리
  router.get('/logout', function (req, res, next) {
    req.logout(function (err) {
      if (err) { return next(err); }

      req.session.destroy(function (err) {
        res.redirect("/");
      })
    });
  });



    ~


  return router;
}

 

 

 

★현재 프로젝트에서는 적용하지 않았으나,

 passReqToCallback: true 옵션값을 넣어 req  파라미터값을 사용할 수 있다.

 

passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'password',
    passReqToCallback: true
}, function verify(req, email, password, done) {
    console.log('local-join callback called');
    console.log(email, password, done);
}
));

 

 

 

 

3) lib/passport.js

const passport = require("passport");
const crypto = require('crypto');
const db = require("../lib/db");
const LocalStrategy = require('passport-local');
const GoogleStrategy = require('passport-google-oidc');



module.exports = function (app) {
    app.use(passport.initialize());
    app.use(passport.session());

    passport.serializeUser(function (user, cb) {
        console.log("로그인 처리시 최초 한번  passport.serializeUser 호출 user 값  :", user);
        process.nextTick(function () {
            cb(null, { id: user.id, email: user.email, username: user.username });
        });
    });

    // 로그인 성공되면 passport.deserializeUser  매번 실행 처리된다
    passport.deserializeUser(function (user, cb) {
        console.log("deserializeUser  :", user);
        process.nextTick(function () {
            return cb(null, user);
        });
    });


    // 로그인 처리 - 2) passport 로그인
    passport.use(new LocalStrategy({
        usernameField: 'email',
        passwordField: 'password'
    }, function verify(email, password, cb) {


        db.query('SELECT * FROM users WHERE email = ?', [email], function (err, row) {
            if (err) { return cb(err); }
            if (!row[0]) { return cb(null, false, { message: '등록된 email 이 없습니다.' }); }
            if (!row[0].salt) return cb(null, false, { message: '소셜 로그인으로 등록된 유저입니다.' });

            crypto.pbkdf2(password, row[0].salt, 310000, 32, 'sha256', function (err, hashedPassword) {
                console.log("로그인 데이터 : ", password, row[0].salt, hashedPassword.toString('hex'));
                console.log("로그인 DB암호: ", row[0].hashed_password);

                if (err) { return cb(err); }
                if (row[0].hashed_password !== hashedPassword.toString('hex')) {
                    console.log("비밀번호 오류");
                    //flash 에 저장 처리됨  - "flash":{"error":["비밀번호가 일치하지 않습니다."]}}
                    return cb(null, false, { message: '비밀번호가 일치하지 않습니다.' });
                }


                console.log("로그인 성공");
                return cb(null, row[0], { message: '로그인 성공' });
            });
        });

    }));


    //구글 - 로그인 처리
    passport.use(new GoogleStrategy({
        clientID: process.env['GOOGLE_CLIENT_ID'],
        clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
        callbackURL: '/auth/oauth2/redirect/google',
        scope: ['profile', 'email']
    }, function verify(issuer, profile, cb) {

        console.log("1.구글 로그인 profile: ", profile);


        db.query('SELECT * FROM federated_credentials WHERE provider = ? AND subject = ?', [
            'Google',
            profile.id
        ], function (err, row) {
            if (err) { return cb(err); }

            console.log("2. 구글 로그인 등록 처리: ", row.length);

            //1.federated_credentials 테이블에 등록되어 있지 않으면 신규등록처리
            if (row.length === 0) {


                db.query('SELECT * FROM users WHERE email = ?', [profile.emails[0].value], function (err, results) {
                    console.log("등록된 이메일이 존재 results : ", results);
                    //등록된 이메일이 존재하면은 로그인 처리
                    if (results.length > 0) {
                        //1) User 테이블에  등록되어 있다면 federated_credentials 테이블에만 등록후 로그인 처리
                        db.query('INSERT INTO federated_credentials (user_id, provider, subject) VALUES (?, ?, ?)', [
                            results[0].id, 'Google', profile.id], function (err) {
                                if (err) { return cb(err); }
                                return cb(null, results[0]);
                            });


                    } else {
                        //2) User 테이블 및   federated_credentials 테이블 모두 등록 되어 있지 않다면 모두 등록후 로그인 처리
                        db.query('INSERT INTO users (username, email) VALUES (?, ?)', [
                            profile.displayName, profile.emails[0].value
                        ], function (err, results, fields) {
                            if (err) return cb(err);

                            const id = results.insertId;
                            db.query('INSERT INTO federated_credentials (user_id, provider, subject) VALUES (?, ?, ?)', [
                                id, "Google", profile.id
                            ], function (err) {
                                if (err) { return cb(err); }
                                const user = {
                                    id: id,
                                    username: profile.displayName
                                };
                                return cb(null, user);
                            });
                        });

                    }

                });



            } else {
                //2.federated_credentials 테이블 등록되어 있다면 바로 로그인처리
                console.log("row.user_id : ", row[0].user_id);
                db.query('SELECT * FROM users WHERE id = ?', [row[0].user_id], function (err, row) {
                    if (err) { return cb(err); }
                    if (!row[0]) { return cb(null, false); }
                    return cb(null, row[0]);
                });
            }
        });



    }));



    return passport;
}

 

 

 

 

 

 

기타 참조

 

lib/db.js

const mysql = require('mysql');
const db = mysql.createConnection({
    host: 'localhost',
    user: 'opentutorials',
    password: '1111',
    database: 'opentutorials'
});
db.connect();

module.exports = db;

 

lib/auth.js

module.exports = {

    isOwner: function (req, res) {
        console.log(" isOwner req.user : ", req.user);
        if (req.user) return true;
        else return false;
    },

    statusUI: function (req, res) {
        let authStatusUI = '<a href="/auth/login">login</a>&nbsp;&nbsp;<a href="/auth/signup">signup</a>';
        if (this.isOwner(req, res)) {
            authStatusUI = `${req.user.username}  <a href="/auth/logout">logout</a>
           
            `;
        }
        return authStatusUI;
    }
}


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

승자는 눈을 밟아 길을 만들지만, 패자는 눈이 녹기를 기다린다. -탈무드

댓글 ( 4)

댓글 남기기

작성