사용자 인증 절차에 대한 로직을 간단하게 구현할수 있도록 도와주는 Node.js 미들웨어다.
어느 사이트에 로그인을 하면, 다른페이지로 이동해도 로그인 상태는 유지되어야한다. 우리는 복잡한 세션과 쿠키를 구현해야하는데, passport 미들웨어로 안전하고 간단하게 설계할수 있다.
많은 전략이 있는데, 우리가 사용할 local전략은 개발자가 DB를 이용하여 직접 인증로직을 구현한다.
다른 전략은 sns로그인 부분으로, passport는 전략부분만 바꿔주면되서 유연하게 대처할수있다.
npm i passport passport-local
npm i cookie-parser
npm i express-session
1. front에서 로그인폼에 로그인
프론트 -> 백엔드 로그인정보 보낸다.
2. server에서 로그인승인, 쿠키와 유저정보 내려줌
회원정보 없으면 에러메세지, 있으면 임의의 쿠키를 만들어서(passport.auth...가자동생성) 프론트에 쿠키 보냄
3. front에서 쿠키확인
로그인하면 쿠키(connect.sid)오는지 확인 , 쿠키를 받았으면 로그인된거임
4. 회원정보를 계속요청하기위해 서버에 요청후 값이 받아지는지 확인 get('/')
로그인했을때 응답값이 잘오는지 확인
5. 페이지 바뀔때마다 계속 요청
이 쿠키는 HTTP ONLY기때문에 클라이언트에서 가져올수없고 백엔드에서 가져와야함.
즉, 프론트에서 서버사이드렌더링으로 쿠키를 가져와 클라이언트에서 사용해야하는것이 큰 흐름이다.
6. 계속 요청하기위해 프론트에서 쿠키가 있으면 요청, 없으면 요청안하는 서버사이드렌더링 (app.tsx에서)
7. app.tsx로 받으면 props로 AuthProvider로 받아 로그인유무판단
쿠키가 있으면 isLoggedIn이 true 없으면 false로 AuthProvider에 전달해준다.
8. AuthProvider에서 true면 전역user값에 정보담음, 없으면 user가 null
[프론트엔드]
app.tsx
axios.defaults.withCredentials = true;
lib > api > user.js
// 로그인
export const login = async (data: { email: string; pw: string }) => {
const response = await defaultAxios.post(`/user/login`, data);
return response.data;
};
passport > index.js
const passport = require("passport");
const local = require("./local");
const { User } = require("../models");
module.exports = () => {
// 로그인하기
passport.serializeUser((user, done) => {
// null - 서버 에러
// user.id - 성공해서 user의 id를 가져온다.
done(null, user.id);
});
// 서버에서 유저에 대한 모든 정보를 갖고 있게되면, 서버 과부화가 생기게된다.
// 그래서 서버는 id만 갖고있다가, 페이지 이동 시 필요한 유저 정보는 DB에서 찾아서 가져온다.
// 그게 deserializeUser 역할이다.
// deserializeUser : serializeUser가 한번 성공하면 그다음 요청부터는 계속 실행. id를 가지고 서버에서 user를 복구해서 req.user에 넣어줌
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();
};
passport > local.js
const passport = require("passport");
const { Strategy: LocalStrategy } = require("passport-local"); // Strategy -> LocalStrategy로 이름 변경
const { User } = require("../models");
const bcrypt = require("bcrypt");
module.exports = () => {
passport.use(
new LocalStrategy(
{
usernameField: "email", // req.body.email 라고 명시적으로 알려줌 (정확한 명을 넣어야한다.)
passwordField: "pw",
},
async (email, pw, done) => {
try {
// 이메일있는지
const user = await User.findOne({
where: {
email: email,
},
});
if (!user) {
return done(null, false, { reason: "존재하지 않는 이메일입니다." }); // done으로 결과판단. (서버에러, 성공여부, 클라이언트에러)
}
// 비밀번호 비교체크 (pw:사용자가입력한비번, password:DB에있는비번)
// 입력한비번과 db에있는비번이 일치하는지. => 일치하면 사용자정보넘기고 일치안하면 에러
const result = await bcrypt.compare(password, user.password);
// 1) 비밀번호 일치할경우
if (result) {
return done(null, user);
}
// 2) 비밀번호 일치하지않을경우
return done(null, false, { reason: "비밀번호가 틀렸습니다." });
} catch (error) {
console.error(error);
return done(error);
}
}
)
);
};
app.js
const passportConfig = require("./passport");
const passport = require("passport");
const session = require("express-session");
const cookieParser = require("cookie-parser");
passportConfig(); // passport 설정적용
app.use(cors({ origin: "http://localhost:3000", credentials: true })); // 브라우저 쿠키 전달 안되서 넣은값
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
session({
secret: process.env.COOKIE_SECRET,
// 하위 두개는 왠만하면 false
saveUninitialized: false,
resave: false,
})
);
// 이 부분의 설정은 반드시 세션 뒤에 사용해야한다.
app.use(passport.initialize()); // 요청에 passport설정을 넣는다.
app.use(passport.session()); // req.session에 passport정보를 저장한다.
routes > user.js
const express = require("express");
const passport = require("passport");
const router = express.Router();
const { User } = require("../models");
// 로그인
router.post("/login", (req, res, next) => {
passport.authenticate("local", (err, user, info) => {
// 서버에러있으면
if (err) {
console.error(err);
return next(err);
}
// 클라이언트에러있으면
if (info) {
return res.status(401).send(info.reason);
}
return req.login(user, async (loginErr) => {
if (loginErr) {
console.error(loginErr);
return next(loginErr);
}
// 이미 user가 있는데 또 찾는 이유는, 비밀번호제외하고받으려고
const fullUserWithoutPassword = await User.findOne({
where: { id: user.id },
attributes: { excludes: ["pw"] }, // 비밀번호 제외하고 받겠다.
});
return res.status(200).json(fullUserWithoutPassword);
});
})(req, res, next);
});
확인해보자.
1) 이 api를 요청했을떄 브라우저 헤더에 쿠키가 붙는지,
2) 네트워크를 봤을때 서버에서 응답값이 잘 내려오는지,
백엔드 routes > user.js
router.get("/", async (req, res, next) => {
console.log(req.headers);
console.log(req.user);
try {
if (req.user) {
const fullUserWithoutPassword = await User.findOne({
where: { id: req.user.id },
attributes: { exclude: ["pw"] },
});
res.status(200).json(fullUserWithoutPassword);
}
res.status(401).json();
} catch (err) {
console.error(err);
next(error);
}
});
프론트 lib > api > user.js
// 회원 정보 (로그인유지)
export const userLogin = async () => {
const response = await defaultAxios.get(`/user`);
return response.data;
};
애플리케이션 열었을때 쿠키가(connect.sid)있으면 응답값이 뜬다.
서버사이드렌더링을 하면 페이지가 로드되기전에 페이지가 새로고침될때마다 계속불러온다.
어차피 api는 다른데서 불러올거라 꼭 api불러오는게 아니더라도, console.log를 찍어보며 새로고침할때 계속요청되는지 본다.
_app.tsx
// 페이지를 렌더링하기전에 데이터를조작 : 서버에서 데이터를 가져와 props로 전달
MyApp.getInitialProps = async ({ ctx }: AppContext) => {
const { data: userInfo } = useQuery<any>(['user'], () => userLogin());
console.log(userInfo);
};
아까 작성했던 app.tsx를 수정해준다.
쿠키가 있으면 요청, 없으면 요청을 안해야하기때문에 connect.sid로 시작하는 쿠키가 있는지 확인해서
쿠키가 있으면 isLoggedIn이 true, 없으면 false를 반환해준다.
// 페이지를 렌더링하기전에 데이터를조작 : 서버에서 데이터를 가져와 props로 전달
MyApp.getInitialProps = async ({ ctx }: AppContext) => {
const loginCookie = ctx.req?.headers.cookie
?.split(' ')
.find((cookie) => cookie.startsWith('connect.sid'));
console.log('로그인쿠키', loginCookie);
return {
props: { isLoggedIn: !!loginCookie },
};
};
쿠키가 있으면 로그인된거고 없으면 안된거다.
app.tsx
function MyApp({
Component,
pageProps,
props, // 여기추가
}: AppProps & { props: { isLoggedIn: boolean } }) {// 여기추가
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return (
<QueryClientProvider client={queryClient}>
<ConfigProvider theme={antdTheme}>
<ThemeProvider theme={theme}>
<GlobalStyles />
<AuthProvider isLoggedIn={props.isLoggedIn}>// 여기추가
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
</ConfigProvider>
</QueryClientProvider>
);
}
// 페이지를 렌더링하기전에 데이터를조작 : 서버에서 데이터를 가져와 props로 전달
MyApp.getInitialProps = async ({ ctx }: AppContext) => {
const loginCookie = ctx.req?.headers.cookie
?.split(' ')
.find((cookie) => cookie.startsWith('connect.sid'));
console.log('로그인쿠키', loginCookie);
return {
props: { isLoggedIn: !!loginCookie },
};
};
export default MyApp;
쿠키가 있는지없는지가 로그인을했는지안했는지기때문에 쿠키가있으면 isLoggedIn이 true, 없으면 isLoggedIn이 false로 반환해주게 개발했었다.
그러므로 여기선 isLoggedIn이 true면 전역user값에 정보를담고, false면 user값을 null값으로 만들면될거같다.
나는 아직 로그아웃은 구현안했기때문에 true일때 user값을 넣는거까지했고,
다음에 로그아웃 구현시 false면 user에 null 넣는것까지 할 예정이다
provider > AuthProvider.tsx
import { userLogin } from '@lib/api/user';
import useAuthStore from '@/stores/auth';
import React, { useEffect } from 'react';
import { useQuery } from 'react-query';
interface AuthProviderProps {
children: React.ReactNode;
isLoggedIn: boolean;
}
const AuthProvider: React.FC<AuthProviderProps> = ({
children,
isLoggedIn,
}) => {
if (isLoggedIn) {
const { data: userInfo } = useQuery<any>(['user'], () => userLogin());
const { user, loginUser, logoutUser } = useAuthStore();
useEffect(() => {
loginUser(userInfo);
}, [userInfo]);
}
return <>{children}</>;
};
export default AuthProvider;