├── .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 | [![Netlify Status](https://api.netlify.com/api/v1/badges/3c98c36b-95c2-4c8d-a3d9-ab5f9342c128/deploy-status)](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 | --------------------------------------------------------------------------------