SNS 만들기 -3(with Node, MySQL, Nunjucks)

백지연·2022년 1월 31일
2

NodeJS

목록 보기
17/26
post-thumbnail

본격적으로 백엔드 부분을 구현해보자! 이번 포스팅은 이 전 게시글과 이어진다. 이전에 작성한 코드(sns2)와 일부 내용이 달라지므로 git에 올라가있는 sns 폴더를 sns3 폴더로 복사해 작업했다. 구현하는 부분이 생길수록 계속 복사할 것이다.( sns4, sns5 ...)

책 Node.js 교과서(개정 2판) 책의 9장의 내용을 참고했다.
+모든 코드는 github주소에 있다.

지금까지 구현한 것

  1. 프로젝트 기본 뼈대 잡기
  2. 프론트엔드 화면 구현하기
  3. DB 세팅하기

이번 포스팅에서 구현할 것

  1. 로그인 구현하기(with Passport 모듈)
    1. Passport 기본 세팅
    2. 로컬 로그인 구현
    3. sns 로그인 구현(with kakao)

4. 로그인 구현하기(with Passport 모듈)

Github: https://github.com/delay-100/study-node/tree/main/ch9/sns3

과거 포스팅에서 로그인을 모듈없이 구현해봤었는데, 검증된 모듈(Passport)을 사용해보자!

1. Passport 기본 세팅

[설치할 모듈]

  • passport
  • passport-local: passport 로컬 로그인
  • passport-kakao: passport 카카오 로그인
  • bcrypt
npm i passport passport-local passport-kakao bcrypt

실행화면(console)

Git [sns3/app.js] - 설치한 passport 모듈을 app.js와 연결

...
const passport = require('passport');
...
const passportConfig = require('./passport'); // require('./passport/index.js')와 같음
...
passportConfig(); // 패스포트 설정, 한 번 실행해두면 ()에 있는 deserializeUser 계속 실행
...
// passport 사용 - req.session 객체는 express-session에서 생성하므로 express-session 뒤에 작성해야함
app.use(passport.initialize()); // 요청(req 객체)에 passport 설정을 심음
app.use(passport.session()); // req.session 객체에 passport 정보를 저장(요청으로 들어온 세션 값을 서버에 저장한 후, passport 모듈과 연결)
...

Git [sns3/passport/index.js] - Passport 관련 기본 코드 작성(serializeUser, deserializeUser)

const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');

module.exports = () => {

    // 세션에 불필요한 데이터를 담아두지 않기 위한 과정들(serializeUser, deserializeUser)

    // serializeUser: 사용자 정보 객체를 세션에 아이디로 저장
    passport.serializeUser((user,done) => { // serializeUser: 로그인 시 실행됨, req.session(세션) 객체에 어떤 데이터를 저장할지 정하는 메서드
        done(null, user.id); // done 첫 번째 인수: 에러 발생 시 사용, done 두 번째 인수: 저장하고 싶은 데이터를 넣음
                             // user.id만 저장한 이유: 세션에 user의 모든 정보를 저장하면 서버의 용량이 낭비되기 때문
    });
    
    // deserializeUser: 세션에 저장한 아이디를 통해 사용자 정보 객체를 불러옴
    // passport.session 미들웨어가 이 메소드를 호출
    passport.deserializeUser((id, done) => { // deserializeUser: 매 요청 시 실행, id: serializerUser의 done으로 id 인자를 받음
        User.findOne({where:{id}}) // db에 해당 id가 있는지 확인
        .then(user => done(null, user)) // req(요청).user에 저장 -> 앞으로 req.user을 통해 로그인한 사용자의 정보를 가져올 수 있음
        .catch(err => done(err));
    });     

    local();
    kakao();
};

2. 로컬 로그인 구현

로컬 로그인?: 타 sns 서비스가 아닌, 서비스 자체적으로 회원가입 후 로그인을 하는 것
+여기서는 로그인 전략(동작) 위주로 다루고 회원가입은 따로 만들어보겠다.
+잘 이해가 되지 않는 부분은 이 블로그를 참고했다.

Passport에서 로컬 로그인을 구현하려면 passport-local 모듈이 필요하다.

Git [sns3/routes/middlewares.js] - 로그인 확인관련 미들웨어 구현(isLoggedIn, isNotLoggedIn)

// 로그인 확인 관련 미들웨어 생성

// 로그인이 된 상태를 확인하는 미들웨어
exports.isLoggedIn = (req, res, next) => {
    // 로그인이면 허용
    if(req.isAuthenticated()){ // req.isAuthenticated(): 로그인 중이면 true, 아니면 false
        next(); // 다음 미들웨어로 넘겨줌
    } else { // 로그인이 아니면 비허용
        res.status(403).send('로그인 필요');
    }
};

// 로그인이 되지 않은 상태를 확인하는 미들웨어
exports.isNotLoggedIn = (req, res, next) => {
    // 로그인이 아니면 허용
    if(!req.isAuthenticated()){
        next(); // 다음 미들웨어로 넘겨줌
    } else{ // 로그인이면 허용
        const message = encodeURIComponent('로그인한 컴포넌트입니다.');
        res.redirect(`/?error=${message}`); // 에러 페이지로 바로 이동시킴
    }
};

Git [sns3/routes/page.js] - middlewares.js에서 만든 미들웨어를 page 라우터에 적용

// app.js에서 기본 router로 설정한 page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares'); // 구조분해할당으로 middlewares의 두 미들웨어를 가져옴

const router = express.Router();

// 모든 요청마다 실행
router.use((req,res,next)=>{
    // res.locals.user = null;  // res.locals는 변수를 모든 템플릿 엔진에서 공통으로 사용, 즉 user는 전역 변수로 이해하면 됨(아래도 동일)
    res.locals.user = req.user; // 요청으로 온 유저를 넌적스에 연결
    res.locals.followerCount = 0;
    res.locals.followingCount = 0;
    res.locals.followerIdList = [];
    next();
});

// http://127.0.0.1:8001/profile 에 get요청이 왔을 때 
router.get('/profile', isLoggedIn, (req, res) => {
    res.render('profile', { title: '내 정보 - sns'});
});

// http://127.0.0.1:8001/join 에 get요청이 왔을 때 
router.get('/join', isNotLoggedIn, (req, res)=>{
    res.render('join', {title: '회원가입 - sns'});
});

// http://127.0.0.1:8001/ 에 get요청이 왔을 때 
router.get('/', (req, res, next) => {
    const twits = [];
    res.render('main', {
        title: 'sns',
        twits,
    });
});

module.exports = router;

Git [sns3/routes/auth.js] - 회원가입, 로컬 로그인, 로그아웃 라우터 작성(/auth/join, /auth/login, /auth/logout)

// 회원가입, 로컬 로그인, 로그아웃 라우터
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

// 회원가입 라우터, /auth/join
router.post('/join', isNotLoggedIn, async (req, res, next) => {
    const { email, nick, password } = req.body;
    try {
        const exUser = await User.findOne({ where: {email}});  // User db에 같은 email이 있는지 확인  
        if(exUser){ // 이미 User가 존재하면
            return res.redirect('/join?error=exist'); // 주소 뒤에 에러를 쿼리스트링으로 표시
        }
        // User이 존재하지 않으면(회원가입 가능)
        const hash = await bcrypt.hash(password, 12); // bcrypt 모듈을 이용해 비밀번호 암호화 - crypto 모듈의 pbkdf2 메서드를 이용해 암호화도 가능
                                                     // 두번째 인수(추천- 12~31): pbkdf2의 반복횟수와 유사, 숫자가 커질수록 비밀번호를 알아내기 어렵지만 암호화 시간도 오래걸림 
        await User.create({
            email,
            nick,
            password: hash,
        });
        return res.redirect('/');
    } catch (error) {
        console.error(error);
        return next(error);
    }
});

// 로그인 라우터, /auth/login
router.post('/login', isNotLoggedIn, (req, res, next) =>{
    passport.authenticate('local', (authError, user, info) => { // passport.authenticate('local') 미들웨어가 로컬로그인 전략(passport/localStrategy.js) 수행
                                                                // 미들웨어인데 라우터 미들웨어 안에 들어있음 - 미들웨어에 사용자 정의 기능을 추가하고 싶은 경우
        if(authError){ // 로그인 전략(동작)이 실패한 경우 - authError 에 값이 존재
            console.error(authError);
            return next(authError);
        }
        if(!user){  // 2번째 매개변수 값(user)이 존재하지 않는 경우 - db에 계정이 X
            return res.redirect(`/?loginError=${info.message}`);
        }
        // 2번째 매개변수 값(user)이 존재하는 경우 - passport가 req 객체에 login, logout 메서드를 추가함
        return req.login(user, (loginError) => { // req.login은 passport.serializeUser를 호출 - req.login에 제공하는 user 객체가 serializeUser로 넘어가게 됨
            if(loginError) {
                console.error(loginError);
                return next(loginError);
            }
            return res.redirect('/');
        });
    })(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙힘
});

// 로그아웃 라우터, , /auth/logout
router.get('/logout', isLoggedIn, (req, res) => {
    req.logout(); // req.user 객체를 제거함
    req.session.destroy(); // req.session 객체의 내용을 제거함 -  세션 정보를 지움
    res.redirect('/'); // 메인 페이지로 돌아감
});

module.exports = router;

Git [sns3/passport/localStrategy.js] - 로컬 로그인 전략 설정

// 로그인 전략
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy; // passport-local 모듈의 전략 생성자를 가져옴
const bcrypt = require('bcrypt');

const User = require('../models/user');

module.exports = () => {
    passport.use(new LocalStrategy({ // LocalStrategy 생성자의 첫 번째 인수(객체): 전략에 관한 설정
        usernameField: 'email', // req.body 내의 속성명(req.body.email)
        passwordField: 'password',
    }, async (email, password, done) => { // LocalStrategy 생성자의 두 번째 인수(함수): 실제 전략 수행
                                         // 첫 번째 인수에서 넣어주었던 email, password가 여기의 매개변수가 됨
                                        // done은 routes/auth.js의 passport.authenticate의 콜백 함수
        try {
            const exUser = await User.findOne({ where: { email }}); // db에 일치하는 Email이 있는지 확인
            if(exUser){ // db에 일치하는 User가 있는 경우+
                const result = await bcrypt.compare(password, exUser.password); // bcrypt: 암호화 모듈, password: req(입력)의 매개변수, exUser.password: db에 저장되어 있는 password 
                if(result){ // 비밀번호까지 일치하는 경우
                    done(null, exUser); // routes/auth.js의 passport.authenticate('local', ~~) 에서 ~~부분에 들어가는 값이 done으로 반환됨
                } else { // 비밀번호 불일치
                    done(null, false, { message: '비밀번호가 일치하지 않습니다.'});
                }
            } else{ // 
                done(null, false, { message: '가입되지 않은 회원입니다.'});
            }
        } catch(error){
            console.error(error);
            done(error);
        }
    }));
};

3. sns 로그인 구현(with kakao)

카카오 로그인?: 로그인 인증 과정을 카카오에게 맡기는 것
+장점

  • 사용자: 새로운 사이트에 번거롭게 회원가입을 하지 않아도 됨
  • 서비스 제공자: 로그인 과정을 검증된 SNS에 맡길 수 있음

+회원가입 절차가 따로 없어, 처음 로그인 시 회원가입 처리를 해야 함

Git [sns3/passport/kakaoStrategy.js] - 카카오 로그인 전략 설정

// 카카오 로그인 전략
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy; // passport-kakao 모듈로부터 Strategy 생성자를 불러와 전략 구현

const User = require('../models/user');

module.exports = () => {
    passport.use(new KakaoStrategy({  
        clientID: process.env.KAKAO_ID, // clientID: 전략 구현을 위해 카카오에서 발급해주는 id, 노출 방지로 .env 파일에서 관리함
        callbackURL: '/auth/kakao/callback', // callbackURL: 카카오로부터 인증 결과를 받을 라우터 주소
    }, async (accessToken, refreshToken, profile, done) => {
        console.log('kakao profile', profile);
        try { 
            const exUser = await User.findOne({
                where: {snsId: profile.id, provider: 'kakao'}, //  snsId: 카카오 아이디와 같은 지?, provider: 카카오에서 로그인했는지?
            }); // 기존에 카카오를 통해 회원가입한 사용자가 있는지 조회
            if(exUser){ // 이미 User로 존재하는 경우(회원가입이 이미 되어있는 경우)
                done(null, exUser); // 사용자 정보와 함께 done함수 호출
            } else { // user가 존재하지 않는 경우 - 회원가입 진행
                const newUser = await User.create({
                    email: profile._json && profile._json.kakao_account_email,
                    nick: profile.displayName,
                    snsId: profile.id,
                    provider: 'kakao',
                });
                done(null, newUser); // 새로운 유저와 함께 done 실행
            }
        } catch (error) {
            console.error(error);
            done(error);
        }
    }));
};

Git [sns3/routes/auth.js] - 카카오 로그인 라우터 설정

...

// 카카오 로그인 라우터, /auth/kakao
router.get('/kakao', passport.authenticate('kakao')); // 카카오 api가 get으로 되어있어서 무조건 get으로 받아옴
                                                      // passport가 알아서 kakao 로그인 창으로 redirect 함
// 카카오 로그인 후 성공 여부 결과를 받음                                                      
router.get('/kakao/callback', passport.authenticate('kakao', { // 카카오 로그인 전략을 다시 수행함
                                                              // 로컬 로그인과 다른 점: passport.authenticate 메서드에 콜백 함수를 제공하지 않음
                                                              // 로그인 성공 시 내부적으로 req.login을 호출함 (내가 직접 호출할 필요X)
    failureRedirect: '/', // failureRedirect 속성: 콜백 함수 대신 로그인에 실패했을 때 어디로 이동할지를 적음
}), (req, res) => { // 성공 시 어디로 이동할지 적는 미들웨어
    res.redirect('/'); 
});

module.exports = router;

Git [sns3/routes/auth.js] - 카카오 로그인 라우터 연결

...
const authRouter = require('./routes/auth');
...
app.use('/auth', authRouter);
...

위와같이 카카오 로그인 관련 코드를 추가해준 후, clientID: process.env.KAKAO_ID를 위해 clientID를 발급 받아야한다.

clientID 발급 받기

  1. 카카오 개발자(https://developers.kakao.com/) 회원가입

  2. 내 애플리케이션 메뉴에 들어가서 애플리케이션 추가하기 버튼 클릭 후 정보 입력

  3. REST API 키를 복사해 .env 파일에 넣음
    Git [sns3/.env]

COOKIE_SECRET=serect
KAKAO_ID=여기
  1. 플랫폼 에서 Web의 Web 플랫폼 등록 클릭 후 http://localhost:포트번호 입력

  2. 제품설정카카오 로그인에서 활성화 설정 상태를 ON으로 변경

  3. 활성화 설정 아래의 Redirect URI에 http://localhost:8001/auth/kakao/callback 입력
    +/auth/kakao/callback은 kakaoStrategy.js의 callbackURL과 일치해야 함

  4. 제품설정동의항목 메뉴에서 로그인 동의항목을 작성

여기까지 모두 완료했으면 서버를 켜서 실행해보자!


진행 상황(실행화면)

Kakao 미연동이라고 떠서 로그를 확인해보았다.
로그 안에 미연동이라고 써있는데 카카오측에서 받은 값이 미연동이라는 것 같고,, 결과 자체는 잘 들어온 것 같다! -> 추후에 회원가입 때 이름을 넣어주면 된다


잘못된 정보 수정 및 피드백 환영합니다!!

profile
TISTORY로 이사중! https://delay100.tistory.com

0개의 댓글