구글, 애플, 페이스북, 네이버 등 거대 플랫폼 회사의 계정을 통해 로그인 하는 기능도 상당히 오래되었기 때문에 대부분의 사용자들이 이러한 기능에 대해서 잘 알고 있을 것이다.
그러나 내부적으로 어떻게 돌아가는지 아는 사람은 별로 없는데,
사용자가 위 화면에서 로그인 할 계정을 누르면 우리 서버로 요청이 redirect 되며, 우리 서버에서 POST https://oauth2.googleapis.com/token
으로 요청을 보내서 access_token
, refresh_token
등을 받아 온다.
그 후, access_token
을 Header의 Authorization
으로 추가하여 GET https://www.googleapis.com/userinfo/v2/me
로 요청을 보낸다.
JS 식으로 표현하면 Authorization: Bearer ${accessToken}
와 같은 형식이다.
그럼 위와 같이 사용자의 사진, 이메일, id 등의 정보가 반환된다.
여기서 id와 이메일은 각 사용자별로 고유한 값이기 때문에, 사용자를 식별하는데 사용될 수 있다.
위 처리 과정은 Google OAuth 2.0 Playground에서 직접 테스트 할 수 있다.
회원 가입을 하는 경우는 DB에 이 정보를 기반으로 새로운 사용자를 생성하면 될 것이고, 로그인을 하는 경우는 DB에서 해당 id 또는 이메일을 가진 사용자를 찾아서 지난 시간에 봤던 것처럼 JWT를 발급하여 쿠키를 생성하면 될 것이다.
이제 직접 코드로 구글 로그인을 구현해보기 위해 oauth라는 디렉토리를 만들자.
npm init -y
npm i express axios
위와 같이 디렉토리를 초기화하고 서버를 만들기 위해 express
를, 그리고 구글 인증 서버에 요청을 보내 사용자 데이터를 가져오기 위해 axios
를 설치한다.
이제 구글 OAuth 로그인 서비스를 사용하기 위해 Google Cloud Platform에 가입하자.
처음 가입했다면 NEW PROJECT 버튼을 눌러 새로운 프로젝트를 생성해야 한다.
구글 로그인을 이용하기 위해 API & Services 탭에 접속한 뒤, 새로운 OAuth Client ID를 생성한다.
우리가 만들 서버는 http://localhost:3000
에서 실행시킬 것이다.
Authorized JavaScript origins에 해당 주소를 등록해준다.
Authorized redirect URIs에는 http://localhost:3000/login/redirect
를 등록할 것이다.
참고로 사진 우측에 Client ID, Client secret이 보이는데, 이를 잘 저장해두자.
잠시 후에 구글 인증 서버로 요청을 보낼 때 사용될 값들이다.
실제 서비스를 운용할 때는 Client secret를 외부에 노출시키면 안된다.
이제 코드를 작성해보자.
이번 장에서 다룰 모든 코드는 main.js
파일에 작성할 것이다.
// main.js
const express = require('express');
const axios = require('axios');
const app = express();
const GOOGLE_CLIENT_ID = // YOUR GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = // YOUR GOOGLE_CLIENT_SECRET;
const GOOGLE_REDIRECT_URI = 'http://localhost:3000/login/redirect';
// 루트 페이지
// 로그인 버튼을 누르면 GET /login으로 이동
app.get('/', (req, res) => {
res.send(`
<h1>Log in</h1>
<a href="/login">Log in</a>
`);
});
// 로그인 버튼을 누르면 도착하는 목적지 라우터
// 모든 로직을 처리한 뒤 구글 인증 서버인 https://accounts.google.com/o/oauth2/v2/auth
// 으로 redirect 되는데, 이 url에 첨부할 몇가지 QueryString들이 필요
app.get('/login', (req, res) => {
let url = 'https://accounts.google.com/o/oauth2/v2/auth';
// client_id는 위 스크린샷을 보면 발급 받았음을 알 수 있음
// 단, 스크린샷에 있는 ID가 아닌 당신이 직접 발급 받은 ID를 사용해야 함.
url += `?client_id=${GOOGLE_CLIENT_ID}`
// 아까 등록한 redirect_uri
// 로그인 창에서 계정을 선택하면 구글 서버가 이 redirect_uri로 redirect 시켜줌
url += `&redirect_uri=${GOOGLE_REDIRECT_URI}`
// 필수 옵션.
url += '&response_type=code'
// 구글에 등록된 유저 정보 email, profile을 가져오겠다 명시
url += '&scope=email profile'
// 완성된 url로 이동
// 이 url이 위에서 본 구글 계정을 선택하는 화면임.
res.redirect(url);
});
// 구글 계정 선택 화면에서 계정 선택 후 redirect 된 주소
// 아까 등록한 GOOGLE_REDIRECT_URI와 일치해야 함
// 우리가 http://localhost:3000/login/redirect를
// 구글에 redirect_uri로 등록했고,
// 위 url을 만들 때도 redirect_uri로 등록했기 때문
app.get('/login/redirect', (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
res.send('ok');
});
app.listen(3000, () => {
console.log('server is running at 3000');
});
주석에 설명히 상당히 자세히 되어있지만 다시 사진과 함께 한 번 설명하자면,
서버를 실행시키고 GET /
로 오면 위와 같은 화면이 나온다.
그리고 Log in 링크를 누르면 우리 서버의 GET /login
으로 이동된다.
그런데 GET /login
은 res.redirect(url)
로 우리를 다른 url로 이동시키는 로직이 존재한다.
우리 서버의 다른 url이 아닌, 구글 인증 서버로 이동시킬 것이다.
그 과정에서 여러 가지 옵션 값들을 넣어주는데, 이는 주석을 읽으면서 확인해보자.
구글 인증 서버로 이동되면 이렇게 계정을 선택하는 화면이 나온다.
혹은 구글 로그인이 되어 있지 않다면, 구글 ID, 비밀번호를 입력하라는 화면이 나올 수도 있다.
구글 로그인 버튼을 누르면 위 화면처럼 계정 선택을 할 수 있는 창이 뜬다는 것은 대부분 경험적으로 알고 있을 것이다.
여기서 계정을 누르면 우리가 Authorized redirect URIs에 등록한 http://localhost:3000/login/redirect
로 이동할 것이다.
아까는 우리 서버 -> 구글 인증 서버로 redirect했는데, 이번에는 구글 인증 서버가 우리 서버로 redirect 시켜주는 것이다.
구글 인증 서버에서 GET /login/redirect
로 redirect 될 때, code
라는 QueryString이 들어오게 된다.
app.get('/login/redirect', (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
res.send('ok');
});
그 부분의 코드만 다시 살펴보자.
code
라는 QueryString을 출력하는 코드다.
여기까지만 작성한 상태로 코드를 실행해서 동작을 확인해보자.
로그인 링크를 누르고
구글 계정을 선택하면
GET /login/redirect
라우터에서 res.send('ok')
로 응답을 반환하기 때문에 ok가 보인다.
여기서 주소창에 주목하자.
localhost:3000/login/redirect
뒤에 code
가 QueryString으로 존재한다.
구글 인증 서버에서 우리 서버로 redirect 할 때 넣어준 값이다.
주소창의 code
는 인코딩때문에 조금 다르게 보이지만, 터미널에도 code
가 출력되었다.
인코딩 때문에 다르게 보이는 것이며, 둘 다 4로 시작하는 매우 긴 문자열이다.
이렇게 받아온 code
를 사용해서 우리는 구글 인증 서버에 access_token
을 요청할 수 있다.
요청을 보내서 응답으로 access_token
을 받아오면, 이번에는 사용자의 구글 계정 정보를 가져올 수 있다.
아까 scope
에 email
, profile
을 입력했던 이유가 바로 나중에 사용자의 구글 계정에 등록된 email
, profile
을 가져올 것이기 때문이다.
이제 전체적인 흐름이 조금 보일 것이다.
GET /
으로 들어오면 로그인 링크가 있음
로그인 링크를 누르면 우리 서버의 /login
으로 이동
/login
라우터에서 여러 정보를 QueryString으로 첨부하여 구글 인증 서버로 redirect 시킴
구글 계정을 선택하면 아주 긴 문자열 값인 code
를 QueryString으로 첨부하여 우리 서버의 /login/redirect
로 다시 redirect 시킴
/login/redirect
라우터에서 code
를 첨부하여 구글 인증 서버에 access_token
요청해서 받아옴
/login/redirect
라우터에서 access_token
을 첨부해서 구글 사용자 정보를 가져옴
마찬가지로 /login/redirect
라우터에서 이제 사용자 email, google id 등의 정보를 가지고 이전에 구현 했던 것과 똑같이 회원가입/로그인 과정을 진행하면 됨
이게 전부다.
물론 회원가입/로그인 용도로 나눠서 2번의 작업을 해야한다.
우리는 /login
, /login/redirect
라우터만 갖고 있는데, 회원가입 용도로 /signup
, /signup/redirect
라우터도 만들어보자.
app.get('/', (req, res) => {
res.send(`
<h1>OAuth</h1>
<a href="/login">Log in</a>
<a href="/signup">Sign up</a>
`);
});
우선 루트 라우터에 GET /signup
링크부터 만들자.
<h1>
태그 안의 제목도 OAuth
로 바꾸자.
Google Cloud Platform에서 Authorized redirect URIs에 http://localhost:3000/signup/redirect
도 새로 등록해야 한다.
// LOGIN, SIGNUP 각 용도별 REDIRECT_URI로 나눔
const GOOGLE_LOGIN_REDIRECT_URI = 'http://localhost:3000/login/redirect';
const GOOGLE_SIGNUP_REDIRECT_URI = 'http://localhost:3000/signup/redirect';
app.get('/login', (req, res) => {
let url = 'https://accounts.google.com/o/oauth2/v2/auth';
url += `?client_id=${GOOGLE_CLIENT_ID}`
// GOOGLE_REDIRECT_URI -> GOOGLE_LOGIN_REDIRECT_URI로 변경
url += `&redirect_uri=${GOOGLE_LOGIN_REDIRECT_URI}`
url += '&response_type=code'
url += '&scope=email profile'
res.redirect(url);
});
이제 redirect 시킬 uri가 2개이므로 각각 GOOGLE_LOGIN_REDIRECT_URI
, GOOGLE_SIGNUP_REDIRECT_URI
변수로 이름과 url을 지정해준다.
app.get('/login')
라우터의 GOOGLE_REDIRECT_URI
-> GOOGLE_LOGIN_REDIRECT_URI
로 바꾸는 것도 잊지 말자.
// 회원가입 라우터
app.get('/signup', (req, res) => {
let url = 'https://accounts.google.com/o/oauth2/v2/auth';
url += `?client_id=${GOOGLE_CLIENT_ID}`
url += `&redirect_uri=${GOOGLE_SIGNUP_REDIRECT_URI}`
url += '&response_type=code'
url += '&scope=email profile'
res.redirect(url);
});
app.get('/signup/redirect', (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
res.send('ok');
});
회원가입 라우터도 일단 임시로 로그인 라우터와 똑같이 만들고 작동이 잘 되는지 확인하자.
루트 페이지가 잘 변경되었다.
계정을 선택한다.
주소가 아까랑 거의 똑같지만 /login
이 아닌 /signup
으로 잘 변경되었다.
이제 구글 인증 서버에 토큰을 요청하기 위한 코드를 작성해보자.
// 토큰을 요청하기 위한 구글 인증 서버 url
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
app.get('/signup/redirect', async (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
// access_token, refresh_token 등의 구글 토큰 정보 가져오기
const resp = await axios.post(GOOGLE_TOKEN_URL, {
// x-www-form-urlencoded(body)
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_SIGNUP_REDIRECT_URI,
grant_type: 'authorization_code',
});
res.json(resp.data);
});
토큰을 가져오기 위해서 POST https://oauth2.googleapis.com/token
로 요청을 보내면 되는데, x-www-form-urlencoded
형식으로 구글 인증 서버로부터 QueryString으로 받아온 code와 우리의 GOOGLE_CLIENT_ID
, GOOGLE_CLIENT_SECRET
등의 정보들을 첨부해서 요청을 보내야 한다.
axios
가 무엇인지 잘 모른다면, 이는 다른 서버로 HTTP 요청을 보내기 위한 외부 모듈이라고 생각하자.
변수명을 resp
로 한 것은 Response의 약자인데, 이미 res
를 사용하고 있기 때문에 p를 하나 더 붙여준 것이다.
express에서는 Response의 약자로 res
가 사용되지만 다른 언어나 프레임워크 등에서는 resp
도 꽤나 사용되는 편이다.
이제 코드를 재실행하고 아까처럼 루트 페이지로 이동하여 Sign up 버튼을 누르고 구글 계정을 선택해보자.
아까와 같은 GET /signup/redirect
페이지다.
code
도 잘 붙어있다.
res.json(resp.data)
로 응답을 보내줬는데, 크롬 익스텐션이 설치되어 있기 때문에 JSON으로 응답을 보내면 아래와 같이 좀 더 보기 쉽게 포맷팅 된 형태로 보여주는 것이다.
resp
는 Response에 관한 데이터를 여럿 담고 있으며, 우리가 가져오고 싶었던 것은 응답의 body다.
axios.post()
로 받아온 응답은 resp.data
로 body만 추출할 수 있다는 사실을 기억하자.
그래서 데이터를 자세히 살펴보면 access_token
외 여러 가지 정보들이 들어있다.
여기서 가장 중요한 것은 access_token
이다.
아까 code
를 첨부해서 access_token
을 받아온 것처럼 access_token
을 첨부해서 또 다른 구글 인증 서버로 요청을 보내야 사용자의 구글 email
, profile
정보를 가져올 수 있기 때문이다.
물론 보안상 대단한 정보는 포함되지 않으며, 사용자를 식별할 수 있는 email
, google id
그리고 성별이나 생일 등의 간략한 정보들을 받아올 수 있다.
// email, google id 등을 가져오기 위한 url
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
app.get('/signup/redirect', async (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
// access_token, refresh_token 등의 구글 토큰 정보 가져오기
const resp = await axios.post(GOOGLE_TOKEN_URL, {
// x-www-form-urlencoded(body)
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_SIGNUP_REDIRECT_URI,
grant_type: 'authorization_code',
});
// email, google id 등의 사용자 구글 계정 정보 가져오기
const resp2 = await axios.get(GOOGLE_USERINFO_URL, {
// Request Header에 Authorization 추가
headers: {
Authorization: `Bearer ${resp.data.access_token}`,
},
});
// 구글 인증 서버에서 json 형태로 반환 받은 body 클라이언트에 반환
res.json(resp2.data);
});
첫번째 resp
의 resp.data
에는 access_token
, refresh_token
등의 구글 토큰 정보들이 json 형태로 담겨있었다.
이제 사용자의 구글 계정 정보를 얻기 위해 https://www.googleapis.com/oauth2/v2/userinfo
로 resp.data.access_token
을 Authorization
헤더에 추가하여 전송한다.
참고로 Authorization
헤더는 위와 같이 Authorization: Bearer ${accessToken}
형태로 구성된다.
실제로 구글 서버에서 사용자 정보를 가져올 때 전송되는 헤더를 보면 위와 같은 모습이다.
그냥 access_token
을 Bearer
옆에 공백을 한 칸 넣고 붙인 문자열이다.
다시 루트 페이지로 가서 Sign up
버튼을 누르고 구글 계정을 선택해서 회원가입 하면 res.json(resp2.data)
로 반환되는 사용자의 구글 계정 정보가 담겨 있는데, 위와 같은 모습이다.
app.get('/login/redirect')
도 똑같은 로직으로 처리해주면 된다.
나머지 구현은 선택의 영역이다.
우리는 앞서 Map을 이용한 회원가입과 로그인을 구현해보았다.
Map에 회원 데이터를 저장할 경우, 메모리에 저장되기 때문에 서버를 종료하면 모든 데이터가 날아갔었다.
그래서 서버를 재실행해도 데이터가 날아가지 않도록, users.json
파일에 JSON.stringify()
로 데이터를 문자열로 변환하여 저장하고, 불러올 때는 JSON.parse()
로 JS 객체로 가져오는 방식으로 회원가입, 로그인 기능을 리팩토링 해보았다.
여기서 더 나아가면 MongoDB나 MySQL, PostgreSQL 등의 데이터베이스에 사용자 정보를 저장할 수도 있을 것이다.
저장소가 달라졌을 뿐, 로직은 비슷하기 때문에 나머지 구현부는 당신의 입맛에 맞게 만들 수 있을 것이다.
회원가입 시, 위에서 작성한 로직으로 google id 혹은 email 등의 유니크한 값을 검사하여 MySQL에 회원이 저장되어 있으면 이미 가입된 계정입니다. 등의 메세지를 보내고, 가입이 되어있지 않으면 사용자 정보를 MySQL에 저장하면 될 것이다.
그리고 이전에 했던 것 처럼 MySQL에 회원 정보를 저장한 뒤, 사용자 id를 payload로 JWT를 발급하고 쿠키에 JWT를 넣어 생성하면 회원가입과 동시에 자동 로그인이 가능할 것이다.
로그인도 마찬가지로 google id를 가져와서 해당 id를 가진 사용자가 MySQL에 저장이 되어 있는지 확인하고, 없으면 존재하지 않는 계정입니다. 등의 메세지를 보내주는 등의 방식으로 처리한다.
해당 google id를 가진 사용자가 존재한다면 JWT + 쿠키 발급으로 로그인을 시킨 뒤, redirect로 루트 페이지로 보내주는 등의 방식으로 처리할 수 있을 것이다.
GOOGLE_CLIENT_SECRET 등의 중요한 정보를 .env 파일 등에 따로 저장하여 dotenv
모듈 등을 통해 실행 시 주입하는 식으로 보안에 좀 더 신경을 쓴 코드를 작성할 수도 있을 것이다.
Google 인증 서버에서 토큰과 계정 정보를 가져오는 핵심을 배웠기 때문에 나머지 부분은 원하는 대로 직접 만들어 보길 권장한다.
참고로 우리는 서버에서 access_token
을 가져와서 구글에 사용자 정보를 요청했는데, 클라이언트에서 access_token
을 요청해서 우리 서버로 전송한 뒤, 구글에 사용자 정보를 요청하는 방식도 존재한다.
어디서 access_token
을 가져오느냐의 차이이고 흐름은 비슷하다.
access_token
도 서버에서 요청하는 것이 보안상 좀 더 안전하긴 할 것이다.
또한 위 코드에서는 회원가입/로그인 라우터를 2개로 나눠서 진행했지만, 하나로 합쳐서 진행하는 방법도 가능할 것이다.
그러면 DB에서 가입된 아이디일 경우 로그인, 가입되지 않은 아이디일 경우 서버에서 원하는 추가적인 사용자 정보를 입력하는 form으로 이동시킨 뒤, 입력을 받으면 회원가입 하는 등의 방식을 생각해볼 수도 있겠다.
그러면 form에서 비밀번호를 입력 받아서 이메일/비밀번호를 통한 로그인과 구글 로그인을 병행하는 방법도 가능할 것이다.
오직 구글 로그인만 지원하면 비밀번호 자체를 아예 사용하지 않을 수도 있겠다.
얼마든지 재량에 따라 응용이 가능한 부분이라고 생각된다.
전체 코드를 첨부하며 마무리하겠다.
const express = require('express');
const axios = require('axios');
const app = express();
const GOOGLE_CLIENT_ID = ''; // YOUR GOOGLE_CLIENT_ID
const GOOGLE_CLIENT_SECRET = '' // YOUR GOOGLE_CLIENT_SECRET;
const GOOGLE_LOGIN_REDIRECT_URI = 'http://localhost:3000/login/redirect';
const GOOGLE_SIGNUP_REDIRECT_URI = 'http://localhost:3000/signup/redirect';
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
app.get('/', (req, res) => {
res.send(`
<h1>OAuth</h1>
<a href="/login">Log in</a>
<a href="/signup">Sign up</a>
`);
});
app.get('/login', (req, res) => {
let url = 'https://accounts.google.com/o/oauth2/v2/auth';
url += `?client_id=${GOOGLE_CLIENT_ID}`
url += `&redirect_uri=${GOOGLE_LOGIN_REDIRECT_URI}`
url += '&response_type=code'
url += '&scope=email profile'
res.redirect(url);
});
app.get('/signup', (req, res) => {
let url = 'https://accounts.google.com/o/oauth2/v2/auth';
url += `?client_id=${GOOGLE_CLIENT_ID}`
url += `&redirect_uri=${GOOGLE_SIGNUP_REDIRECT_URI}`
url += '&response_type=code'
url += '&scope=email profile'
res.redirect(url);
});
app.get('/login/redirect', (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
res.send('ok');
});
app.get('/signup/redirect', async (req, res) => {
const { code } = req.query;
console.log(`code: ${code}`);
const resp = await axios.post(GOOGLE_TOKEN_URL, {
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: GOOGLE_SIGNUP_REDIRECT_URI,
grant_type: 'authorization_code',
});
const resp2 = await axios.get(GOOGLE_USERINFO_URL, {
headers: {
Authorization: `Bearer ${resp.data.access_token}`,
},
});
res.json(resp2.data);
});
app.listen(3000, () => {
console.log('server is running at 3000');
});
좋은 글 감사합니다.