기타 라이브러리 및 프레임워크

23-09-10 passport를 활용하여 login기능 구현

모건이삼촌 2023. 9. 10. 22:57

※ 목차

  • 패키지 구조
  • passport 동작 과정
  • auth.js
  • local strategy.js, index.js
  • middlewares.js
  • app.js 적용
  • postman test
  • 후기

 
 
0. 패키지 구조

 
1. passport 동작 과정
 
routes/auth.js
/login 으로 POST요청이 올경우 passport.authenticate실행.
첫번째 매개변수(전략)을 통해 내가 설정한 전략을 실행함.
* local 전략이라는 가정하에 작성

passport/localStrategy.js
LocalStrategy 객체에 내가 options으로 설정한 필드값을 검증받아 객체 생성
상수(이하 user)에 id를 req.params값으로 받아 db에서 id탐색
탐색 후 찾는 id가 있다면 비밀번호 검증 실행
검증 후 비밀번호가 일치하면 done(null, user) 리턴
id 혹은 비밀번호가 일치하지 않으면 message 전송

routes/auth.js
다시 돌아와 user값이 있으면 session에 값 저장
session id를 cookie에 저장
user가 없거나 서버에 에러가 났을경우 에러메세지 출력
 
passport/index.js
login 검증이 끝나면 passport.serializeUser실행
req.user 호출시 deserializeUser을 실행하여 response
 
위 설명을 그림으로 표현하자면 아래와 같다.

 
 
2. auth.js
 2-1. 회원가입 로직 생성
root/models/member.js

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const Account = new mongoose.Schema({
	//unique를 사용하여 중복방지, required를 사용하여 not null
    username : {type:String, unique:true, required: true},
    pw : {type:String, required: true},
    nickName : {type:String, required: true},
    // default:Date.now()를 사용하여 가입날짜 지정
    regDate: {type:Date, default:Date.now()},
    updateDate:{type:Date, default: Date.now()}
});

Account.statics.create = async function(payload) {
    const member = new this(payload);
    // bcrypt를 사용하여 pw 암호화
    member.pw = await bcrypt.hash(payload.pw, 10);
    return member.save();
};

module.exports = mongoose.model("Member", Account);

 
root/routes/members.js

const router = require('express').Router();
const Member = require('../models/member');

const register = router.post('/signup', (req, res) => {
    Member.create(req.body)
        .then(member => res.send(member))
        .catch(err => res.status(500).send(err));
});

module.exports = router;

 
2-2. 로그인 로직생성
root/routes/auth.js

const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const Member = require('../models/member');
const {isLoggedIn, isNotLoggedIn} = require('./middlewares');
const router = express.Router();

router.post('/login', isNotLoggedIn, (req, res, next) => {
    console.log('login user ->', req.body);
    // local은 local전략을 사용하겠다는 뜻
    // passport.authenticate가 실행되면 전략에 맞춰 구성해놓은 전략이 실행됨
    // 예를들어 local로 지정해놨다하면 passport/localStrategy.js가 실행되어 회원검증을 함
    passport.authenticate('local', (err, member, info) => {
        if(err) {
            console.error(err);
            return next(err);
        }
        if(!member) {
            return res.redirect(`/auth/?loginError=${info.message}`);
        }
        return req.login(member, loginError => {
            if(loginError) {
                console.error(loginError);
                return next(loginError);
            }
            // return res.json({message:"로그인"});
            // return res.send(member);
            
            // 로그인 검증이 끝나면 http://localhost로 redirect
            return res.redirect('/');
        });
    })(req, res, next);
});
   
router.get('/logout', isLoggedIn, (req, res) => {
    req.logout()
    req.session.destroy();
    res.redirect('/')
});

module.exports = router;

* 필자는 Missing Credentials에러가 나서 에러를 해결했는데, 원인을 확인해보니 localStratege.js 설정중 회원을 제대로찾지 못해 나온 에러였다. 본인이 js를 잘 하지 못하면 log를 찍어 코드가 어디까지 통과했는지 어디서 막혔는지 잘 찾아내는게 중요하다.
 
3. LocalStrategy.js, Index.js
 3-1. root/passport/localStrategy.js

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

const Member = require('../models/member');

module.exports = () => {
    passport.use(
        new LocalStrategy(
            {
            // 본인이 구성한 Member Schema대로 필드값을 받아야한다
            // 예를들어 id의 Documnet가 userId일경우 usernameField에 userId라고 적어야 한다.
            // pw도 위와 같다.
                usernameField : 'username', // 
                passwordField : 'pw',
            },
            // exUser를 먼저 찾아와야하기 때문에 콜백함수(async, await) 사용
            async (username, pw, done) => {
                try {
                    console.log(username);
					
                    // usernameField에 적혀진대로 username을 받아 db에 값이 있는지 찾는다.
                    const exUser = await Member.findOne({ username : username});
                    console.log("exUser : ", exUser)
                    if(exUser) {
                    	// 값이 있는지 확인 후 pw 검증
                        const result = await bcrypt.compare(pw, exUser.pw)
                        if(result) {
                        	// pw 일치시 done 호출
                            return done(null, exUser);
                        }else {
                        	// pw 일치하지 않을경우 message 출력
                            done(null, false, {message : '비밀번호가 일치 하지 않습니다.'})
                        }
                        // exUser를 찾지못했을경우 message 출력
                        done(null, false, {message : '가입되지 않은 회원입니다.'})
                    }
                }catch (err) {
                    console.error("err =",err)
                    done(err)
                }
            },
        ),
    );
};

 
 3-2. root/passport/index.js

const passport = require('passport');
const local = require('./localStrategy');
const Member = require('../models/member');

module.exports = () => {
	// passport를 사용하여 로그인이 성공했을때 실행
    passport.serializeUser((user, done) => {
        console.log('serializeUser -> ', user)
        done(null, user.username);
    })

	// 로그인한 유저정보를 호출했을시(req.user) 실행
    passport.deserializeUser((username, done) => {
        Member.findOne({username: username})
            .then(user => done(null, user))
            .catch(err => done(err));
    })

    local();
}

 
4. 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}`);
    }
};

 
5. app.js 적용

require('dotenv').config();

const mongoose = require('mongoose');
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const indexRouter = require('./routes/index');
const bodyParser = require("body-parser");
const app = express();
const cors = require('cors')


/* cors start */
const corsOptions = {
    origin: 'http://localhhost:3000', // 배포시 주석
    // origin: 'https://pf6.chanyongyang.com:3000', // 배포시 주석 해제
    credentials: true
}

app.use(cors(corsOptions));
/* cors end */

/* passport start */
const passportConfig = require('./passport');
const session = require('express-session');
const passport = require('passport');
passportConfig();
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
    session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly:true,
        secure: false // https일경우 true
    },
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/auth', require('./routes/auth'));
/* passport end */

app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

const Member = require('./models/member');

/* MongoDB connect start */
const uri = process.env.MONGODB_URI;
mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;
db.on('error', console.error);
db.once('open', function(){
  console.log("Connection Success");
});
/* MongoDB connect end */


module.exports = app;

 
6. postman test
 
6-1. 회원가입

 
6-2 login
 6-2-1. root/routes/index.js

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

/* GET home page. */
router.get('/', function(req, res, next) {
  if(req.user) {
    console.log("req.user = ", req.user);
    console.log(req.user);
    console.log(req.user.username);
  }
  res.render('index', { title: 'Express' });
});

module.exports = router;

 
 6-2-2. postman

 
6-2-3. log 출력

{
  _id: new ObjectId("6502b77354db2b91d0174d3b"),
  username: 'tistory',
  pw: '$2b$10$/teaT1QROYwgjsOwQpR/F.55tVpoYQd1H3RjgOkJ/0bmYPNIppvim',
  nickName: 'loginTest',
  regDate: 2023-09-14T07:34:07.679Z,
  updateDate: 2023-09-14T07:34:07.679Z,
  __v: 0
}
tistory

 
 
7. 후기
js를 가지고 백엔드 구축하는데 있어 어려움을 많이 느꼈다.
다 하고나서 spring security의 인증부분과 상당히 비슷하다고 느꼈다(사실 처음부터 느꼈더라면 금방 구현했을텐데..)
이 글을 읽고있는 여러분도 에러없이 금방 구현하길 바란다.