설치

$ npm install -g swagger or sudo npm install swagger
$ swagger project create swagger
$ swagger project edit

$ swagger project create swagger

? Framework? 
  connect 
  express 
  hapi 
  restify 
❯ sails 
  • express를 선택

$ swagger project edit

  • API 문서 수정

Swagger Server 에 비즈니스 로직 추가하기

function hello(req, res) {
    var name = req.swagger.params.name.value || 'stranger';
    var hello = util.format('Hello, %s!', name);
    res.json({ "message": hello });
}

Swagger Edit에서 수정

x-swagger-router-controller에 아래와 같이 추가

    paths:
        /hello:
            x-swagger-router-controller: hello_world

이슈

설치시 이슈

  • sudo를 사용했음에도 아래와 같이 에러가 발생
  • swagger-install 을 살펴보면 npm에서 -g옵션을 사용은 설정에 따라서 사용
  • $ sudo npm install swagger 해도 실패
  • xcode install & node-gyp 을 하니 정상적으로 설치가 완료
gyp ERR! stack Error: EACCES: permission denied, mkdir '/Users/direcision/.nvm/versions/node/v12.15.0/lib/node_modules/swagger/node_modules/fsevents/.node-gyp'

Swagger 시작시에 오류

  • swagger project start를 했을때 아래와 같이 에러가 발생했다.
  • github-issue를 확인해보니 nodev12에서 생기는 에러여서 node10으로 downgrade를 하던지
  • (실패) 첫번째 방법 npm install --save swagger-router을 하라고 하는구나. 이렇게 하니까 정상적으로 실행이 되는데 음...~ 이 방법으로 하면http://127.0.0.1:10010/hello?name=Scott`으로 했을때 에러가 난다. 정상적으로 수정하는 방법이 아님. 아래 방법이 좋을듯
  • (실패) 2번째 방법 또는 nodev12에서 실행하는방법
    • swagger-express-mw의 버전이 기존에는 ^0.1.0인데 ^0.7.0으로 업데이트를 하고
      • $ npm install swagger-express-mw@0.7.0
      • swagger_params_parserswagger_controllers로 추가 (config/default.yaml에`
      • 동일하게 curl http://127.0.0.1:10010/hello?name=Scott을 실행하면 Cannot read property 에러가 발생
  • (성공) 세번째 방법으로 node_modules/bagpipes/lib/fittingTypes/user.js file을 아래와 같이 변경
    • var split = err.message.split(path.sep);
    • var split = err.message.split('\n')[0].split(path.sep);
Error: Cannot find module '/Users/direcision/Desktop/ToyProjects/web/server/swagger/api/fittings/swagger_router'
Require stack:
- /Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/fittingTypes/user.js
- /Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/bagpipes.js
- /Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/index.js
- /Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/swagger-node-runner/index.js
- /Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/swagger-express-mw/lib/index.js
- /Users/direcision/Desktop/ToyProjects/web/server/swagger/app.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:793:17)
    at Function.Module._load (internal/modules/cjs/loader.js:686:27)
    at Module.require (internal/modules/cjs/loader.js:848:19)
    at require (internal/modules/cjs/helpers.js:74:18)
    at createFitting (/Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/fittingTypes/user.js:18:20)
    at Bagpipes.newFitting (/Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/bagpipes.js:158:17)
    at Bagpipes.createFitting (/Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/bagpipes.js:147:22)
    at Bagpipes.createPipe (/Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/bagpipes.js:111:19)
    at Bagpipes.getPipe (/Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/bagpipes.js:50:38)
    at /Users/direcision/Desktop/ToyProjects/web/server/swagger/node_modules/bagpipes/lib/bagpipes.js:34:10
$ yarn add @elastic/elasticsearch

github가면 예제랑 사용방법이 다 나와있음

이 문제는 react에서 호출할때 생기는 문제로 frontend의 문제가 아닌 backend에서 처리를 해줘야 하는 에러이다.
CORS policy는 도메인이 이름이 다르거나, 뭐 포트가 다르거나 이러면 정책상 접근을 못하도록 block을 해준다.그러니 열어주면 된다.

Access to XMLHttpRequest at 'http://localhost:4000/api/aptComplex' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • cors를 설치해야하는데, express가 아닌 koa를 사용했으므로 koa에 맞는 패키지를 설치해야 한다.
  • koa-cors-github
  • 아마도 express 용으로 cors을 다운받았다면 node res.setHeader is not a function의 에러가 난다.
$ npm install @koa/cors@2 --save
const Koa = require('koa');
const cors = require('@koa/cors');

const app = new Koa();
app.use(cors());
  • 백엔드 서버를 통해 리액트 앱을 제공할 수 있도록 빌드
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 });
  }
});
  • 글 삭제/수정은 작성자만 할 수 있어야 한다.
  • 미들웨어를 수정
  • /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
};
  • 포스트 목록을 볼때 한 페이지에 보이는 포스트의 개수는 적당히 보여줘야 한다.
  • 전체 포스트를 가져온다면 network I/O가 상당할것이다
  • 포스트 내용도 동일하게 전부가 아닌 일부만 보여주는게 I/O를 줄이는데 도움이 될것!
export const list = async ctx => {
  const page = parseInt(ctx.query.page | '1', 10);

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

  try {
    const posts = await Post.find()
      .sort({ _id: -1 })
      .limit(10)
      .skip((page - 1) * 10)
      .exec();
    const postCount = await Post.countDocuments().exec();
    ctx.set('Last-Page', Math.ceil(postCount / 10));
    ctx.body = posts
  • 위에 sort는 최근에 작성한 글을 먼저 오도록
  • limit은 10개의 post만 가져오도록
  • skip은 pagination을 할때 사용
    • skip(10)은 10개 뒤로 이동
  • Last-Page의 값은 Header에 함께 포함

body에 대한 처리

    ctx.body = posts
      .map(post => post.toJSON())
      .map(post => ({
        ...post,
        body:
          post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
      }));
  • 결과로 가져온 body에서 ㅅ길이를 조절하기 위해서는 다음과 같이 body를 가져와 줄이면 된다.
  • 위 결과는 서버쪽에서 처리하는 내용, db에서도 처리가 가능할까?

+ Recent posts