• 백엔드 서버를 통해 리액트 앱을 제공할 수 있도록 빌드
yarn build
  • 서버에서 정적 파일을 제공하기 위해서는 koa-static을 설치
yarn add koa-static
  • /src/index.js에 아래와 같이 추가
import serve from 'koa-static';
import path from 'path';
import send from 'koa-send';

const buildDirectory = path.resolve(__dirname, '../../client/build');
app.use(serve(buildDirectory));
app.use(async ctx => {
  // Not Found이고, 주소가 /api로 시작하지 않는 경우
  if (ctx.status === 404 && ctx.path.indexOf('/api') !== 0) {
    // index.html 내용을 반환
    await send(ctx, 'index.html', { root: buildDirectory });
  }
});
  • .gitignore에서 분명 node_modules를 무시하라고 명시해줬는데 $ git status 를 하면 해당 변경분에 해서 표시를 해줬다.
  • 이럴때는 git의 cahce를 초기화 해주면 된다.
$ git rm -r --cached .
$ git add .
$ git commit -m "fixed untracked files"
  • 웹 크롤러가 웹페이지를 수집할때는 meta 태그를 읽는다
$ yarn add react-helmet-async
  • src/index.js파일을 열어서 HelmetProvider 컴포넌트로 App 컴포넌트를 포장
  • src/App.js 파일에 태그를 설정
  • 각각의 pagesHelmet의 태그를 추가
  • 각각의 componentsHelmet의 태그 추가

1. brew설치.

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2. 

아래 virtualbox 홈페이지에서 맥용으로 다운로드 하여 패키지 설치.
https://www.virtualbox.org/wiki/Downloads

Downloads – Oracle VM VirtualBox

Download VirtualBox Here you will find links to VirtualBox binaries and its source code. VirtualBox binaries By downloading, you agree to the terms and conditions of the respective license. If you're looking for the latest VirtualBox 6.0 packages, see Virt

www.virtualbox.org

3. minikube 설치

brew install minikube
#권한 에러시 아래 명령어로 권한 변경.
sudo chown -R [:username] /usr/....

 

5. minikube 시작

minikube start

이제 kubectl 명령어 사용 가능! 


minikube설치를 위해 하이퍼바이저가 필요해서 virtualbox를 설치한 것인데,
하이퍼바이저란, 컴퓨터에서 다수의 운영 체제를 동시에 실행하기 위한 논리적 플랫폼.

 

 

 

  • API에서 username, page, tag 값을 쿼리 값으로 넣어서 사용
  • qs 라이브러리를 사용하여 쿼리 값을 생성
  • qs를 사용하면 쿼리값을 더 편하게 JSON으 형태로 변환이 가능
$ yarn add qs

Front

src/lib/api/posts.js

export const listPosts = ({ page, username, tag }) => {
  const queryString = qs.stringify({
    page,
    username,
    tag,
  });
  return client.get(`/api/posts?${queryString}`);
};
  • 요청시에는 /api/posts?username=tester&page=2와 같이 주소를 호출
  • redux 모듈은 modules/posts.js에 생성
  • modules/index.js에 만든 리듀서와 saga를 등록
  • containers/posts/PostListContainer.js를 생성
  • 기존 pages/PostListPage.jsContainer로 변경
  • components/posts/PostList.js에 props 결과를 보여주도록 수정

Server

sanitize-html 을 추가

yarn add sanitize-html
  • 글 삭제/수정은 작성자만 할 수 있어야 한다.
  • 미들웨어를 수정
  • /src/lib/checkLoggedIn.js를 생성
  • /src/api/posts/posts.ctrl.jscheckOwnPost, getPostById를 추가
export const checkOwnPost = (ctx, next) => {
  const { user, post } = ctx.state;
  if (post.user._id.toString() !== user._id) {
    ctx.status = 403;
    return;
  }
  return next();
};

export const getPostById = async (ctx, next) => {
  const { id } = ctx.params;
  if (!ObjectId.isValid(id)) {
    ctx.status = 400; // Bad Request
    return;
  }
  try {
    const post = await Post.findById(id);
    if (!post) {
      ctx.status = 404; // Not found
      return;
    }
    ctx.state.post = post;
    return next();
  } catch (e) {
    ctx.throw(500, e);
  }
  return next();
};
  • /src/api/posts/index.js에 아래와 같이 미들웨어 추가
posts.get('/', postsCtrl.list);
posts.post('/', checkLoggedIn, postsCtrl.write);

const post = new Router(); // /api/posts/:id
post.get('/', postsCtrl.read);
post.delete('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.remove);
post.patch('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.update);

posts.use('/:id', postsCtrl.getPostById, post.routes());
export default posts;

username/tags로 포스트 필터

  • 포스트 조회, 태그 조회
/* 포스트 목록 조회
GET /api/posts
/api/posts?username=jhl
/api/posts?tag=다리디리
*/
export const list = async ctx => {
  const page = parseInt(ctx.query.page | '1', 10);

  if (page < 1) {
    ctx.status = 400;
    return;
  }

  const { tag, username } = ctx.query;
  const query = {
    // username, tag가 유효할때만 객체 안에 해당 값을 넣겠다는 의미
    ...(username ? { 'user.username': username } : {}),
    ...(tag ? { tags: tag } : {}),
  };

  try {
    const posts = await Post.find(query)
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .exec();
    const postCount = await Post.countDocuments(query).exec();
    ctx.set('Last-Page', Math.ceil(postCount / 10));
    ctx.body = posts
      .map(post => post.toJSON())
      .map(post => ({
        ...post,
        body:
          post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
      }));
  } catch (e) {
    ctx.throw(500, e);
  }
};
  • 토큰 기반 인증 구현
  • hash를 만드는 함수와 has 검증을 하는 함수
$ yarn add bcrypt

모델 메소드 만들기

  • 모델에서 사용할 수 있는 함수
  • 인스턴스 메서드 모델을 통해 만든 문서 인스턴스에서 사용할 수 있는 함수
  • static 메소드로 모델에서 바로 사용할 수 있는 함수

instance method

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

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

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; // true or false
};

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

static method

UserSchema.statics.findByUsername = function(username) {
  return this.findOne({ username });
};

회원 인증 API 만들기

src/api/auth/auth.ctrl.js

import Joi from 'joi';
import User from '../../models/user';

/*
POST /api/auth/register
{
  username: 'zzz',
  password: 'zzz'
}
*/
export const register = async ctx => {
  const schema = Joi.object().keys({
    username: Joi.string()
      .alphanum()
      .min(3)
      .max(20)
      .required(),
    password: Joi.string().required(),
  });
  const result = Joi.validate(ctx.request.body, schema);
  if (result.error) {
    ctx.status = 400;
    ctx.body = result.error;
    return;
  }
  const { username, password } = ctx.request.body;
  try {
    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();
    ctx.body = user.serialize(); // delete hashedPassword
  } catch (e) {
    ctx.throw(500, e);
  }
};

/*
POST /api/auth/login
{
  username: 'zzz'
  password: 'zzz'
}
*/
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 = 401;
      return;
    }

    const valid = await user.checkPassword(password);
    if (!valid) {
      ctx.status = 401;
      return;
    }
    ctx.body = user.serialize();
  } catch (e) {
    ctx.throw(500, e);
  }
};

export const check = async ctx => {};
export const logout = async ctx => {}; 

토큰 발급 및 검증

$ yarn add jsonwebtoken
$ openssl rand -hex 64
  • root의 .envJWT_SECRET값으로 설정
# .env
JWT_SECRET=dflkjdlkfdjflkdjflkjd
  • 위 키는 JWT 토큰의 서명을 만드는 과정에 사용되기 때문에 외부에 절대 공개되면 안됨
  • user 모델 파일에서 generateToken이라는 인스턴스 메서드 생성
UserSchema.methods.generateToken = function() {
  const token = jwt.sign(
    // 토큰안에 넣고 싶은 데이터
    {
      _id: this.id,
      username: this.username,
    },
    // JWT 암호
    process.env.JWT_SECRET,
    {
      expiresIn: '7d', // 7일동안 토큰 유효
    },
  );
  return token;
}; 
  • 회원가입과 로그인에 성공했을때 토큰을 사용자에게 전달
  • 사용자가 브라우저에서 토큰을 사용
    • localStorage, sessionStorage
      • 매우 편리하고 구현이 쉽지만, 쉽게 해킹이 가능 (XSS(Cross Site Scripting)
    • 브라우저 쿠키에 담아서 사용
      • httpOnly 속성을 활성화하면 자바스크립트등을 통해 쿠키를 조회할 수 없음
      • CSRF (Cross Site Request Forgery) 공격에 취약
      • 토큰을 쿠키에 담으면 사용자가 서버로 요청할때 무조건 토큰이 함께 전달
        • 내가 모르는 상황에서 글이 작성, 삭제, 탈퇴가 가능...
      • CSRF 토큰 사용 및 Referer 검증 등의 방식으로 제대로 막을 수 있는 반면
      • XSS는 보안장치를 적용해도 개발자가 놓칠 수 있는 다양한 취약점으로 공격 가능
    const token = user.generateToken();
    ctx.cookies.set('access_token', token, {
      maxAge: 1000 * 60 * 60 * 24 * 7, // 7days
      httpOnly: true,
    });
  • login, register에 다음과 같이 추가하면 access_token이 Header에서 확인

토큰 검증

  • 사용자의 토큰을 확인한 후 검증 작업
  • src/lib아래 다음과 같은 파일을 생성
import jwt from 'jsonwebtoken';

const jwtMiddleware = (ctx, next) => {
  const token = ctx.cookie.get('access_token');
  if (!token) return next(); // token is not exists
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    console.log(decoded);
    return next();
  } catch (e) {
    // fail token validation
    return next();
  }
};

export default jwtMiddleware;

토큰 재발급받기

  • 토큰의 만료 기간을 7일로 했는데 만료되면 재발급
  • generateToken에서 토큰 유효기간을 3일로 변경하고 아래 함수가 실행되는지 확인
    // 토큰의 남은 유효 기간이 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, // 7days
        httpOnly: true,
      });
    }

로그아웃

  • 로그아웃은 access_token을 지워주면 된다.
export const logout = async ctx => {
  ctx.cookies.set('access_token');
  ctx.status = 240; // No Content
};
  • JWT (Json Web Token)
  • 데이터가 JSON으로 이루어져 있는 토큰

세션 기반 인증 vs 토큰 기반 인증

  • 사용자의 로그인 상태를 서버에서 처리
    • 세션 기반 인증
    • 토큰 기반 인증

세션 기반 인증

  • 서버가 사용자의 로그인 상태를 기억
  • 사용자 > 로그인 > 서버 > 정보저장 > 세션 id 발급 > 요청 > 세션 조회 > 응답
  • 세션 id는 브라우저의 쿠키에 저장
  • 세션 저장소: 메모리, 디스크, 데이터베이스
  • 단점
    • 서버를 확장하기가 어려움
      • 서버의 인스턴스가 여러개가 되면 세션 공유가 필요해 별도의 데이터베이스를 만들어야 한다

토큰 기반 인증

  • 로그인 이후 서버가 만들어주는 문자열
    • 문자열에는 로그인 정보가 들어있고, 해당 정보가 서버에서 발급되었음을 증명하는 서명
  • 사용자 > 로그인 > 서버 > 토큰 발급 > 토큰과 함께 요청 > 토큰 유효성 검사 > 응답
  • 서명 데이터는 해싱 알고리즘을 통해 생성
    • HMAC SHA256, RSA, SHA256 알고리즘
  • 서버에서 만들어주기때문에 무결성이 보장
    • 정보가 변경되거나 위조되지 않음
  • API 요청할때마다 토큰을 함께 요청
  • 장점
    • 로그인 정보를 기억하기 위해 사용하는 리소스가 적다는 것
    • 확장성이 높다. (인스턴스끼리 로그인 상태를 공유할 필요가 없음)

+ Recent posts