[Next] 로그인 구현 ( passport )

    728x90
    반응형

    [Next] 로그인 구현 ( passport )

    지난 블로그의 back단(back 서버) 초기설정회원가입을 완료 했다면

    로그인을 구현해보자

     

     

    로그인을하면 브라우저, 서버가 같은 정보를 들고 있어야한다.
    서버에서 로그인이 되고나서 브라우저도 이 정보를 알고있어야하는데, 통째로 보내주면
    비밀번호가 들어있기 때문에 해킹에 취약할 수 있다.
    그래서 로그인 정보대신에 암호화 되어있는 임의의 문자열을 브라우저로 보내준다. --> 요게 쿠키
    그럼 서버는 보내줬던 쿠키정보를 로그인한 유저 정보(아이디)와 연결 시킨다. --> 쿠키와 서버쪽 세션
    이렇게 되면 해커들이 서버에서 브라우저로 보낸 쿠키값을 알게 되도 해킹할 수없기 때문에(유저정보를 알수없다) 안전하다.
    이후 브라우저에서 로그인 정보가 필요한 이벤트(게시글 쓰기 등등)가 발생할 때 해당 쿠키를 넣어서
    백엔드 서버에 보내주면 백엔드에서 그 쿠키를 읽고 그에 맞게 이벤트 진행

     

    로그인은 기본이면서도 제일 중요한 정보기 때문에 복잡해질 수 밖에 없다.

     

     

     

    사용되는 라이브러리

    passport

    Node.js를 위한 유연하고 강력한 인증 미들웨어입니다.

    다양한 방식의 인증을 제공하는 '전략'을 통해 애플리케이션에 쉽게 통합할 수 있습니다.

    각 전략은 다른 인증 메커니즘을 처리하며, 예를 들어 'Local Strategy'는 기본적인 사용자 이름과 비밀번호를 사용하고, 'OAuth Strategy'는 Google, Facebook 등의 소셜 로그인을 지원합니다.

    여러가지 로그인 전략들을 한번에 관리해주는 라이브러리

    • done : return done ( 서버에러, 성공, 클라이언트 에러 (보내는측이 잘못보냄) ) 이런 형식으로 사용된다.
    • 예시: return done(null, 유저정보), return done(null, false, {reason: 비밀번호가 틀렸습니다.})
    • passport.initialize()
      • Passport 모듈을 초기화하는 미들웨어입니다.
      • 이 함수는 req 객체에 passport 관련 함수를 추가합니다.
      • 예를 들어, req.login(), req.logout(), req.isAuthenticated() 등의 메소드가 여기서 추가됩니다.
      • 미들웨어는 Passport를 사용하는 애플리케이션에서 반드시 설정되어야 하며, 일반적으로 다른 미들웨어보다 먼저 호출되어야 합니다.
    • passport.session()
      • express-session 미들웨어가 생성한 세션을 활용하여 인증된 사용자의 세션을 관리합니다.
      • 이 미들웨어는 사용자의 로그인 세션을 유지하기 위해 필요하며, 로그인한 사용자의 정보를 세션에 저장하고, 후속 요청에서 사용자를 식별할 수 있게 해줍니다.
      • 즉, 사용자가 로그인한 후에 다른 페이지나 리소스에 접근할 때마다 사용자를 다시 인증할 필요 없이 세션 정보를 통해 사용자가 누구인지 알아낼 수 있게 해줍니다.

     

    passport-local

    Passport의 전략 중 하나로, 사용자 이름과 비밀번호를 사용한 인증을 구현합니다.

    ( email & 비번 || id & passwor로 로그인할 수 있게 도와주는 라이브러리 )

    이 전략은 주로 이메일과 비밀번호를 통해 사용자를 인증할 때 사용되며, 로컬 데이터베이스에 저장된 사용자 정보와 비교하여 인증을 처리합니다.

    • routes에서 실행시키며, routes에서 받은 정보를 가지고 유저정보 체크하고 리턴시켜준다.
    • 후에 routes에서 req.login 함수를 실행하면 /passport/index에서 serializeUser함수 실행
    • 로그인 후 유저정보와 함게 connect.sid라는 이름의 문자열을 쿠키에 넣어서 브라우저에 보낸다.

     

    express-session

    Express 애플리케이션에서 세션 관리를 위한 미들웨어입니다.

    세션은 서버 측에서 생성되며, 클라이언트와 서버 간의 상태를 유지하는 데 사용됩니다.

    Express-Session은 사용자별로 고유한 세션 ID(secret과 조합한 문자열)를 생성하고, 이 ID를 쿠키로 클라이언트에게 전송하여, 후속 요청에서 사용자를 식별할 수 있게 합니다.

     

    cookie-parser

    요청에 포함된 쿠키를 파싱하여 쉽게 사용할 수 있게 해주는 Express의 미들웨어입니다.

    이 라이브러리는 요청된 쿠키를 해석하여 req.cookies 객체로 만들어 주어, 쿠키의 각 값을 손쉽게 접근할 수 있게 합니다.

     

    dotenv

    환경 변수를 .env 파일에서 로드하여 Node.js 애플리케이션의 환경에 접근할 수 있게 해주는 모듈입니다.

    개발 중에 API 키나 데이터베이스 비밀번호와 같은 중요한 정보를 코드에 직접 쓰지 않고 관리할 수 있어 보안을 강화할 수 있습니다.

    중요한 데이터를 하드코딩해두면 한번 해킹당하면 모든걸 털릴 수 있는 위험이 있다.
    이를 막기 위해서 사용하는 라이브러리 + 중요정보 관리하기도 쉽게하기 위해.

    소중히 보관. 깃헙이나 깃에도 올리면안된다.

    json파일이면 쓸수없어서 js로 파일을 바꾸고 module.exports하고 진행.

    .env 파일만들고 값키 = 값 모양으로 입력하고 process.env.값키로 값을 가져온다.

     

     

     

     

    설치

    npm i passport passport-local
    
    npm i -D express-session cookie-parser dotenv

     

     

     

     

    사용

    흐름을 어느정도 이해하고 코드를 보면 이해가 될 것같다.

    [front] 로그인 이벤트 발생 -->

    [front] reducer에서 로그인 request요청 후 saga/user.js의 로그인 함수 호출 -->

    [back] /route/user.js에서 url에 해당하는 이벤트 발생 -->

    [back]  passport 전략 함수 실행 -->

    [back]  passport 전략 함수에서 작업된 데이터를 다시 routes/user로 보내주고 콜백함수 실행 -->

    [back]  받아온 유저 정보 + 추가로 필요한 정보 세팅해서 브라우저로 보내준다.

    [back]  유저의 정보 + 쿠키에 유저 정보와 세션만들 문자열을 만들어서 같이 브라우저로 보내준다.

    [back]  서버에서 들고있는건 유저 정보의 id, 쿠키에 보내줬던 문자열 -->

    [front] 정보를 받아서 로그인 성공 리듀서 액션 호출 -->

    이후 필요에 따른 작업

     

    [front]

    로그인 요청/성공/실패 리듀서 만들고 export 해주기

    /reducers/user.js

    const userSlice = createSlice({
    	name: 'user',
        initialState,
        reducers: {
        	...,
            loginSuccessAction: (state, action) => {
    			state.logInLoading = false;
    			state.logInDone = true;
    			state.me = action.payload;
    		},
    		loginFailureAction: (state, action) => {
    			state.logInLoading = false;
    			state.logInError = action.payload;
    		},
    		logoutRequestAction: (state) => {
    			state.logOutLoading = true;
    			state.logOutDone = false;
    			state.logOutError = null;
    		},
        }
    })
    
    export const {
    	..., 
    	loginSuccessAction,
        loginFailureAction,
        logoutRequestAction,
    } = userSlice.actions
    
    export default userSlice.reducer;

     

     

    리듀서 이벤트에 따라 로그인 함수를 실행하고, 결과에 맞춰 리듀서 액션 실행

    /sagas/user.js

    function loginAPI(data) {
        // 로그인 정보와 함께 백단 서버로 요청
        return axios.post("/user/login", data);
    }
    function* login(action) {
        try {
            // call로 비동기 함수 기다기도록 호출
            const result = yield call(loginAPI, action.payload);
            yield put(loginSuccessAction(result.data));
        } catch (err) {
            yield put(loginFailureAction(err.response.data));
        }
    }
    
    function* watchLogIn() {
        yield takeLatest(loginRequestAction, login);
    }
    
    export default function* userSaga() {
        yield all([fork(watchLogIn)]);
    }

     

     

     

    [back]

    서버로 요청이 오면 app.js에 적용했던 userRouter로 이동해서 이벤트 진행

    /app.js

    ...
    const session = require("express-session");
    const cookieParser = require("cookie-parser");
    const passport = require("passport");
    const dotenv = require("dotenv");
    
    const userRouter = require("./routes/user");
    // sequelize한 모델들을 db라는 객체에 넣어서 가져옴
    // express에서 그 sequelize를 등록(sync)해야 한다.
    const db = require("./models");
    const passportConfig = require("./passport");
    
    dotenv.config(); // dotenv 사용
    const app = express();
    // sequelize된 모델들 데이터를 db라는 객체에 넣고
    // mySQL에 연동시킨다. (db에 있는 모델들의 테이블을 만든다)
    db.sequelize 
        .sync()
        .then(() => {
            console.log("db연결 성공!");
        })
        .catch(console.error);
    passportConfig(); // passport 실행
    
    // .env 에 있는 정보들이 치환되서 process.env.으로 들어간다.
    app.use(cookieParser(process.env.COOKIE_SECRET));
    app.use(
        session({
            saveUninitialized: false,
            resave: false,
            // 로그인 후 쿠키에 랜덤한 문자열을 보내줄때 유저를 데이터로 만들어낸 문자.
            // 그래서 secret이 해킹당하면 데이터 노출 위험이 있다.
            // 그래서 secret은 꽁꽁 숨겨둬야한다. dotenv 활용.
            secret: process.env.COOKIE_SECRET,
        })
    );
    app.use(passport.initialize());
    app.use(passport.session());
    
    app.use("/user", userRouter); //
    
    app.listen(3065, () => {
    	console.log("서버 실행 중");
    });

     

     

    /routes/user.js

    const express = require("express");
    // sequelize가 다른 테이블의 정보까지 합쳐서 보내줘서 편함
    const passport = require("passport");
    const db = require("../models"); // sequelize된 모델들
    
    const router = express.Router();
    
    // POST /user/login 
    router.post("/login", (req, res, next) => {
    
        // 로그인 전략 실행
        // /passport/local.js 실행
        // 아래 함수의 콜백함수에 들어가는 정보는 passport 전략에서 보내준 정보들
        passport.authenticate("local", (err, user, info) => {
            if (err) {
                console.error(err);
                return next(err);
            }
            if (info) {
                return res.status(403).send(info.reason);
            }
            
            // passport 로그인 중
            // req.login 할때 동시에 실행되는게
            // /passport/index.js의 serializeUser
            return req.login(user, async (loginErr) => {
            	// 로그인하게되면 내부적으로 res.setHeader('Cookie', 임의의 문자열) 이렇게 보내준다
                // 그리고 알아서 세션과 연결해준다 --> 브라우저엔 문자 (쿠키), 서베에서 데이터 보관.
                // 서버쪽에서 통째로 들고있는건 세션(쿠키와 정보 연결) 이런식으로 보안 위협 최소
                if (loginErr) {
                    console.error(loginErr);
                    return next(loginErr);
                }
                
                // 추가로 필요한 데이터를 뽑거나 제거하고 브라우저에 유저정보 제공
                const fullUserWithoutPassword = await db.User.findOne({
                    where: { id: user.id },
                    
                        // 원하는 정보만 받을 수 있음
                        // attreibute: ['id', 'nickname', 'email'],
                        // 원하지 않는 정보만 빼고 가져올 수 있음
                        attributes: {
                            exclude: ["password"],
                        },
                        include: [
                            {
                                // model: Post는 hasMany라서 복수형이 되어 프론트 me.Posts가 됩니다.
                                model: db.Post,
                                // attributes: ["id"],
                            },
                            {
                                model: db.User,
                                as: "Followings",
                                // attributes: ["id"],
                            },
                            {
                                model: db.User,
                                as: "Followers",
                                // attributes: ["id"],
                            },
                        ],
                    });
                    
                    // 임의문자열을 쿠키로 사용자정보와 프론트로 보내줌
                    return res.status(200).json(fullUserWithoutPassword);
                });
    	})(req, res, next);
    });
    
    router.post("/logout", (req, res, next) => {
    	// 로그인 한 후 부터는 req에 user정보가 들어가있다. (req.user)
    	req.logOut();
    	req.session.destroy(); // 세션 지우고 쿠키 지우면 로그아웃 끝
    	res.send("ok");
    });
    
    module.exports = router;

     

     

    user router에서 진행중 passport.authenticate 실행으로 local.js의 전략 함수 실행

    /passport/local.js

    const passport = require("passport");
    const { Strategy: LocalStrategy } = require("passport-local");
    const { User } = require("../models");
    const bcrypt = require("bcrypt");
    
    // /passport/index.js에서 local();으로 실행되는 부분
    module.exports = () => {
        // 로그인 전략에는 두개의 인자가 들어간다. (객체, 함수)
        passport.use(
        	// user router에서 passport.authenticate 함수로 실행
            new LocalStrategy(
                {
                    usernameField: "email", // req.body.email 라는 뜻
                    passwordField: "password", // req.body.password 라는 뜻
                },
                async (email, password, done) => {
                    try {
                        const user = await User.findOne({
                            where: { email },
                        });
                        if (!user) {
                        
                            // done은 마치 callback 함수 같은 것 --> /routes/user 에서 호출하면 전달된다
                            // done ( 서버에러, 성공, 클라이언트 에러 (보내는측이 잘못보냄) )
                            return done(null, false, { reason: "존재하지 않는 이메일입니다!" });
                        }
                        
                        // db에 있는 유저의 비번과, 사용자가 입력한 비번 비교
                        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);
                        }
                    }
            )
        );
    };

     

     

    user router에서 req.login하면 그때 사용하는 user정보를 받아서 passport.serializeUser 함수 실행

    /passport/index.js

    const passport = require("passport");
    const local = require("./local");
    const { User } = require("../models");
    
    module.exports = () => {
        // /routes/user에서 req.login할때 같이 실행되는 함수로 그때 사용한 user정보를 가져와서 실행
        passport.serializeUser((user, done) => {
            // 다 저장하면 무거우니가 user정보중에서 쿠키와 묶어줄 아이디만 저장
            // (세션화 할 아이디만 쿠키와 저장 - 이것만 서버에서 들고있음)
            // done ( 서버에러, 성공, 클라이언트 에러 (보내는측이 잘못보냄) )
            done(null, user.id);
        });
    
        // 로그인 후 그 다음 로그인 요청부터
        // connect.sid라는 쿠키와 함께 서버로 요청이 오면 매번 실행 (유저 라우터 실행전)
        // 그래서 routes에서 정보를 가져오거나할때 req.user 에 유저 정보가 들어가 있는것
        // 아이디로 부터 db에서 사용자정보 복구
        passport.deserializeUser(async (id, done) => {
            try {
                // 저장했던 id를 가지고 db에서 데이터 복구
                const user = await User.findOne({ where: id });
                done(null, user); // req.user에 user를 넣어준다.
            } catch (error) {
                console.error(error);
                done(error);
            }
        });
    
        local();
    };

     

     

     

     

     

     

     

     

     

     

    참조!

    https://www.inflearn.com/course/lecture?courseSlug=%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC&unitId=48836&category=questionDetail&tab=curriculum

     

    학습 페이지

     

    www.inflearn.com

     

    728x90
    반응형

    댓글