728x90
반응형
passport 개념
Node.js 를 위한 인증 미들웨어이며, Express 기반 웹에서 유용하게 사용할 수 있다. Passport는 이름 그대로 서비스를 사용할 수 있게끔 해주는 여권 같은 역할을 하는 모듈이다. 로그인을 쉽게 할 수 있게 도와준다.
strategy 종류 (로그인 인증 방식) :
Local Strategy(passport-local) : 로컬 DB에서 로그인 인증 방식
Social Authentication (passport-kakao, passport-twitter 등) : 소셜 네트워크 로그인 인증 방식
Passport 설치
내가 지금 구현하고자 하는 것은 local strategy로 DB에서 로그인 인증 방식을 구현할 것이다.
- passport-local : 직접 구현할 때 사용
- express-session : passport를 통해 로그인 후 유저 정보를 세션에 저장하기 위해 사용
- crypto : 비밀번호를 암호화하여 로컬에 있는 비밀번호를 비교하기 위해서 사용
$ npm install passport passport-local express-session crypto
반응형
server.js
const express = require("express");
const app = express();
const port = 3001; // <- 3000에서 다른 숫자로 변경
const cors = require("cors");
const bodyParser = require("body-parser");
const mysql = require("mysql");
const { sequelize } = require('./db/models'); //----------------------(1)
const passport = require('passport'); // ------------------------------(2)
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
var FileStore = require('session-file-store')(session)//---------------(3)
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors(cors));
app.use(bodyParser.json());
app.use(
cors({
origin: '<http://localhost:3000>',
credentials: true,
})
);
sequelize // --------------------------------------------------------(1)
.sync({ force: false })
.then(() => {
console.log("데이터베이스 연결 성공");
})
.catch((err) => {
console.error(err);
});
app.use(session({ // -------------------------------------------------(2-a)
resave: false,
saveUninitialized: false,
secret: process.env.SESSION_SECRET,
store: new FileStore(),
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use('/user', require('./db/routes/login'));
app.listen(port, () => {
console.log(`Example app listening at <http://localhost>:${port}`);
});
- mysql을 사용하여 db에 저장하기 위해서 sequelize을 사용하였다.
- passport 사용을 위하여 이와 관련된 모듈을 설치하여 주었다. 로그인했을 때 페이지 이동하여도 로그인 정보를 세션에 남겨주기 위하여 설치하였다. 그리고 2-a에 보면 session과 관련한 설정을 주었다.
- express-session option
- secret : 필수 옵션이며, 세션을 암호화할 때 사용한다. 이를 여기 코드에 쓰면 안되고, 따로 파일로 만들어서 빼줘야 한다. 유출되면 안되기 때문이다.
- cookie
- path : 쿠키 경로 설정
- httpOnly : 클라이언트 측 자바스크립트를 통하여 쿠키에 접근을 제한
- secure : HTTPS 필요
- maxAge : 쿠키 유효기간 설정
- cookie의 기본 값은 { path: '/', httpOnly: true, secure: false, maxAge: null }
- resave : 세션에 변경사항이 없어도 요청마다 세션을 다시 저장, 기본 옵션인 true는 deprecated 상태로 false 권장한다. 너무 많은 세션이 생기면 부하가 생긴다.
- saveUninitialized : 세션에 저장할 내용이 없더라도 uninitialized 상태의 세션을 저장, 기본 옵션인 true는 deprecated 상태로 false 권장
- express-session option
- var FileStore = require('session-file-store')(session)이 구문의 경우 session이 잘 담기는지 확인하고 싶다면, 이 모듈을 추가하는 것이 좋다. 그러면 현재 파일보다 앞의 파일에 session이라는 파일이 생성되고 이 안에 session과 관련된 설정한 것들이 담겨질 것이다.
- 그리고 session에 store:new FileStore()를 추가해주면 된다.
auth.js
const express = require("express");
const router = express.Router();
const util = require("util");
const crypto = require("crypto");
const cors = require("cors");
const models = require("../models");
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const { isLoggedIn, isNotLoggedIn } = require("../middlwares/auth");
router.use(passport.initialize()); // -------------------------------(1)
router.use(passport.session());
passport.serializeUser((user, done) => { // ------------------------------(2)
console.log("serializeUser :: ", user);
done(null, user.join_id); // 세션에 사용자 정보인 id를 저장
// console.log('serializeUser :: ',);
});
passport.deserializeUser(async (join_id, done) => {
// 세션에 저장되어 있는 id를 인자 값으로 전달받음
console.log("deserializeUser :: ", join_id);
try {
const user = await models.Users.findOne({
where: {
join_id: join_id,
},
raw: true,
});
done(null, user); // req.user 객체 생성
} catch {
done(null, false, {
message: "서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.",
});
}
});
passport.use( // -----------------------------------------------------------(3)
new LocalStrategy(
{
session: true, // 세션 저장 여부
usernameField: "join_id", // form > input name
passwordField: "password",
},
async (join_id, password, done) => {
try {
// 회원정보 조회
const user = await models.Users.findOne({
where: {
join_id: join_id,
},
raw: true,
});
const pbkdf2Promise = util.promisify(crypto.pbkdf2);
let result = await models.Users.findOne({
attributes: ["salt", "password"],
where: {
join_id: join_id,
},
});
// console.log("result :: ", result);
if (result !== null) {
console.log("salt :: ", result.dataValues.salt);
console.log("userPassword :: ", result.dataValues.password);
}
const userSalt = result.dataValues.salt;
const userPassword = result.dataValues.password;
async function verifyPassword(password, userSalt, userPassword) {
const key = await pbkdf2Promise(
password,
userSalt,
104906,
64,
"sha512"
);
const hashedPassword = key.toString("base64");
if (hashedPassword === userPassword) return true;
return false;
}
verifyPassword(password, userSalt, userPassword).then((answer) => {
console.log("answer :: ", answer);
// let answer_ch = answer;
});
const result_ans = await verifyPassword(
password,
userSalt,
userPassword
);
console.log("result_ans :: ", result_ans);
// 회원정보가 없거나 비밀번호가 일치하지 않는 경우 ------------------(4)
if (!user || !result_ans) {
done(null, false, {
message: "존재하지 않는 아이디이거나 비밀번호가 일치하지 않습니다.",
});
} else {
console.log("존재하는 아이디입니다.");
done(null, user); // serializeUser로 user 전달
}
} catch {
done(null, false, {
message: "서버에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.",
});
}
}
)
);
router.post("/login", isNotLoggedIn, (req, res, next) => { // --------------(5)
// res.set('access-control-allow-origin', '<http://localhost:3000/>')
const password = req.body.password;
console.log("req.session :: ", req.session);
console.log("/user/login-----> ", req.body);
// -> passport LocalStrategy 으로 전송.
passport.authenticate("local", (err, user, info) => {
if (err) {
console.log("[post]/user/login (Error) ", err);
return next(err);
}
if (info) {
console.log("[post]/user/login (Fail) ", info);
return res.status(401).send(info);
}
console.log("[post]/user/login ", user);
// 세션에다 사용자 정보 저장( 어떻게 저장할 것인가? )
return req.login(user, (err) => {
if (err) {
return next(err);
}
return res.redirect("<http://localhost:3000/>");
});
})(req, res, next);
});
module.exports = router;
- initialize와 session는 passport 초기화와 session 등록을 해주는 코드이다.
- passport.serializeUser()와 passport.deserializeUser()
- passport.serializeUser() 함수를 통해 로그인 성공시 사용자 정보를 세션에 저장한다. LocalStrategy 객체의 콜백 함수에서 done()으로 전달받은 user의 join_id를 세션(req.session.passport.user)에 저장하기 위해 user.join_id를 반환한다. 세션의 무게를 줄이기 위해, user의 id만 세션에 저장한다. + 이 부분이 헷갈려서 많이 헤맸다. 처음에 user을 console.log로 하여 받은 다음 변수명이 어떻게 되어 있는지 확인하면 좋을 듯하다.
- passport.deserializeUser() 함수는 로그인 인증이 되어있는 경우, 요청할 때마다 호출하여 실행한다. 세션에 저장된 id를 이용하여 DB에 접근하여 사용자 정보를 추가로 select 한 후 해당 유저 정보가 있으면 done을 통해 req.user에 저장한다. serializeUser에서 done으로 넘겨주는 user가 deserializeUser의 첫 번째 매개변수로 전달되기 때문에 둘의 타입은 항상 일치해야 한다.
- 여기서 진짜진짜진짜 많이 헤맸다. new LocalStrategy() 를 통해 로컬 전략을 세운다. 여기서는 로컬만 하지만, 소셜 로그인 인증을 위한 전략도 세울 수 있다. usernameField와 passwordField는 프론트의 데이터를 넘겨주는 input 태그의 name값을 넣어준다. 여기서 session은 세션의 저장여부를 의미한다. 여기서 좀 더 복잡한게 비밀번호를 암호화하여 회원가입을 시켰기 때문에 로그인할 때도 암호화된 비밀번호를 비교하기 위하여 로그인할 때 쳐준 비밀번호도 암호화시켜 db에 저장된 비밀번호와 일치하는지 확인해야 한다. 그리고 일치하면 콜백함수에 넘겨준다. + 4와 5를 연결해서 사용해야 하는데, 콘솔로 하여 출력을 시켜보았을 때 연결이 계속 안되었다. 근데 5번의 저 코드를 써주니 연결되는 것을 확인하였다.
- 콜백 함수에서 해당 태그들의 value를 인자 값으로 전달받아, DB에서 받은 정보가 일치하는지 본다. 일치하면 done()을 통해 passport.serializeUser()로 user를 전달한다. 그렇지 않는다면 err message를 출력한다.
- passReqToCallback: express의 req 객체에 접근 가능 여부가 true일 때, 뒤의 callback 함수에서 req 인자가 더 붙게 된다. (req, email, password, done) => {}
- done
- 첫 번째 인자: DB조회시 발생하는 서버 에러. 무조건 실패하는 경우에만 사용한다.
- 두 번째 인자: 성공했을 때 return할 값이다. 성공하게 된다면 첫번 째 인자의 경우 null이 된다.
- 세 번째 인자: 사용자가 임의로 실패를 만들고 싶을 때 사용한다. 이러기에 세번째 인자에 에러 메시지를 작성한다.
- 라우터에서 /login으로 post 요청을 보내면 passport에서 local에 대한 인증 작업을 시작한다. 실패할 경우와 성공할 경우 어디로 돌려보낼 지 각각 failureRedirect와 res.redirect에 위치를 지정할 수 있다.
순서 : 5 - 3 - 4 - 2
다음과 같은 사이트를 참고하였습니다.
https://velog.io/@jiheon/Node.js-Passport
https://opentutorials.org/course/2136/12134
https://mindflip.github.io/javascript/node.js/nodejs_passport/
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=pjok1122&logNo=221565691611
728x90
반응형