next.js

[Next] 로그인 구현 ( passport )

JJIMJJIM 2024. 5. 2. 12:11
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
반응형