├── .gitignore
├── LICENSE
├── README.md
├── dummy
├── data.js
└── fetch.js
├── functions
├── dummy.js
└── youtube.js
├── netlify.toml
├── package.json
├── utils
└── stringify.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .netlify
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Danny Kim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hide-api-key-with-serverless-functions
2 |
3 | [](https://app.netlify.com/sites/bigsaigon333/deploys)
4 |
5 | ### 이 레포지토리는 Client-Side 에서 API Key를 노출하지 않고 Youtube API와 통신하기 위한 redirect server 입니다.
6 |
7 | serveless functions 중 Netlify Functions를 활용하였습니다. 따라서 netlify 계정이 반드시 필요합니다. (netlify sign up은 무료입니다)
8 |
9 |
10 |
11 | ### 1. 설정방법
12 |
13 | 1. **repository를 fork 합니다.**
14 |
15 | 2. **netlify 에 github repository를 등록합니다.**
16 |
17 | - [Netlify signup 링크](https://app.netlify.com/signup)
18 | - fork한 repository 와 main 브랜치를 선택하며, 이외의 설정(Build Command, Publish Directory)은 공란으로 두세요.
19 | (Functions directory 설정은 netlify.toml에서 하고 있으므로 신경쓰지 않아도 됩니다.)
20 |
21 | 3. **netlify 에 환경변수를 설정합니다.**
22 |
23 | 아래의 환경변수는 반드시 설정해야 합니다. 또한 환경변수를 설정한 후에는 반드시 deploy를 하여야 합니다. 새로이 deploy한 후에 변경된 환경변수가 적용됩니다.
24 |
25 | - API_KEY: Youtube API Key
26 |
27 | - HOST: CORS 를 위한 Origin으로, Response 헤더의 Access-Control-Allow-Origin: HOST 로 설정됩니다.
28 |
29 | ```
30 | 예시) *, https://bigsaigon333.github.io, http://localhost:5500, http://127.0.0.1:5500
31 |
32 | ※ HOST는 하나만 설정할 수 있습니다. 따라서, 두군데 이상을 설정하고자 하는 경우에는 * 로 하여야 합니다.
33 | ```
34 |
35 |
36 | ⇒ 이로써 설정을 모두 마쳤습니다. 환경변수 설정후 deploy하는거 꼭 잊지 마세요!
37 |
38 |
39 |
40 | ### 2. Client-Side 사용법
41 |
42 | 기존에는 Youtube API Endpoint 인 `https://www.googleapis.com/youtube/v3/search` 으로 직접 통신하였다면 이제부터는 방금 만든 Netlify Functions의 Endpoint와 통신하시면 됩니다.
43 |
44 | ```
45 | // 기존
46 | https://www.googleapis.com/youtube/v3/search
47 |
48 | // Endpoint
49 | https://my-netlify-site-name.netlify.app/youtube/v3/search
50 |
51 | // 🌟New Feature: dummy data를 반환하는 Endpoint🌟
52 | https://my-netlify-site-name.netlify.app/dummy/youtube/v3/search
53 |
54 | ```
55 |
56 |
57 |
58 | **구체적인 Client-Side 사용법 예시**
59 |
60 | ```jsx
61 | try {
62 | const ORIGINAL_HOST = "https://www.googleapis.com"; // 기존 유튜브 API 호스트
63 | const REDIRECT_SERVER_HOST = "https://bigsaigon333.netlify.app"; // my own redirect server hostname
64 |
65 | const url = new URL("youtube/search", REDIRECT_SERVER_HOST);
66 | const parameters = new URLSearchParams({
67 | part: "snippet",
68 | type: "video",
69 | maxResults: 10,
70 | regionCode: "kr",
71 | safeSearch: "strict",
72 | pageToken: nextPageToken || "",
73 | q: query,
74 | // key: "Abdsklfulasdkf-d0f9" // key를 절대로 포함해서 보내지 마세요!
75 | });
76 | url.search = parameters.toString();
77 |
78 | const response = await fetch(url, { method: "GET" });
79 | const body = await response.json();
80 |
81 | if (!response.ok) {
82 | throw new Error(body.error.message); // <-- 이렇게 하시면 디버깅하실때 매우 편합니다.
83 | }
84 |
85 | // write a code below that you want to do here!
86 | } catch (error) {
87 | console.error(error);
88 | }
89 | ```
90 |
91 |
92 |
93 | ### 🔥 주의사항 🔥
94 |
95 | Netlify Functions의 무료사용량 한도는 아래와 같습니다. Netlify 설정에서 확인할 수 있습니다.
96 |
97 | - 1달간 125,000 Request
98 |
99 | - 1달간 functions run time 100시간
100 |
101 |
102 |
103 | ### 🌟 New Feature: Dummy Data를 반환하는 Endpoint 추가 🌟
104 |
105 | Youtube API는 일일 제한량이 있으므로 이를 초과하여 사용한 경우에 403 Error를 보냅니다.
106 |
107 | Youtube API를 사용하지 않아 제한량에 영향을 주지 않고, 서버에 저장되어 있는 Dummy Data를 랜덤하게 반환하는 Endpoint를 추가하였습니다.
108 | (현재 33종류의 데이터를 랜덤으로 반환합니다. )
109 |
110 | ```
111 | // 🌟 New Feature: dummy data를 반환하는 Endpoint 🌟
112 | https://my-netlify-site-name.netlify.app/dummy/youtube/v3
113 | ```
114 |
115 |
116 |
117 | ### 🛠 Fix: Fetch Error 메세지 속 API_KEY를 그대로 반환하는 에러 수정
118 |
119 | zych1751 님께서 유튜브 에러를 그대로 내려줄 경우 에러 메세지 속에 API_KEY가 포함되어 있어 API_KEY가 노출될 수 있는 점을 지적해주셨습니다.
120 |
121 | 이에 JSON.stringify의 replacer로 API_KEY를 모두 빈 문자열("")으로 치환하는 함수를 전달하여, response의 body 내 API_KEY가 절대 포함되지 않도록 수정하였습니다.
122 |
123 | ```javascript
124 | // stringify.js
125 | const keyReplacer = (_, value) => {
126 | if (typeof value !== "string") {
127 | return value;
128 | }
129 |
130 | return value.replace(process.env.API_KEY, "");
131 | };
132 |
133 | const stringify = (subject) => JSON.stringify(subject, keyReplacer, " ");
134 |
135 | module.exports = stringify;
136 |
137 | // youtube.js
138 | const stringify = require("../utils/stringify.js");
139 | ...
140 | try {
141 | const response = await fetch(url);
142 | const body = await response.json();
143 |
144 | if (body.error) {
145 | return {
146 | statusCode: body.error.code,
147 | ok: false,
148 | headers,
149 | body: stringify(body),
150 | };
151 | }
152 |
153 | return {
154 | statusCode: 200,
155 | ok: true,
156 | headers,
157 | body: stringify(body),
158 | };
159 | } catch (error) {
160 | return {
161 | statusCode: 400,
162 | ok: false,
163 | headers,
164 | body: stringify(error),
165 | };
166 | }
167 | ```
168 |
169 |
170 |
171 | 더욱 상세한 설명은 아래의 블로그를 참고해주세요~!
172 |
173 | - [bigsaigon333 - Client-Side에서 Youtube API Key 숨기기](https://velog.io/@bigsaigon333/Client-Side%EC%97%90%EC%84%9C-Youtube-API-Key-%EC%88%A8%EA%B8%B0%EA%B8%B0)
174 |
175 | - [365kim - 쉽게 쓰인 유튜브 API 튜토리얼](https://365kim.tistory.com/93)
176 |
--------------------------------------------------------------------------------
/dummy/fetch.js:
--------------------------------------------------------------------------------
1 | const data = require("./data.js");
2 |
3 | module.exports = () => {
4 | const randomIndex = Math.floor(Math.random() * data.length);
5 |
6 | return Promise.resolve(data[randomIndex]);
7 | };
8 |
--------------------------------------------------------------------------------
/functions/dummy.js:
--------------------------------------------------------------------------------
1 | const fetchDummySearchData = require("../dummy/fetch.js");
2 | const stringify = require("../utils/stringify.js");
3 |
4 | exports.handler = async () => {
5 | const headers = {
6 | "Access-Control-Allow-Origin": process.env.HOST,
7 | Vary: "Origin",
8 | };
9 |
10 | try {
11 | const body = await fetchDummySearchData();
12 |
13 | return {
14 | statusCode: 200,
15 | ok: true,
16 | headers,
17 | body: stringify(body),
18 | };
19 | } catch (error) {
20 | return {
21 | statusCode: 400,
22 | ok: false,
23 | headers,
24 | body: stringify(error),
25 | };
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/functions/youtube.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const querystring = require("querystring");
3 | const stringify = require("../utils/stringify.js");
4 |
5 | const GOOGLEAPIS_ORIGIN = "https://www.googleapis.com";
6 | const headers = {
7 | "Access-Control-Allow-Origin": process.env.HOST,
8 | "Content-Type": "application/json; charset=utf-8",
9 | };
10 |
11 | exports.handler = async (event) => {
12 | const {
13 | path,
14 | queryStringParameters,
15 | headers: { referer },
16 | } = event;
17 |
18 | const url = new URL(path, GOOGLEAPIS_ORIGIN);
19 | const parameters = querystring.stringify({
20 | ...queryStringParameters,
21 | key: process.env.API_KEY,
22 | });
23 |
24 | url.search = parameters;
25 |
26 | try {
27 | const response = await fetch(url, { headers: { referer } });
28 | const body = await response.json();
29 |
30 | if (body.error) {
31 | return {
32 | statusCode: body.error.code,
33 | ok: false,
34 | headers,
35 | body: stringify(body),
36 | };
37 | }
38 |
39 | return {
40 | statusCode: 200,
41 | ok: true,
42 | headers,
43 | body: stringify(body),
44 | };
45 | } catch (error) {
46 | return {
47 | statusCode: 400,
48 | ok: false,
49 | headers,
50 | body: stringify(error),
51 | };
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | functions = "./functions"
3 |
4 | [[redirects]]
5 | from = "dummy/youtube/v3/*"
6 | to = "/.netlify/functions/dummy/:splat"
7 | status = 200
8 | force = true
9 |
10 | [[redirects]]
11 | from = "/youtube/v3/*"
12 | to = "/.netlify/functions/youtube/:splat"
13 | status = 200
14 | force = true
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hide-api-key-with-serverless-functions",
3 | "version": "0.1.2",
4 | "main": "index.js",
5 | "repository": "https://github.com/bigsaigon333/hide-api-key-with-serverless-functions.git",
6 | "author": "bigsaigon333 ",
7 | "license": "MIT",
8 | "dependencies": {
9 | "node-fetch": "^2.6.1",
10 | "querystring": "^0.2.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/utils/stringify.js:
--------------------------------------------------------------------------------
1 | const keyReplacer = (_, value) => {
2 | if (typeof value !== "string") {
3 | return value;
4 | }
5 |
6 | return value.replace(process.env.API_KEY, "");
7 | };
8 |
9 | const stringify = (subject) => JSON.stringify(subject, keyReplacer, " ");
10 |
11 | module.exports = stringify;
12 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | node-fetch@^2.6.1:
6 | version "2.6.1"
7 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
8 | integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
9 |
10 | querystring@^0.2.1:
11 | version "0.2.1"
12 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
13 | integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==
14 |
--------------------------------------------------------------------------------