지난 시간에 이어서 보안강화를 위해 토큰을 적용한다.
먼저 API를 만들어 유저만 이용할 수 있도록합니다.
app/api/user/[id]/route.ts
import { prisma } from "@/app/utils/prisma";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
console.log(params);
const id = params.id;
const userPosts = await prisma.post.findMany({
where: {
authorId: id,
},
include: {
author: {
select: {
email: true,
name: true,
},
},
},
});
return new Response(JSON.stringify(userPosts));
}
/api/user/[ObjectId]에 GET요청시 정상적으로 Post를 보여줍니다. 쉽게 검색하는 로직이 완료된 것 입니다.
하지만 만약 Post가 중요한 문서라면 어떨까요? 위처럼 따로 인증없이도 이용할 수 있다면 보안상 좋지않습니다.
이것을 해결하기위해 NextAuth에서 JWT토큰을 이용합니다.
npm install jsonwebtoken
npm install -D @types/jsonwebtoken
먼저 라이브러리를 설치합니다.
그후 .env 파일에 SECRET_KEY를 만들고 적절한 보안문자를 적어줍니다.
이후 app/utils/jwt.ts 파일을 만들고 jwt를 만드는 함수, 확인하는 함수를 추가합니다.
app/utils/jwt.ts
import jwt, { JwtPayload } from "jsonwebtoken";
interface SignOption {
expiresIn?: string | number;
}
const DEFAULT_SIGN_OPTION: SignOption = {
expiresIn: "1h",
};
export function signJwtAccessToken(payload: JwtPayload, options: SignOption = DEFAULT_SIGN_OPTION) {
const secret_key = process.env.SECRET_KEY;
const token = jwt.sign(payload, secret_key!, options);
return token;
}
export function verifyJwt(token: string) {
try {
const secret_key = process.env.SECRET_KEY;
const decoded = jwt.verify(token, secret_key!);
return decoded as JwtPayload;
} catch (error) {
console.log(error);
return null;
}
}
자 그럼 이렇게 만들어진 jwt함수들을 app/api/login에 이용해 로그인 시 적용하겠습니다.
만약 process.env.SECRET_KEY와 관련해 오류가 생긴하다면 `${process.env.SECRET_KEY}`로 변경 후에 시도 합니다.
app/api/login/route.ts
import { signJwtAccessToken } from "@/app/utils/jwt";
import { prisma } from "@/app/utils/prisma";
import bcrypt from "bcryptjs";
interface RequestBody {
username: string;
password: string;
}
export async function POST(request: Request) {
const body: RequestBody = await request.json();
const user = await prisma.user.findFirst({
where: {
email: body.username,
},
});
if (user && (await bcrypt.compare(body.password, user.password))) {
const { password, ...userWithoutPass } = user;
// 추가된 부분
const accessToken = signJwtAccessToken(userWithoutPass);
const result = {
...userWithoutPass,
accessToken,
};
return new Response(JSON.stringify(result));
} else return new Response(JSON.stringify(null));
}
자 이제 accessToken을 만들어서 이를 반환할 수 있습니다.
다음으로 app/api/auth/[...nextauth]/route.ts에서 인증과정을 변경합니다.
app/api/auth/[...nextauth]/route.ts
const handler = NextAuth({
providers: [
CredentialsProvider({
//...
}],
// 추가된 부분
callbacks: {
async jwt({ token, user }) {
return { ...token, ...user };
},
async session({ session, token }) {
session.user = token as any;
return session;
},
},
});
위 코드처럼 providers 아래에 callbacks를 만들어 jwt과정을 만듭니다.
그리고 이렇게 만들어진 accessToken을 포함하는 user정보를 타입으로 만듭니다.
types/next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: number;
name: string;
email: string;
accessToken: string;
};
}
}
자 그럼 이제 api를 보호해 보겠습니다.
api/user/[id]/route.ts
import { verifyJwt } from "@/app/utils/jwt";
import { prisma } from "@/app/utils/prisma";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// 추가된 부분
const accessToken = request.headers.get("authorization");
if (!accessToken || !verifyJwt(accessToken)) {
return new Response(JSON.stringify({ error: "No Authorization" }), {
status: 401,
});
}
console.log(params);
const id = params.id;
const userPosts = await prisma.post.findMany({
where: {
authorId: id,
},
include: {
author: {
select: {
email: true,
name: true,
},
},
},
});
return new Response(JSON.stringify(userPosts));
}
위의 코드로 인해서 헤더에서 authorization 가져오고 올바른 토큰일 때 정보를 반환합니다.
물론 이렇게 api자체를 막을 수 있지만 미들웨어를 이용하여 페이지 자체를 막을 수 있습니다.
먼저 src폴더 혹은 app에 middleware.ts 파일을 만듭니다.
middleware.ts
export { default } from 'next-auth/middleware'
export const config = {
matcher: ['/userposts/:path*'],
}
그리고 위처럼 막고 싶은 페이지를 막으면 가능합니다.
막을 임시 페이지를 만들어 보겠습니다.
app/userposts/page.tsx
import React from "react";
function UserPosts() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
UserPosts
</main>
);
}
export default UserPosts;
그 후 로그인과 비로그인으로 나누어 접속을 시도하면 확인할 수 있습니다.
자 그럼 핵심 로그인 기능은 완료입니다. 그렇다면 로그인 페이지를 적절하게 커스텀해서 만들어 보겠습니다.
app/api/auth/[...nextauth]/route.ts
//...
pages: {
signIn: "/signin",
},
// 여기가 추가된 부분
});
export { handler as GET, handler as POST };
//...
마지막 부분에 pages를 추가합니다.
쉽게 signIn경로는 /signin이라고 선언하는 것입니다.
그후 해당 페이지를 만들어서 커스텀해줍니다.
app/signin/page.tsx
'use client'
import React, { useRef } from 'react'
import { signIn } from 'next-auth/react'
function Login() {
const emailRef = useRef(null)
const passwordRef = useRef(null)
// 수정된 부분
const handleSubmit = async () => {
// console.log(emailRef.current)
// console.log(passwordRef.current)
const result = await signIn("credentials", {
username: emailRef.current,
password: passwordRef.current,
redirect: true,
callbackUrl: "/",
});
}
return (
<main className='flex min-h-screen flex-col items-center space-y-10 p-24'>
<h1 className='text-4xl font-semibold'>Login</h1>
<div>
<div>
<label
htmlFor='email'
className='block text-sm text-gray-800 dark:text-gray-200'
>
Email
</label>
<div className='mt-1'>
<input
ref={emailRef}
onChange={(e: any) => {
emailRef.current = e.target.value
}}
id='email'
name='email'
type='email'
required
autoFocus={true}
className='mt-2 block w-full rounded-md border bg-white px-4 py-2 text-gray-700 focus:border-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-40 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:focus:border-blue-300'
/>
</div>
</div>
<div className='mt-4'>
<label
htmlFor='password'
className='block text-sm text-gray-800 dark:text-gray-200'
>
Password
</label>
<div className='mt-1'>
<input
type='password'
id='password'
name='password'
ref={passwordRef}
onChange={(e: any) => (passwordRef.current = e.target.value)}
className='mt-2 block w-full rounded-md border bg-white px-4 py-2 text-gray-700 focus:border-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-40 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:focus:border-blue-300'
/>
</div>
</div>
<div className='mt-6'>
<button
onClick={handleSubmit}
className='w-full transform rounded-md bg-gray-700 px-4 py-2 tracking-wide text-white transition-colors duration-200 hover:bg-gray-600 focus:bg-gray-600 focus:outline-none'
>
Log In
</button>
</div>
</div>
</main>
)
}
export default Login
그러면 새로운 모습으로 페이지가 변경된것을 알 수 있습니다. 중요한 부분은 위처럼 signIn함수를 통해서 적절하게 로직을 실행해야합니다.
더 자세히 알고싶다면 링크를 참조해주세요!