기존 로그인시 jsonwebtoken을 활용하여 기존 액세스 토큰과 재발급 토큰으로 구현했었다. 그러나 소셜로그인 기능을 구현하기 위해, 구글 공식 api를 참조해봤지만 작성하는데 어려움이 있었고, 더 간편한 방법을 찾았다 대표적으로 firebase
라는 훌륭한 시스템이 존재하지만
나는 passport
라는 라이브러리를 사용해보고 싶었다. 이번엔 passport
로 로그인 코드와 JWT 토큰을 인증 받는 코드를 작성해보겠다.
Passport는 공식홈페이지에서 이렇게 설명하고있다.
Simple, unobtrusive authentication for Node.js
Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.
passport
는 여권을 뜻하는데, 여권은 여행용 증서로 다른 국가에 여행할때 자신을 알려주는 증명서이다. 이 라이브러리 passport
는 외부인(사용자)이 웹페이지를 이용할 때 로그인, 소셜 로그인을 통해 세션이나 쿠키에 토큰을 발급받고, 인증하는 편리한 라이브러리이다.
passport
는 정말 간편한 점이 코드를 한번 익혀놓으면 다른 소셜 로그인 구현에도 거의 복붙 수준으로 간단하고, 쉽게 작성이 가능하다.
일반여권, 관용여권, 외교관여권 등 다양한 여권이 존재하는 것처럼, passport
라이브러리도 passport-jwt
, passport-facebook
등 다양한 종류가 있다.
register: async (req, res) => {
const { password, email, username } = req.body;
const hash = await bcrypt.hashSync(password, 10);
const newUser = {
username: username,
email: email,
password: hash,
};
try {
await User.create(newUser).then((data) => {
res.json({ data: data });
});
} catch (e) {
console.log(e);
}
},
login: async (req, res) => {
const { password, email } = req.body;
const origin = await User.findOne({ where: { email } });
if (!origin) {
return res.status(404).json({
message: "유저를 찾을 수 없습니다.",
});
}
if (!(await bcrypt.compareSync(password, origin.password))) {
return res.status(400).json({
message: "비밀번호가 일치하지 않습니다.",
});
}
// delete data.dataValues.password;
const accTokens = generateAccessToken({ _id: origin.id });
const refTokens = generateRefreshToken({ _id: origin.id });
sendAccessToken(res, accTokens);
sendRefreshToken(res, refTokens);
},
기존 코드는 DB의 USER테이블을 조회하고, 중복 여부를 검사해 진행하도록 작성되어있다.
sendRefreshToken: (res, refTkn) => {
res
.cookie("ref_token", refTkn, {
httpOnly: true,
})
.json({ message: "리프레시 토큰 발급 완료" });
},
sendAccessToken: (res, accTkn) => {
// res.json({
// token: accTkn,
// message: "로그인 성공",
// });
res
.cookie("auth_token", accTkn, {
httpOnly: true,
})
.json({
maessage: "토큰 발급 완료",
});
},
이때 발급받는 Access Token
은 auth_token
이라는 이름의 쿠키에다 저장하는데, 문제는 accesstoken
만료 시 refreshToken
을 가져와 accesstoken
으로 활용되어야 한다는 점인데, 아직 실력이 부족하여 그 부분은 진행하지 못하고있다.
아무튼 Passport
와 passport-jwt
를 활용해 토큰 인증 기반 로그인 폼을 작성하려고한다.
로그인을 구현하는데 필요한 라이브러리는 다음과 같다.
bcrypt
passport
passport-jwt
passport-local
jsonwebtoken
npm i bcrypt passport passport-jwt passport-local jsonwebtoken --save
으로 한번에 설치해준다.
패스포트 로컬
패스포트 로컬 깃허브
passport-local
사용법을 보면 다음과 같은데..
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'passwd',
passReqToCallback: true,
session: false
},
function(req, username, password, done) {
// request object is now first argument
// ...
}
));
첫번째 인자로 옵션과 두번째 인자로 콜백 함수를 적용할 수 있다.
session
을 사용하지 않을경우 false
로 둘 수 있으며,
passReqToCallback
을 true
로 활성화 하게되면 콜백 함수로 전달해준다.
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const MicrosoftStrategy = require("passport-google-oauth20").Strategy;
const LocalStragegy = require("passport-local").Strategy;
const JWTStrategy = require("passport-jwt").Strategy;
const ExtractJWT = require("passport-jwt").ExtractJwt;
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { User } = require("../models");
require("dotenv").config();
const passportloginVerify = async (username, password, done) => {
try {
const user = await User.findOne({ where: { email: username } });
if (!user) {
done(null, false, { message: `존재하지 않는 사용자입니다.` });
return;
}
const compareResult = await bcrypt.compare(password, user.password);
if (compareResult) {
done(null, user);
return;
}
done(null, false, { reason: "올바르지 않은 비밀번호 입니다." });
} catch (e) {
console.log(e);
done(e);
}
};
let passportConfig = {
usernameField: "email",
passwordField: "password",
};
passport.use(
"signin",
new LocalStragegy(passportConfig, passportloginVerify)
);
우선 전체 코드를 보면 다음과같다.
const LocalStragegy = require("passport-local").Strategy;
const JWTStrategy = require("passport-jwt").Strategy;
const ExtractJWT = require("passport-jwt").ExtractJwt;
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { User } = require("../models");
require("dotenv").config();
필요한 라이브러리를 싹다 긁어 모아준다.
const {Strategy: JWTStrategy, ExtractJwt: ExtractJWT}= require("passport-jwt").Strategy;;
이렇게도 작성이 가능하다.
let passportConfig = {
usernameField: "email",
passwordField: "password",
session: false,
};
첫번째 인자로 옵션이 들어간다. 보안상 세션이 더 유리하다고 하지만 AWS 프리티어인 나로서는 약간이나마 부담을 덜기 위해 쿠키로 사용하려고한다..
그렇기 때문에 session: false
const passportloginVerify = async (username, password, done) => {
try {
const user = await User.findOne({ where: { email: username } });
if (!user) {
done(null, false, { message: `존재하지 않는 사용자입니다.` });
return;
}
const compareResult = await bcrypt.compare(password, user.password);
if (compareResult) {
done(null, user);
return;
}
done(null, false, { reason: "올바르지 않은 비밀번호 입니다." });
} catch (e) {
console.log(e);
done(e);
}
};
두번째 콜백함수이다.
async
와 await
로 비동기로 작성해주었다.
가장 먼저 User
테이블을 조회하여 중복된 이메일이 있는지 찾고난 후,
done()
을 통해 빠져나온다.
그리고 현재 Input
에 입력한 패스워드와 DB에 저장된 유저의 페스워드를 검증한다.
검증된다면, 결과를 반환한다.
마지막으로
passport에 적용한다.
passport.use(
"signin",
new LocalStragegy(passportConfig, passportloginVerify)
);
passport.serializeUser((user,done)=>{
done(null,user);
});
passport.deserializeUser((user,done)=>{
done(null,user);
});
const router = require("express").Router();
const {
signin,
signup,
logout,
profile,
} = require("../controller/userController/userController");
const { authorization } = require("../config/JWTConfig");
router.post("/auth/login", signin);
router.post("/auth/register", signup);
router.get("/auth/logout", authorization, logout);
router.get("/auth/profile", authorization, profile);
module.exports = router;
signin: async (req, res, next) => {
try {
passport.authenticate("signin", (err, user, info) => {
if (err || !user) {
res.status(400).json({ message: info });
return;
}
req.login(user, (err) => {
console.log(user);
if (err) {
res.json({
message: err,
});
}
User.findOne({
where: { email: user.email },
}).then((user) => {
const token = generateAccessToken({
id: user.id,
});
sendAccessToken(res, token);
});
});
})(req, res, next);
} catch (e) {
res.json({
message: e,
});
}
},
passport.authenticate()
로signin
이라는 전략을 호출했는데 그 이후의 코드를 이렇게 작성해주는게 맞는건지 확실치 않지만 결과는 잘되더라..
signin: async (req, res, next) => {
try {
passport.authenticate("signin", (err, user, info) => {
if (err || !user) {
res.status(400).json({ message: info });
return;
}
이부분은 passportconfig
를 잘못입력했거나, input
값이 잘못되면 걸러준다.
req.login(user, (err) => {
console.log(user);
if (err) {
res.json({
message: err,
});
}
User.findOne({
where: { email: user.email },
}).then((user) => {
const token = generateAccessToken({
id: user.id,
});
sendAccessToken(res, token);
});
});
})(req, res, next);
DB에 유저를 조회하고, 이메일이 일치한다면, 회원가입할때 자동으로 생성된 고유값 ID(auto increase)를 반으로 토큰을 발급한다.
토큰은 res.cookie
를 통해 cookie로 저장된다.
이제 작성이 끝났으니 결과를 보자
????????
[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
이 오류를 구글링 해봤더니, 서버가 클라이언트에게 두번 이상의 요청을 보낼때 발생하는 에러라고한다..
요청은 겹치지 않은것 같은데 뭔가 이상하다.. 그래서 계속 코드를 찾아봤다.
req.login(user, (err) => {
console.log(user);
if (err) {
res.status(404).json({
message: err,
});
}
User.findOne({
여러차래 수색 끝에 원인을 찾았는데, if(err)
문이 passport
전략을 작성한 부분에서 error
요청이 겹친거였다.. 그래서 이부분을 지운 최종 로그인 코드는 아래와 같다.
signin: async (req, res, next) => {
try {
passport.authenticate("signin", (err, user, info) => {
if (err || !user) {
res.status(400).json({ message: info });
return;
}
req.login(user, (err) => {
console.log(user);
User.findOne({
where: { email: user.email },
}).then((user) => {
const token = generateAccessToken({
id: user.id,
});
sendAccessToken(res, token);
// res.json({
// auth: true,
// token: token,
// maessage: "토큰 발급 완료",
// });
// res.redirect("/");
});
});
})(req, res, next);
} catch (error) {
res.json({
message: error,
});
}
},
결과를 확인해보자
이번엔 jsonwebtoken을 통해 인증을 받아보자