Lsiron
HELLO FOLIO(express, mongoose)- 5(jwt를 이용한 토큰 발급 로그인 구현) 본문
현재 폴더 구조
web_project/
├── db/
│ ├── model # 각종 모델 폴더
│ ├── schemas # 각종 스키마 폴더
│ └── index.js # db 연결파일
├── middleware/ # 각종 middleware 폴더
├── passport/ # 로그인 관련 passport 폴더
│ ├── strategies # 전략 구성
│ └── index.js # 전략 exports 파일
├── routes/ # 라우트 관련 폴더
│ └── index.js # 라우트 exports 파일
├── services/ # 유저 관련 service 폴더
├── views/ # EJS 템플릿 폴더
├── public/ # static 파일 폴더
│ ├── css/ # CSS 파일들
│ ├── js/ # JS 파일들
│ └── images/ # 이미지 파일들
├── .env # 환경 변수 파일
├── .gitignore # Git 무시 파일
├── package.json # 프로젝트 메타데이터 및 종속성 목록
├── app.js # 애플리케이션 진입점
├── index.js # 서버 실행 파일
└── README.md # 프로젝트 설명 파일
현재 설치한 모듈
"dependencies": {
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.7.0",
"mongoose": "^8.4.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0"
},
전 날 로그인에 필요한 전략을 다 만들었다. 이제 본격적으로 토큰을 쿠키에 담아서 저장시킬 차례이다.
먼저 userRouter.js로 가보자.
https://www.npmjs.com/package/passport
위 사이트에서 Authenticate Requests 단락을 보면 라우터에 어떻게 적용을 시켜야하는지 나온다. passport 라이브러리를 불러 온 후, 그대로 긁어와서 로그인 라우터에 적용시키고, local로 가져갔던 로그인 검증을 빼주자.
const express = require('express');
const router = express.Router();
const UserModel = require('./db/model/userModel'); // db파일의 models에서 User class 불러오기
const bcrypt = require('bcrypt');
const passport = require('passport'); // passport 라이브러리
router.get('/login', (req, res) => {
res.render('login.ejs')
})
router.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), (req, res, next) => {
res.redirect('/my-page');
});
router.get('/register', (req, res) => {
res.render('register.ejs')
})
router.post('/register', async (req, res, next) => {
const { email, password, name } = req.body;
// 사용자가 이미 존재하는지 확인
const checkingUser = await UserModel.findOne({ email });
if (checkingUser) {
console.log('이미 존재하는 사용자입니다');
}
// 비밀번호를 해싱함
const hashedPassword = await bcrypt.hash(password, 10); // 첫번째 인자는 password 변수 두번째 인자는 몇번 꼬을지 횟수
// 새로운 사용자 생성
const newUser = {
email,
password : hashedPassword,
name,
};
await UserModel.create( newUser );
res.redirect('/login');
});
module.exports = router;
뭔가 되게 거창해보이지만, 나는 로그인 라우터에 미들웨어를 추가한 것 밖에 없다. 또한 더이상 로그인 라우터에서 mongoose 함수를 필요로 하지 않기 때문에 기존에 적용했던 async 함수도 빼준다.
즉, 로그인 라우터에 우리가 넷째 날에 만들었던 local 로직을 적용시키는 것이다.
지금 코드는 미들웨어를 적용한 뒤, 로그인에 실패하면 /login 경로로 이동하고 성공하면 /my-page 경로로 이동하는 코드밖에 짜여져있지 않다.
지금부터 우리는 이 로그인 미들웨어를 확장하려고 한다.
먼저 현재는 passport.authenticate 가 미들웨어로 직접 사용되어 있다. 허나 이를 콜백 함수로 변경하고, 쿠키를 적용할 예정이기 때문에 세션을 사용하지 않도록 설정해보자.
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user) => {
res.redirect('./my-page')
})(req, res, next);
});
passport.authenticate('local', { session: false }, (err, user) => { ... })는 미들웨어 함수를 반환한다.
err,user 인자는 어디에서 나온 것 이냐?
npm 사이트에서 긁어 왔을때 부터 있었고 우리가 이전에 만들어줬던 local 전략이 done(null, user) 이렇게 던져주는 것을 작성 했을 것이다. 바로 err는 null 즉 없고, user는 검증된 user 정보를 던져주는 것.
이 반환된 미들웨어 함수는 req, res, next를 인자로 받아야 정상적으로 작동한다. 따라서 (req, res, next)를 전달하여 미들웨어가 현재 요청에 대해 작동할 수 있도록 한다.
쉽게 말해서 Math.max(x+1 , 1) 처럼 미들웨어 함수(req, res, next) 이렇게 되는 것.
router.post('/login', (req, res, next) => {
미들웨어함수(req, res, next);
});
저렇게 길었던 코드가 위 처럼 되는 것 이다. 그냥 미들웨어 함수를 반환해 주는 값에 다 때려넣은 것 이라 생각하면 된다.
이제 다음으로 넘어가보자.
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// 로그인 실패 시 local strategy에 설정한 메시지 전달
return res.status(401).json(info.message);
}
// 성공 시 처리할 코드
res.redirect('./my-page')
})(req, res, next);
});
에러 발생 시 어떻게 할 것인지, 로그인 실패시 어떻게 처리 할 것인지를 삽입해준다.
err, user에 더하여 info 인자도 추가하였다. 이 info는 local Strategy에서 발생한 메시지를 전달하는 역할을 한다.
local Strategy에서 아래 코드를 봤을 것이다. message 객체가 바로 info 이다. 객체이기 때문에 info.message로 작성하였고 이 값을 응답 해 주기로 했다.
return done(null, false, { message: '회원을 찾을 수 없습니다' });
다음으로 가장 중요한 JWT 토큰 생성 코드를 삽입하면 끝이다.
토큰 발급을 위한 코드는 https://www.npmjs.com/package/jsonwebtoken
위 사이트에서 긁어오자 보면 jwt.sign() 함수로 되어있는데
jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: '1h' });
이렇게 써진 코드를 긁어오자. 우리는 accessToken과 refreshToken을 지급 할 것이다.
각각 변수로 선언하여 넣어주자.
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// 로그인 실패 시 local strategy에 설정한 메시지 전달
return res.status(401).json(info.message);
}
// 성공 시 처리할 코드
const accessToken = jwt.sign({ data: 'foobar'}, 'secret', { expiresIn: '1h' });
const refreshToken = jwt.sign({data: 'foobar'}, 'secret', { expiresIn: '1h' });
res.redirect('./my-page')
})(req, res, next);
});
위에서 있는 data: 'foobar' 가 바로 토큰에 담길 실제 데이터인데 이를 payload 라고 한다. 토큰만으로 사용자 정보를 확인 할 수 있도록 하기위해 넣어주는 값이다.
즉 우리의 페이지에 로그인 하는 사용자의 정보를 집어 넣어줘야한다.
payload 변수를 선언 해 주고 그 값을 넣어보자.
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// 로그인 실패 시 local strategy에 설정한 메시지 전달
return res.status(401).json(info.message);
}
// 성공 시 처리할 코드
const userObject = user.toObject(); // 사용자 객체를 JSON 형태로 변환
const payload = { _id: userObject._id }; // JWT 페이로드 생성
// Access Token 발급
const accessToken = jwt.sign(payload, 'secret', { expiresIn: '1h' });
// Refresh Token 발급
const refreshToken = jwt.sign(payload, 'secret', { expiresIn: '1h' });
res.redirect('./my-page')
})(req, res, next);
});
자 위와 같이 설정 해 주었다. 그런데 이러면 accessToken이나 refreshToken이나 별반 차이가 없다.
jwt.sign의 가운데 인자는 토큰의 키 역할을 하며, refresh token의 키를 바꾸어주자. 동시에 유효기간도 바꿔보자.
일반적으로 accessToken은 1시간 refreshToken은 2주이다.
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// 로그인 실패 시 local strategy에 설정한 메시지 전달
return res.status(401).json(info.message);
}
// 성공 시 처리할 코드
const userObject = user.toObject(); // 사용자 객체를 JSON 형태로 변환
const payload = { _id: userObject._id }; // JWT 페이로드 생성
// Access Token 발급
const accessToken = jwt.sign(payload, 'secret', { expiresIn: '1h' });
// Refresh Token 발급
const refreshToken = jwt.sign(payload, 'toosecret', { expiresIn: '14d' });
res.redirect('./my-page')
})(req, res, next);
});
이 토큰을 이제 각각 쿠키에 넣어 보자 단, refreshToken은 DB에도 저장을 해야한다.
쿠키에 넣는김에 refreshToken도 DB에 저장해주자.
하지만, 쿠키를 사용하기 위해서는 먼저 라이브러리를 설치해야한다.
다음 명령어를 사용하여 cookie-parser를 다운받아주자.
$ npm i cookie-parser
그리곤 app.js로 가서, cookie-parser 라이브러리를 사용하기 위한 코드를 넣어주자.
const express = require('express');
const app = express();
const bcrypt = require('bcrypt');
const passport = require('passport');
const { userRouter, myPageRouter } = require("./routes");
require('./passport')();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); // cookie-Parser 라이브러리 사용
app.use(passport.initialize());
app.use('/', userRouter);
app.use('/my-page', authMiddleware ,myPageRouter)
module.exports = { app };
다시 userRouter.js 로 가서, cookie와 db에 저장하는 코드를 입력하자.
그 전에 db에 refreshToken을 저장 해 주려면 UserSchema.js에 refreshToken 필드를 추가해야한다.
userSchema.js로 가서 refreshToken 필드를 추가해주자.
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
// 로그인 토큰
refreshToken: {
type: String,
},
},
{
collection: "users",
timestamps: true,
});
module.exports = UserSchema;
다시 userRouter.js 로 가서, 각 Token을 cookie에 담아 전달하고 refreshToken을 db에 저장하는 코드를 입력하자.
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// 로그인 실패 시 local strategy에 설정한 메시지 전달
return res.status(401).json(info.message);
}
// 성공 시 처리할 코드
const userObject = user.toObject(); // 사용자 객체를 JSON 형태로 변환
const payload = { _id: userObject._id }; // JWT 페이로드 생성
// Access Token 발급
const accessToken = jwt.sign(payload, 'secret', { expiresIn: '1h' });
// Refresh Token 발급
const refreshToken = jwt.sign(payload, 'toosecret', { expiresIn: '14d' });
// Refresh Token을 DB에 저장
await UserModel.updateOne({ _id: userObject._id }, { refreshToken });
// 토큰을 쿠키로 전달, httpOnly로 설정된 쿠키는 클라이언트 측 스크립트에서는 읽거나 수정 안됨
res.cookie('accessToken', accessToken, { httpOnly: true });
res.cookie('refreshToken', refreshToken, { httpOnly: true });
res.redirect('./my-page')
})(req, res, next);
});
보통 쿠키를 전달할 땐 res.cookie()를 사용하며 삭제할 땐 res.clearCookie()를 사용한다. httpOnly 옵션도 기본으로 달아준다. 기억해두자. 이제 모두 완성했으나, 보안과 관련된 특히 키 값은 .env파일에 저장하는게 좋다.
.env파일로 가서, toosecret 값을 저장해두자. secret 값은 이미 사용중이니 그대로 가져와서 사용하면 된다.
# DB 접속 URL
DB_URL="디비주소"
# DB 프로젝트 이름
DB_NAME="디비이름"
# 서버 포트 번호
PORT=3000
# JWT토큰 발급 키
SECRET="secret"
# REFRESH 토큰 발급 키
REFRESH_SECRET="toosecret"
다시 userRouter.js로 가서 마무리 작업을 해주고 router에 try / catch 문을 사용하여 에러처리도 해주자.
아래는 완성본이다.
const express = require('express');
const router = express.Router();
const UserModel = require('./db/model/userModel');
const bcrypt = require('bcrypt');
const passport = require('passport');
const jwt = require('jsonwebtoken'); // JWT 라이브러리
router.get('/login', (req, res) => {
res.render('login.ejs')
})
router.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), (req, res, next) => {
res.redirect('/myPage');
});
router.get('/register', (req, res) => {
res.render('register.ejs')
})
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: false }, async (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json(info.message);
}
try {
const userObject = user.toObject();
const payload = { _id: userObject._id };
// Access Token 발급, 유효 기간 1시간
const accessToken = jwt.sign(payload, process.env.SECRET, { expiresIn: '1h' });
// Refresh Token 발급, 유효 기간 2주
const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET, { expiresIn: '14d' });
// Refresh Token을 DB에 저장
await UserModel.updateOne({ _id: userObject._id }, { refreshToken });
// 토큰을 쿠키로 전달, httpOnly로 설정된 쿠키는 클라이언트 측 스크립트에서는 읽거나 수정 안됨.
res.cookie('accessToken', accessToken, { httpOnly: true });
res.cookie('refreshToken', refreshToken, { httpOnly: true });
res.redirect('/my-page');
} catch (error) {
next(error);
}
})(req, res, next);
});
module.exports = router;
이제 토큰 발급도 받았으니, 마지막으로 토큰을 검증하는 코드를 만들차례이다.
'개발일지 > HELLO FOLIO' 카테고리의 다른 글
HELLO FOLIO(express, mongoose)- 7(Nodemailer 를 이용한 비밀번호 재설정 구현) (0) | 2024.07.08 |
---|---|
HELLO FOLIO(express, mongoose) - 6(jwt 을 이용한 토큰 검증 미들웨어 구현) (0) | 2024.07.02 |
HELLO FOLIO(express, mongoose)- 4(passport를 이용한 로그인 전략 구현) (0) | 2024.06.29 |
HELLO FOLIO(express, mongoose)- 3(bcrypt를 이용한 회원가입 구현) (0) | 2024.06.27 |
HELLO FOLIO(express, mongoose)- 2(MongoDB,Mongoose 연결 구현) (0) | 2024.06.27 |