기본 플로우
로그인 버튼을 누르면, Next의 API로 사용자 정보를 확인하는 POST 요청을 보냅니다. 이 과정 중에서 로그인 요청이 적절하다면 PassportJS는 세션을 생성합니다.
사용자가 마주하는 프론트엔드에서는 쿠키의 형태로 저장되고, 서버사이드에서는 메모리에 저장되거나, Redis같은 스토리지에 저장됩니다. 프론트엔드에 저장된 쿠키는 서버로 요청할 때마다 요청에 포함되어 서버는 쿠키에 저장된 세션을 특정하는 관련 정보를 확인하여 실제 세션을 저장해놓은 메모리나 Redis에서 세션에 저장된 정보를 가져와 요청(request)과 함께 처리합니다.
프로젝트 준비
시작하기전에 "npx create-next-app { 앱 혹은 프로젝트 명 }" 명령어를 통해 최신의 NextJS 소스코드가 반영된 프로젝트를 생성합니다. NextJS프로젝트가 생성되었다면, 프로젝트 안으로 이동한 후, 아래의 명령어를 통해 프로젝트에 필요한 패키지들을 설치합니다.
npm i --save \
express body-parser express-session axios \
passport passport-local redis connect-redis \
express-session redis connect-redis
NextJS에서 server.js를 통한 서버 사이드 수정
// project/server.js
const express = require("express");
const next = require("next");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = 3000;
app.prepare().then(() => {
const server = express();
server.get("*", (req, res) => handle(req, res));
server.listen(PORT, err => {
if (err) throw err;
console.info(`Ready on http://localhost:${PORT}`);
});
});
NextJS에서 server.js파일을 생성하여 서버 측에서 express와 세션에 관련된 패키지들을 설정해 줍니다. passport는 세션 인증 프로세스를 도와주는 패키지이며, passport의 여러 strategy 중 local strategy를 사용할 겁니다. express-session은 express의 세션 관리를 도와주는 패키지입니다.
// project/server.js
const express = require("express");
const next = require("next");
const passport = require("passport");
const session = require("express-session");
const bodyParser = require("body-parser");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = 3000;
app.prepare().then(() => {
const server = express();
const sessionConfig = {
secret: process.env.SESSION_SECRET,
cookie: { maxAge: 86400 * 1000 },
resave: false,
saveUninitialized: true,
};
server.use(bodyParser.json());
server.use(session(sessionConfig));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
server.use(passport.initialize());
server.use(passport.session());
server.get("*", (req, res) => handle(req, res));
server.listen(PORT, err => {
if (err) throw err;
console.info(`Ready on http://localhost:${PORT}`);
});
});
세션 설정을 생성하고 서버에서 세션을 사용합니다. 또한, 서버의 요청 체인에서 passport를 초기화(initialize)하고, serialzer와 deserializer와 같은 기능을 설정하여 user 정보를 요청 체인에서 다룰 수 있도록 합니다.
// project/server.js
const express = require("express");
const next = require("next");
const passport = require("passport");
const session = require("express-session");
const bodyParser = require("body-parser");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = 3000;
app.prepare().then(() => {
const server = express();
const sessionConfig = {
secret: process.env.SESSION_SECRET,
cookie: { maxAge: 86400 * 1000},
resave: false,
saveUninitialized: true,
};
server.use(bodyParser.json());
server.use(session(sessionConfig));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
server.use(passport.initialize());
server.use(passport.session());
server.get("/", (req, res, next) => {
if(!req.isAuthenticated()) return res.redirect("/login");
next();
});
server.get("*", (req, res) => handle(req, res));
server.listen(PORT, err => {
if (err) throw err;
console.info(`Ready on http://localhost:${PORT}`);
});
});
인증 미들웨어를 다룰 차례입니다. passport의 설정이 완료되었으므로, req.isAuthenticated()를 사용하여 사용자가 인증되었는지 체크할 수 있습니다. 인증이 되었다면, next()를 이용하여 다음 미들웨어로 이동하고, 인증이 되지 않았다면, login페이지로 리다이렉션 합니다.
// project/routes/auth.js
const express = require("express")
const passport = require("passport")
const LocalStrategy = require("passport-local").Strategy
const User = require("../db/models/user");
const router = express();
const validateUser = async (username, password, done) => {
const user = await User.findOne({ username });
if (!user) return done(null, false);
if (!user.password === password) return done(null, false);
return done(null, user);
};
passport.use(new LocalStrategy(validateUser));
router.post("/login", (req, res, next) => {
passport.authenticate("local",
(err, user) => {
if (error) return res.status(400).json({ error });
if (!user) return res.status(400).json({ error: "Login failed" });
req.logIn(user, (error) => {
if (error) return next(error);
return res.status(200).send();
});
})(req, res, next);
});
router.post("/logout", (req, res) => {
req.logout();
res.status(200).send();
});
module.exports = router;
auth엔드포인트를 다루는 route를 설정할 차례입니다. 이곳에 local strategy passport 인증 절차와 로그인, 로그아웃 엔드포인트를 구현합니다. validateUser 함수는 DB를 조회하여 로그인 정보가 사실인지 확인합니다.
// project/server.js
const express = require("express");
const next = require("next");
const passport = require("passport");
const session = require("express-session");
const bodyParser = require("body-parser");
const authRoutes = require("./routes/auth");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = 3000;
app.prepare().then(() => {
const server = express();
const sessionConfig = {
secret: process.env.SESSION_SECRET,
cookie: { maxAge: 86400 * 1000 },
resave: false,
saveUninitialized: true,
};
server.use(bodyParser.json());
server.use(session(sessionConfig));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
server.use(passport.initialize());
server.use(passport.session());
server.use("/auth", authRoutes);
server.get("/", (req, res, next) => {
if(!req.isAuthenticated()) return res.redirect("/login");
next();
});
server.get("*", (req, res) => handle(req, res));
server.listen(PORT, err => {
if (err) throw err;
console.info(`Ready on http://localhost:${PORT}`);
});
});
auth route를 서버에 마운트 합니다. 이제, 프론트엔드를 구현할 차례입니다.
NextJS의 프론트엔드
NextJS의 룰을 따라 pages에 login.js를 생성하고 프론트엔드에서 로그인 프로세스를 진행할 수 있도록 아래와 같이 코드를 구현합니다.
// pages/login.js
import { useState, useCallback } from "react";
import axios from "axios"
const Home = () => {
const [usenameValue, setUsernameValue] = useState();
const [passwordValue, setPasswordValue] = useState();
const usernameOnChangeHandler = useCallback(
(e) => setUsernameValue(e.target.value), [setUsernameValue]
);
const passwordOnChangeHandler = useCallback(
(e) => setPasswordValue(e.target.value), [setPasswordValue]
);
const loginButtonHandler = useCallback(async(e) => {
e.preventDefault();
try {
const { data } = await axios({
method: "post",
url: "/auth/login",
data: {
username: usernameValue,
password: passwordValue,
};
});
window.location = "/";
} catch(e) {
alert("Login Error");
}
},[usernameValue, passwordValue]);
return (
<div>
<input
name="username"
value={usernameValue}
onChange={usernameOnChangeHandler}
/>
<input
name="password"
value={passwordValue}
onChange={passwordOnChangeHandler}
/>
<button onClick={loginButtonHandler}>Login</button>
</div>
);
};
export default Home;
홈의 서버사이드 코드에서 세션 내부의 passport객체에서 user객체를 확인하여 로그인이 되었는지 확인하고, user객체를 발견하지 못했다면(로그인이 되지 않았다면), login 페이지로 리다이렉션 합니다. 로그인이 되었다면, 프론트엔드가 그대로 렌더 되고, 로그아웃이 가능한 로그아웃 버튼을 볼 수 있습니다.
import { useCallback } from "react";
import axios from "axios";
import Home from "./Home";
const Index = () => {
const logoutHandler = useCallback(async(e) => {
e.preventDefault();
await axios.post("/auto/logout");
window.location = "/login";
}, []);
return (
<>
<Home/>
<button onClick={logoutHandler}>Logout</button>
</>
);
}
Index.getInitialProps = (ctx) => {
let pageProps = {};
if (ctx.req && ctx.req.session.passport) {
pageProps.user = ctx.req.session.passport.user;
}
if (!pageProps.user) {
ctx.res.writeHead(302, {Location: "/login"}).end();
}
return { pageProps };
};
export default Index;
Redis 적용
// project/server.js
const express = require("express");
const next = require("next");
const passport = require("passport");
const session = require("express-session");
const bodyParser = require("body-parser");
const redis = require("redis");
const authRoutes = require("./routes/auth");
const RedisStore = require("connect-redis")(session)
const redisClient = redis.createClient()
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = 3000;
app.prepare().then(() => {
const server = express();
const sessionConfig = {
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
cookie: { maxAge: 86400 * 1000 },
resave: false,
saveUninitialized: true,
};
server.use(bodyParser.json());
server.use(session(sessionConfig));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
server.use(passport.initialize());
server.use(passport.session());
server.use("/auth", authRoutes);
server.get("/", (req, res, next) => {
if(!req.isAuthenticated()) return res.redirect("/login");
next();
});
server.get("*", (req, res) => handle(req, res));
server.listen(PORT, err => {
if (err) throw err;
console.info(`Ready on http://localhost:${PORT}`);
});
});
PassportJS serializeUser deserializeUser 차이 (LocalStrategy 기준)
serializeUser
1. 로그인 폼이 POST 요청으로 들어오면, passport.authenticate 미들웨어가 작동합니다.
2. authenticate에 설정된 strategy가 작동됩니다. 이 예제에서는 local을 기준으로 합니다.
3. local strategy설정에서 구현한 verification 함수로 username과 password를 넘깁니다.
4. DB에서 사용자 정보를 가져와 로그인 폼 데이터와 비교합니다.
5. 올바른 접근이라면 verification 함수의 done(null, user)가 작동합니다.
6. 위의 done이 작동하면, passport.authenticate로 다시 돌아옵니다.
7. 넘겨진 user 파라미터를 이용해 req.logIn함수를 작동시킵니다.
8. 마지막으로 passport.serializeUser가 작동됩니다.
serializeUser는 세션에 저장될 객체를 결정하는 역할을 합니다.
deserializeUser
1. express가 세션을 로드하고 요청 객체에 포함시킵니다. (req.session.passport.user)
2. passport.initialize가 req.session에 붙은 passport.user를 찾고 만약 없다면, req.session.passport.user = {};로 정리합니다.
3. passport.session이 실행됩니다. 이 미들웨어는 매 요청마다 실행되는 미들웨어로서, serialized user객체를 세션에서 찾으면, 인증된 것으로 간주합니다.
4. passport.session 미들웨어는 passport.deserializeUser을 실행하여 찾은 user 객체를 req객체에 포함시켜 req.user의 형태를 구성합니다.
'개발과 기술' 카테고리의 다른 글
[serverless] serverless 프레임워크 AWS 설정하기 2 (1) | 2020.05.15 |
---|---|
[Mecab] Mecab(konlpy) 사용자 단어 사전 추가 방법 (0) | 2020.05.15 |
[Docker] 도커 컨테이너 실행유지 방법 및 /dev/null 설명 (0) | 2020.05.15 |
[serverless] serverless 프레임워크 AWS 설정하기 1 (6) | 2020.05.13 |
[NextJS] 마크다운으로 글 작성하는 방법 (8) | 2020.05.11 |
댓글