npm install express passport passport-local passport-jwt jsonwebtoken express-session bcrypt
- passport: 메인 인증 모듈
- passport-local: 아이디/비번 인증
- passport-jwt: JWT 인증
- jsonwebtoken: JWT 생성/검증
- bcrypt: 비번 암호화
- express-session: 세션 미들웨어
#### bcrypt.compare
bcrypt.compare(입력한 비번, db에 저장된 해시, 콜백함수)
- 입력한 비번을 내부적으로 같은 알고리즘으로 해시해서
- db에 저장된 해시랑 같은지 비교함.
맞으면 true, 다르면 false
#### strategy에서 done을 하면
- done은 passport에서 제공하는 콜백함수이다.
- LocalStrategy 내부에서 인증 (로그인 ) 작업을 끝내면 반드시 이 done을 호출해야 한다.
- 형식은
done(에러, 사용자, 추가정보);
- 에러 : 에러가 발생하면 Error 객체, 아니면 null
- 사용자 : 인증 성공 시 사용자 객체, 실패하면 false
- 추가정보 : 메시지나 이유 같은 추가 데이터 (선택)
그러므로 done(null, user); 이 코드는 passport가 '로그인 성공했네' 하고 그 다음에 미들웨어 passport.authenticate로 req.user에 user 데이터를 넣어준다.
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json()); // body - json 파싱
const pool = mysql.createPool({~~~~~~});
// 리프레시 토큰은 메모리에 저장 ( 실제로는 db에 저장해야 안전 )
const refreshTokens = [];
// LocalStrategy - 로그인에서만 사용한다.
passport.use(new LocalStrategy(
async(username, password, done) => {
try{
const [rows] = await pool.query('SELECT * FROM users WHERE username = ?', [username]);
if(rows.length === 0) return done(null, false, {message:'사용자 없음'});
const user = rows[0];
// 비밀번호 비교
const match = await bcrypt.compare(password, user.password);
if(!match) return done(null, false, {message:'비밀번호 틀림'});
return done(null, user);
}catch(exception){
return done(exception);
}
}
));
const jwtOptions = {
// 요청 헤더의 Authorization : Bearer <token>에서 토큰 추출
jwtFromRequest : ExtractJwt.fromAuthHeaderAsBearerToken(),
// 액세스 토큰의 서명을 검증할 때 사용할 비밀키
secretOrKey : 'ACCESS_SECRET'
};
passport.use(
new JwtStrategy(jwtOptions,
async(jwtPayload, done) => {
try{
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [jwtpayload.id]);
if( rows.length === 0 ) return done(null, false);
return done(null, rows[0]);
}catch(err){
return done(err, false);
}
}
)
);
app.use(passport.initialize());
// 회원가입
app.post('/register', async(req, res)=>{
const {username, password} = req.body;
const hash = await bcrypt.hash(password, 10);
try{
await pool.query('INSERT INTO users (username, password) VALUES (?, ?)', [username,hash]);
res.json({message:'회원가입 성공'});
}catch(err){
res.status(500).json({messge:'회원가입실패', error:err});
}
});
// 로그인 : 여기서 액세스토큰 발급, 리프레시 토큰 발급/DB저장
app.post(
'/login',
(req, res, next) => {
passport.authenticate('local', async(err, user, info)=>{
if(err || !user) return res.status(401).json({message:'인증실패'});
const accessToken = jwt.sign(
{id : user.id},
'ACCESS_SECRET',
{expiresIn:'15m'}
);
const refreshToken = jwt.sign(
{ id: user.id },
'REFRESH_SECRET',
{ expiresIn: '7d' }
);
// 리프레시 토큰 DB에 저장 ( 기존 것 삭제 후 추가 )
await pool.query('DELETE FROM tokens WHERE user_id=?',[user.id]);
await pool.query('INSERT INTO tokens (user_id, token) VALUES (?,?)', [user.id, refreshToken];
res.json({accessToken, refreshToken});
})(req,res, next);
});
// 보호라우터 : 액세스 토큰을 검사한다.
app.get(
'/protected',
passport.authenticate('jwt', {session:false}), // 미들웨어
(req, res) => {
res.json({message:'보호된 데이터', user : req.user});
}
);
// 액세스토큰 재발급 : 리프레시 토큰 사용
app.post('/token', async(req, res) => {
const {refreshToken} = req.body;
if(!refreshToken) return res.status(401).json({message:'리프레시 토큰이 필요한데 없음'});
try{
const decoded = jwt.verify(refreshToken, 'REFRESH_SECRET');
const [rows] = await pool.query('SELECT * FROM tokens WHERE user_id = ? AND token = ?', [decoded.id, refreshToken]);
if(rows.length === 0 ) return res.status(403).json({message:'유효하지 않은 리프레시 토큰'});
const newAccessToken = jwt.sign({id:decoded.id}, 'ACCESS_SECRET', {expiresIn:'15m'});
res.json({accessToken : newAcessToken });
}catch(exception){
return res.status(403).json({message:'리프레시 토큰 검증 실패'});
}
});
// 로그아웃 : 리프레시 토큰 데이터베이스에서 제거
app.post('/logout', async(req, res)=>{
const {refreshToken} = req.body;
try{
const decoded = jwt.verify(refreshToken, 'REFRESH_SECRET');
await pool.query('DELETE FROM tokens WHERE user_id = ? AND token = ?',[decoded.id, refreshToken]);
res.json({message:'로그아웃 완료'});
}catch(exception){
res.status(400).json({message:'로그아웃 실패'});
}
});
app.listen(3000, () => console.log('서버 실행 중! 🚀'));
passport.authenticate('local', callback)
보이듯이 이 함수는 콜백 함수를 반환한다.
그래서 여기에 passport.authenticate('local', callback)(req, res,next)를 붙여서 호출하는 것은 이 미들웨어 함수를 즉시 실행하겠다는 것으로서 localStrategy 함수가 즉시 실행된다.