[node.js] passport로 로그인
1. 로그인을 위해 user/login 라우터를 하나 만든다.
// back/app.js
const express = require('express');
const server = express();
const userRouter = require('./routes/user');
server.use('/user', userRouter);
server.listen(3065, () => {
console.log('서버를 실행중입니다.');
});
back/routes/user
const router = require('express').Router();
// POST /user/login, 로그인
router.post('/login', (req, res, next) => {
// todo
});
2. back/passport 폴더를 만들고 안에 index.js와 local.js를 만든다.
passport의 local.js를 통해 로그인 전략을 세울 것이다. 잘보고 차근차근 이해하며 따라오면 된다.
먼저 passport와 로그인 전략을 세우기 위해 passport-local을 npm install 해준다.
$npm install passport
$npm install passport-local
passport/index.js
const passport = require('passport');
const local = require('./local');
//
module.exports = () => {
passport.serializeUser((user, done) => {
// todo
}); // Strategy 성공 시 호출됨
passport.deserializeUser((id, done) => {
// todo
});
local(); // 로그인 전략을 실행
};
index.js는 passport 설정 파일이다. 위의 코드는 기본 꼴이라고 보면된다. local에서 로그인 전략을 실행할 것이고 전략을 통해
상공적으로 user의 정보를 받아오면 user.id 만을 가지고 database에서 해당 id로 정보를 복구해 전달해 줄것이다. 이유는 아래와 같다.
프론트에서 cookie를 전달 받으면 해당 cookie를 통해 server에서 모든 정보를 가지고있는 session을 찾아 정보를 가지게 되는데
server에서 모든 session을 가지고 있기에는 메모리에 부담이되고 용량이 무거워진다.
때문에 passport에서 user.id의 정보(cookie)만 가지고 있다가 로그인 요청이 오면 database에서 정보를 찾아와 복구하는 전략이다.
이렇게하면 메모리에 부담이 적어진다.
passport/local.js (로그인 전략)
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
// 로그인 전략
module.exports = () => {
passport.use(
new LocalStrategy(
{
// todo-01
},
(email, password, done) => {
// todo-02
}
)
);
};
passport를 활용 passport-local의 Strategy를 통해 전략을 세워보자. 위의 코드도 기본 꼴이니 잘 알아두자.
todo-01에는 로그인 시 받아올 정보를 입력할 것이고, todo-02에는 인자로 받아온 정보(email, password)를 가지고 정의한
User 테이블(model)에서 정보를 찾아 반환(return)해줄 것이다.
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
// 로그인 전략
module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email', // req.body.email
passwordField: 'password', // req.body.password
},
(email, password, done) => {
// todo-02
}
)
);
};
로그인 전략으로 먼저 front에서 email과 password를 보내오면 server에서는 req.body.email과 req.body.password로 받아온다.
그럼 이 받아온 정보는 passport에서 usernameField: 'email' 과 passwordField: 'password' 로 받아온다.
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const { User } = require('../models');
const bcrypt = require('bcrypt');
// 로그인 전략
module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => { // todo-02
try {
const user = await User.findOne({
where: { email },
});
if (!user) {
return done(null, false, { reason: '존제하지 않는 사용자 입니다.' });
}
const result = await bcrypt.compare(password, user.password); // 입력한 pw와 user의 pw가 같으면 true
if (result) {
return done(null, user);
}
return done(null, false, { reason: '비밀번호가 틀렸습니다.' });
} catch (err) {
console.error(err);
return done(err);
}
}
)
);
};
이제 받아온 정보 email과 password를 통해 data를 찾아 보내주면 된다. 비동기적으로 처리하기 위해 async와 await을 활용
passport는 done으로 결과를 판단해 주는데 null 자리는 server에러, false 자리는 성공 여부, cilent error 이다.
password의 보안을 위해 bcrypt 라이브러리를 사용했다.
1. todo-02 부분에서 email과 password를 인자로 넘겨받아 model(table)의 User에서 email 정보를 찾는다.
2. user(찾은 정보)가 없다면 존재하지 않는 사용자라고 알린다.
3. 사용자가 존재하지만 입력한 password와 user.password가 같으면 true이므로 user의 정보를 반환해준다.
4. 만일 입력한 password와 user.password가 같지 않으면 비밀번호가 틀렸다고 알린다.
5. 마지막으로 서버 딴에서 error가 발생 시 catch(err)를 통해 err를 판단해준다.
passport/index.js
const passport = require('passport');
const local = require('./local');
const { User } = require('../models');
//
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
}); // Strategy 성공 시 호출됨
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findOne({ where: { id } });
done(null, user); // req.user
} catch (err) {
console.error(err);
done(err);
}
});
local();
};
위의 설명했듯 server의 메모리 문제를 최소화하기 위해 user의 id만을 가지고 data를 복구하고 가져올 것이다.
1. index.js는 passport 설정 파일로 로그인 전략이 실행이되고 성공적으로 받아온 user의 정보를 serializeUser의 인자로 받아온다.
2. done을 통해 user.id 정보만을 가지며, 이를 아래의 deserializeUser의 인자로 id만 넘겨준다.
3. 비동기 적으로 작업을 하기 위해 async/await을 활용한다. try{...} catch(err) {...} 를 통해 err 발생 시 알린다.
4. 해당 id를 가지고 User model(table)에서 User의 정보를 찾는다. 찾았다면 변수(user)에 결과를 담아준다.
5. done을 통해 user의 정보를 복구해준다. 이 때 user의 정보는 req.user가 된다.
back/routes/user.js
const router = require('express').Router();
const passport = require('passport');
// POST /user/login, 로그인
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, clientError) => {
if (err) {
console.error(err);
return next(err);
}
if (clientError) {
return res.status(401).send(clientError.reason); // 401: 허가되지 않은
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
return res.status(200).json(user);
});
})(req, res, next);
});
user router에서 로그인 요청이 왔을 때 passport의 authenticate를 통해 인증을 해주면 된다.
1. passport.authenticate의 인자로 err: server error, user: 받아온 user의 정보(성공 객체), clientError: 클라이언트 에러 이다.
2. err(server error)가 발생했다면, next를 통해 브라우저에 err를 비춰준다.
3. 클라이언트에서 error가 발생 했다면 status 상태코드로 401(허가되지 않은)을 반환하고 전략에서 받아온 에러메세지를 띄운다.
4. req.login은 passport의 login으로 req.login으로 로그인을 할 수 있다.
5. req.login의 인자로 user(성공객체)를 넘겨받아 return res.status(200).json(user); 로 200 상태코드와 json형식으로 정보를
반환한다.
6. 만일 로그인을 하는데 에러가 있다면 next를 통해 loginErr를 전달한다.
[정리]
1. front {email, password} -> server email: req.body.email, password: req.body.password
2. 로그인 전략 실행('local') -> LocalStorategy의 usermaneField: 'email', passwordField: 'password'
3. 성공 시 done(null, user) -> passport index.js의 serializeUser에서 user.id 만을 가짐
4. deserializeUser에서 받아온 user.id를 통해 user 정보 복구
5. user router에서 res.status(200).json(user)로 user의 정보를 반환 (json 형식으로)