본문 바로가기
카테고리 없음

passport + jwt + refresh

by 잘먹는 개발자 에단 2025. 5. 7.
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 함수가 즉시 실행된다.