728x90
반응형
- 2024-09-30 : First posting.
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
728x90
반응형