목록Node.js (2)

Node.js + passport.js 를 이용한 소셜 로그인 구현 (페이스북, 깃허브, 구글)

기존 [마크다운 에디터 리뉴얼 관련 포스트](https://massivcode.com/8) 에서 1차 리뉴얼 때 사용했던 passport.js 관련 포스트. 로그인 프로세스는 다음과 같다. 1. 로그인 페이지 진입 2. 특정 소셜 로그인 엘리먼트 클릭 (페이스북, 깃허브, 구글) 3. 해당하는 라우트로 이동 및 passport 미들웨어 호출 4. 각 로그인 플랫폼에 대응하는 콜백 라우트 정의 및 진입시 미들웨어 호출 5. 로그인 성공시 처리 ## 1. npm 종속성 --- ```npm { "name": "markdown-editor", "version": "0.0.0", "private": true, "scripts": { "start": "pm2-dev start ecosystem.config.js", "deploy": "npm install && pm2 start ecosystem.config.js --env production" }, "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "~1.18.2", "cookie-parser": "~1.4.3", "debug": "~2.6.9", "ejs": "~2.5.7", "express": "~4.15.5", "express-session": "^1.15.6", "jsonwebtoken": "^8.1.0", "morgan": "~1.9.0", "mysql2": "^1.5.1", "passport": "^0.4.0", "passport-facebook": "^2.1.1", "passport-github": "^1.1.0", "passport-google-oauth": "^1.0.0", "request-ip": "^2.0.2", "sequelize": "^4.28.6", "serve-favicon": "~2.4.5" }, "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-env": "^1.6.1", "babel-preset-es2015": "^6.24.1" } } ``` ## 2. app.js - passport.js 관련 설정 --- ```javascript "use strict"; const express = require('express'); const path = require('path'); const favicon = require('serve-favicon'); const logger = require('morgan'); const cookieParser = require('cookie-parser'); const bodyParser = require('body-parser'); const SESSION_SECRET_KEY = require('./config/SecureConfig').SESSION_SECRET_KEY; const session = require('express-session'); const passport = require('passport'); ... const app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); // uncomment after placing your favicon in /public app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json({limit: '50mb'})); app.use(bodyParser.urlencoded({extended: false, limit: '50mb'})); app.use(cookieParser()); app.use('/markdown', express.static(path.join(__dirname, 'public'))); app.use(session({ secret: SESSION_SECRET_KEY, resave: false, saveUninitialized: true })); require('./config/passport')(passport); app.use(passport.initialize()); app.use(passport.session()); //로그인 세션 유지 ... module.exports = app; ``` ## 3. passport.js - passport 설정 모듈 --- ```javascript const FacebookStrategy = require('passport-facebook').Strategy; const GithubStrategy = require('passport-github').Strategy; const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; const userService = require('../service/userService'); module.exports = (passport) => { passport.serializeUser((user, done) => { done(null, user); }); passport.deserializeUser((user, done) => { done(null, user); }); passport.use(new FacebookStrategy({ clientID: "", clientSecret: "", profileFields: ['id', 'displayName', 'photos'], callbackURL: 'http://localhost:3000/markdown/auth/facebook/callback' }, function (accessToken, refreshToken, profile, done) { const socialId = profile.id; const nickname = profile.displayName; const profileImageUrl = profile.photos[0].value; onLoginSuccess('Facebook', socialId, nickname, profileImageUrl, done); } )); passport.use(new GithubStrategy({ clientID: "", clientSecret: "", callbackURL: 'http://localhost:3000/markdown/auth/github/callback' }, function (accessToken, refreshToken, profile, done) { const socialId = profile.id; const nickname = profile.username; const profileImageUrl = profile.photos[0].value; onLoginSuccess('Github', socialId, nickname, profileImageUrl, done); } )); passport.use(new GoogleStrategy({ clientID: "", clientSecret: "", callbackURL: 'http://localhost:3000/markdown/auth/google/callback', scope: ['https://www.googleapis.com/auth/plus.me'] }, function (accessToken, refreshToken, profile, done) { const socialId = profile.id; const nickname = profile.displayName; const profileImageUrl = profile.photos[0].value; onLoginSuccess('Google', socialId, nickname, profileImageUrl, done); } )); function onLoginSuccess(platformName, socialId, nickname, profileImageUrl, done) { userService.findOrCreate(platformName, socialId, nickname, profileImageUrl) .spread((user, created) => { if (user.state === 1) { userService.updateUserToJoinedState(user) .then(result => { done(null, user); }) .catch(err => { done(null, user); }) } else { done(null, user); } }); } }; ``` 클라이언트 ID, 클라이언트 시크릿, 콜백 URL (리다이렉트 URL) 은 로그인 플랫폼에서 발급받은 데이터를 넣어준다. onLoginSuccess 에서 로그인 성공한 사용자 정보를 db 에 넣어준다. 각 콜백 메소드의 역할 및 기초적인 사용법은 [passport.js 문서](http://www.passportjs.org/docs/downloads/html/) 와 [생활코딩](https://opentutorials.org/course/2136/12134) 을 참고하면 된다. ## 4. auth.js - 인증 관련 라우터 --- ```javascript "use strict"; const express = require('express'); const router = express.Router(); const passport = require('passport'); const userService = require('../service/userService'); // 로그인 페이지 진입 router.get('/login', function (req, res) { let redirectUrl = req.query.redirectUrl; if (redirectUrl) { res.cookie("redirectUrl", redirectUrl, { expires: new Date(Date.now() + (60 * 1000 * 2)), httpOnly: true }); } res.render('login', {title: '마크다운 에디터 - 로그인', isLogin: false}); }); // 로그아웃 router.get('/logout', (req, res) => { req.logout(); res.redirect('/markdown'); }); // 페이스북 로그인 시작 router.get('/facebook', passport.authenticate('facebook')); // 페이스북 로그인 결과 콜백 router.get('/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/markdown/auth/login' }), (req, res) => { loginSuccessHandler(req, res); }); // 깃허브 로그인 시작 router.get('/github', passport.authenticate('github')); // 깃허브 로그인 결과 콜백 router.get('/github/callback', passport.authenticate('github', { failureRedirect: '/markdown/auth/login' }), (req, res) => { loginSuccessHandler(req, res); }); // 구글 로그인 시작 router.get('/google', passport.authenticate('google')); // 구글 로그인 결과 콜백 router.get('/google/callback', passport.authenticate('google', { failureRedirect: '/markdown/auth/login' }), (req, res) => { loginSuccessHandler(req, res); }); // 로그인 성공시 처리 function loginSuccessHandler(req, res) { let successRedirectUrl = "/markdown"; if (req.cookies.redirectUrl) { successRedirectUrl = req.cookies.redirectUrl; res.clearCookie("redirectUrl"); } return res.redirect(successRedirectUrl); } module.exports = router; ``` 로그인 성공시 loginSuccessHandler 가 호출되는데, 이렇게 따로 분리해놓은 이유는 로그인 성공 후 로그인 요청시의 URL 로 리다이렉트 해주기 위함이다. 만약 이런 부분이 필요 없고, 나는 무조건 특정 페이지로 이동시키고 싶다면 다음과 같이 코드를 수정한다. ```javascript router.get('/google/callback', passport.authenticate('google', { successRedirect: '/', failureRedirect: '/markdown/auth/login' })); ```

Node.js 2018.01.30 6:15
Node.js 의 프로세스 관리자인 PM2 모듈 사용하기

Java 웹 애플리케이션을 배포할 때는 Tomcat 따위의 WAS를 이용하여 배포한다. 정적 파일을 요청자에게 제공하거나, 로직상의 문제가 생겨 예외가 발생하면 애플리케이션을 재시작해주기도 한다. Node.js 의 경우 자체적인 웹서버 기능이 존재하여 별도의 WAS 나 웹서버를 이용하여 배포할 필요는 없다. ```text node app.js ``` 일반적으로 위의 명령어를 이용하여 애플리케이션을 시작하게 된다. 물론 express.js 를 이용하여 제작했을 경우 bin 폴더의 www 파일을 실행하면 애플리케이션이 시작되는 차이는 있다. 하지만 위의 명령어를 이용하여 애플리케이션을 시작하게 되면 에러가 발생하여 서버가 뻗어버렸을 때 서버를 자동으로 재시작해준다던가 하는 편리함은 없다. node.js 애플리케이션은 위와 같은 불편함을 극복하기 위해 별도의 프로세스 관리자 모듈을 통해 애플리케이션을 배포한다. ## 1. PM2 설치하기 node.js 관련 모듈은 npm 이라는 패키지 매니저를 이용하여 설치한다. 안드로이드나 Java 애플리케이션을 개발할 때 Maven 이나 Gradle 따위를 이용하여 외부 모듈을 추가하는 것처럼 node.js 에서도 npm 을 이용하여 관련 작업을 진행할 수 있다. ```text // pm2 모듈의 최신 버전을 전역설치한다. // 전역설치를 하게 된다면 최초 1회만 설치하면 이후에도 사용할 수 있다. // 이후에 bash 에서 pm2 xxx 따위로 관련 기능을 진행할 수 있다. npm install pm2@latest -g ``` ## 2. PM2 기본 명령어 ### 1. 프로세스 실행 ```text // xxx 파일을 pm2 를 이용하여 실행한다. // 주로 xxx 파일은 express.js 미사용시 app.js 를, 사용시에는 bin/www 파일을 실행한다 pm2 start xxx.js ``` ![pm2 start](/images/gallery/1510718826400_pm2_start.png) ```text // xxx 파일을 쌍따옴표 안에 지정한 이름을 지정하여 실행한다. // pm2 를 이용하여 애플리케이션을 여러개 실행하게 될건데 별도의 이름을 지정하지 않을 경우 실행한 파일의 이름이 프로세스 이름으로 지정되어 문제가 생길 수 있다. pm2 start --name "앱 이름" xxx.js ``` ![pm2 start with name](/images/gallery/1510718817207_pm2_start_naming.png) ### 2. 실행중인 프로세스 목록 확인 ```text // pm2 를 이용하여 실행한 프로세스의 목록을 확인할 수 있다. pm2 list ``` 아래의 이미지는 bash 상에서 pm2 list 를 실행한 결과이다. ![pm2 list](/images/gallery/1510718764400_pm2_list.png) ### 3. 실행중인 프로세스 정지 ```text // id(숫자) 또는 name 에 해당되는 실행중인 프로세스를 정지한다. pm2 stop id|name ``` ![pm2 stop by id](/images/gallery/1510718837754_pm2_stop_id.png) ### 4. 실행중인 프로세스 재시작 ```text // id(숫자) 또는 name 에 해당되는 프로세스를 재시작한다. pm2 restart id|name ``` ![pm2 restart by name](/images/gallery/1510718794440_pm2_restart_name.png) ### 5. 실행중인 프로세스 제거 ```text // id(숫자) 또는 name 에 해당되는 프로세스를 제거한다. // PM2 의 프로세스 관리 목록에서 제거되며, 실제 프로젝트 파일은 삭제되지 않는다. pm2 delete id|name ``` ![pm2 delete by id](/images/gallery/1510718735552_pm2_delete_id.png) ### 6. 특정 프로세스에 대한 상세정보 출력 ```text // id(숫자) 또는 name 에 해당되는 프로세스의 상세 정보를 출력한다. pm2 show id|name ``` ![pm2 show](/images/gallery/1510718804558_pm2_show_id.png) ### 7. 특정 프로세스의 실시간 로그 확인 ```text // id(숫자) 또는 name 에 해당되는 프로세스의 실시간 로그를 출력한다. pm2 logs id|name ``` ![pm2 logs](/images/gallery/1510718776617_pm2_logs_id.png) ### 8. PM2 셧다운 ```text // PM2 를 셧다운한다. // PM2 에서 관리중인 모든 프로세스가 delete 된다. pm2 kill ``` ![pm2 kill](/images/gallery/1510718747017_pm2_kill.png) ## 3. PM2 의 ecosystem.config.js 를 활용한 개발/배포 모드 실행 ### 1. ecosystem.config.js ```js module.exports = { apps: [ { // pm2로 실행한 프로세스 목록에서 이 애플리케이션의 이름으로 지정될 문자열 name: "PM2Exam", // pm2로 실행될 파일 경로 script: "./bin/www", // 개발환경시 적용될 설정 지정 env: { "PORT": 3000, "NODE_ENV": "development" }, // 배포환경시 적용될 설정 지정 env_production: { "PORT": 8080, "NODE_ENV": "production" } } ] }; ``` ### 2. ecosystem.config.js 를 활용한 실행 ```text // 개발모드로 실행 pm2 start ecosystem.config.js // 배포모드로 실행 pm2 start ecosystem.config.js --env production ``` ### 3. npm script 를 활용한 실행 ```json { "name": "pm2exam", "version": "0.0.0", "private": true, "scripts": { "start": "pm2-dev start ecosystem.config.js", "deploy" : "npm install && pm2 start ecosystem.config.js --env production" }, "dependencies": { "body-parser": "~1.16.0", "cookie-parser": "~1.4.3", "debug": "~2.6.0", "ejs": "~2.5.5", "express": "~4.14.1", "morgan": "~1.7.0", "serve-favicon": "~2.3.2" } } ``` 위의 내용에서 scripts 부분을 보면 ```json ... "scripts": { "start": "pm2-dev start ecosystem.config.js", "deploy" : "npm install && pm2 start ecosystem.config.js --env production" } ... ``` start 와 deploy 가 정의되어 있다. start 에는 pm2 가 아닌 pm2-dev 로 명령어가 시작되는데, pm2-dev 로 애플리케이션을 실행할 경우 프로젝트의 파일이 변경될 때마다 서버가 재시작된다. 단, 웹 페이지 개발시 페이지가 수정되어도 브라우저가 자동으로 새로고침 되지는 않는다. deploy 에서는 우선 package.json 에 존재하는 모든 모듈을 설치하고 pm2 를 이용하여 ecosystem.config.js 를 실행하는데 뒤에 --env production 이 붙어있다. --env production 이 붙어있어서 ecosystem.config.js 내부에 존재하는 env_production 에 해당되는 설정대로 서버가 시작된다. ### 4. 샘플 프로젝트 링크 및 실행 https://github.com/prChoe/PM2Exam 프로젝트 clone 이나 download 후 프로젝트 루트 단에서 터미널로 npm install 을 타이핑하여 연관 모듈들을 다운받고 npm 스크립트를 실행하여 샘플 프로젝트를 테스트할 수 있다. ``` // start 스크립트 실행 - 개발모드 npm run start // deploy 스크립트 실행 - 배포모드 npm run deploy ```

Node.js 2017.11.15 4:32