본문 바로가기

WINK-(Web & App)/Express.js (Node.js) 스터디

[2024-2 Node.js 스터디] 류상우 #6주차

반응형

웹 API 서버 만들기

원래는 9장과 연계돼서 진행되는 분량인데 나는 9장을 따로 만들었어서 기존 서버에 10장 내용의 기능들을 추가하고, 프론트를 수정하는 방식으로 진행해보려고했다.

근데 내가 만든 SNS 서비스와 책에서 나온 예제가 너무 달라서 정상적으로 안되는 부분이 많아서... 실습은 우선 두고 개념만 정리해보았다.

 

API 서버 이해하기

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

웹 API: 서버에 API를 올려서 URL을 통해 접근할 수 있게 만든 것.

크롤링: 표면적으로 보이는 웹 사이트의 정보를 일정 주기로 수집해 자체적으로 가공하는 기술이다. 웹 사이트가 제공하는 API가 없거나 API 이용에 제한이 있을 때 사용하는 방법이다.

프로젝트 구조 갖추기

우선 users 테이블과 일대다 관계를 갖고, host, type, clientSecret 등의 컬럼을 갖는 domains 테이블이 필요하다.

type컬럼은 ENUM속성이다. ('free', 'premium') 둘 중 하나만 값으로 입력 가능하고 이를 어기면 에러가 발생한다.

클라이언트 비밀 키는 타인이 API를 사용할 때 필요한 비밀 키이다. UUID라는 랜덤한 문자열을 타입으로 가진다. 이것과 요청을 보낸 도메인이 동일해야만 요청을 보낼 수 있도록 할 것이다.

 

const { v4: uuidv4 } = require('uuid')

clientSecret의 값은 uuid 패키지를 통해 생성한다. uuid 중에서도 4 버전을 사용하며, 8-4-4-4-12 형식인 36자리(하이픈 포함) 문자열 형식으로 생겼다. 세 번째 마디의 첫 번째 숫자가 버전을 알려준다.

{ v4: uuidv4 } 는 패키지의 변수나 함수를 불러올 때 v4를 uuidv4로 바꾸는 것이다.

 

내가 사용했던 오픈 API들은 등록하면 토큰만 제공했는데 여기서는 등록할 때 도메인도 제공을 해야하고, API를 신청한 도메인에서만 사용할 수 있도록 하고있다.

 

웹 브라우저에서 요청을 보낼 때, 응답을 하는 곳과 도메인이 다르면 CORS 에러가 발생할 수 있다. 이 문제를 해결하려면 API 서버 쪽에서 미리 허용할 도메인을 등록해야 한다. 딱히 도메인을 등록할 필요가 없는 API들은 이 CORS를 모든 출처에서 허용하도록 두었기 때문이었다.

 

또한 무료와 프리미엄을 구분했는데 이는 나중에 사용량 제한을 구현하기 위한 값이다.

JWT 토큰으로 인증하기

데이터를 가져갈 수 있게 하는 만큼 별도의 인증 과정이 필요한데, 이 책에서는 이를 JWT 토큰을 사용해 인증하는 방법을 사용한다.

JWT는 JSON Web Token의 약어로, JSON 형식의 데이터를 저장하는 토큰이다. JWT는 다음 세 부분으로 구성된다.

  • 헤더: 토큰 종류와 해시 알고리즘 정보
  • 페이로드: 토큰의 내용물이 인코딩된 부분
  • 시그니처: 토큰이 변조되었는지 여부를 확인할 수 있는 일련의 문자열

이 중 시그니처는 JWT 비밀 키로 만들어진다. 이 비밀 키가 노출되면 JWT 토큰을 위조할 수 있으므로 비밀 키는 숨겨야 하지만, 시그니처 자체는 숨기지 않아도 된다.

https://jwt.io 에서 JWT토큰의 내용을 확인할 수 있다.

그런데, 사진을 보면 페이로드 부분이 모두 노출되어 있다. 그럼 이렇게 내용이 노출되어있는 토큰을 왜 사용할까? 바로 내용이 들어있기 때문이다. 만약 내용이 없는 랜덤한 토큰이라면 해당 토큰의 주인, 권한 등을 모든 요청마다 체크해야한다.

 

JWT 토큰은 JWT 비밀 키를 알지 않으면 변조가 불가능하다. 만약 변조했더라고 시그니처를 통해 들통난다. 변조가 불가능한 믿을 수 있는 정보이므로 DB를 조회하지 않고도 권한을 내어줄 수 있다.

 

JWT 토큰의 단점은 내용물이 들어 있어 용량이 크다는 것이다. 요청 때마다 토큰이 오가며 데이터양이 증가한다. 이렇게 장단점이 뚜렷하므로 상황에 따라 적절히 사용해야한다.

res.locals.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);

jwt.verify 메서드로 토큰을 검증할 수 있다. 첫 번째 인수에 토큰을, 두 번째 인수에 토큰의 비밀 키를 넣는다.

 

책의 예제를 보면 라우터의 버전을 지정해주었다. 이는 라우터를 수정하면 API를 사용하는 프로그램들이 오작동할 수 있기 때문에 버전을 나누어 둔 것인데, 예전에 API가 수정되어 만들었던 프로그램이 망가진 경험이 있기 때문에 해당 API도 이러한 작업을 해줬으면 어땠을까 하는 생각이 들었다.

 

const token = jwt.sign({
    id: domain.user.id,
    nick: domain.user.nick,
}, process.env.JWT_SECRET, {
    expiresIn: '1m', //유효기간
    issuer: 'nodebird' //발급자
});

jwt.sing 메서드로 토큰을 발급받을 수 있다. 펏 번째 인수는 토큰의 내용, 두 번째 인수는 토큰의 비밀 키, 세 번째 인수는 토큰의 설정이다.

const { verifyToken } = require('../middlewares');

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

발급받은 토큰을 테스트해볼 수 있는 라우터이다. 토큰을 검증하는 미들웨어를 거친 후, 검증이 성공하면 토큰의 내용물을 응답한다.

다른 서비스에서 호출하기

const tokenResult = await axios.post('http://localhost:8002/v1/token', {
	clientSecret: process.env.CLIENT_SECRET,
}

const result = await axios.get('http://localhost:8002/v1/test', {
	headers: { authorization: req.session.jwt },
}

코드는 생략됐지만 토큰이 성공적으로 발급되었으면 토큰을 세션에 저장하고 그 토큰으로 API에 접근한다.

보통 토큰은 headers의 authorization에 넣어 사용한다.

SNS API 서버 만들기

나머지 API 라우터를 완성하고 프론트에 구현하는 부분인데, 계속 해왔던 부분이고 지금 실습을 진행하지 않고 있어서 임의로 생략하였다.

사용량 제한 구현하기

API를 과도하게 호출하면 서버에 무리가 올 수도 있다. 이를 방지하기 위해 일정 기간 내에 API를 사용할 수 있는 횟수를 제한하는 것이 좋다. 예제에서는 free와 premium을 구분해 횟수를 제한할 것이다.

 

이러한 기능을 제공하는 패키지인 express-rate-limit가 있다.

npm install express-rate-limit
const rateLimit = require('express-rate-limit')

exports.apiLimiter = rateLimit({
    windowMs: 60 * 1000, //1분 ms로 표현
    max: 1,
    handler(req, res) {
    	res.status(this.statusCode).json({
            code: this.statusCode,
            mseeage: '1분에 한 번만 요청할 수 있습니다.',
        });
    },
});

이 apiLimiter 미들웨어를 라우터에 넣으면 라우터에 사용량 제한이 걸린다. 이 미들웨어의 옵션으로는 wundowMs(기준 시간), max(허용 횟수), handler(제한 초과 시 콜백 함수) 등이 있다.

 

현재는 서버를 재시작하면 사용량이 초기화되므로 래디스같은 데이터베이스에 사용량을 따로 저장하는 것이 좋다. 다만 express-rate-limit는 데이터베이스와 연결하는 것을 지원하지 않는다. ioredis라는 패키지로 따로 설정이 가능한 것 같다.

CORS 이해하기

윙크 자체 프로젝트를 할 때나 저번 테크톡 때 주제로 나왔어서 CORS 자체는 익숙하지만 직접 오류를 해결해보거나 한 적은 없어서 어떨지 궁금했었다.

 

서버에서 서버로 API를 호출하는 것은 문제가 없지만, 앞에서 말했듯이 클라이언트에서 요청하면 에러가 발생한다.

Access-Control-Allow-Origin이라는 헤더가 없다는 내용의 에러이다.

 

CORS 문제를 해결하려면 응답 헤더에 Access-Control-Allow-Origin 이라는 헤더를 넣어야 한다. res.set 메서드로 직접 넣어도 되지만 cors패키지를 설치하면 더 편하게 할 수 있다.

const cors = require('cors');

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

credentials 옵션을 활성화하면 다른 도메인 간에 쿠키가 공유된다. 프론트와 서버의 도메인이 다른 경우에 이 옵션을 활성화하지 않으면 로그인이 되지 않을 수도 있다. 쿠키를 공유해야하는 경우 axios에서도 withCredentials 옵션을 true로 설정해 요청을 보내야한다.

 

하지만 이렇게 작성하는 경우, 요청을 보내는 주체가 클라이언트라서 비밀 키가 모두에게 노출된다.

이 문제를 막기 위해 비밀 키 발급 시 도메인을 작성하게 하였다. 호스트와 비밀 키가 모두 일치할 때만 CORS를 허용하게 수정하면 된다.

또한 origin 속성을 추가하여 허용할 도메인만 따로 적으면 된다. *처럼 모든 도메인을 허용하는 대신 입한 도메인만 허용한다. 여러 개의 도메인을 허용하고 싶다면 배열을 사용하면 된다.

 

cors 미들웨어 사용 방식에는 특이한 점이 있다. (req, res, next) 인수를 직접 줘서 호출하는 것이다. 이는 미들웨어의 작동 방식을 커스터마이징하고 싶을 때 사용하는 방법이다. 다음 두 코드는 같은 역할을 한다.

router.use(cors())

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

 

반응형