리액트 학습 노트 (중급) 7일차 정리

아놀드·2021년 8월 18일
1
post-thumbnail

passport

카카오, 트위터, 구글 등등 소셜 로그인 지원 모듈 (추후 업데이트)

passport-local

기본적인 이메일, 아이디 로그인 지원 모듈

app.use(passport.initialize());
app.use(passport.session());

일단 passport모듈 내부의 세션 관련 지원 모듈을 미들웨어로 등록한다.

// passport/local.js
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const { User } = require('../models');

module.exports = () => {
    passport.use(new LocalStrategy(
        {
            usernameField: 'email',
            passwordField: 'password',
        },
        async (email, password, done) => {
            try {
                const user = await User.findOne({
                    where: { email },
                });
    
                if (!user) {
                    return done(null, false, { reason: '존재하지 않는 이메일입니다!' });
                }
    
                const result = await bcrypt.compare(password, user.password);
    
                if (result) {
                    return done(null, user);
                }
    
                return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' });
            } catch (error) {
                console.error(error);
                return done(error);
            }
        },
    ));
};

passport모듈의 use메서드의 인자로
passport-local모듈 안의 Strategy인스턴스를 넘긴다.

Strategy의 생성자는 두 개의 인자를 필요로 하는데

첫 번째 인자는 로그인시 필요한 파라미터의 필드명을 담은 오브젝트
{ usernameField: 'email', passwordField: 'password' }

두 번째 인자는 로그인 로직을 담은 함수
async (email, password, done) => { dosomething... }

첫 번째 인자에서 명시한 필드를 차례로 인자로 받고
마지막 인자로는 done함수를 받는다.

done함수는 3개의 인자를 받는데
첫 번째 인자는 서버 에러,
두 번째 인자는 성공 객체,
세 번째 인자는 클라이언트 에러를 받는다.
커스텀도 가능하다.

// passport/index.js
const passport = require('passport');
const local = require('./local');
const { User } = require('../models');

module.exports = () => {
    // 로그인 성공시 실행될 콜백 등록
    passport.serializeUser((user, done) => {
        done(null, user.id); // 세션에 userId를 저장한다.
    });
    
    // 로그인 이후 자동으로 실행되는 콜백 등록
    passport.deserializeUser(async (id, done) => {
        try {
            const user = await User.findOne({ where: { id }});
            
            done(null, user); // req.user에 넣어준다.
        } catch (error) {
            console.error(error);
            done(error);
        }
    });

    local(); // import한 로컬 로그인 전략 실행, passport의 로그인 전략에 로컬 전략이 추가됐다.
};

그 다음 index.js에서 각종 로그인 전략 파일들을 import 해서 실행한다.
지금은 local만 있지만 google, kakao도 파일을 만들어 추가할 수 있다.

// POST /user/login 로그인
router.post('/login', (req, res, next) => (
    passport.authenticate('local', (error, user, info) => {
        if (error) {
            console.error(error);
            next(error);
        }

        if (info) {
            return res.status(401).send(info.reason);
        }

        return req.login(user, async (loginError) => {
            if (loginError) {
                console.error(loginError);
                return next(loginError);
            }

            return res.status(200).json(user);
        });
    })(req, res, next)
));

그 다음 userRouter.js에서 passport모듈을 import해서
로그인 api 에 passport.authenticate를 등록시킨다.
passport.authenticate
첫 번째 인자로 전략 객체명을 받고
두 번째 인자로 커스텀 done함수를 받는다.

나중에 유저가 로그인할 때 passport.authenticate가 리턴한 함수가 실행되고
그 함수는 passport/local.js에서 Strategy 생성자의 두 번째 인자로 넘겼던

        // Strategy 생성자의 두 번째 인자로 넘겼던 함수
        async (email, password, done) => {
            try {
                const user = await User.findOne({
                    where: { email },
                });
    
                if (!user) {
                    return done(null, false, { reason: '존재하지 않는 이메일입니다!' });
                }
    
                const result = await bcrypt.compare(password, user.password);
    
    		// 로그인 성공
                if (result) {
                    return done(null, user);
                }
    
                return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' });
            } catch (error) {
                console.error(error);
                return done(error);
            }
        }

로그인 전략 함수를 실행한다.
성공하면 done(null, user)이 부분이 실행되는데
done함수는 아까 passport.authenticate의 두 번째 인자로 넘겼던 커스텀 done함수이다.

    // passport.authenticate의 두 번째 인자로 넘겼던 커스텀 done함수
    (error, user, info) => {
        if (error) {
            console.error(error);
            next(error);
        }

        if (info) {
            return res.status(401).send(info.reason);
        }

        req.login(user, async (loginError) => {
            if (loginError) {
                console.error(loginError);
                return next(loginError);
            }

            return res.status(200).json(user);
        });
    }

로그인을 성공했다면 마지막에 req.login을 실행하는데
원래 기본 express가 지원하는 req객체에는 login메서드가 없지만
passport모듈이 붙여준다.

login메서드를 실행하면 내부적으로
res.setHeader('Set-Cookie', 'afsdfs');를 실행시켜서 쿠키를 등록해주고
passport/index.js에서 등록한 함수를 실행시킨다.

    // 로그인 성공시 실행될 함수 등록
    passport.serializeUser((user, done) => {
        done(null, user.id);
    });

done함수를 통해 user.id만 세션에 등록한다.

간단한 passport모듈 내부 구현 상상도

// 로컬 전략 클래스
class LocalStrategy {
    constructor(fields, strategyFn) {
        this.fields = fields;
        this.strategyFn = strategyFn;
    }

    get getName() {
        return 'local';
    }

    // 전략 함수 리턴
    getStrategy() {
        const fieldNames = Object.values(this.fields);

        // reqBody와 customDone을 받아서
        return (reqBody, customDone) => {
            // 필드를 모은 다음
            const fields = fieldNames.map((fieldName) => reqBody[fieldName]);

            // 등록한 전략 함수를 실행
            this.strategyFn(...fields, customDone);
        };
    }
}

// 패스포트 모듈
const passport = (() => {
    // 로그인 전략들을 담을 오브젝트
    const strategys = {}; 

    // 로그인 성공시 실행할 함수
    let serializeUserFn;

    // 로그인 전략 추가
    const use = (strategyInstance) => {
        Strategys[strategyInstance.getName] = strategyInstance.getStrategy();
    };

    // 로그인 성공시 실행할 함수 등록
    const serializeUser = (fn) => {
        serializeUserFn = fn;
    };

    // 인증 함수
    const authenticate = (strategyName, customDone) => (req, res, next) => {
        // req에 로그인 함수 붙여주기
        req.login = (user, callback) => {
            // 쿠키를 등록
            res.setHeader('Set-Cookie', 'adsfsad');

            // 로그인 성공시 실행할 함수 실행 (세션 등록)
            serializeUserFn(user, (id) => res.session.email = id);

            callback();
        };

        // 전략 함수 실행
        strategys[strategyName](req.body, customDone);
    };

    return {
        use,
        serializeUser,
        authenticate,
    };
})();

대충 이런 흐름으로 구현돼있지 않을까 싶다.

기타 유용한 모듈

1. react-virtualized

인피니트 스크롤 구현 때

메모리 안 터지도록 구현해주는 모듈.

2. shortid, faker 라이브러리

더미데이터 테스트 도와줌

profile
함수형 프로그래밍, 자바스크립트에 관심이 많습니다.

0개의 댓글