[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();
};
참조!
학습 페이지
www.inflearn.com
'next.js' 카테고리의 다른 글
[Next] <Image>으로 레이아웃 시프트 예방 (feat. plaiceholder, skeloton) (0) | 2024.05.26 |
---|---|
[Next] UseSelectedLayoutSegment / UsePathname Hook (0) | 2024.05.13 |
[Next] App Router의 폴더들 (0) | 2024.05.13 |
[Next] getServerSideProps 와 getStaticProps (0) | 2024.05.07 |
[Next] 회원가입 구현 (express, sequelize, mySQL) (0) | 2024.05.01 |
댓글