MySQL Sequelize를 설치했다고 가정하고 진행되었습니다. 그러나 무슨 SQL을 사용하던 흐름이나 코드는 동일합니다.
local의 로그인/로그아웃 순서는 대략 이렇습니다.
1. form에 회원 정보를 입력하고 Login 버튼을 누름으로써 웹 서버에 POST 메소드를 보냅니다.
2. 백엔드측은 POST를 받으면 passport.authenticate를 실행합니다.
3. passport.authenticate("local", (authError, user, info) => {...}) 을 통해 local 전략을 수행합니다.(LocalStrategy.js) 전략의 수행 결과로 done 함수에 (에러, 성공, 실패) 가 담깁니다.
4. local 전략을 통과했다면(DB에 있는가, 비밀번호는 일치하는가 등) passport.authenticate의 콜백 부분을 실행하여 유저 정보 객체와 req.login를 호출합니다.
5. req.login 메서드는 자동으로 passport.serializeUser를 호출합니다.
6. serializeUser에서 지정한대로 세션에 사용자 아이디를 저장합니다.
7. 그 이후 passport.session() 미들웨어는 항상 passport.deserializeUser를 호출합니다.
8. req.session에 저장된 유저의 아이디를 DB에서 찾고 req.user에 저장합니다.
9. 라우터에서 req.user를 사용할 수 있게 됩니다. middleware.js에 등록해서 전역 객체화 해서 편하게 사용합시다
10. form에서 Logout을 누르면 req.logout 메서드를 실행합니다. req.user 객체를 제거합니다. req.session.destroy() 까지 실행한다면 req.session 객체의 내용도 제거합니다.
* passport를 import함으로써 req.user(로그인이 된 유저의 도큐먼트), req.login, req.logout, req.isAuthenticated(로그인 여부를 boolean값으로 알려줌)이 활성화됩니다. req.session은 serialize, req.user는 deserialize의 결과로 생성됩니다.
* passport.authenticate('local', ...), passport.authenticate('kakao', ...) 꼴로 로그인 처리를 합니다.
* passport와 관련된 미들웨어 (passport.initialize(), passport,session()은 가급적 middleware의 최하단에 위치시킵시다. express-session을 사용하기 때문에 더 위에 있으면 작동하지 않습니다.
* async/await가 자주 사용됩니다. 로그인 정보를 처리할 때까지 기다려야 하기 때문입니다. async/await가 적용될 때는 try/catch로 오류를 잡아주는 습관을 들입시다.
* done(cb)은 passport가 주는 함수입니다.
* req.user가 undefined가 떴다면 미들웨어의 순서가 잘못되었을 가능성이 높습니다. 미들웨어의 순서가 굉장히 중요합니다. 미들웨어의 순서가 잘못되면 req.user가 undefined으로 뜬다던지 세션이 저장이 안되는 등의 오류가 발생합니다. 순서를 위해 가져와봤습니다.
app.use(helmet());
app.set("view engine", "pug");
app.use("/uploads", express.static("uploads"));
app.use("/static", express.static("static"));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
app.use(
session({
secret: process.env.COOKIE_SECRET,
resave: true,
saveUninitialized: false
})
);
app.use(passport.initialize());
app.use(passport.session());
app.use(localsMiddleware);
app.use(routes.home, globalRouter);
app.use(routes.users, userRouter);
app.use(routes.videos, videoRouter);
* Unknown authentication strategy "local"과 같이 인증 방법을 못 찾겠다는 오류가 나오면 대개 app.js에서 passport와 passport의 전략들을 import하지 않았기 때문에 발생하는 오류입니다.
* req.login시 serializeUser를 호출함. 반면 deserializeUser는 passport.session()에서 매 요청시마다 매번 실행됨. 그래서 deserializeUser는 캐싱해서 DB 요청 횟수를 줄여 효율적으로 만드는 것이 중요하다.
* .find() was removed on sequelize V5. instead of using .find() you can use findOne(). 그러니까 sequelize v5 부터 find가 사라졌으니 findOne을 사용해야 한다.
npm i passport passport-local passport-kakao bcrypt
passport를 사용하기 위해 중앙 통제실에 해당하는 app.js에 다음을 import, 연동, middleware를 추가합니다.
// app.js
import passport from "passport";
import passportConfig from "./passport";
//mysql 연동
db.sequelize.sync();
//passport config
passportConfig(passport);
// middleware
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
session({
secret: process.env.COOKIE_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
httpOny: true,
secure: false
}
})
);
app.use(passport.initialize());
app.use(passport.session());
🚗 회원가입과 local 로그인
1️⃣회원가입과 로그인 form의 input을 받을 auth router를 코딩합니다 (+ middleware)
2️⃣ passport에 로컬 전략을 코딩합니다.
3️⃣ app.js와 연결합니다
반드시 순서를 따를 필요는 없습니다. 설명을 위해서 나열했을 뿐이고, 실제로는 왔다갔다 하면서 코딩합니다.
우선, 필요한 패키지를 설치한다. passport, passport-local, bcrypt
npm i passport passport-local bcrypt
passport 폴더 내 사용하고자하는 Strategy.js를 정의하고 인증을 처리할 router도 만들어주자.
1️⃣ 회원가입과 로그인 form의 input을 받을 auth router를 코딩합니다 (+ middleware)
- routes/auth.js
로그인, 로그아웃, 회원가입을 눌렀을 때 이동하는 /auth 라우터입니다. 회원가입에는 <form action="/auth/join">, 로그인에는 <form action"/auth/login> 따위가 있을 겁니다. 또, /auth는 db 정보를 가공하기 위해 거치는 곳이며 곧바로 다른 페이지로 이동합니다. /auth는 단순 처리를 위해 거쳐가는 라우터라고 생각하면 좋습니다.
회원가입부터 봅시다.
우선 동일한 아이디를 가진 유저가 있는지 탐색한후 받은 비밀번호를 bcrypt를 이용해 암호화하여 db에 저장합니다.
import express from "express";
import bcrypt from "bcrypt";
import passport from "passport";
import { isLoggedIn, isNotLoggedIn } from "./middlewares";
// User 테이블을 가져옵니다
import { User } from "../models";
const authRouter = express.Router();
// 회원가입을 처리합니다 POST /auth/join
authRouter.post("/join", isNotLoggedIn, async (req, res, next) => {
// form을 통해 전달된 input 결과물을 가져옵니다
const { email, nick, password } = req.body;
try {
// email이 중복되지는 않는지 확인합니다
const exUser = await User.findOne({ where: { email } });
// 중복되었다면 redirect로 돌려보냅니다.
if (exUser) {
req.flash("joinError", "이미 가입된 이메일입니다");
return res.redirect("/join");
} else {
// 이메일이 중복되지 않았다면 가입시키기 위해 비밀번호를 암호화합니다
// hash함수의 두번째 인자는 암호화의 복잡도이며 큰 수일수록 오래 걸립니다
// 1초 정도 걸리게 console.time, console.timEnd를 찍어봅시다
console.time("암호화에 걸리는 시간");
const hash = await bcrypt.hash(password, 12);
console.timeEnd("암호화에 걸리는 시간");
await User.create({ email, nick, password: hash });
}
return res.redirect("/");
} catch (error) {
console.log(error);
next(error);
}
});
// 로그인을 처리합니다 POST /auth/login
authRouter.post("/login", isNotLoggedIn, (req, res, next) => {
passport.authenticate("local", (authError, user, info) => {
// authError, user, info에는 각각 done(에러, 성공, 실패) 정보가 담겨있습니다
if (authError) {
// 에러면 에러 핸들러로 보냅니다
console.log(authError);
return next(authError);
}
if (!user) {
// 유저 정보가 없다는 것을 로그인 실패를 의미합니다
req.flash("loginError", info.message);
return res.redirect("/");
}
// 유저 정보가 있으면 성공이므로 req.login 시킨다.
// 다만 여기서도 혹시 오류가 날 수도 있으니 에러 처리를 해준다
return req.login(user, loginError => {
if (loginError) {
console.log(loginError);
return next(loginError);
}
});
})(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});
// 로그아웃을 처리합니다 GET /auth/logout
authRouter.get("/logout", isLoggedIn, (req, res) => {
req.logout();
req.session.destroy(); // 안해도 됨
res.redirect("/");
});
export default authRouter;
- routes/middlewares.js
로그인 하지도 않았는데 로그아웃 페이지나 프로필 페이지에 접속할 수 있다면 오류를 발생시키게 될 것입니다. 이를 막기 위해 다음과 같은 미들웨어를 만들어 미들웨어로 포함 시켜주면 오류를 줄일 수 있습니다. 이와 같은 처리는 특수한 것이 아니라 passport.js를 사용함에 있어 일반적인 것입니다. 다들 한다는 말입니다.
예를 들어 프로필 페이지에는 다음과 같이 isNotLoggedIn을 삽입해줍니다
// 프로필 페이지
router.get("/profile", isLoggedIn, (req, res, next) => {
res.render("profile", { title: "내 정보 - Twitter", user: null });
});
// 로그인이 된 사람만 다음 내용을 볼 수 있게 합니다.
export const isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(403).send("로그인 필요");
}
};
// 로그인이 안 된 사람만 다음 내용을 볼 수 있게 합니다.
export const isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
next();
} else {
res.redirect("/");
}
};
- routes/index.js
로그인이 되었다는 것을 보여줘야하므로 controller 함수에 전달하는 변수에 user를 추가합니다. 유저 정보를 사용하는 라우터는 모두 user:req.user를 통해서 유저 정보를 전달해줘야 합니다. 유저 정보를 사용할지 안 할지 모르겠다면 우선 다 넣고 보세요.
// 메인페이지
router.get("/", (req, res, next) => {
res.render("main", {
title: "Twitter",
twits: [],
// deserialize가 보내준 req.user
user: req.user,
loginError: req.flash("loginError")
});
});
// 프로필 페이지
router.get("/profile", isLoggedIn, (req, res, next) => {
res.render("profile", { title: "내 정보 - Twitter", user: req.user });
});
// 회원가입 페이지
router.get("/join", isNotLoggedIn, (req, res) => {
res.render("join", {
title: "회원가입 - Twitter",
// deserialize에서 담아준 req.user
user: req.user,
joinError: req.flash("joinError")
});
});
2️⃣ passport에 로컬 전략을 코딩합니다.
- /passport/index.js
import local from "./localStrategy";
import kakao from "./kakaoStrategy";
import { User } from "../models";
const user = {};
export default passport => {
passport.serializeUser((user, done) => {
// user에는 {id: 1, email:xx@mail.com} 등이 들어 있습니다.
// 세션에 모두 저장하기에는 무거우므로 고유값(id)만 저장합시다
// mysql에서 자동생성하는 건 id, mongoDB는 _id 입니다
done(null, user.id);
});
passport.deserializeUser((id, done) => {
// 세션에서 고윳값을 가지고 DB에서 유저의 정보를 찾아냅니다.
// 찾아낸 정보는 req.user에 담아줍니다
if (user[id]) {
done(user[id]);
} else {
User.findOne({ where: id })
.then(user => done(null, user))
.catch(err => done(err));
}
});
local(passport);
kakao(passport);
};
- /passport/localStrategy.js
로컬 전략을 사용할 경우의 논리적인 처리입니다. 폼에게 받은 input을 검증하고 암호화하여 User 모델에 추가합니다.
import { Strategy as LocalStorage } from "passport-local";
import bcrypt from "bcrypt";
// User 모델을 불러옵니다
import { User } from "../models";
export default passport => {
passport.use(
new LocalStorage(
{
//제출된 input의 name과 동일해야 합니다
// urlencoded 미들웨어가 해석한 req.body.email, req.body.password를
// usernameField, passwordField에 연결합니다.
usernameField: "email",
passwordField: "password"
},
async (email, password, done) => {
// done(에러, 성공, 실패)
// 빈 값은 done(null, false) 입니다
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
// 비밀번호가 일치하면 로그인 성공 아니면 실패
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, { message: "비밀번호가 일치하지 않습니다." });
}
} else {
// 이메일이 없으면 가입되지 않은 회원
done(null, false, { message: "가입되지 않은 회원입니다." });
}
} catch (error) {
console.log(error);
done(error);
}
}
)
);
};
3️⃣ app.js와 연결합니다
app.js에는 다음과 같이 passport 패키지를 연결하고 사용합니다
// app.js
import passport from "passport";
import passportConfig from "./passport";
// passport/index.js 활용
passportConfig(passport);
// middleware 최하단에
app.use(passport.initialize());
app.use(passport.session());
이제 회원가입을 하고 그 정보로 로그인하는 것이 가능해졌다. mysql도 확인해보면 가입한 정보들이 쌓인 것을 볼 수 있다.
🚗 로그아웃
passport에서 마련해준 req.logout을 실행하면, 쿠키, 세션 삭제 등 모든 일을 다 해준다. 소셜 로그인도 마찬가지이다. 꿀!
globalRouter.get("/logout", (req, res) => {
req.logout();
res.redirect(routes.home);
};);
'Node, Nest, Deno > 🔑 JWT, Passport.js' 카테고리의 다른 글
Passport JWT(JSON Web Token) (0) | 2020.04.25 |
---|---|
passport.js : (5) facebook 로그인 (0) | 2020.04.08 |
passport.js : (4) github 로그인 (0) | 2020.04.03 |
passport.js : (3) kakao 로그인 (0) | 2020.04.02 |
passport.js : (1) passport-local-mongoose를 활용한 local auth (0) | 2020.03.27 |