(update) 20.02.09 로그인을 위한 준비 & passport & 로그인 리덕스 사이클

sykim·2019년 12월 20일
0

로그인을 위한 준비

로그인 과정은 회원가입과 마찬가지로 db에서 해당 유저를 찾고 data를 클라이언트에 보내는 과정은 같지만 거기서 끝이 아니다. 서버가 로그인 완료를 알아차리기 위해선 브라우저에 기록을 남겨야하는데 프론트단에는 이 기록을 쿠키를 이용해 남긴다.
https://velog.io/@mollang/2019-12-23-1512-%EC%9E%91%EC%84%B1%EB%90%A8-hmk4i1f624#19.12.23-%EC%84%A4%EB%AA%85-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8

즉, 로그인시 서버와 클라이언트단 둘 다 기록을 남기는데
사용자 정보는 서버에는 세션에 기록이 되고 프론트단에 쿠키에 기록이 되는 셈.
쿠키의 역할을 서버가 세션(사용자 정보)을 조회할 수 있도록 하는 것이다.

로그인을 위한 준비 미들웨어들

npm i cookie-parser express-session

index.js

...
app.use(cookieParser(process.env.COOKIE_SECRET)); // 쿠키분석
app.use(expressSession({ // 세션 사용
    resave : false, 
    saveUninitialized : false,
    secret: process.env.COOKIE_SECRET, // 쿠키 이름 (.env 파일에 기재)
    cookie : {
        httpOnly : true // 자바스크립트로 쿠키 접근 불가 (해킹방지)
        secure : false, // https를 사용할때 true로 
    }
}));

passport를 이용해 로그인

image.png
관련 패키지 >
passport : 로그인을 쉽게 할 수 있도록 도와주는 패키지
passport-local: 로그인을 직접 구현할 때 사용하는 패키지
express-session: passport로 로그인 후 유저 정보를 세션에 저장하기 위한 패키지

관련함수 >
passport.serializeUser : 로그인을 할 때 유저 정보 그대로 전부 저장하는 것이 아닌, 서버 쪽에 [{id : 1}, {cookie : 'cookie'}] 이런 가벼운 배열식으로 저장을 하고 쿠키는 프론트로 보낸다. 그리고 서버는 쿠키를 통해 해당 유저의 id 값을 serializeUser를 통해 찾는다.
passport.deserializeUser : 해당 유저의 id 값을 찾았다면 그 id를 토대로 deserializeUser로 유저 정보를 되찾는다. 그리고 그 불러진 유저 정보는 req.user에 저장된다.
LocalStrategy:

소스는 아래와 같다.

passport/passport-local.js

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

module.exports = () => {
    passport.use(new LocalStrategy({
    	// step1
        usernameField: 'userId',
        passwordField: 'password',
    }, async (userId, password, done) => {
        try {
            const user = await db.User.findOne({
                where: {userId}
            });
            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 (e) {
            console.error(e);
            return done(e);
        }
    }));
};

passport/index.js

const passport = require('passport');
const db = require('../models');
const local = require('./local');

module.exports = () => {
	// step 2
    passport.serializeUser((user, done) => {
        return done(null, user.id);
    });
    
    passport.deserializeUser( async (id, done) => {
        try {
            const user = await db.User.findOne({ 
                where : { id },
            });
            // 서버가 id 값으로 찾아온 유저 정보는 req.user 에 저장
            return done(null, user);
        } catch (e) {
            console.error(e);
            return done(e);
        }
    });
    local(); // LocalStrategy 연결
};

routes/user.js

// POST /api/user/login
router.post('/login', (req, res, next) => { 
    passport.authenticate('local', (err, user, info) => {
        console.log(err, user, info)
        if (err) {
            console.error(err);
            return next(err);
        }
        if (info) {
            return res.status(401).send(info.reason);
        }
        return req.login(user, (loginErr) => {
            if (loginErr) {
                return next(loginErr);
            }
            const filteredUser = Object.assign({}, user.toJSON());
            delete filteredUser.password;
            return res.json(filteredUser);
        });
    })(req, res, next);
});

step 1

LocalStrategy를 통해 usernameField와 passwordField를 어떤 폼 필드로부터 아이디와 비밀번호를 전달받을 지 설정한다.
프론트단이 로그인을 요청하면 요청의 본문에 데이터가 { userId: 'ksy', password: '123' } 이런식으로 담겨서 오고 usernameField 에 본문의 id 데이터를, passwordField 에 본문의 password 데이터를 넣어준다.
이렇게 값이 들어오면 콜백 함수가 실행이 되는데 이 콜백 함수로 db에서 User를 찾는다.

step 2

serializeUser에서 방금 전 로그인 성공 시 실행되는 return done(null, user); 에서 user 객체를 전달 받아 세션에 저장한다.
deserializeUser 는 서버로 들어오는 요청마다 세션 정보를 실제 디비의 데이터와 비교한다.
해당하는 유저 정보가 있으면 return done(null, user); 와 같이 done의 두번째 인자에 req.user 를 저장한다.
( serializeUser에서 done으로 넘겨주는 user가 deserializeUser의 첫 번째 매개변수로 전달되기 때문에 만일 serializeUser에서 id만 넘겨줬다면 deserializeUser의 첫 번째 매개변수도 id를 받아야 한다)

image.png

step 3

/api/user/login으로 post 요청을 보내면 passport에서 인증 작업을 시작한다.
passport.authenticate 의 콜백 인자인 err, user, info는
LocalStrategy에서 리턴한 done 함수의 세 개의 인자값에서 받아진 것이다.
err : 서버 에러
user : 성공시 유저 정보
infor : 로직상 에러
image.png

성공시 passport에서 제공하는 req.login 함수를 실행해 return res.json(filteredUser); 와 같이 유저를 json으로 응답한다.

step 4

중앙 통제실, 그러니까 index.js에 passport 파일을 연결하는 것도 잊지 말자.

const passport = require('passport');
const passportConfig = require('./passport');
...
passportConfig();
...
app.use(passport.initialize());
app.use(passport.session());

=> 요약 정리
1. 로그인 api를 요청하면 passport.authenticate 인증작업이 실행된다
2. LocalStrategy 전략부분이 실행된다.
3. 성공시 다시 passport.authenticate의 콜백 부분이 실행되면서 req.login 실행된다.
4. req.login 실행시 passport.serializeUser 실행된다. (서버에 id 저장, 생성한 쿠키는 프론트에 저장)
4-1. passport.deserializeUser로 보낸 쿠키로 id 값을 찾아내 사용자 정보를 req.user로 저장한다.

  1. req.user를 res.json(user)로 프론트에 응답한다.

로그인 리덕스 사이클

파일 구조
components/LoginForm.js
reducers/user.js
sagas/user.js

LoginForm.js

...
    const onSubmitForm = useCallback((e) => {
        e.preventDefault();
        dispatch({
            type: LOG_IN_REQUEST,
            data: {
                id, password
            }
        });
    }, [id, password]);

return(
<Form onSubmit={onSubmitForm}>
	...
    <Button type="primary" htmlType="submit" loading={isLoggingIn}>로그인</Button>
    ...
</Form>
)
  1. 로그인 버튼을 누르면 콜백 함수가 진행되면서
    LOG_IN_REQUEST 리듀서가 dispatch(진행)된다.
    LOG_IN_REQUEST 사가가 진행된다.

reducers/user.js

...
			case LOG_IN_REQUEST : {
				return {
					...state,
					isLoggingIn: true,
					logInErrorReason: '',
				};
			}
...
  1. 리듀서 user에서 isLoggingIn 상태가 true가 된다.

sagas/user.js

// 3
function* login(){
    try {
        yield delay( 2000 );
        yield put({
            type: LOG_IN_SUCCESS
        });
    } catch (e) {
        console.error(e);
        // 로그인 실패
        yield put({
            type: LOG_IN_FAILURE
        });
    }
}

// 2
function* watchLogin(){
    yield takeEvery(LOG_IN_REQUEST, login)
}
...
export default function* userSaga() {
    yield all ([
    	// 1
        fork(watchLogin),
    ]);
}
  1. 사가 user에서 2초 후에 LOG_IN_SUCCESS가 실행

reducers/user.js

...
			case LOG_IN_SUCCESS : {
				return {
					...state,
					isLoggingIn: false,
					isLoggedIn: true,
					me: dummyUser,
					isLoading: false
				};
			}
...
  1. isLoggingIn 상태가 false 된다.
    me(내 로그인 정보)에 더미유저가 추가가 된다.

profile
블로그 이전했습니다

0개의 댓글