๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

javascript/[TIL] ๋ฆฌ์•กํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ 

๋ฆฌ์•กํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  - JWT๋ฅผ ํ†ตํ•œ ํšŒ์› ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

728x90

JWT(JSON Web Token)

: ๋ฐ์ดํ„ฐ๊ฐ€ JSON์œผ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋Š” ํ† ํฐ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๋‘ ๊ฐœ์ฒด๊ฐ€ ์„œ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ •๋ณด๋ฅผ ์ฃผ๊ณ  ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์›น ํ‘œ์ค€์œผ๋กœ ์ •์˜๋˜์—ˆ๋‹ค.

 

์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋Œ€ํ‘œ์ ์ธ ๋‘ ๊ฐ€์ง€ ์ธ์ฆ๋ฐฉ์‹

1. ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ 

2. ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ

 

1. ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ

: ์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ์ค‘์ž„์„ ๊ธฐ์–ตํ•˜๊ณ  ์žˆ๋‹ค.

์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์‹œ, 

์„œ๋ฒ„๋Š” ์„ธ์…˜ ์ €์žฅ์†Œ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ  ์„ธ์…˜ id๋ฅผ ๋ฐœ๊ธ‰ํ•œ๋‹ค.

๋ฐœ๊ธ‰๋œ id๋Š” ์ฃผ๋กœ ๋ธŒ๋ผ์šฐ์ €์˜ ์ฟ ํ‚ค์— ์ €์žฅํ•œ๋‹ค.

๊ทธ ๋‹ค์Œ์— ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„๋Š” ์„ธ์…˜ ์ •์žฅ์†Œ์—์„œ ์„ธ์…˜์„ ์กฐํšŒํ•œ ํ›„

๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜์—ฌ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‘๋‹ตํ•œ๋‹ค.

์„ธ์…˜ ์ €์žฅ์†Œ๋Š” ์ฃผ๋กœ ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ์„ ์‚ฌ์šฉํ•œ๋‹ค.

 

๋‹จ์  : ์„œ๋ฒ„ ํ™•์žฅ์ด ์–ด๋ ต๋‹ค.

๋งŒ์•ฝ ์„œ๋ฒ„์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ๊ฐ€ ๋œ๋‹ค๋ฉด, ๋ชจ๋“  ์„œ๋ฒ„๋ผ๋ฆฌ ๊ฐ™์€ ์„ธ์…˜์„ ๊ณต์œ ํ•ด์•ผํ•˜๋ฏ€๋กœ, ์„ธ์…˜ ์ „์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•  ๋ฟ ์•„๋‹ˆ๋ผ ์‹ ๊ฒฝ ์จ์•ผ ํ•  ๊ฒƒ๋„ ๋งŽ๋‹ค.

 

 

2. ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ

ํ† ํฐ : ๋กœ๊ทธ์ธ ์ดํ›„ ์„œ๋ฒ„๊ฐ€ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋ฌธ์ž์—ด๋กœ,

ํ•ด๋‹น ๋ฌธ์ž์—ด ์•ˆ์—๋Š” ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์ •๋ณด์™€ ํ•ด๋‹น ์ •๋ณด๊ฐ€ ์„œ๋ฒ„์—์„œ ๋ฐœ๊ธ‰๋˜์—ˆ์Œ์„ ์ฆ๋ช…ํ•˜๋Š” ์„œ๋ช…์ด ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค. 

์„œ๋ช… ๋ฐ์ดํ„ฐ๋Š” ํ•ด์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ํ†ตํ•ด ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค, (์ฃผ๋กœ HMAC SHA256, RSA SHA256 ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ์‚ฌ์šฉ๋œ๋‹ค.)

ํ† ํฐ์€ ์„œ๋ช…์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌด๊ฒฐ์„ฑ์ด ๋ณด์žฅ๋œ๋‹ค.

๋ฌด๊ฒฐ์„ฑ : ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜ ์œ„์กฐ๋˜์ง€ ์•Š์•˜์Œ์„ ์˜๋งˆํ•˜๋Š” ์„ฑ์งˆ

์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ํ•˜๋ฉด,

์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ง€๋‹ˆ๊ณ  ์žˆ๋Š” ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•ด์ฃผ๊ณ ,

์ถ”ํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ API๋ฅผ ์š”์ฒญํ• ๋•Œ, ๋ฐœ๊ธ‰๋ฐ›์€ ํ† ํฐ๊ณผ ํ•จ๊ป˜ ์š”์ฒญํ•œ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์„œ๋ฒ„๋Š” ํ•ด๋‹น ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์‚ฌํ•˜๊ณ , ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‘๋‹ตํ•œ๋‹ค.

 

์žฅ์  

- ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ๊ธฐ์–ตํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ๋ฆฌ์†Œ์Šค๊ฐ€ ์ ๋‹ค.

- ์‚ฌ์šฉ์ž ์ชฝ์—์„œ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์ง€๋‹Œ ํ† ํฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ์„œ๋ฒ„๋ผ๋ฆฌ ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•  ํ•„์š”๊ฐ€ ์—†์–ด, ์„œ๋ฒ„์˜ ํ™•์žฅ์„ฑ์ด ๋งค์šฐ ๋†’๋‹ค.

- ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ๊ฐ„ํŽธํ•˜๊ณ  ์‚ฌ์šฉ์ž๋“ค์˜ ์ธ์ฆ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ๋„ ํ›จ์”ฌ ์‰ฝ๋‹ค.

 


User ์Šคํ‚ค๋งˆ/๋ชจ๋ธ ๋งŒ๋“ค๊ธฐ

๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ DB์— ์ €์žฅํ• ๋•Œ, ๋‹จ๋ฐฉํ–ฅ ํ•ด์‹ฑ ํ•จ์ˆ˜๋ฅผ ์ง€์›ํ•ด ์ฃผ๋Š” 'bcrypt' ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•˜๊ธฐ

yarn add bcrypt

 

๋ชจ๋ธ ๋ฉ”์„œ๋“œ ๋งŒ๋“ค๊ธฐ

๋ชจ๋ธ ๋ฉ”์„œ๋“œ : ๋ชจ๋ธ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋กœ ๋‘ ๊ฐ€์ง€ ์ข…๋ฅ˜๊ฐ€ ์žˆ๋‹ค.

1. instance method : ๋ชจ๋ธ์„ ํ†ตํ•ด ๋งŒ๋“  ๋ฌธ์„œ ์ธ์Šคํ„ด์Šค์—์„œ ์‚ฌ์šฉ / 2. static method : ๋ชจ๋ธ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉ 

import mongoose, { Schema } from 'mongoose';
import bcrypt from 'bcrypt';

const UserSchema = new Schema({
  username: String,
  hashedPassword: String,
});

// ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์•„ ๊ณ„์ •์˜ hashedPassword ๊ฐ’์„ ์„ค์ •
UserSchema.methods.setPassword = async function (password) {
  const hash = await bcrypt.hash(password, 10);
  this.hashedPassword = hash;
};

// ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ•ด๋‹น ๊ณ„์ •์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ
UserSchema.methods.checkPassword = async function (password) {
  const result = await bcrypt.compare(password, this.hashedPassword);
  return result;
};

// username์œผ๋กœ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ, static ํ•จ์ˆ˜์—์„œ์˜ this๋Š” ๋ชจ๋ธ(User)๋ฅผ ๊ฐ€๋ฆฌํ‚ด.
UserSchema.statics.findByUsername = function (username) {
  return this.findOne({ username });
};

const User = mongoose.model('User', UserSchema);
export default User;

setPassword(password), checkPassword(password)๋Š” instance method

โ˜… ์ด๋•Œ ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ, function ํ‚ค์›Œ๋“œ๋ฅผ ๊ผญ ์‚ฌ์šฉ

→ ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ this์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ, ์—ฌ๊ธฐ์„œ this๋Š” ๋ฌธ์„œ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ฐ€๋ฆฌํ‚จ๋‹ค.

findByUsername(username) ๋Š” static method

์—ฌ๊ธฐ์„œ this๋Š” ๋ชจ๋ธ์„ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค. (User๋ฅผ ๊ฐ€๋ฆฌํ‚ด)

 

2. ํšŒ์› ์ธ์ฆ API ๋งŒ๋“ค๊ธฐ

2-1. ๋จผ์ €, ์ƒˆ๋กœ์šด ๋ผ์šฐํŠธ๋ฅผ ์ •์˜ํ•˜๊ณ ,

// api/auth/auth.ctrl.js
export const register = async (ctx) => {};

export const login = async (ctx) => {};

export const check = async (ctx) => {};

export const logout = async (ctx) => {};

2-2. auth ๋ผ์šฐํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ

// api/auth/index.js
import Router from 'koa-router';
import * as authCtrl from './auth.ctrl';

const auth = new Router();

auth.post('/register', authCtrl.register);
auth.post('/login', authCtrl.login);
auth.post('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);

export default auth;

2-3. ๊ทธ ๋‹ค์Œ auth ๋ผ์šฐํ„ฐ๋ฅผ api ๋ผ์šฐํ„ฐ์— ์ ์šฉํ•˜๊ธฐ

// api/index.js
import Router from 'koa-router';
import auth from './auth';
import posts from './posts';

const api = new Router();

api.use('/posts', posts.routes()); // posts ๋ผ์šฐํŠธ ์ ์šฉ
api.use('/auth', auth.routes());

export default api;

3. ํšŒ์› ๊ฐ€์ž… ๊ตฌํ˜„ํ•˜๊ธฐ

// api/auth/auth.ctrl.js
import Joi from '../../../node_modules/joi/lib/index';
import User from '../../models/user';
/*
    POST /api/auth/register
    {
        username:'velopert',
        password : 'mypass123'
    }
*/

export const register = async (ctx) => {
  // Request Body ๊ฒ€์ฆํ•˜๊ธฐ
  const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(20).required(),
    password: Joi.string().required(),
  });

  const result = schema.validate(ctx.request.body);

  if (result.error) {
    ctx.status = 400;
    ctx.body = result.error;
    return;
  }

  const { username, password } = ctx.request.body;

  try {
    // username์ด ์ค‘๋ณต์ธ์ง€ ํ™•์ธ
    const exists = await User.findByUsername(username);

    if (exists) {
      ctx.status = 409; // Conflict
      return;
    }
    const user = new User({
      username,
    });
    await user.setPassword(password); // ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •
    await user.save(); // DB์— ์ €์žฅ

    // ์‘๋‹ตํ•  ๋ฐ์ดํ„ฐ์—์„œ hashedPassword ํ•„๋“œ ์ œ๊ฑฐ
    ctx.body = user.serialize();
  } catch (error) {
    ctx.throw(500, error);
  }
};

export const login = async (ctx) => {};

export const check = async (ctx) => {};

export const logout = async (ctx) => {};
// models/user.js
// + ์ถ”๊ฐ€
UserSchema.methods.serialize = function () {
  const data = this.toJSON();
  delete data.hashedPassword;
  return data;
};

 

4. ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

// api/auth/auth.ctrl.js
import Joi from '../../../node_modules/joi/lib/index';
import User from '../../models/user';
/*
    POST /api/auth/register
    {
        username:'velopert',
        password : 'mypass123'
    }
*/

export const register = async (ctx) => { ... };

export const login = async (ctx) => {
  const { username, password } = ctx.request.body;

  if (!username || !password) {
    ctx.status = 401; // Unauthorized
    return;
  }
  try {
    // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ
    const user = await User.findByUsername(username);

    if (!user) {
      ctx.status = 400;
      return;
    }

    const valid = await user.checkPassword(password);

    if (!valid) {
      ctx.status = 400;
      return;
    }

    ctx.body = user.serialize();
  } catch (error) {
    ctx.throw(500, error);
  }
};

export const check = async (ctx) => {};

export const logout = async (ctx) => {};

 

5. ํ† ํฐ ๋ฐœ๊ธ‰ ๋ฐ ๊ฒ€์ฆํ•˜๊ธฐ

ํด๋ผ์ด์–ธํŠธ์—์„œ ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์ง€๋‹ˆ๊ณ  ์žˆ์„ ์ˆ˜ ์žˆ๋„๋ก ์„œ๋ฒ„์—์„œ ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๊ธฐ

jsonwebtoken : JWT ํ† ํฐ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„  ํ•ด๋‹น ๋ชจ๋“ˆ์„ ์„ค์น˜ํ•ด์•ผํ•œ๋‹ค.

yarn add jsonwebtoken

5-1. ๋น„๋ฐ€ํ‚ค ์„ค์ •ํ•˜๊ธฐ

.env ํŒŒ์ผ์— JWTํ† ํฐ์„ ๋งŒ๋“ค๋•Œ ์‚ฌ์šฉํ•  ๋น„๋ฐ€ํ‚ค๋ฅผ ๋งŒ๋“ ๋‹ค. (๋ฌธ์ž์—ด ์•„๋ฌด๊ฑฐ๋‚˜ ๊ฐ€๋Šฅ)

์ด ๋น„๋ฐ€ํ‚ค๋Š” ๋‚˜์ค‘์— JWT ํ† ํฐ์˜ ์„œ๋ช…์„ ๋งŒ๋“œ๋Š” ๊ณผ์ •์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.

๋น„๋ฐ€ํ‚ค๊ฐ€ ๊ณต๊ฐœ๋˜๋Š” ์ˆœ๊ฐ„, ๋ˆ„๊ตฌ๋“ ์ง€ ๋งˆ์Œ๋Œ€๋กœ JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ ˆ๋Œ€ ์™ธ๋ถ€์— ๊ณต๊ฐœ๋˜์„  ์•ˆ๋ฉ๋‹ˆ๋‹ค.

// .env
JWT_SECRET=//////////////////////////////////////

 

5-2. ํ† ํฐ ๋ฐœ๊ธ‰ํ•˜๊ธฐ

// models/user.js
// + ์ถ”๊ฐ€

import jwt from 'jsonwebtoken';

// ํ† ํฐ ๋ฐœ๊ธ‰ํ•˜๊ธฐ
UserSchema.methods.generateToken = function () {
  // jwt.sing(ํ† ํฐ ์•ˆ์— ์ง‘์–ด ๋„ฃ๊ณ  ์‹ถ์€ ๋ฐ์ดํ„ฐ, JWT ์•”ํ˜ธ)
  const token = jwt.sign(
    {
      _id: this.id,
      username: this.username,
    },
    process.env.JWT_SECRET,
    {
      expiresIn: '7d', // 7์ผ๋™์•ˆ ์œ ํšจ
    },
  );
  return token;
};

์ด์ œ ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์„ ๋•Œ ํ† ํฐ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•ด์ฃผ๊ฒ ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ† ํฐ์„ ์‚ฌ์šฉํ• ๋•Œ๋Š” ์ฃผ๋กœ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค.

1. ๋ธŒ๋ผ์šฐ์ €์˜ localStorage ํ˜น์€ sessionStorage์— ๋‹ด์•„์„œ ์‚ฌ์šฉํ•˜๊ธฐ

- ๋งค์šฐ ํŽธ๋ฆฌํ•˜๊ณ  ๊ตฌํ˜„ํ•˜๊ธฐ๋„ ์‰ฝ๋‹ค.

ํ•˜์ง€๋งŒ *XSS(Cross Site Scripting)๊ณต๊ฒฉ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

* XSS : ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ํŽ˜์ด์ง€์— ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค๋ฉด ์‰ฝ๊ฒŒ ํ† ํฐ์„ ํƒˆ์ทจํ•  ์ˆ˜ ์žˆ์Œ.

 

2. ๋ธŒ๋ผ์šฐ์ €์˜ cookie์— ๋‹ด์•„์„œ ์‚ฌ์šฉํ•˜๊ธฐ

์ฟ ํ‚ค๋„ XSS ๊ณต๊ฒฉ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์ง€๋งŒ, httpOnly๋ผ๋Š” ์†์„ฑ์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด ์ฟ ํ‚ค๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์—†์–ด

์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ ๋Œ€์‹  CSRF(Cross Stie Request Forgery) ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ, CSRF ํ† ํฐ ์‚ฌ์šฉ ๋ฐ Referer ๊ฒ€์ฆ ๋“ฑ์˜ ๋ฐฉ์‹์œผ๋กœ ์ด ๊ณต๊ฒฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

* CSRF : ํ† ํฐ์„ ์ฟ ํ‚ค์— ๋‹ด์œผ๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋ฒ„๋กœ ์š”์ฒญ์„  ํ•  ๋•Œ๋งˆ๋‹ค ๋ฌด์กฐ๊ฑด ํ† ํฐ์ด ํ•จ๊ป˜ ์ „๋‹ฌ๋˜๋Š” ์ ์„ ์ด์šฉํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋ฅด๊ฒŒ ์›ํ•˜์ง€ ์•Š์€ API ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค. (์‚ฌ์šฉ์ž๋„ ๋ชจ๋ฅด๋Š” ์ƒํ™ฉ์—์„œ ๊ธ€์„ ์ž‘์„ฑ, ์‚ญ์ œ, ํƒˆํ‡ดํ•˜๊ฒŒ ๋งŒ๋“ฌ)

// api/auth/auth.ctrl.js

export const register = async (ctx) => {
  ...
    // ํšŒ์›๊ฐ€์ž…์— ์„ฑ๊ณตํ–ˆ์„๋•Œ ํ† ํฐ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๊ธฐ
    const token = user.generateToken();

    ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
      httpOnly: true,
    });
  } catch (error) {
    ctx.throw(500, error);
  }
};

export const login = async (ctx) => {
	...
    // ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์„๋•Œ ํ† ํฐ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๊ธฐ
    const token = user.generateToken();

    ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
      httpOnly: true,
    });
  } catch (error) {
    ctx.throw(500, error);
  }
};

5-3. ํ† ํฐ ๊ฒ€์ฆํ•˜๊ธฐ

์‚ฌ์šฉ์ž์˜ ํ† ํฐ์„ ํ™•์ธํ•œ ํ›„ ๊ฒ€์ฆํ•˜๋Š” ์ž‘์—…์„ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•œ๋‹ค.

// lib/jwtMiddleware.js

// ์‚ฌ์šฉ์ž์˜ ํ† ํฐ์„ ํ™•์ธํ•˜ ํ›„ ๊ฒ€์ฆํ•˜๋Š” ์ž‘์—…
import jwt from 'jsonwebtoken';
import User from '../models/user';

const jwtMiddleware = async (ctx, next) => {
  const token = ctx.cookies.get('access_token');

  if (!token) return next();

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    ctx.state.user = {
      _id: decoded._id,
      username: decoded.username,
    };

    // ํ† ํฐ ๋‚จ์€ ์œ ํšจ๊ธฐ๊ฐ„์ด 3.5์ผ ๋ฏธ๋งŒ์ด๋ฉด ์žฌ๋ฐœ๊ธ‰
    const now = Math.floor(Date.now() / 1000);
    if (decoded.exp - now < 60 * 60 ** 24 * 3.5) {
      const user = await User.findById(decoded._id);
      const token = user.generateToken();
      ctx.cookies.set('access_token', token, {
        maxAge: 1000 * 60 * 60 * 24 * 7,
        httpOnly: true,
      });
    }

    //console.log(decoded);
    return next();
  } catch (error) {
    // ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ
    return next();
  }
};

export default jwtMiddleware;

๋ฏธ๋“ค์›จ์–ด ์ ์šฉํ•˜๊ธฐ

 // main.js
 ...
 
const app = new Koa();
const router = new Router();
import api from './api';
import jwtMiddleware from './lib/jwtMiddleware';

// api ๋ผ์šฐํŠธ ์ ์šฉ
router.use('/api', api.routes()); // => /api/test

// ๋ผ์šฐํ„ฐ ์ ์šฉ ์ „์— bodyparser ์‚ฌ์šฉํ•˜๊ธฐ
app.use(bodyParser());

// ๋ผ์šฐํ„ฐ ์ ์šฉ ์ „์— jwMiddleware๋ฅผ ์ ์šฉํ•˜๊ธฐ
app.use(jwtMiddleware);

// app ์ธ์Šคํ„ด์Šค์— ๋ผ์šฐํ„ฐ ์ ์šฉ
app.use(router.routes()).use(router.allowedMethods());

check ํ•จ์ˆ˜(๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ๊ธฐ๋Šฅ) ๊ตฌํ˜„

// api/auth/auth.ctrl.js

// ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ
export const check = async (ctx) => {
  const { user } = ctx.state;
  if (!user) {
    // ๋กœ๊ทธ์ธ ์ค‘ ์•„๋‹˜
    ctx.state = 401; // Unauthorized
    return;
  }
  ctx.body = user;
};

 

5-4. ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

์ฟ ํ‚ค๋ฅผ ์ง€์›Œ ์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋!

// api/auth/auth.ctrl.js

export const logout = async (ctx) => {
  ctx.cookies.set('access_token');
  ctx.status = 204; //No Content
};

 

728x90