Next.js에서 middleware란?

 

Next.js에서 middleware.ts라는 파일을 프로젝트 루트 디렉토리에 생성하면 자동으로 미들웨어로 인식됩니다. 이 파일은 요청이 완료되기 전에 실행되며, 요청에 따라 응답을 수정하거나 리디렉션, 헤더 수정 등을 할 수 있습니다

 

문제발생

현재 그냥 api/auth를 사용하면서 middleware를 적용하면 에러발생

    if (!token) {
        console.log('No token found, redirecting to login');
        // if (
        //     request.nextUrl.pathname !== '/login' &&
        //     request.nextUrl.pathname !== '/'
        // ) {
        //     return NextResponse.redirect(new URL('/login', request.url));
        // }
    } else {
        console.log('Token found:', token);
    }

 

에러발생이유

미들웨어가 전역으로 진행중인데, 로그인화면에서 아이디와 비밀번호를 제출할땐, token이 생성되기 전이라서 if문의 기능을 하지 못함

 

문제해결

config에서 로그인과 관련된 /login /api 폴더를 제외함

export const config = {
    matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\\\.png$|login).*)'],
};

토큰이 없을때 리다이렉션하는 로직을 좀 더 세밀하게 조정

if (!token) {
    if (
        !request.nextUrl.pathname.startsWith('/api/auth') &&
        !request.nextUrl.pathname.startsWith('/login') &&
        request.nextUrl.pathname !== '/'
    ) {
        return NextResponse.redirect(new URL('/login', request.url));
    }
    console.log('No token found, but on an allowed path');
} else {
    console.log('Token found:', token);
}

 

전체코드

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(request: NextRequest) {
    console.log('Middleware is running');
    const token = await getToken({
        req: request,
        secret: process.env.NEXTAUTH_SECRET,
    });

    if (!token) {
        if (
            !request.nextUrl.pathname.startsWith('/api/auth') &&
            !request.nextUrl.pathname.startsWith('/login') &&
            request.nextUrl.pathname !== '/'
        ) {
            return NextResponse.redirect(new URL('/login', request.url));
        }
        console.log('No token found, but on an allowed path');
    } else {
        console.log('Token found:', token);
    }

    return NextResponse.next();
}

export const config = {
    matcher: [
        '/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$|login).*)',
    ],
};
728x90

'일기 > 개발일기' 카테고리의 다른 글

[241015] 모드버스 RTU와 TCP  (0) 2024.10.15
[241014] RS-485 Modbus RTU 통신  (1) 2024.10.14
[241008] Next.js와 nextAuth 자동화 기능  (1) 2024.10.08
[241002-3] Next.js signIn의 redirect  (0) 2024.10.02
[240930] Next.js 인증  (2) 2024.09.30

https://nextjs.org/docs/app/building-your-application/authentication#database-sessions

 

Building Your Application: Authentication | Next.js

Learn how to implement authentication in your Next.js application.

nextjs.org

 

 

인증에 대한 프로세스를 세 가지 개념으로 분류

  1. 인증: 사용자가 본인이 맞는지 확인합니다. 사용자 이름과 비밀번호 등 사용자가 가지고 있는 정보로 신원을 증명해야 합니다.
  2. 세션 관리: 요청 전반에서 사용자의 인증 상태를 추적합니다.
  3. 인증: 사용자가 액세스할 수 있는 경로와 데이터를 결정합니다

인증 (가입 및 로그인)

  1. 사용자 자격 증명 (프론트에서 이름, 이메일 등 받는 방식)
  2. 서버에서 형식 검증
import { z } from 'zod'
 
export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: 'Name must be at least 2 characters long.' })
    .trim(),
  email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
  password: z
    .string()
    .min(8, { message: 'Be at least 8 characters long' })
    .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
    .regex(/[0-9]/, { message: 'Contain at least one number.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Contain at least one special character.',
    })
    .trim(),
})
 
export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined
  1. 사용자 생성 또는 자격 증명 확인
export async function signup(state: FormState, formData: FormData) {
  // 1. Validate form fields
  // ...
 
  // 2. Prepare data for insertion into database
  const { name, email, password } = validatedFields.data
  // e.g. Hash the user's password before storing it
  const hashedPassword = await bcrypt.hash(password, 10)
 
  // 3. Insert the user into the database or call an Auth Library's API
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })
 
  const user = data[0]
 
  if (!user) {
    return {
      message: 'An error occurred while creating your account.',
    }
  }
 
  // TODO:
  // 4. Create user session
  // 5. Redirect user
}

세션 관리

세션의 2가지 유형

  1. Stateless : 세션을 브라우저의 쿠키에 저장 → 보안성이 떨어질 수 있음
  2. Database : 세션을 데이터베이스에 저장 → 안전하지만 리소스 사용 증가

데이터베이스 세션

데이터베이스 세션을 만들고 관리하려면 다음 단계를 따라야 합니다.

  1. 세션과 데이터를 저장할 데이터베이스에 테이블을 만듭니다(또는 인증 라이브러리가 이를 처리하는지 확인합니다).
  2. 세션을 삽입, 업데이트, 삭제하는 기능을 구현합니다.
  3. 사용자의 브라우저에 저장하기 전에 세션 ID를 암호화하고, 데이터베이스와 쿠키가 동기화된 상태를 유지하도록 합니다(권장)
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
 
export async function createSession(id: number) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
 
  // 1. Create a session in the database
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Return the session ID
    .returning({ id: sessions.id })
 
  const sessionId = data[0].id
 
  // 2. Encrypt the session ID
  const session = await encrypt({ sessionId, expiresAt })
 
  // 3. Store the session in cookies for optimistic auth checks
  cookies().set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

권한 부여

사용자가 인증되고 세션이 생성되면 애플리케이션 내에서 사용자가 액세스하고 수행할 수 있는 작업을 제어하는 권한 부여를 구현할 수 있습니다.

권한 확인에는 두 가지 주요 유형이 있습니다.

  1. Optimistic
  2. 쿠키에 저장된 세션 데이터를 사용하여 사용자가 경로에 액세스하거나 작업을 수행할 권한이 있는지 확인합니다. 이러한 확인은 UI 요소를 표시/숨기거나 권한이나 역할에 따라 사용자를 리디렉션하는 것과 같은 빠른 작업에 유용합니다.
  3. 보안
  4. 사용자가 데이터베이스에 저장된 세션 데이터를 사용하여 경로에 액세스하거나 작업을 수행할 권한이 있는지 확인합니다. 이러한 검사는 보다 안전하며 민감한 데이터 또는 작업에 액세스해야 하는 작업에 사용됩니다.

Middleware를 사용한 낙관적 검사(선택 사항)

권한에 따라 미들웨어를 사용하고 사용자를 리디렉션하려는 경우가 있습니다 .

  • 낙관적 검사를 수행하려면 Middleware가 모든 경로에서 실행되므로 리디렉션 논리를 중앙 집중화하고 허가받지 않은 사용자를 사전 필터링하는 좋은 방법입니다.
  • 사용자 간에 데이터를 공유하는 정적 경로를 보호합니다(예: 유료 콘텐츠).

그러나 미들웨어는 사전 페치된 경로를 포함한 모든 경로에서 실행되므로 쿠키에서만 세션을 읽고(낙관적 검사) 성능 문제를 방지하기 위해 데이터베이스 검사를 피하는 것이 중요합니다.

import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
 
// 1. Specify protected and public routes
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
 
export default async function middleware(req: NextRequest) {
  // 2. Check if the current route is protected or public
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)
 
  // 3. Decrypt the session from the cookie
  const cookie = cookies().get('session')?.value
  const session = await decrypt(cookie)
 
  // 4. Redirect to /login if the user is not authenticated
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 5. Redirect to /dashboard if the user is authenticated
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// Routes Middleware should not run on
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\\\.png$).*)'],
}

미들웨어는 초기 검사에 유용할 수 있지만, 데이터를 보호하는 유일한 방어선이 되어서는 안 됩니다. 대부분의 보안 검사는 데이터 소스에 최대한 가깝게 수행해야 합니다. 자세한 내용은 데이터 액세스 계층을 참조하세요.

데이터 엑세스 계층(DAL) 생성

(• DAL은 데이터베이스와 직접 상호작용하여 데이터를 읽고 쓰는 계층입니다.)

데이터 요청과 권한 부여 논리를 중앙에서 관리하려면 DAL을 만드는 것이 좋습니다.

DAL에는 사용자가 애플리케이션과 상호 작용할 때 사용자의 세션을 확인하는 기능이 포함되어야 합니다. 최소한 이 기능은 세션이 유효한지 확인한 다음 추가 요청을 하는 데 필요한 사용자 정보를 리디렉션하거나 반환해야 합니다.

예를 들어, verifySession() 함수를 포함하는 DAL에 대한 별도 파일을 만듭니다. 그런 다음 React의 캐시를 사용합니다.

import 'server-only'
 
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
 
export const verifySession = cache(async () => {
  const cookie = cookies().get('session')?.value
  const session = await decrypt(cookie)
 
  if (!session?.userId) {
    redirect('/login')
  }
 
  return { isAuth: true, userId: session.userId }
})

그런 다음 데이터 요청, 서버 작업, 경로 핸들러에서 함수를 호출 할 수 있습니다.

export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null
 
  try {
    const data = await db.query.users.findMany({
      where: eq(users.id, session.userId),
      // Explicitly return the columns you need rather than the whole user object
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })
 
    const user = data[0]
 
    return user
  } catch (error) {
    console.log('Failed to fetch user')
    return null
  }
})

데이터 전송 개체(DTO) 사용

(• DTO는 데이터를 캡슐화하여 계층 간에 전달하는 객체입니다.)

데이터를 검색할 때는 애플리케이션에서 사용할 필수 데이터만 반환하고 전체 객체는 반환하지 않는 것이 좋습니다. 예를 들어, 사용자 데이터를 가져오는 경우 비밀번호, 전화번호 등을 포함할 수 있는 전체 사용자 객체가 아닌 사용자의 ID와 이름만 반환할 수 있습니다.

그러나 반환된 데이터 구조를 제어할 수 없거나 전체 객체가 클라이언트에 전달되는 것을 피하려는 팀에서 작업하는 경우, 클라이언트에 노출해도 안전한 필드를 지정하는 등의 전략을 사용할 수 있습니다.

import 'server-only'
import { getUser } from '@/app/lib/dal'
 
function canSeeUsername(viewer: User) {
  return true
}
 
function canSeePhoneNumber(viewer: User, team: string) {
  return viewer.isAdmin || team === viewer.team
}
 
export async function getProfileDTO(slug: string) {
  const data = await db.query.users.findMany({
    where: eq(users.slug, slug),
    // Return specific columns here
  })
  const user = data[0]
 
  const currentUser = await getUser(user.id)
 
  // Or return only what's specific to the query here
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}
728x90

JWT란 무엇인가?

JWT는 JSON Web Token의 약자로, JSON 형태의 데이터를 네트워크를 통해 안전하게 전송하기 위해 설계된 방식입니다. JWT는 마치 편지와 같은 구조를 가지고 있습니다:

  • 봉투 (Header): 토큰의 타입과 사용된 알고리즘 정보
  • 편지지 (Payload): 실제 전달하려는 데이터 (Claims)
  • 서명 (Signature): 토큰의 유효성을 검증하기 위한 서명

 

JWT의 구조

JWT는 세 부분으로 구성되며, 각 부분은 Base64로 인코딩되어 점(.)으로 구분됩니다:

header.payload.signature

 

 

JWT의 핵심: 서명(Signature)

서명은 JWT의 무결성을 보장하는 핵심 요소입니다. 서명 과정은 다음과 같습니다:

  • 비밀키(Secret Key)를 사용
  • 헤더와 페이로드를 입력으로 사용
  • 지정된 알고리즘(예: HMAC)으로 서명 생성

 

JWT의 활용 분야

JWT는 다양한 분야에서 활용되지만, 가장 흔한 용도는 인증(Authentication)입니다. 주요 활용 분야는 다음과 같습니다:

  • 인증
  • 정보 공유
  • 권한 부여
  • 단일 로그인(SSO)
  • 서버 간 통신

 

JWT 기반 인증 과정

로그인

  • 사용자가 로그인 정보 제공
  • 서버가 정보 확인 후 JWT 생성 및 클라이언트에 전송

인증

  • 클라이언트가 JWT를 서버에 전송
  • 서버가 JWT를 검증
  • 검증 성공 시 요청 처리

JWT의 장점

  • 서버 부하 감소: 세션 저장소가 필요 없음
  • 확장성: 다중 서버 환경에서 유리
  • 클라이언트 측 저장: 필요한 정보를 페이로드에 포함 가능

JWT의 단점

  1. 토큰 크기: JWT는 모든 정보를 자체적으로 포함하므로, 토큰 크기가 상대적으로 클 수 있습니다. 이는 네트워크 대역폭 사용량을 증가시킬 수 있습니다.
  2. 보안 위험: 토큰 탈취: JWT가 탈취되면, 만료되기 전까지 공격자가 사용할 수 있습니다.
  3. 토큰 무효화의 어려움: 일단 발급된 JWT는 만료 전까지 유효하며, 개별 토큰을 즉시 무효화하기 어렵습니다. 이는 보안 문제 발생 시 대응을 어렵게 만들 수 있습니다.
  4. 갱신 로직의 복잡성: 토큰 만료 관리와 갱신 프로세스가 복잡할 수 있습니다.
  5. 상태 저장의 한계: JWT는 기본적으로 무상태(stateless)이므로, 사용자 상태 변경을 실시간으로 반영하기 어려울 수 있습니다.
  6. 암호화 부재: 기본 JWT는 서명은 되지만 암호화되지 않아, 중요한 정보는 페이로드에 포함시키지 말아야 합니다.
  7. 구현의 복잡성: 올바른 JWT 구현과 관리는 복잡할 수 있으며, 잘못 구현 시 보안 취약점이 발생할 수 있습니다.
  8. 만료 시간 설정의 딜레마: 짧은 만료 시간은 보안에 유리하지만 사용자 경험을 해칠 수 있고, 긴 만료 시간은 그 반대입니다.

 

JWT 실습

JWT.io 사이트를 통해 JWT를 직접 생성하고 검증해볼 수 있습니다. 이를 통해 JWT의 구조와 작동 방식을 실제로 확인할 수 있습니다.

 

결론

JWT는 현대 웹 애플리케이션에서 인증과 정보 교환을 위한 효율적이고 안전한 방법을 제공합니다. 그러나 보안을 위해 적절한 사용과 관리가 필요합니다. JWT의 이해는 웹 개발자에게 필수적인 지식이 되어가고 있습니다.

 

출처

https://www.youtube.com/watch?v=36lpDzQzVXs

 

728x90

OAuth 2.0이란?

OAuth 2.0서로 다른 서비스 간에 안전하게 인증과 권한 부여를 할 수 있게 해주는 표준 프로토콜입니다. 이를 통해 사용자의 민감한 정보를 직접 공유하지 않고도 다른 서비스의 기능을 이용할 수 있습니다.

 

 

OAuth 2.0이 필요한 이유

예를 들어, '우아한 캘린더'라는 서비스를 만들었다고 가정해봅시다. 이 서비스는 구글 캘린더와 연동하여 일정을 관리하려고 합니다. OAuth 2.0 없이는 다음과 같은 문제가 발생할 수 있습니다

  • 사용자의 구글 아이디와 비밀번호를 직접 받아야 함
  • 보안 위험 증가
  • 불필요한 접근 권한 발생

OAuth 2.0을 사용하면 이러한 문제를 해결할 수 있습니다.

 

 

OAuth 2.0의 주요 개념

  • Resource Owner: 사용자
  • Client: 우리가 만든 서비스 (예: 우아한 캘린더)
  • Authorization Server: 인증을 담당하는 서버 (예: 구글)
  • Resource Server: 실제 데이터를 제공하는 서버 (예: 구글 캘린더)

OAuth 2.0의 동작 과정

  1. 사용자가 우아한 캘린더에서 "구글 로그인" 버튼 클릭
  2. 구글의 로그인 페이지로 리다이렉트
  3. 사용자가 구글에 직접 로그인
  4. 구글이 우아한 캘린더에 권한 코드 전달
  5. 우아한 캘린더가 권한 코드로 액세스 토큰 요청
  6. 구글이 액세스 토큰 발급
  7. 우아한 캘린더가 액세스 토큰으로 구글 캘린더 데이터 접근

 

OAuth 2.0의 장점

  • 보안 강화: 사용자의 비밀번호를 직접 다루지 않음
  • 권한 제한: 필요한 기능에만 접근 가능
  • 사용자 경험 개선: 별도의 회원가입 없이 기존 계정으로 로그인 가능

 

OAuth 2.0 구현 시 주의사항

  • HTTPS 사용 필수
  • 클라이언트 ID와 시크릿 안전하게 보관
  • Redirect URI 정확히 설정

 

소셜 로그인과 OAuth 2.0

OAuth 2.0은 주로 소셜 로그인 구현에 사용됩니다. 하지만 OAuth 2.0은 인가(Authorization)를 위한 프로토콜이며, 인증(Authentication)을 위해서는 OpenID Connect라는 추가적인 레이어가 사용됩니다.

결론: OAuth 2.0은 현대 웹 서비스에서 필수적인 보안 프로토콜입니다. 이를 통해 사용자의 정보를 안전하게 보호하면서도 다양한 서비스 간의 연동을 가능하게 합니다. 개발자로서 OAuth 2.0의 개념과 흐름을 이해하는 것은 매우 중요합니다.

 

출처

https://www.youtube.com/watch?v=Mh3LaHmA21I

 

728x90

1.JWT (JSON Web Token)란?

JWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준입니다.

 

주요 특징:

  • 구조: Header, Payload, Signature 세 부분으로 구성
  • 사용 사례: 인증 및 정보 교환
  • 장점: 상태를 저장할 필요가 없어 서버 부하 감소

JWT 작동 방식:

  1. 사용자 로그인
  2. 서버가 JWT 생성 및 클라이언트에 전송
  3. 클라이언트가 후속 요청에 JWT 포함
  4. 서버가 JWT 검증 후 요청 처리

 

2. OAuth란?

OAuth는 사용자 데이터에 대한 접근 권한을 제3자 애플리케이션에 부여할 수 있게 하는 개방형 표준 프로토콜입니다.

 

주요 특징

  • 버전: OAuth 1.0과 OAuth 2.0 (더 널리 사용됨)
  • 목적: 안전한 위임 접근
  • 역할: Resource Owner, Client, Authorization Server, Resource Server

OAuth 2.0 승인 흐름

  1. 클라이언트가 리소스 소유자에게 권한 요청
  2. 리소스 소유자가 승인 부여
  3. 클라이언트가 인증 서버에 액세스 토큰 요청
  4. 인증 서버가 액세스 토큰 발급
  5. 클라이언트가 액세스 토큰으로 보호된 리소스 접근

3. JWT vs OAuth

  • JWT는 토큰 형식이고, OAuth는 권한 부여 프로토콜입니다.
  • JWT는 주로 인증에 사용되고, OAuth는 권한 부여에 중점을 둡니다.
  • OAuth는 JWT를 토큰 형식으로 사용할 수 있습니다.

보안 고려사항

  • JWT: 서명 검증, 만료 시간 설정, 민감한 정보 암호화
  • OAuth: HTTPS 사용, 상태 매개변수 검증, 안전한 클라이언트 비밀 관리

구현 팁

  • JWT: jsonwebtoken 라이브러리 (Node.js)
  • OAuth: Passport.js (Node.js), Spring Security OAuth (Java)

결론

JWT와 OAuth는 현대 웹 애플리케이션의 보안과 사용자 경험을 향상시키는 강력한 도구입니다. 개발자는 이들의 작동 원리와 적절한 사용 사례를 이해하고 있어야 합니다.

728x90

+ Recent posts