블로그 관리자 기능
By Yoon
·

블로그 관리자 기능에 대한 내용입니다 - 블로그 개발 후기 (2)
글을 시작하며
동적인 블로그를 개발하기 위해서 관리자 기능은 빠질 수 없는 부분입니다.
열심히 작성한 글을 누군가에 의해 쉽게 수정되거나 삭제되면 슬프니까요.
관리자 기능을 어떻게
개발했는지 이야기해 보려고 합니다.
관리자 기능
생각 정리
개발 시에 블로그 사용자는 두 가지 시나리오를 가질 수 있게 하고 싶었습니다.
Admin
은 포스트를Create, Read, Update, Delete
를 할 수 있습니다.User
는 포스트를Read
할 수 있습니다.
관리자 권한을 얻는 방법을 요약하자면 다음과 같습니다.
- 관리자 비밀번호를 암호화합니다.
- 입력된 비밀번호와 일치하면
JWT
를 발급해 줍니다. JWT
와 로그인이 됐는지를 판단하는isLogin
을쿠키
에 담습니다.Next.js
Middleware
에서JWT
가 유효한지 판단합니다.
암호화 방법 정하기
관리자 비밀번호 암호화로 hash
함수를 이용하기로 했습니다.
hash
함수를 사용하면 비밀번호를 단방향
으로 암호화할 수 있으므로, 암호화된 비밀번호를 원래 값으로 복원할 수 없습니다.
관리자 비밀번호를 안전하게 보호할 수 있다고 생각했습니다.
hash
중에서도 bcrypt
나 scrypt
와 같은 hash
함수들은 Salt
와 함께 사용될 수 있어서 보안성이 더욱 강화됩니다.
Salt
로 고유한 값을 추가해 주어 비밀번호의 보안성을 더욱 높였습니다.
저는 bcrypt
라이브러리를 사용했습니다.
bcrypt & JWT 발급 & 쿠키 설정
- 주석을 참고해주세요.
로그인은 loginAction(Server Action)을 사용해 LoginForm(Form)을 다루고 있습니다.
서버에서 폼 다루기에 대한 글을 확인 할 수 있습니다.
// app/(root)/login/loginAction.ts
"use server";
import { compare, hash } from "bcrypt";
import { SignJWT } from "jose";
import { cookies } from "next/headers";
export async function loginAction(
prevState: {
success: boolean;
error: boolean;
message: string;
},
formData: FormData,
) {
// 1. 로그인 input의 name 속성 값으로 입력된 password 저장
const inputPW = formData.get("password")?.toString();
// 2. Salt 값 저장
const saltRounds = parseInt(process.env.SALT_ROUNDS!);
// 3. hash 함수와 Salt값으로 암호화 진행하여 저장
const hashedPW = await hash(process.env.PASSWORD!, saltRounds);
if (inputPW) {
// 4-1. 암호화된 값이랑 입력된 값 비교
const isAdmin = await compare(inputPW, hashedPW);
if (isAdmin) {
// 5-1. 일치시 JWT 발급
const alg = "HS256";
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
const jwt= await new SignJWT({ username: "admin" })
.setProtectedHeader({ alg })
.setExpirationTime("24h")
.sign(secret);
// 6. 쿠키 설정
cookies().set("isLogin", "true");
cookies().set("jwt", jwt, {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24,
});
return {
success: true,
error: false,
message: "로그인 성공!",
};
} else {
// 5-2. 불일치
return {
success: false,
error: true,
message: "비밀번호가 일치하지 않습니다.",
};
}
}
// 4-2. 입력된 값이 없음
return {
success: false,
error: true,
message: "비밀번호를 입력해주세요!",
};
}
Next.js의 Middleware로 로그인 여부 판단하기
Next.js
의Middleware
는 페이지를 렌더링하기 전에 모든 요청을 가로채 서버에서 실행되는 함수입니다.Middleware
에서 페이지 렌더링 전에JWT
가 유효한지 검증하여 로그인 인증을 했습니다.- 서버의 모든 요청을 가로채기 때문에 무거운 로직을 거치지 않으려고 했습니다.
- 주석을 참고해 주세요.
// middleware.ts
import { jwtVerify } from "jose";
import { NextResponse, NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
// 1. JWT 값을 쿠키에서 꺼내옴.
const jwt = request.cookies.get("jwt")?.value;
// 2. secret 값을 꺼내옴
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
try {
// 3-1. 토큰 값이 없다면 에러를 발생
if (!jwt) {
throw new Error("There is no jwt!");
}
// 3-2. 토큰 값이 유효한지 검사
await jwtVerify(jwt, secret);
// 4-1. 응답을 이어감
return NextResponse.next();
} catch (error) {
// 4-2. 토큰이 없거나 검증에 실패한 경우
// 5-1. 인증이 필요한 Route handlers인 경우 Error Response
if (request.nextUrl.pathname.startsWith("/api/admin")) {
return NextResponse.json({ error: "Not authorized" }, { status: 401 });
}
// 5-2. 인증이 필요한 페이지인 경우, 로그인 페이지로 이동
return NextResponse.redirect(
new URL(`/login?redirect=${request.nextUrl.pathname}`, request.url),
{ headers: { "Set-Cookie": "isLogin=false; Path=/" } },
);
}
}
// 해당 경로에 대해 검사
export const config = {
matcher: ["/write", "/posts/:slug/edit", "/api/admin/:path*"],
};
Next.js에서 쿠키 다루기
- 관리자 기능을 개발하면서 쿠키를
어떤 상황에서 어떻게 사용했는지
이야기해 보겠습니다.
1. Server Action & Route Handlers에서 쿠키 다루기
- 로그인, 로그아웃 시에 사용
- 쿠키 Set 할 때 사용,
Next.js
에서 메서드 제공
- 로그인
// app/(root)/login/loginAction.ts
"use server";
import { cookies } from "next/headers";
export async function loginAction( // 생략.. ) {
// 생략..
cookies().set("isLogin", "true");
cookies().set("jwt", jwt, {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24,
});
}
- 로그아웃
// app/(route-handlers)/api/admin/logout/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const headers = new Headers();
headers.append("Set-Cookie", "jwt=deleted; Path=/");
headers.append("Set-Cookie", "isLogin=false; Path=/");
return NextResponse.json({ message: "logout success" }, { headers });
} catch (error) {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
}
2. 클라이언트 컴포넌트에서 쿠키 다루기
- 로그인 여부 판단 시에 사용
- 쿠키 Get 할 때 사용,
react-cookie
라이브러리 사용
// app/_components/Header/LoginButton.tsx
"use client";
import { useCookies } from "react-cookie";
export default function LoginButton() {
// 생략..
const [cookies] = useCookies(["isLogin"]);
if (pathname === "/login") {
return null;
}
async function handleLogoutClick() {
try {
await fetch("/api/admin/logout", {
method: "POST",
});
// 생략..
router.replace("/");
router.refresh();
} catch (error) {
// 생략..
}
}
return (
<li>
{cookies.isLogin ? (
<div>
<Link href="/write">Write</Link>
<button onClick={handleLogoutClick}>Logout</button>
</div>
) : (
<Link href={`/login?redirect=${pathname}`}>Admin</Link>
)}
</li>
);
}
로그아웃
- 로그아웃은
button
태그의onClick
속성을 사용하므로Client Component
에서 작동해야 합니다. /api/admin/logout
요청이 가면JWT
를 유효하지 않은 값(여기서는 deleted),isLogin
값을false
로 바꾸어 로그아웃되도록 했습니다.- 로그아웃 성공 시
/
(홈)으로 이동되며,refresh
(새로고침) 해Header
가 쿠키 값에 따라 반영되도록 해주었습니다.
// app/_components/Header/LoginButton.tsx
"use client";
export default function LoginButton() {
const pathname = usePathname();
const router = useRouter();
const [cookies] = useCookies(["isLogin"]);
if (pathname === "/login") {
return null;
}
async function handleLogoutClick() {
try {
await fetch("/api/admin/logout", {
method: "POST",
});
// 생략..
router.replace("/");
router.refresh();
} catch (error) {
// 생략..
}
}
return (
<li>
{cookies.isLogin ? (
<div>
<Link href="/write">Write</Link>
<button onClick={handleLogoutClick}>Logout</button>
</div>
) : (
<Link href={`/login?redirect=${pathname}`}>Admin</Link>
)}
</li>
);
}
결과
- User
- Admin
글을 마치며
배운점
- 비밀번호 암호화 방법
- Server Action & Route Handlers & Client Component에서 쿠키 다루기
- Next.js middleware
마무리
지금까지 블로그 관리자 기능에 대한 내용입니다.
읽어주셔서 감사합니다.