본문 바로가기
Back-End/🌿Express (nodeJS)

[Express] Passport 모듈로 로그인 구현하기(1) - local

by 코딩하는 동현😎 2022. 11. 11.

passport 모듈

sns등 서비스에 로그인을 구현할때, 회원가입과 로그인을 직접 구현할수도 있지만, 세션이랑 쿠키처리 등 복잡한 과정들이 있습니다.

이 모듈을 사용해서 더 간단하게 구현 할 수 있습니다.


패키지 설치

npm i passport passport-local bcrypt

// 아래는 카카오 로그인 모듈 설치 (이번 실습때는 이용 X)
npm i passport-kakao

passport 모듈과 로컬(자체)로그인 모듈인 passport-local , 암호화 모듈인 bcrypt 모듈을 다운받았습니다.

데이터베이스에 비밀번호 그대로 저장하면 보안이 취약하기 때문에 사용자가 입력한 비번을 해시암호화해서 저장하고, 나중에 로그인할때, 입력한 비번을 똑같은 원리로 암호화해서 입력한 비번이랑 데이터베이스에 있는 비번을 비교해서 구현하려고 합니다.


passport 모듈을 app.js와 연결

passport 모듈은 조금 뒤에 만들고 app.js에 먼저 연결하겠습니다.

passport 폴더를 만들고 여기에 passport 모듈을 만듭니다.

그리고 그 모델을 passportConfig라는 상수에 받아 들여옵니다.

 

app.js

const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const { sequelize } = require('./models')
dotenv.config();

//passport
const passport = require('passport')
const passportConfig = require('./passport')

// router
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth')

sequelize.sync({force : false}).then(()=> {console.log('db ok')}).catch((error)=>{console.error(error)})

const app = express();
passportConfig();
app.set('port', process.env.PORT || 8000);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
}));

// passport
// session 미들웨어 뒤에 와야합니다.
app.use(passport.initialize());
app.use(passport.session());

app.use('/', pageRouter);
app.use('/auth',authRouter )

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});

passport.initialize 미들웨어는 요청(req)에 passport 설정을 심고, passport.session 미들웨어는 req.session객체에 passport 정보를 저장합니다.

그러므로 두 미들웨어는 반드시 express-session 미들웨어 뒤에다가 연결해야합니다.


passport/index.js

만든 passport 폴더에 index.js 파일을 만들고 Passport

(index 이름의 파일은 이름을 생략하고 호출가능 ./passport == ./passport/index)

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

module.exports = () => {
    passport.serializeUser((user, done)=>{
        done(null, user.id);
    })

    passport.deserializeUser((id, done)=>{
        User.findOne({where : {id}})
        .then(user => done(null, user))
        .catch(err => done(err))
    })
    // localStrategy 미들웨어 적용
    local();
}

passport.serializeUser 함수는 로그인시 실행되며, req.session 객체에 어떤 정보를 저장할지 done콜백함수로 저장합니다.

user의 id를 저장하겠습니다.

passport.deserializeUser는 최초 로그인말고, 매 요청시 실행됩니다. serializeUser에 넣었던 데이터가 이 함수의 인자로 들어옵니다. user의 id를 넣었기 때문에 id를 인자로 받습니다.

id를 받으면 그 id로 db에 검색을 해서 user 객체 전체를 불러올수 있습니다.

콜백함수인 done 함수의 두번째 인자에 넣으면 됩니다.


1. 로그인 할때 유저의 id를 req 세션에 넣습니다.

2. 로그인 된 상태에서 뭘 할때마다 미리 넣어준 id를 이용해서 user객체를 불러옵니다.


로그인 여부 미들웨어 만들기

로그인이나 회원가입, 로그아웃할때 예외처리는 필수입니다.

로그인 하지 않았는데 로그아웃할수 없고, 로그인(이미 계정이 있음)했는데 회원가입은 할수 없습니다.

로그인 여부를 판별하는 미들웨어가 필요합니다.

로그인 여부 미들웨어는 어느 폴더에 넣어도 상관없지만, 미들웨어는 보통 라우터에서 자주 호출합니다.

그러므로 라우터 폴더내에 js 파일을 생성하겠습니다.

 

routes/middlewares.js

exports.isLoggedIn = (req, res, next)=>{
    if (req.isAuthenticated()) {
        next();
    } else {
        res.status(403).send('로그인 필요')
    }
}

exports.isNotLoggedIn = (req, res, next) =>{
    if(!req.isAuthenticated()){
        next();
    } else{
        const message = encodeURIComponent('이미 로그인한 상태입니다.')
        res.redirect(`/?error=${message}`)
    }
}

isLoggedIn 미들웨어는 로그인 된것이 확인되면 다음 미들웨어로 넘겨서 정상작동시키고, 로그인 되지 않았다면, 다음미들웨어로 넘기지 않고 예외를 처리합니다.

isNotLoggedIn 미들웨어는 로그인이 되지 않은것이 확인되면 다음 미들웨어로 넘겨서 정상작동시키고, 로그인 되지 않았다면, 다음미들웨어로 넘기지 않고 예외를 처리합니다.

isAuthenticated 함수는 passport가 req객체에 추가해준 함수로, 로그인 여부를 확인하는 함수입니다.


로그인 로그아웃 회원가입 라우터 설정

실질적인 로그인작업 ( passport.authenticate 함수 )은 localStrategy에 정의를 할 것입니다.

지금은 이미 만들었다고 가정하고 라우터 파일을 작성하겠습니다.

 

routes/auth.js

const express = require('express')
const passport = require('passport')
const bcrypt = require('bcrypt')

const User = require('../models/user');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');

const router = express.Router();

// post join 회원가입
router.post('/join', isNotLoggedIn ,async (req, res, next) =>{
    try {
        const {email, nick, password} =  req.body
        exUser = await User.findOne({where : {email}})
        if (exUser) {
            return res.redirect('/join?error=exists')
        }
        // 비밀번호를 암호화해서 저장
        // 12번 반복해서 암호화 (12~31 권장)
        const hash = await bcrypt.hash(password , 12)
        await User.create({
            email,
            nick,
            password : hash
        })
        return res.redirect('/')
    } catch (error) {
        console.error(error);
        next(error);
    }
})

// post login 로그인
router.post('/login' , isNotLoggedIn , (req, res, next)=>{
    const {email, password} = req.body
    // 인증 함수 
    passport.authenticate('local', (authError , user, info)=>{
        if(authError){
            console.error(authError);
            return next(authError)
        }
        if(!user){
            return res.redirect(`/?loginError=${info.message}`)
        }
        return req.login(user, (loginError)=>{
            if (loginError){
                console.error(loginError);
                return next(loginError)
            }
            return res.redirect('/')
        })
    })(req,res,next);
})

//get logout
router.get('/logout',isLoggedIn ,(req, res)=>{
    req.logout()
    req.session.destroy();
    res.redirect('/')
});

// 이 모듈을 router로 익스포트한다.
module.exports = router;

로그인 돼어 있으면 회원가입을 막아야하므로 회원가입 함수에 미들웨어에 isNotLoggedIn 미들웨어를 추가해줍니다.

회원가입할때 당연히 같은 이메일의 계정이 이미 있으면 예외처리를 해야합니다.

 

로그인 과정에서는 입력한 이메일과 비밀번호로 인증과정을 거쳐야하는데, 그것은 localStrategy에서 정의할 passport.authenticate 함수로 처리합니다.


local 인증 절차 정의하기

드디어 인증 과정을 정의합니다.

인증 과정은 생각하시는 것과 같게, 입력한 비밀번호와 데이터베이스의 비밀번호와 같으면 로그인 성공입니다.

이메일에 해당하는 계정이 데이터베이스에 없으면 없는 회원이고, 비밀번호가 불일치하면 인증 실패인 것입니다.

 

passport/localStrategy.js

const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');

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

module.exports = () => {
    // localStrategy 미들웨어를 passport 과정에서 자동사용
    // 두 인자 옵션, vertify 미들웨어
    passport.use(new localStrategy({
        // 첫 번째 인자 - 아디 비번 옵션
        usernameField : 'email',
        passwordField : 'password'
    }, async (email, password, done) => {
        // vertify되면 done 하는 미들웨어
        try {
            const logUser = await User.findOne({where : {email}})
            if (logUser) {
                const result = await bcrypt.compare(password, logUser.password)
                if (result) {
                    done(null, logUser);
                } else {
                    done(null, false, {message : '비밀번호가 일치하지 않습니다.'})
                }
            } else {
                // 사용자가 없습니다.
                done(null, false, { message : '가입되지 않은 회원입니다.' })
            }
        } catch (error) {
            console.error(error);
            done(error)
        }
    }))
}

passport-local 모듈을 다운 받았을 것입니다.

passport-local모듈의 .Strategy 속성을 LocalStrategy에 저장합니다.

localStrategy 객체의 생성자는 두가지 인자를 받습니다.

 

옵션(계정과 비번 정보)과 인증함수입니다.

 

옵션에는 usernameField와 passwordField가 있는데, req.body 객체에 어느 부분이 이 두 Field에 해당하는 지 입력해야지

 post 객체로 받은 정보를 받을수 있습니다.

프론트(클라이언트)에서 email, password라는 이름으로 json객체를 보낼 예정이라고 하면 위와 같이 작성하시면 됩니다.

 

인증함수는 그대로 위에서 설명한 인증 과정 그대로 작성하고 done 함수로 통보합니다.

성공하면 user객체를 반환하고, 실패하면 false를 반환하고, 다음 미들웨어로 오류표시를 넣습니다.


LocalStrategy를 passport과 연결

passport/index.js에서

local 상수에 인증Strategy를 불러오고, 미들웨어를 호출합니다.

local();

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

module.exports = () => {
    passport.serializeUser((user, done)=>{
        done(null, user.id);
    })

    passport.deserializeUser((id, done)=>{
        User.findOne({where : {id}})
        .then(user => done(null, user))
        .catch(err => done(err))
    })
    
    // localStrategy 미들웨어 적용
    local();
}
반응형

댓글