티스토리 뷰

책/nodejs 교과서

10. API 서버 이해하기

안양사람 2021. 2. 27. 01:19
728x90
SMALL

이 장에서는 NodeBird 앱의 REST API 서버를 만든다. API 서버는 프런트엔드와 분리되어 운영되므로 모바일 서버로도 사용할 수 있다. 노드를 모바일 서버로 사용하려면 이번 장과 같이 서버를 REST API 구조로 구성하면 된다. 특히 JWT 토큰은 모바일 앱과 노드 서버 간에 사용자 인증을 구현할 때 자주 사용된다.

 

API(Application Programming Interface) : 다른 애플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점

웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 창구.

위와 같은 서버에 API를 올려서 URL을 통해 접근할 수 있게 만든 것을 웹 API 서버라고 한다.

 

크롤링 : 웹 사이트가 자체적으로 제공하는 API가 없거나 API 이용에 제한이 있을 때 사용하는 방법

표면적으로 보이는 웹 사이트의 정보를 일정 주기로 수집해 자체적으로 가공하는 기술. 하지만 웹 사이트에서 직접 제공하는 API가 아니므로 원하는 정보를 얻지 못할 가능성이 있다. 또한, 웹 사이트에서 제공하길 원치 않는 정보를 수집한다면 법적인 문제가 발생할 수도 있다. 서비스 제공자 입장에서도 주기적으로 크롤링을 당하면 웹 서버의 트래픽이 증가하므로 공개해도 되는 정보들은 API로 만들어 API를 통해 가져가게 하는 것이 좋다.

 

        type: {
          type: Sequelize.ENUM('free', 'premium'),
          allowNull: false,
        },
        clientSecret: {
          type: Sequelize.UUID,
          allowNull: false,
        },

ENUM은 넣을 수 있는 값을 제한하는 데이터 형식.

UUID는 충돌 가능성이 매우 적은 랜덤한 문자열

 

JWT 토큰

JSON Web Token : JSON 형식의 데이터를 저장하는 토큰

헤더 : 토큰 종류와 해시 알고맂므 정보가 들어 있다.

페이로드 : 토큰의 내용물이 인코딩된 부분

시그니처 : 일련의 문자열, 시그니처를 통해 토큰이 변조되었는지 여부를 확인

 

JWT 토큰은 비밀키를 알지 않는 이상 변조가 불가능하다. 즉, 사용자 이름, 권한 같은 것을 넣어두고 사용할 수 있다. 단, 외부에 노출되어도 좋은 정보만. 이러면 DB조회없이 권한 주는것가능

단점은 용량이 크다는 것이다. 매 요청 시 토큰이 오고 가서 데이터양이 증가한다.

 

.env

JWT_SECRET=jwtSecret

 

middlewares.js

exports.verifyToken = (req, res, next) => {
  try {
    req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
    return next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      // 유효 기간 초과
      return res.status(419).json({
        code: 419,
        message: '토큰이 만료되었습니다.',
      });
    }
    return res.status(401).json({
      code: 401,
      message: '유효하지 않은 토큰입니다.',
    });
  }
};

 

verify 첫번째 인수는 토큰, 두번째 인수는 토큰의 비밀키

인증에 성공한다면 req.decoded에 저장.

 

v1.js

const express = require('express');
const jwt = require('jsonwebtoken');

const { verifyToken } = require('./middlewares');
const { Domain, User } = require('../models');

const router = express.Router();

router.post('/token', async (req, res) => {
  const { clientSecret } = req.body;
  try {
    const domain = await Domain.findOne({
      where: { clientSecret },
      include: {
        model: User,
        attribute: ['nick', 'id'],
      },
    });
    if (!domain) {
      return res.status(401).json({
        code: 401,
        message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
      });
    }
    const token = jwt.sign(
      {
        id: domain.User.id,
        nick: domain.User.nick,
      },
      process.env.JWT_SECRET,
      {
        expiresIn: '1m', // 1분
        issuer: 'nodebird',
      }
    );
    return res.json({
      code: 200,
      message: '토큰이 발급되었습니다.',
      token,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 1500,
      message: '서버 에러',
    });
  }
});

router.get('/test', verifyToken, (req, res) => {
  res.json(req.decoded);
});

module.exports = router;

 

sign 메서드의 첫번째 인수는 토큰의 내용

두번째 인수는 토큰의 비밀키

세번째 인수는 토큰의 설정

 

* JWT 토큰 로그인

최근에는 JWT 토큰으로 로그인하는 방법이 많아지고 있다. 로그인 완료 시 세션에 데이터를 저장하고 세션 쿠키를 발급하는 대신 JWT 토큰을 쿠키로 발급하면 된다.

다음과 같이 authenticcate 메서드의 두번째 인수로 옵션을 주면 세션을 사용하지 않을 수 있다.

router.post('/login', isNotLoggedIn, (req,res,next)=>{
  passport.authenticate('local', {session:false}, (authError,user,info)=>{
    if(authError){

 

세션에 데이터를 저장하지 않으므로 serializeUser와 deserializeUser는 사용하지 않는다. 그 후 모든 라우터에 verifyToekn 미들웨어를 넣어 클라이언트에서 쿠키를 검사한 후 토큰이 유효하면 라우터로 넘어가고, 그렇지 않으면 401이나 419 에러를 응답하면 된다.

사용자 권한 확인을 위해 데이터베이스를 사용하지 않으므로 서비스의 규모가 클수록 데이터베이스의 부담을 줄일 수 있다.

 

nodecat

const express = require('express');
const axios = require('axios');

const router = express.Router();

router.get('/test', async (req, res, next) => {
  // 토큰 테스트 라우터
  try {
    if (!req.session.jwt) {
      // 세션에 토큰이 없으면 토큰 발급 시도
      const tokenResult = await axios.post('http://localhost:8002/v1/token', {
        clientSecret: process.env.CLIENT_SECRET,
      });
      if (tokenResult.data && tokenResult.data.code === 200) {
        // 토큰 발급 성공
        req.session.jwt = tokenResult.data.token; // 세션에 토큰 저장
      } else {
        // 토큰 발급 실패
        return res.json(tokenResult.data); // 발급 실패 사유 응답
      }
    }
    // 발급받은 토큰 테스트
    const result = await axios.get('http://localhost:8002/v1/test', {
      headers: { authorization: req.session.jwt },
    });
    return res.json(result.data);
  } catch (error) {
    console.error(error);
    if (error.response.status === 419) {
      // 토큰 만료 시
      return res.json(error.response.data);
    }
    return next(error);
  }
});

module.exports = router;

 

4000/test

// 20210226234930
// http://localhost:4000/test

{
  "id": 2,
  "nick": "123",
  "iat": 1614350970,
  "exp": 1614351030,
  "iss": "nodebird"
}

 

사용량 제한하기

npm i express-rate-limit

const RateLimit = require('express-rate-limit');

exports.apiLimiter = new RateLimit({
  windowMs: 60 * 1000, // 1분
  max: 1,
  delayMs: 0,
  handler(req, res) {
    res.status(this.statusCode).json({
      code: this.statusCode, // 기본값 429
      message: '1분에 한 번만 요청할 수 있습니다.',
    });
  },
});

exports.deprecated = (req, res) => {
  res.status(410).json({
    code: 410,
    message: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
  });
};

이 미들웨어의 옵션으로는 windowMs(기준 시간),  max(허용 횟수), delayMs(호출 간격), handler(제한 초과 시 콜백 함수) 등이 있다.

 

사용량 제한이 추가되었으므로 기존 API 버전과 호환X

새로운 v2라우터를 만들어야 해

v1으로 접속하면 deprecated use해서 알려주고

 

현재는  nodebird-api 서버가 재시작되면 사용량이 초기화되므로 실제 서비스에서 사용량을 저장할 데이터베이스를 따로 마련하는 것이 좋다. 보통 레디스가 많이 사용. 단, express-rate-limit은 데이터베이스와 연결되지 않음

 

CORS

routes/index.js

router.get('/', (req, res) => {
  res.render('main', { key: process.env.CLIENT_SECRET });
});

views/main.html

<!DOCTYPE html>
<html>
  <head>
    <title>프론트 API 요청</title>
  </head>
  <body>
  <div id="result"></div>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script>
    axios.post('http://localhost:8002/v2/token', {
      clientSecret: '{{key}}',
    })
      .then((res) => {
        document.querySelector('#result').textContent = JSON.stringify(res.data);
      })
      .catch((err) => {
        console.error(err);
      });
  </script>
  </body>
</html>

 

실제 서비스에서는 서버에서 사용하는 비밀키와 프런트에서 사용하는 비밀키를 따로 두는 게 좋다.

NodeCat의 프런트에서 nodebird-api의 서버 API를 호출하면 오류가 뜬다.

Access-Control-Allow-Origin이라는 헤더가 없다는 내용의 에러이다. 이처럼 브라우저와 서버의 도메인이 일치하지 않으면 기본적으로 요청이 차단된다. 이 현상은 브라우저에서 서버로 요청을 보낼때만 발생한다.

 

nodebird-api에서 npm i cors

v2.js

const cors = require('cors');
const router = express.Router();

router.use(
  cors({
    credentials: true,
  })
);

 

네트워크에서 token을 보면 Access-Control-Allow-Origin:*

*은 모든 클라이언트의 요청을 허용한다는 뜻

이러면 비밀키가 모두에게 요청돼

수정

router.use(async (req, res, next) => {
  const domain = await Domain.findOne({
    where: { host: url.parse(req.get('origin')).host },
  });
  if (domain) {
    cors({
      origin: req.get('origin'),
      credentials: true,
    })(req, res, next);
  } else {
    next();
  }
});

    req.get('origin'),    
    http://localhost:4000
    url.parse(req.get('origin')),
    Url {
  	protocol: 'http:',
  	slashes: true,
  	auth: null,
 	 host: 'localhost:4000',
  	port: '4000',
  	hostname: 'localhost',
 	 hash: null,
     search: null,
 	 query: null,
 	 pathname: '/',
   	 path: '/',
 	 href: 'http://localhost:4000/'
	}
    url.parse(req.get('origin')).host
    localhost:4000
    

 

 

cors 미들웨어에 옵션 인수를 주었는데 origin 속성에 허용할 도메인만 따로 적으면 된다.

여러 개의 도메인을 허용하고 싶다면 배열을 사용

또 cors 미들웨어에도 (req,res,next) 인수를 줘서 호출했다. 이는 미들웨어의 작동 방식을 커스터마이징하고 싶을 때 사용하는 방법(passport에서도 사용했어) 아래 코드 같다는걸 기억하자

router.use(cors());

rou0ter.use((req,res,next)=>{
  cors()(req,res,next);
});

 

현재 클라이언트와 서버에서 같은 비밀키를 써서 문제가 될 수 있어. 카카오처럼 환경별로 키를 구분하자

 

* 프록시 서버

CORS 문제를 해결하는 또 다른 방법.

서버에서 서버로 요청할때는 CORS 문제가 발생하지 않는다는 것을 이용

 

추천할것

팔로워나 팔로잉 목록을 가져오는 api 만들기(nodebird-api에 새로운 라우터 추가)

무료 도메인과 프리미엄 도메인 간에 사용량 제한을 다르게 적용하기(apiLimiter를 두 개 만들어서 도메인별로 다르게 적용)

클라이언트용 비밀 키와 서버용 비밀 키를 구분해서 발급하기(domain 모델 수정)

클라이언트를 위해 api 문서 작성하기(swagger나 apidoc 사용)

 

728x90
LIST

' > nodejs 교과서' 카테고리의 다른 글

12. 웹 소켓으로 실시간 데이터 전송하기  (0) 2021.03.04
11. 노드 서비스 테스트하기  (0) 2021.03.02
9. 익스프레스로 SNS 서비스 만들기  (1) 2021.02.25
8. 몽고디비  (0) 2021.02.24
7. MYSQL  (0) 2021.02.22
댓글
공지사항