본문 바로가기

[IT/Programming]

Sign-up/Log-in with Google/Kakao (구글/카카오로 가입하기/로그인 구현하기): 구글 Google, 카카오 Kakao Oauth

728x90
반응형
# Sign-up/Log-in with Google/Kakao (구글/카카오로 가입하기/로그인 구현하기): 구글 Google, 카카오 Kakao Oauth 구글로 가입하기랑 로그인하기를 어떻게 구현할 수 있는지 알아봅시다. 작성중... ## PH
  • 2024-09-30 : First posting.
## TOC ## 구글로 가입하기 ### 우선 console 로 가서 Oauth 를 활성화시켜 줍시다. (Google Developers)
https://console.cloud.google.com/ 로 가서 My First Project 링크로 들어갑시다.
왼쪽 사이드바에서 APIs & Services > Credentials 를 클릭. OAuth 2.0 Client IDs 를 하나 발급 받읍시다. 여기서 Authorized JavaScript origins 을 자신이 Hosting 하는 서버 이름으로 설정해 두고, Authorized redirect URIs 를 인증을 위해 사용되는 자신의 URL 을 적어줍니다. Client ID 와 Client secret 은 안전한 저장소에 잘 넣어둡시다. 다시 왼쪽 사이드바의 OAuth consent screen 에 접속해서 여러가지 설정들을 마무리 해 줍시다. ### 구글로 로그인/가입하기 창 띄우기 ```[.linenums] m.state = m.generateRandomHexString(); // Google's OAuth 2.0 endpoint for requesting an access token m.oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth'; // Parameters to pass to OAuth 2.0 endpoint. m.params = { client_id: 'id', // 발급받은 client_id redirect_uri: 'redirect_uri /log-in-with-google', // 설정해놓은 redirect_uri response_type: 'token', scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile', // scope:'email profile', state: m.state, prompt: "consent" }; m.paramsSearch = ""; // Add form parameters as hidden input values. for (let p in m.params) { m.paramsSearch += `&${p}=${encodeURIComponent(m.params[p])}`; } m.paramsSearch = "?" + m.paramsSearch.substring(1); m.authGoogleURL = m.oauth2Endpoint + m.paramsSearch; m.logInWithGoogle = function () { setLogInDisabled(true); if (userId === "") { $.ajax({ type: "POST", url: "/account/log-in/with/pre-google", data: `state\tgoto\tid\n` + `${m.state}\t${m.searchVars.goto?.val ? m.searchVars.goto.val : ""}\t${userId}` // Decoded goto. , dataType: "text" // 정말 구글에서 온 요청인지 판별하기 위해 state 값을 미리 server 에 ip 와 같이 저장해놓기. // goto 는 login 후 redirect 될 주소를 받아 놓은 것. // 여기는 구글로 로그인 부분이라 userId 는 empty. }).done(function (resp) { console.log(resp); setLogInErrs((errs) => [...errs, resp]); window.open(m.authGoogleURL, "_blank"); setLogInDisabled(false); }); } else if (m.regExId.test(userId)) { setLogInErrs((errs) => [...errs, "[--Checking ID and E-mail--]: [--Please wait.--]"]); $.ajax("/account/check", { type: "POST" , data: userId + "\tAnonymous@Recoeve.net" // 구글로 가입하기 부분. 우선 입력받은 id 가 available 한지 체크. }).fail(function () { setLogInErrs((errs) => [...errs, "[--Request time out.--] [--Please click the Sign-up button again.--]"]); setLogInDisabled(false); }).done(function (resp) { // resp: idAvailable emailAvailable tToken authToken(char(64)) let res = resp.split('\t'); if (res && res[0] === "true") { setLogInErrs((errs) => [...errs, "[--ID is available.--]"]); setTimeout(function () { $.ajax({ type: "POST", url: "/account/log-in/with/pre-google", data: `state\tgoto\tid\n${m.state}\t${m.searchVars.goto?.val ? m.searchVars.goto.val : ""}\t${userId}` // Decoded goto. , dataType: "text" // state 값과 ip 값을 서버에 저장해 놓는다. (ip 는 server 단에서 확인해서 값을 집어넣음.) }).done(function (resp) { console.log(resp); setLogInErrs((errs) => [...errs, resp]); window.open(m.authGoogleURL, "_blank"); // 구글 로그인/가입하기 창 띄우기 setLogInDisabled(false); }); }, m.wait); } else { setLogInErrs((errs) => [...errs, `[--ID--] '${userId}' [--is already in use. Please try another ID.--]`]); setLogInDisabled(false); } }); } else { setLogInErrs((errs) => [...errs, `[--ID is in invalid form.--]`]); setLogInDisabled(false); } }; ```/ 버튼은 다음과 같이 ```[.linenums] <button id="log-in-with-google" className="button" onClick={m.logInWithGoogle} tabIndex="6" disabled={logInDisabled}> <div className="logo-google"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" className="LgbsSe-Bz112c"> <g> <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"> </path> <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"> </path> <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"> </path> <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"> </path> <path fill="none" d="M0 0h48v48H0z"></path> </g> </svg> </div>[--Sign-up/Log-in with Google--] </button> ```/ ### 구글로 로그인/가입하기 창을 띄운 뒤, 유저가 승락을 하면 Authorized redirect URIs 를 통해 다음과 같은 정보가 들어옴. https://[Authorized redirect URIs]#state=(요청 보낼때 같이 보낸 보안키)&access_token=(access_token)&token_type=(token_type) 받은 access_token 과 token_type 을 이용해 다음의 URL 로 userinfo 를 가져올 수 있음. `https://www.googleapis.com/oauth2/v2/userinfo?access_token=${access_token}&token_type=${token_type}` ```[.linenums] $.ajax({ type: "GET", url: `https://www.googleapis.com/oauth2/v2/userinfo?access_token=${m.hashMap.access_token.val}&token_type=${m.hashMap.token_type.val}` , dataType: "json" }).done(function (resp) { console.log("Google data: ", resp); if (resp.verified_email && resp.email) { // Log-in with this email or Sign-up with this email and ID of sign-up, but state and ip check also. let dataToBeSent = `log\tiehack\tscreenWidth\tscreenHeight\tstate\temail\n` + `web\t☠\t${m.sW}\t${m.sH}\t${m.hashMap.state.val}\t${resp.email}`; console.log("Do POST to google.do!: ", dataToBeSent); $.ajax({ type: "POST", url: `/account/log-in/with/google.do`, data: dataToBeSent , dataType: "text" // 미리 서버에 임시로 저장해 둔 state 값과 비교. 무결성을 입증하기 위해 log, screenWidth, screenHeight 값도 같이 보냄. iehack 은 Internet Explore 에서 문자열이 깨지는걸 방지하는 용도였던듯? }).fail(function (resp) { console.log("fail: ", resp); }).done(async function (resp) { console.log("resp: ", resp); let res = resp.substring(15); res = await m.strToJSON(res); console.log("res: ", res); if (resp.startsWith("log-in success") && m.hashMap.state.val === String(res[1]?.state)) { // gtag('event', 'conversion', { 'send_to': 'AW-11407455434/R4DjCJSIzPwYEMrpv78q', 'value': 0.1, 'currency': 'USD' }); m.$log_in_with_google.before(m.encloseErr("log-in success")); // wait until saving session/rmb cookies. And conversion. setTimeout(function () { if (res[1]?.goto) { window.location.hash = ""; window.location.search = ""; window.location.href = res[1].goto; // console.log(res[1].goto); // window.open(unescape(res[1].goto), "_blank"); } else if (m.searchVars.goto?.val) { window.location.href = m.searchVars.goto.val; } else { window.location.href = "/"; } }, m.wait); } else { m.$log_in_with_google.before(m.encloseErr(resp)); } }); } m.$log_in_with_google[0].disabled = false; }); ```/ ## 카카오로 가입하기 카카오로 가입하기/로그인하기는 구글보다 한단계를 더 거침. ### 콘솔로 가서 API 등록하기
https://developers.kakao.com/console/app 로 가서 애플리케이션 추가하기를 눌러 CLIENT_ID 랑 SECRET 을 발급 받읍시다. (이건 다른 사이트에서도 그림 파일로 잘 설명해 주셨으니 전 생략.)
### 가입/로그인 동의 요청 보내기 가입/로그인 동의 요청 보내기는 https://kauth.kakao.com/oauth/authorize URL 을 통해 보낸다. ``` const loginWithKakao = async () => { const state = generateRandomHexString(); // 무결성 검증을 위한 random 32 length hex string. let sW; let sH; if (window.screen.width > window.screen.height) { sW = window.screen.width; sH = window.screen.height; } else { sW = window.screen.height; sH = window.screen.width; } const res = await postPreSocial({ nickname: '', state, sW, sH, authorizor: 'kakao' }); // 무결성 검증을 위해 state, sW, sH 값을 서버에 먼저 보내놓음. if (res?.result) { window.location.href = `https://kauth.kakao.com/oauth/authorize?client_id=[KAKAO_CLIENT_ID]&redirect_uri=${encodeURIComponent('[KAKAO_REDIRECT_URI]')}&response_type=code&scope=account_email&state=${state}`; } else { setError({ message: '서버와의 통신에 문제가 있습니다. 잠시 후 다시 시도해 주세요.' }); } }; ```/ ### REDIRECTED_URI 에서 받은 정보 카카오는 search 로 보낸다. hash 는 client 단에서만 가지고 노는 놈들이라 구글에서는 보안을 위해 hash 에 데이터를 넣어서 보내준거 같고, 카카오는 search 로 보내되 한단계 인증을 더 거치도록 만든거 같다. ``` async function loginWithKakao() { let { search } = window.location; if (search) { search = search.substring(1); const searchVars = search.split('&'); const searchMap = {}; for (const searchVar of searchVars) { const keyNValue = searchVar.split('='); searchMap[decodeURIComponent(keyNValue[0])] = decodeURIComponent(keyNValue[1]); } const { code, state } = searchMap; // window.location.search 에서 code 와 state 값을 얻어낸다. code 가 Bearer access if (code && state) { let sW; let sH; if (window.screen.width > window.screen.height) { sW = window.screen.width; sH = window.screen.height; } else { sW = window.screen.height; sH = window.screen.width; } try { const session = await postLoginWithSocial({ sW, sH, state, code, email: '', authorizor: 'kakao', }); if (session?.message) { setError(session); return; } setUser(session); return; } catch (err) { setError(err); return; } } setError({ message: '제대로 된 입력값 (code & state) 이 들어오지 않았습니다.' }); return; } setError({ message: '유효한 search 값이 들어오지 않았습니다.' }); } loginWithKakao(); ```/ ### 서버단에서 구글 로그인과 카카오 로그인을 한방에 처리. 중간 중간 await 를 빼먹는 다던지 하는 실수는 하지 말자. 저처럼 하루가 그냥 날라간다 ㅠㅇㅜ ``` postLoginWithSocial = async (req, res) => { assert(req.body, loginWithSocialBody); const { sW, sH, state, email, authorizor } = req.body; const ip = getClientIp(req); const checkPassed = await this.socialLoginService.checkAccountSocial({ sW, sH, state, ip, authorizor, }); if (checkPassed) { const socialLoginData = await this.socialLoginService.findSocialLogin(state, ip); if (email.trim()) { // 구글 로그인 부분. const user = await this.userService.getUserByEmail(email); if (!user?.id) { if (!socialLoginData.nickname.trim()) { return res.json({ message: `해당 계정 (Email: ${email}) 이 존재하지 않습니다.` }); } const checkAvailability = await this.userService.checkAvailability({ email, nickname: socialLoginData.nickname }); if (!(checkAvailability.email && checkAvailability.nickname)) { return res.json({ message: `Email (${email}) 사용 가능: ${checkAvailability.email}\n닉네임 사용 가능: ${checkAvailability.nickname}` }); } const user = await this.userService.create({ email, name: 'Unknown', nickname: socialLoginData.nickname, salt: generateRandomHexString(), pwdEncrypted: generateRandomHexString(104) }); const ssnResponse = await this.#createSession(user, ip); return res.json(ssnResponse); } const ssnResponse = await this.#createSession(user, ip); return res.json(ssnResponse); } else if (authorizor.trim() === 'kakao') { // 카카오 로그인 부분. const { code } = req.body; const data = { grant_type: 'authorization_code', client_id: process.env.KAKAO_CLIENT_ID, // ID 는 .env 파일에 따로 안전하게 보관하자. redirect_uri: process.env.KAKAO_REDIRECT_URI, // REDIRECT_URI 도 .env 파일에 안전하게 보관하자. code, // 여기서 code 를 같이 보내 인증을 받는 것. client_secret: process.env.KAKAO_CLIENT_SECRET, // SECRET 도 안전하게 .env 파일에 보관하자. }; axios.post(`https://kauth.kakao.com/oauth/token`, querystring.stringify(data), { headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', // json 이 아닌게 아쉽지만... 이렇게 보내라는데 이렇게 보내야겠죠? } }) .then(async resp => { const { access_token } = resp.data; // 해당 response 로부터 access_token 을 발급 받음. const resp1 = await axios.get(`https://kapi.kakao.com/v2/user/me`, { params: { secure_resource: false, // property_keys: ['kakao_account.email'], }, headers: { 'Authorization': `Bearer ${access_token}`, // 이 access_token 이 Bearer Authorization 에 사용됨. }}); const email1 = resp1.data.kakao_account.email.trim(); if (email1) { // 이메일을 얻어내서 가입 혹은 로그인 시켜주기. const user = await this.userService.getUserByEmail(email1); if (!user?.id) { if (!socialLoginData.nickname.trim()) { return res.json({ message: `해당 계정 (Email: ${email1}) 이 존재하지 않습니다.` }); } const checkAvailability = await this.userService.checkAvailability({ email: email1, nickname: socialLoginData.nickname }); if (!(checkAvailability.email && checkAvailability.nickname)) { return res.json({ message: `Email (${email1}) 사용 가능: ${checkAvailability.email}\n닉네임 사용 가능: ${checkAvailability.nickname}` }); } const user = await this.userService.create({ email: email1.trim(), name: 'Unknown', nickname: socialLoginData.nickname.trim(), salt: generateRandomHexString(), pwdEncrypted: generateRandomHexString(104) }); const ssnResponse = await this.#createSession(user, ip); return res.json(ssnResponse); } const ssnResponse = await this.#createSession(user, ip); return res.json(ssnResponse); } return res.json({ message: '카카오 계정에서 Email 을 찾을 수 없습니다.' }); }) .catch(err => { console.error('Error: ', err.response ? err.response.data : err.message); return res.json(err); }); } } else { return res.json({ message: 'Social 로그인에 실패했습니다.' }) } } ```/ ## RRA
  1. https://console.cloud.google.com/
  2. https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
728x90
반응형