๊ด€๋ฆฌ ๋ฉ”๋‰ด

Coding Archive

[Next.js] Next.js๋กœ HttpOnly ์ฟ ํ‚ค + BFF(ํ”„๋ก์‹œ)์•„ํ‚คํ…์ฒ˜๋กœ ์ธ์ฆ ๋ณด์•ˆ/์„ฑ๋Šฅ ์žก๊ธฐ - ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ธ์ฆ์˜ ๋ณด์•ˆ ๋ฌธ์ œ์™€ HttpOnly ์ฟ ํ‚ค๋ฅผ ํ™œ์šฉํ•œ ๊ฐœ์„  ๋ฐฉ๋ฒ• ๋ณธ๋ฌธ

๐Ÿ’ป Programming/Next.js & project

[Next.js] Next.js๋กœ HttpOnly ์ฟ ํ‚ค + BFF(ํ”„๋ก์‹œ)์•„ํ‚คํ…์ฒ˜๋กœ ์ธ์ฆ ๋ณด์•ˆ/์„ฑ๋Šฅ ์žก๊ธฐ - ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ธ์ฆ์˜ ๋ณด์•ˆ ๋ฌธ์ œ์™€ HttpOnly ์ฟ ํ‚ค๋ฅผ ํ™œ์šฉํ•œ ๊ฐœ์„  ๋ฐฉ๋ฒ•

์ฝ”๋“ฑ์–ด 2025. 8. 22. 18:41

 

 

Next.js ํ”„๋กœ์ ํŠธ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ณด์•ˆ ์ด์Šˆ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ฟ ํ‚ค์— ์ €์žฅ๋œ ํ† ํฐ์ด ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋˜๊ณ  ์žˆ์—ˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ํ† ํฐ์€ ์‚ฌ์šฉ์ž ์ธ์ฆ๊ณผ ๋ณด์•ˆ์„ ์œ„ํ•ด ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฐ’ ์ค‘ ํ•˜๋‚˜์ธ๋ฐ, ์ด ๊ฐ’์ด ๊ทธ๋Œ€๋กœ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด XSS(๊ต์ฐจ ์‚ฌ์ดํŠธ ์Šคํฌ๋ฆฝํŠธ ๊ณต๊ฒฉ)๋‚˜ ํ† ํฐ ํƒˆ์ทจ ๋“ฑ์˜ ๋ณด์•ˆ ์ทจ์•ฝ์ ์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ์กด ์ฝ”๋“œ ๊ตฌ์กฐ์™€ ์ธ์ฆ ๋ฐฉ์‹์„ ๋‹ค์‹œ ์ ๊ฒ€ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ํŠนํžˆ ํ† ํฐ์„ ์–ด๋””์— ์ €์žฅํ•˜๊ณ , ์–ด๋–ป๊ฒŒ ์ „๋‹ฌํ•  ๊ฒƒ์ธ๊ฐ€๋ผ๋Š” ์ฃผ์ œ์— ์ง‘์ค‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ ๊ฐ„ ํ†ต์‹  ๊ตฌ์กฐ, ์ฟ ํ‚ค ์˜ต์…˜ ์„ค์ •(HttpOnly, Secure ๋“ฑ), ํ”„๋ก์‹œ ์„œ๋ฒ„์˜ ์—ญํ• ๊นŒ์ง€ ์ „๋ฐ˜์ ์œผ๋กœ ์‚ดํŽด๋ณด๋ฉด์„œ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด ๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ œ๊ฐ€ ๊ฒช์€ ๋ฌธ์ œ ์ƒํ™ฉ๊ณผ, ๊ทธ๊ฒƒ์„ ์–ด๋–ป๊ฒŒ ๊ฐœ์„ ํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ๊ณผ์ •์„ ์ •๋ฆฌํ•ด ๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. 

 

๊ธฐ์กด ์ฝ”๋“œ: ํด๋ผ์ด์–ธํŠธ(JS)์—์„œ ํ† ํฐ์„ ์ง์ ‘ ์ €์žฅ/์ฃผ์ž…HttpOnly ์‚ฌ์šฉ ๋ถˆ๊ฐ€, XSS/ํƒˆ์ทจ ์œ„ํ—˜.
๋ฆฌํŒฉํ† ๋ง: Next.js ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ HttpOnly ์ฟ ํ‚ค๋กœ๋งŒ ๊ด€๋ฆฌํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ ๋ณดํ˜ธ API๋Š” ํ”„๋ก์‹œ(BFF)๋กœ ํ˜ธ์ถœ.

 

 

 


 

๐Ÿ› ๊ธฐ์กด ์ฝ”๋“œ ๋ฌธ์ œ ์งš์–ด๋ณด๊ธฐ

1๏ธโƒฃ XSS ๊ณต๊ฒฉ์— ์™„์ „ํžˆ ๋…ธ์ถœ๋œ ํ† ํฐ

// ๊ธฐ์กด AuthService.ts
class AuthService {
  private static userAccount: string | null = StorageService.get('userAccount');
  private static token: string | null = StorageService.get('token');

  static login(userAccount: string, token: string): void {
    this.userAccount = userAccount;
    this.token = token;
    StorageService.setStorageAdapter('localStorage');
    StorageService.set('userAccount', userAccount);
    StorageService.setStorageAdapter('cookieStorage');
    StorageService.set('token', token, {
      expires: 3,
      path: '',
    });
  }

  static getToken(): string | null {
    return this.token || StorageService.get('token');
  }
}

// LoginPage์—์„œ์˜ ์‚ฌ์šฉ
const onSubmit = async (data: ILoginRequest) => {
  try {
    const response = await login(data);
    const { token, accountname } = response.user;
    if (token && accountname) {
      AuthService.login(accountname, token); // ํด๋ผ์ด์–ธํŠธ์—์„œ ์ง์ ‘ ์ €์žฅ
      goTo('/feed');
    }
  } catch (error) {
    console.error('๋กœ๊ทธ์ธ ์—๋Ÿฌ:', error);
  }
};
  • JavaScript๋กœ ์ฟ ํ‚ค๋ฅผ set/get ํ•˜๋ฉด HttpOnly๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์–ด XSS๊ฐ€ ํ„ฐ์ง€๋ฉด ํ† ํฐ ํƒˆ์ทจ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ํ† ํฐ์ด ์ผ๋ฐ˜ ์ฟ ํ‚ค๋‚˜ localStorage์— ์ €์žฅ๋˜์–ด JavaScript๋กœ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  • ์•„๋ž˜์™€ ๊ฐ™์ด XSS ๊ณต๊ฒฉ์ž๊ฐ€ ๋‹จ ํ•œ ์ค„์˜ JavaScript ์ฝ”๋“œ๋กœ ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
// ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‰ฝ๊ฒŒ ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์Œ
console.log(document.cookie); // ํ† ํฐ์ด ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ
console.log(localStorage.getItem('token')); // ํ† ํฐ ์ง์ ‘ ์ ‘๊ทผ ๊ฐ€๋Šฅ

 

2๏ธโƒฃ CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด ๋ถ€์กฑ

// ๊ธฐ์กด ์ฝ”๋“œ - SameSite ๋ณดํ˜ธ ์—†์Œ
StorageService.set('token', token, {
  expires: 3,
  path: '', // SameSite, Secure ์˜ต์…˜ ์—†์Œ
});
  • ์ผ๋ฐ˜ ์ฟ ํ‚ค๋Š” SameSite ์„ค์ •์ด ์—†์œผ๋ฉด ๋ชจ๋“  ๋„๋ฉ”์ธ์—์„œ ์ž๋™์œผ๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.
  • ์•…์„ฑ ์‚ฌ์ดํŠธ์—์„œ ์‚ฌ์šฉ์ž ๋ชจ๋ฅด๊ฒŒ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ํ† ํฐ์„ ํฌํ•จํ•ด์„œ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

 

3๏ธโƒฃ ๋ณต์žกํ•œ ์ €์žฅ์†Œ ๊ด€๋ฆฌ ๋กœ์ง

// ์ €์žฅ์†Œ ํƒ€์ž…์„ ๊ณ„์† ๋ฐ”๊พธ๋Š” ๋ณต์žกํ•œ ๋กœ์ง
StorageService.setStorageAdapter('localStorage');
StorageService.set('userAccount', userAccount);
StorageService.setStorageAdapter('cookieStorage');
StorageService.set('token', token, { expires: 3, path: '' });
  • localStorage์™€ cookieStorage๋ฅผ ๋ฒˆ๊ฐˆ์•„ ์‚ฌ์šฉํ•˜๋Š” ๋ณต์žกํ•œ ๋กœ์ง์œผ๋กœ ์ธํ•œ ๋ฒ„๊ทธ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.
  • ์ง์ ‘์ ์ธ ๋ณด์•ˆ ์œ„ํ—˜์€ ์•„๋‹ˆ์ง€๋งŒ, ๋ณต์žก์„ฑ์œผ๋กœ ์ธํ•ด ๋‹ค๋ฅธ ๋ณด์•ˆ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

 


 

 

๋ฌธ์ œ ํ๋ฆ„์„ ์ •๋ฆฌํ•ด ๋ณด๋ฉด

AuthService.login()์ด localStorage + cookieStorage์— ํ† ํฐ์„ ์ €์žฅ
StorageService๋Š” JS์—์„œ ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ set() (HttpOnly ์„ค์ • ๋ถˆ๊ฐ€)
Axios ์ธ์Šคํ„ด์Šค/์ธํ„ฐ์…‰ํ„ฐ๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ํ† ํฐ์„ ํ—ค๋”์— ๋ถ™์—ฌ ์ „์†กํ•˜๋Š” ํ๋ฆ„

 

๐Ÿ‘‰ ์ง€๊ธˆ ๊ตฌ์กฐ๋Š” ๊ตฌํ˜„์€ ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ์‹ค์„œ๋น„์Šค ๋ณด์•ˆ ๊ธฐ์ค€(ํŠนํžˆ XSS/ํ† ํฐ ๋ณดํ˜ธ)์—์„  ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

 


 

๐Ÿ’ก Next.js HttpOnly ์ฟ ํ‚ค๋ฅผ ํ™œ์šฉํ•œ ๋ณด์•ˆ ๊ฐœ์„ ํ•˜๊ธฐ

1. ๊ฐœ์„  ๋ชฉํ‘œ: ํ”„๋ก์‹œ ํŒจํ„ด์œผ๋กœ ๊ธฐ์กด ์ฝ”๋“œ ์ตœ๋Œ€ํ•œ ๋ณด์กดํ•˜๋ฉฐ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ

๊ธฐ์กด ์ฝ”๋“œ์˜ 95%๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ณด์•ˆ๋งŒ ํฌ๊ฒŒ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ• ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด: ํด๋ผ์ด์–ธํŠธ → ์™ธ๋ถ€ API → ์‘๋‹ต → ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฟ ํ‚ค ์ €์žฅ
๊ฐœ์„ : ํด๋ผ์ด์–ธํŠธ → Next.js API Route → ์™ธ๋ถ€ API → ์„œ๋ฒ„์—์„œ HttpOnly ์ฟ ํ‚ค ์ €์žฅ
// ํด๋” ๊ตฌ์กฐ
frontend/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ api/                # โœ… Next.js API Route Handlers (BFF)
โ”‚   โ”‚   โ”œโ”€โ”€ user/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ login/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ route.ts    # POST /api/user/login
โ”‚   โ”‚   โ””โ”€โ”€ post/
โ”‚   โ”‚       โ””โ”€โ”€ feed/
โ”‚   โ”‚           โ””โ”€โ”€ route.ts    # GET /api/post/feed
โ”‚   โ”œโ”€โ”€ (auth)/
โ”‚   โ”‚   โ””โ”€โ”€ login/page.tsx
โ”‚   โ””โ”€โ”€ feed/page.tsx
โ”‚
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ”œโ”€โ”€ apiRequests/     # โœ… ํด๋ผ์ด์–ธํŠธ์—์„œ ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜ ๋ชจ์Œ
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ auth.ts      # login, signup ๋“ฑ
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ post.ts      # postList ๋“ฑ
โ”‚   โ”‚   โ””โ”€โ”€ index.ts         # axios instance
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ components/          # ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”œโ”€โ”€ services/            # AuthService, StorageService ๋“ฑ (ํด๋ผ ์ „์šฉ)
โ”‚   โ””โ”€โ”€ queries/             # react-query hooks
โ”‚
โ””โ”€โ”€ .env.local               # BACKEND_BASE_URL ๋“ฑ ํ™˜๊ฒฝ๋ณ€์ˆ˜

 

2. ๊ฐœ์„  ๋ฐฉํ–ฅ: Next BFF(ํ”„๋ก์‹œ) + HttpOnly ์ฟ ํ‚ค

  • ๋กœ๊ทธ์ธ: Next ์„œ๋ฒ„ ๋ผ์šฐํŠธ๊ฐ€ ๋ฐฑ์—”๋“œ ๋กœ๊ทธ์ธ API๋ฅผ ํ˜ธ์ถœ → ์‘๋‹ต๋ฐ›์€ ํ† ํฐ์„ HttpOnly ์ฟ ํ‚ค๋กœ ์„œ๋ฒ„๊ฐ€ ์„ค์ •.
  • ๋ณดํ˜ธ API ํ˜ธ์ถœ: ํด๋ผ์ด์–ธํŠธ๋Š” ๋ฐฑ์—”๋“œ๋กœ ์ง์ ‘ ๊ฐ€์ง€ ์•Š๊ณ  Next ํ”„๋ก์‹œ๋กœ ํ˜ธ์ถœ → ํ”„๋ก์‹œ๊ฐ€ ์ฟ ํ‚ค์—์„œ ํ† ํฐ์„ ๊บผ๋‚ด Authorization ํ—ค๋”๋ฅผ ์„œ๋ฒ„์—์„œ๋งŒ ์ฃผ์ž….
  • ์„ธ์…˜ ํ™•์ธ: ์„œ๋ฒ„ ๋ผ์šฐํŠธ์—์„œ ์ฟ ํ‚ค ํ† ํฐ์œผ๋กœ ํ™•์ธ → ํด๋ผ์—๋Š” “์ธ์ฆ๋จ/์•„๋‹˜ + ์ตœ์†Œ ์ •๋ณด”๋งŒ ์ „๋‹ฌ.
  • ํด๋ผ์ด์–ธํŠธ๋Š” ํ† ํฐ์„ ์˜์˜ ๋ชจ๋ฅธ๋‹ค. (์ด๊ฒŒ ํฌ์ธํŠธ!)

 

3. ๋‹จ๊ณ„๋ณ„ ๋ฆฌํŒฉํ† ๋ง

ํด๋ผ์ด์–ธํŠธ์—์„œ ํ† ํฐ ๋‹ค๋ฃจ๋Š” ์ฝ”๋“œ ์ œ๊ฑฐ → Next ๋ผ์šฐํŠธ/ํ”„๋ก์‹œ์—์„œ๋งŒ ์ฟ ํ‚ค/ํ† ํฐ ์ œ์–ด

1๏ธโƒฃ ๋ณด์•ˆ ๊ฐ•ํ™”๋œ ๋กœ๊ทธ์ธ API Route ์ƒ์„ฑ

// app/api/auth/login/route.ts (BFF, Next.js ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰)
import { cookies } from 'next/headers'; // (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋‚˜ Route Handler(app/api)์—์„œ๋งŒ ํ˜ธ์ถœ ๊ฐ€๋Šฅํ•œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ „์šฉ API)
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  // 1. ๊ธฐ์กด ์™ธ๋ถ€ API ํ˜ธ์ถœ
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_BASE_URL}/user/login`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    },
  );

  const data = await response.json();

  if (response.ok && data.user?.token) {
    // 2. ๐Ÿ›ก๏ธ HttpOnly ์ฟ ํ‚ค๋กœ ๋ณด์•ˆ ์ €์žฅ
    cookies().set('token', data.user.token, {
      httpOnly: true, // XSS ๊ณต๊ฒฉ ๋ฐฉ์–ด
      secure: process.env.NODE_ENV === 'production', // HTTPS์—์„œ๋งŒ ์ „์†ก
      sameSite: 'strict', // CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด
      maxAge: 60 * 60 * 24 * 3, // 3์ผ
      path: '/',
    });

    // 3. ๐Ÿ”’ ํด๋ผ์ด์–ธํŠธ์—๋Š” ํ† ํฐ ์—†์ด ์•ˆ์ „ํ•œ ์‘๋‹ต
    return NextResponse.json({
      ...data,
      user: {
        ...data.user,
        token: undefined, // ํ† ํฐ ์ œ๊ฑฐ๋กœ ํด๋ผ์ด์–ธํŠธ ๋…ธ์ถœ ๋ฐฉ์ง€
      },
    });
  }

  return NextResponse.json(data, { status: response.status });
}
  • ๋กœ๊ทธ์ธ ์š”์ฒญ ํ›„ ํ† ํฐ์„ HttpOnly + Secure ์ฟ ํ‚ค์— ์ €์žฅ
  • HttpOnly ์ฟ ํ‚ค๋ฅผ ์ฝ๊ฑฐ๋‚˜ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ์Œ
  • ๋ฐฑ์—”๋“œ API์— ์š”์ฒญ์„ ๋Œ€๋ฆฌ๋กœ ๋ณด๋ƒ„

 

2๏ธโƒฃ Axios ์ธ์Šคํ„ด์Šค ํ”„๋ก์‹œ๋ฅผ ๊ธฐ๋ณธ baseURL๋กœ

// src/api/index.ts
import axios, { AxiosInstance } from 'axios';

import { handleRequest, handleRequestError } from './helper/requestHandlers';
import { handleResponse, handleResponseError } from './helper/responseHandlers';

const instance: AxiosInstance = axios.create({
  baseURL: '/api', // Next.js์˜ API ๋ผ์šฐํŠธ ์‚ฌ์šฉ (๊ธฐ์กด: process.env.NEXT_PUBLIC_BASE_URL)
  withCredentials: true, // ์ฟ ํ‚ค ์ž๋™ ์ „์†ก
});

instance.interceptors.request.use(handleRequest, handleRequestError);
instance.interceptors.response.use(handleResponse, handleResponseError);

export default instance;

 

3๏ธโƒฃ AuthService/StorageService ์ •๋ฆฌ ๋ฐ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

// services/AuthService.ts
import StorageService from './StorageService';

class AuthService {
  private static userAccount: string | null = StorageService.get('userAccount');

  static login(userAccount: string): void {
    this.userAccount = userAccount;
    StorageService.setStorageAdapter('localStorage');
    StorageService.set('userAccount', userAccount);
  }
...
}

export default AuthService;
  • ํ† ํฐ์„ ์ €์žฅ/์กฐํšŒํ•˜๋Š” ๋ชจ๋“  ๋ฉ”์„œ๋“œ ์‚ญ์ œ

 

4๏ธโƒฃ ํ”ผ๋“œ ๋ผ์šฐํŠธ ์ƒ์„ฑ (ํ—ค๋”์— ํ† ํฐ ์ฃผ์ž…)

// app/api/post/feed/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const limit = searchParams.get('limit');
  const skip = searchParams.get('skip');

  const token = cookies().get('token')?.value;

  const res = await fetch(
    `${process.env.BACKEND_BASE_URL}/post/feed/?limit=${limit}&skip=${skip}`,
    {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {}),
      },
      cache: 'no-store', // ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ
    },
  );

  const data = await res.json();
  return NextResponse.json(data);
}

 

  • ํด๋ผ์ด์–ธํŠธ → BFF ์š”์ฒญ ์‹œ ์ฟ ํ‚ค ํฌํ•จ (withCredentials: true)
  • BFF → ๋ฐฑ์—”๋“œ ์š”์ฒญ ์‹œ ์ฟ ํ‚ค์—์„œ token ์ถ”์ถœ → Authorization ํ—ค๋”๋กœ ๋ณ€ํ™˜

๐Ÿ‘‰ ํด๋ผ์ด์–ธํŠธ๋Š” ๋‹จ์ˆœํžˆ /api/... ๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋˜๊ณ , ๋ณด์•ˆ/ํ† ํฐ ๊ด€๋ฆฌ๋Š” ์ „๋ถ€ Next.js ์„œ๋ฒ„ (app/api)๊ฐ€ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ์ตœ์ข… ๊ตฌ์กฐ ํ๋ฆ„

[ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ]
      โ”‚
      โ–ผ
๋กœ๊ทธ์ธ ์š”์ฒญ (email, password)
      โ”‚
      โ–ผ
Axios → /api/auth/login
      โ”‚
      โ–ผ
[Next.js Route Handler (/api/auth/login)]
  - ๋ฐฑ์—”๋“œ๋กœ ๋กœ๊ทธ์ธ ์š”์ฒญ ์ „๋‹ฌ
  - ์„ฑ๊ณต ์‹œ JWT ํ† ํฐ ์ˆ˜์‹ 
  - Response ์ฟ ํ‚ค(token=..., HttpOnly)๋กœ ์ €์žฅ
      โ”‚
      โ–ผ
๋ธŒ๋ผ์šฐ์ € ์ €์žฅ์†Œ
  - HttpOnly ์ฟ ํ‚ค(token) ์ž๋™ ๋ณด๊ด€
  - JS์—์„œ ์ง์ ‘ ์ ‘๊ทผ ๋ถˆ๊ฐ€
      โ”‚
      โ–ผ
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      ์ดํ›„ API ์š”์ฒญ ํ๋ฆ„
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      โ”‚
      โ–ผ
ํด๋ผ์ด์–ธํŠธ์—์„œ postList(limit, skip) ํ˜ธ์ถœ
      โ”‚
      โ–ผ
Axios → /api/post/feed
      โ”‚
      โ–ผ
[Next.js Route Handler (/api/post/feed)]
  - cookies() ๋กœ HttpOnly ํ† ํฐ ๊บผ๋ƒ„
  - Authorization: Bearer <token> ํ—ค๋” ๋ถ™์ž„
      โ”‚
      โ–ผ
[๋ฐฑ์—”๋“œ API ์„œ๋ฒ„ ํ˜ธ์ถœ]
  - ํ† ํฐ ๊ฒ€์ฆ
  - ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
      โ”‚
      โ–ผ
Next.js Route Handler → ํด๋ผ์ด์–ธํŠธ ์‘๋‹ต

 

๐Ÿ›ก๏ธ ๋ณด์•ˆ ๊ฐœ์„  ํšจ๊ณผ

Before vs After ๋น„๊ต

HttpOnly ์ฟ ํ‚ค ๋ฆฌํŒฉํ† ๋ง ์ „/ํ›„

๋ณด์•ˆ ์š”์†Œ ๊ธฐ์กด ๋ฐฉ์‹ ๊ฐœ์„ ๋œ ๋ฐฉ์‹
XSS ๋ฐฉ์–ด โŒ JavaScript๋กœ ํ† ํฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ โœ… HttpOnly๋กœ JavaScript ์ ‘๊ทผ ์ฐจ๋‹จ
CSRF ๋ฐฉ์–ด โŒ SameSite ์„ค์ • ์—†์Œ โœ… SameSite=strict๋กœ ์™„๋ฒฝ ์ฐจ๋‹จ
HTTPS ๊ฐ•์ œ โŒ HTTP์—์„œ๋„ ํ† ํฐ ์ „์†ก โœ… Secure ์†์„ฑ์œผ๋กœ HTTPS๋งŒ ํ—ˆ์šฉ
ํ† ํฐ ๋…ธ์ถœ โŒ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์™„์ „ ๋…ธ์ถœ -> ํƒˆ์ทจ ๊ฐ€๋Šฅ โœ… ์„œ๋ฒ„์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ -> ํƒˆ์ทจ ๋ถˆ๊ฐ€
์ฝ”๋“œ ๋ณต์žก์„ฑ โŒ ๋ณต์žกํ•œ ์ €์žฅ์†Œ ๋กœ์ง โœ… ๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•œ ๊ตฌ์กฐ

 


๐Ÿ› ๏ธ ์ดํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ

ํ˜„์žฌ๋Š” ๊ฐ API ๋ผ์šฐํŠธ์—์„œ ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ์ฝ๊ณ , ์š”์ฒญ๋งˆ๋‹ค Authorization ํ—ค๋”๋ฅผ ๋ถ™์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ์ ์œผ๋กœ๋Š” ๋ฌธ์ œ์—†์ง€๋งŒ, ํ† ํฐ์ด ํ•„์š”ํ•œ API๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋Š˜์–ด๋‚œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ๋Š” ๋ฒ”์šฉ ํ”„๋ก์‹œ ๊ตฌ์กฐ๋กœ ๋ฆฌํŒฉํ„ฐ๋ง ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

 

  • ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ๋‹จ์ˆœํžˆ /app/api/... ๋กœ ์š”์ฒญ
  • ํ”„๋ก์‹œ ๋ ˆ์ด์–ด๊ฐ€ ์ž๋™์œผ๋กœ ์ฟ ํ‚ค์—์„œ ํ† ํฐ์„ ์ฝ์–ด Authorization ํ—ค๋”๋ฅผ ๋ถ™์ด๊ณ  ๋ฐฑ์—”๋“œ์— ์ „๋‹ฌ
  • API ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ˆ˜์‹ญ, ์ˆ˜๋ฐฑ ๊ฐœ๋กœ ๋Š˜์–ด๋‚˜๋”๋ผ๋„ ํ”„๋ก์‹œ ์ฝ”๋“œ ํ•˜๋‚˜๋งŒ ์œ ์ง€ํ•˜๋ฉด ์ „์ฒด๊ฐ€ ๋™์ž‘

๐Ÿ‘‰  ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ + ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ + ๋ณด์•ˆ ์ผ๊ด€์„ฑ ๊ฐ•ํ™”

 

 

 


 

 

 

์ฒ˜์Œ์—๋Š” “ํ† ํฐ์ด ๋ณด์ด๋ฉด ์•ˆ ๋˜๊ฒ ๋‹ค”๋Š” ๋‹จ์ˆœํ•œ ๋ฌธ์ œ์˜์‹์—์„œ ์ถœ๋ฐœํ–ˆ์ง€๋งŒ, HttpOnly ์ฟ ํ‚ค์™€ ํ”„๋ก์‹œ ๋ผ์šฐํŒ…์„ ์ ์šฉํ•˜๋ฉด์„œ ๋ณด์•ˆ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•ด์ง€๊ณ , ๊ฒฐ๊ณผ์ ์œผ๋กœ ๊ฐœ๋ฐœ ํ๋ฆ„๋„ ๋งค๋„๋Ÿฌ์›Œ์กŒ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ ์ด๋ฒˆ ์ž‘์—…์œผ๋กœ ๋‹จ์ˆœํ•œ ๋ฒ„๊ทธ ์ˆ˜์ •์ด ์•„๋‹ˆ๋ผ, ์„œ๋น„์Šค๋ฅผ ๋” ์•ˆ์ „ํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ  ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊นŒ์ง€ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์•ž์œผ๋กœ๋„ ์ด๋Ÿฐ ์ž‘์€ ๊ฐœ์„ ๋“ค์„ ๊พธ์ค€ํžˆ ์Œ“์•„๊ฐ€๋ฉด์„œ ๋” ์•ˆ์ •์ ์ด๊ณ  ์ฆ๊ฒ๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ๋งŒ๋“ค์–ด๊ฐ€๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

๋์œผ๋กœ ์ด ๊ธ€์ด ๋น„์Šทํ•œ ๊ณ ๋ฏผ์„ ํ•˜๊ณ  ๊ณ„์‹  ๋ถ„๋“ค๊ป˜ ์ž‘์€ ๋„์›€์ด ๋˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค!

 

Comments