상똥이의 Back-End 공부방
[Nest.js] REST API 방식으로 소셜로그인 구현 샘플 코드 (naver, google, kakao) 본문
목표
- 코드 중복 최소화하기
- 소셜로그인 로직 이해하기
- 소셜로그인 환경설정하기
목차
0. 들어가며 (전체 코드)
1. 기본 설정 (유저 생성 로직)
2. 플랫폼별 로그인 과정 이해하기
3. 플랫폼별 환경 설정하기(naver, google, kakao)
4. 코드 작성하기
[0. 들어가며]
1. 전체 코드는 아래에서 확인하세요
https://github.com/Sangddong/Social-log-in
2. 코드를 적용할 때 필요한 것
- 사실 코드 전체를 복붙해도 잘 돌아갈 것이다
- 필요한 것은 목차 3번 과정 수행과, 그를 통해 나오는 env에 저장해야 할 값이다
3. 왜 이걸 글로 쓰고 있냐면
- 카카오 소셜로그인을 한 번 구현해본 후 다른 소셜로그인들을 구현할 일이 생겼다
- 이를 구현하기 위해 naver, google, kakao 폴더를 다 나눠서 만들었음
- 구현하면서 실질적인 로직은 같다는 걸 느끼고 코드 중복을 크게 줄일 수 있겠다 뒤늦게 생각했다
- 난 바보구나
- 그래서 줄임
- 대부분은 알겠지만 나 같은 바보가 분명히 있어
- 그 사람들도 알아야 해
- 티스토리에 글쓰기 시작
[1. 기본 설정]
1. 설정할 것들
- 회원가입을 위해 데이터베이스에 유저를 등록하는 로직을 미리 작성해둬야 한다
- Nest.js, TypeOrm을 사용할 것이다
2. TypeOrm + User entity
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { User } from './users/users.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.PGHOST,
username: process.env.PGUSER,
password: process.env.PGPASSWORD,
database: process.env.PGDATABASE,
entities: [User],
synchronize: true,
ssl: {
rejectUnauthorized: false,
},
}),
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './users.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
}
users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [TypeOrmModule],
})
export class UsersModule {}
users.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
social: string;
@Column()
socialId: string;
}
3. 유저 등록 및 유저 검색
users.service.ts
async createUser(data: DUser) {
const user = this.usersRepository.create(data);
return await this.usersRepository.save(user);
}
async findUser(id: string) {
return await this.usersRepository.findOne({ where: { socialId: id } });
}
users.dto.ts (잠시 작성해뒀을 뿐 필요하진 않다)
export class DUser {
social: string;
socialId: string;
}
[2. 플랫폼별 로그인/회원가입 과정 이해하기]
1. 플랫폼별 과정
1. 네이버
- 사용자가 서버에 네이버 로그인/회원가입 요청
- 서버에서 네이버에게 사용자 정보 요청
- 네이버에서 서버에게 사용자 정보 지급
- 미확인 시 회원 등록
- 서비스 서버가 네이버에 인가 코드 발급 요청
- 네이버에서 사용자에게 인증, 인가(동의) 요청
- 사용자가 네이버에게 인증, 인가(동의) 완료
- 네이버에서 Redirect URI(인가 코드)로 리다이렉션
- 서버에서 네이버에 토큰 발급 요청
- 네이버에서 서버에 토큰 발급
2. 구글
- 사용자가 서버에 구글 로그인/회원가입 요청
- 서버에서 구글에게 사용자 정보 요청
- 구글에서 서버에게 사용자 정보 지급
- 미확인 시 회원 등록
- 서비스 서버가 구글에 인가 코드 발급 요청
- 구글에서 사용자에게 인증, 인가(동의) 요청
- 사용자가 구글에게 인증, 인가(동의) 완료
- 구글에서 Redirect URI(인가 코드)로 리다이렉션
- 서버에서 구글에 토큰 발급 요청
- 구글에서 서버에 토큰 발급
3. 카카오
- 사용자가 서버에 카카오 로그인/회원가입 요청
- 서버에서 카카오에게 사용자 정보 요청
- 카카오에서 서버에게 사용자 정보 지급
- 미확인 시 회원 등록
- 서비스 서버가 카카오에 인가 코드 발급 요청
- 카카오에서 사용자에게 인증, 인가(동의) 요청
- 사용자가 카카오에게 인증, 인가(동의) 완료
- 카카오에서 Redirect URI(인가 코드)로 리다이렉션
- 서버에서 카카오에 토큰 발급 요청
- 카카오에서 서버에 토큰 발급
2. 추상화 (공통점 추출)
- 공통 로직은 아래와 같다
회원가입 | 로그인 |
[3. 플랫폼별 환경 설정하기]
1. 네이버
- 다음 사이트에 접속하여 애플리케이션을 등록한다 https://developers.naver.com/apps/#/list
- 앱 이름 기재
- 사용 API: 네이버 로그인, 사용할 API 아무거나 선택 (필자는 이름만 했음)
- 로그인 open API 서비스 환경: 모바일 웹/pc웹 추가
- 서비스 URL: 사용중인 URL (필자는 개발중이라 http://localhost:3000)
- Redirect URL: http://localhost:3000/auth/naver/callback
- N_REDIRECT_URI로 .env에 저장
- 등록 버튼을 누르면 바로 Client ID, Client Secret이 있는 화면으로 이동된다
- .env에 각각 복사하여 N_CLIENT_ID, N_SECRET_KEY로 저장
- .env에 아무 문자열을 조합한 후 N_STATE로 저장
2. 구글
- 다음 사이트에 접속하여 좌측 상단의 프로젝트 선택을 눌러 프로젝트를 새로 생성한다 https://console.cloud.google.com/customer-identity/providers?hl=ko&_ga=2.135474609.-188788975.1728469285&project=criends
- 프로젝트명 기재 후 만들기
- 결제정보를 입력해야 한다!!!!
- 대시보드로 이동하면 좌측 메뉴 중 'API 및 서비스'에서 '사용자 인증 정보'로 이동
- '+사용자 인증 정보 만들기'에서 'API 키' 클릭, 발급된 키 복사해서 .env파일에 G_REST_API_KEY로 저장
- 다시 '+사용자 인증 정보 만들기'에서 'OAuth 클라이언트 ID 만들기' 선택
- 동의화면 구성에서 User Type = 외부 선택
- 다음 페이지에서 앱 이름, 사용자 지원 이메일, 승인된 도메인(test.com 이런거 아무거나), 개발자 연락처 정보만 기재
- 테스트 사용자에 테스트할 이메일 기재 (다른 플랫폼과 다르게 개발 단계에서는 접근이 제한된다)
- 저장 후 대시보드로 돌아오게 된다
- 다시 '+사용자 인증 정보 만들기'에서 'OAuth 클라이언트 ID 만들기' 선택
- 애플리케이션 유형: 데스크톱 앱 선택
- 승인된 리디렉션 URI : http://localhost:3000/auth/google/callback
- G_REDIRECT_URI로 .env에 저장
- 그럼 클라이언트 ID와 클라이언트 보안 비밀번호가 뜬다
- .env에 각각 G_CLIENT_ID, G_SECRET_KEY로 저장
3. 카카오
- 다음 사이트에 접속하여 애플리케이션 추가하기 클릭 https://developers.kakao.com/console/app
- 앱 이름, 회사명, 카테고리 선택
- 좌측 메뉴 중 앱키에서 REST API 키 복사 후 .env에 K_REST_API_KEY로 저장
- 좌측 메뉴 중 카카오 로그인 클릭 후 카카오 로그인 활성화 ON
- Redirect URI 등록 : http://localhost:3000/auth/kakao/callback
- K_REDIRECT_URI로 .env에 저장
- .env 파일에 이렇게 저장되어 있어야 함
[4. 코드 작성하기]
1. 인가 코드 받기
- 플랫폼별 인가 코드를 받아야 한다
- 접근할 base url과 플랫폼별 파람값을 URLSearchParams로 조합해 반환한다
- 구글의 경우 scope(가져올 사용자 정보 범위), access-type이 추가로 필요하다
- 구글의 scope는 최소한만 가져오고자 id로 제한했다
// auth.service.ts
getSocialSignInCode(platform: string) {
const baseUrl = this.getSocialBaseUrl(platform);
const params = this.getSocialParams(platform);
return `${baseUrl}?${new URLSearchParams(params)}`;
}
private getSocialBaseUrl(platform: string) {
const baseUrls = {
naver: 'https://nid.naver.com/oauth2.0/authorize',
google: 'https://accounts.google.com/o/oauth2/v2/auth',
kakao: 'https://kauth.kakao.com/oauth/authorize',
};
return baseUrls[platform];
}
private getSocialParams(platform: string) {
const queryParams = {
naver: {
response_type: 'code',
client_id: process.env.N_CLIENT_ID,
redirect_uri: process.env.N_REDIRECT_URI,
state: process.env.N_STATE,
},
google: {
response_type: 'code',
client_id: process.env.G_CLIENT_ID,
redirect_uri: process.env.G_REDIRECT_URI,
scope: `https://www.googleapis.com/auth/userinfo.email`,
access_type: 'offline',
},
kakao: {
response_type: 'code',
client_id: process.env.K_REST_API_KEY,
redirect_uri: process.env.K_REDIRECT_URI,
},
};
return queryParams[platform];
}
// auth.controller.ts
import { Controller, Get, Param, Query, Res } from '@nestjs/common';
import { Response } from 'express';
...
@Get(':platform')
getSocialCode(
@Param('platform') platform: string,
@Res() response: Response,
) {
const url = this.authService.getSocialSignInCode(platform);
response.redirect(url);
}
@Get(':platform/callback')
async socialCallback(
@Param('social') social: string,
@Query('code') code: string,
@Res() response: Response,
) {
console.log(platform, ' code: ', code);
}
localhost:3000/auth/{platform}으로 이동하면 아래와 같이 뜬다.
동의해도 다음 로직을 아직 작성하지 않았기 때문에 무한 로딩에 걸림
naver | kakao | |
하지만 이동한 url을 보거나 콘솔에 찍어보면 인가코드는 받았음을 알 수 있다
2. 인가코드로 액세스 토큰 받기
- 코드를 받으면 이 코드를 통해 액세스 토큰을 받을 수 있게 된다
- 각 플랫폼별로 url, params, headers를 조합해 post 요청을 보내면 된다
- env가 아닌 값은 모두 각 플랫폼별 문서에 공개되어 있으므로, 그냥 코드 복사해서 쓰면 된다
// auth.service.ts
// 토큰을 발급받아오는 로직
async getSocialAccessToken(platform: string, code: string) {
const url = this.getSocialTokenUrl(platform);
const params = this.getSocialTokenParams(platform, code);
const header = this.getSocialTokenHeader(platform);
const response = await axios.post(url, params, { headers: header });
return response.data.access_token;
}
// 플랫폼별 토큰을 발급받을 url을 반환하는 로직
private getSocialTokenUrl(platform: string) {
const url = {
naver: `https://nid.naver.com/oauth2.0/token`,
google: `https://oauth2.googleapis.com/token`,
kakao: `https://kauth.kakao.com/oauth/token`,
};
return url[platform];
}
// 플랫폼별 필요한 파라미터값을 반환하는 로직
private getSocialTokenParams(platform: string, code: string) {
const params = {
naver: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.N_CLIENT_ID,
client_secret: process.env.N_SECRET_KEY,
redirect_uri: process.env.N_REDIRECT_URI,
state: process.env.N_STATE,
code,
}),
google: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.G_CLIENT_ID,
client_secret: process.env.G_SECRET_KEY,
redirect_uri: process.env.G_REDIRECT_URI,
code,
}),
kakao: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.K_REST_API_KEY,
redirect_uri: process.env.K_REDIRECT_URI,
code,
}),
};
return params[platform];
}
// 플랫폼별 헤더를 반환하는 로직
private getSocialTokenHeader(platform: string) {
const header = {
naver: {
'X-Naver-Client-Id': process.env.N_CLIENT_ID,
'X-Naver-Client-Secret': process.env.N_CLIENT_SECRET,
},
google: {
'Content-Type': 'application/x-www-form-urlencoded',
},
kakao: {
'Content-type': 'application/x-www-form-urlencoded;charset=utf-8',
},
};
return header[platform];
}
// auth.controller.ts
// callback
@Get(':platform/callback')
async socialCallback(
@Param('platform') platform: string,
@Query('code') code: string,
@Res() response: Response,
) {
const access_token = await this.authService.getSocialAccessToken(
platform,
code,
);
console.log(platform, 'access token: ', access_token);
return access_token;
}
- 콘솔에는 아래와 같이 나타난다. 액세스 토큰을 성공적으로 받아왔음을 알 수 있다
3. access_token으로 사용자 정보 가져오기
- 액세스토큰을 통해 사용자 정보를 받아올 수 있다
- 각 플랫폼마다 제공하는 고유한 id 값으로 데이터베이스에 id를 설정해줄 수 있다
// auth.service.ts
// 플랫폼별 사용자 정보 받아오기
async getSocialUserInfo(platform: string, accessToken: string) {
const url = {
naver: `https://openapi.naver.com/v1/nid/me`,
google: `https://www.googleapis.com/oauth2/v2/userinfo`,
kakao: `https://kapi.kakao.com/v2/user/me`,
};
return await axios.get(url[platform], {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
}
// auth.controller.ts
@Get(':platform/callback')
async socialCallback(
@Param('platform') platform: string,
@Query('code') code: string,
@Res() response: Response,
) {
const access_token = await this.authService.getSocialAccessToken(
platform,
code,
);
const userInfo = await this.authService.getSocialUserInfo(
platform,
access_token,
);
console.log(platform, userInfo.data);
return access_token;
}
- 콘솔에 아래와 같이 플랫폼별 제공해주는 정보가 찍힌다
4. 제공받은 정보를 통해 사용자 등록하기
- 제공받은 정보에는 id 값이 존재하므로, 이 id 값을 데이터베이스에 unique한 id 값으로 저장할 것이다
- auth 모듈의 providers에 UsersService를 추가하고, user 등록 로직을 구현할 것이다.
- users.service에 사용자를 찾는 로직과 사용자가 없다면 새로 등록하는 로직을 만들 것이다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './users.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
// 사용자 등록 로직
async createUser(platform: string, userInfo) {
let id: string;
if (platform === 'naver') id = userInfo.response.id;
else id = userInfo.id;
const checkUser = await this.usersRepository.findOne({
where: { socialId: id },
});
if (!checkUser) {
const user = this.usersRepository.create({
social: platform,
socialId: id,
});
return await this.usersRepository.save(user);
} else return true;
}
// 사용자 검색 로직
async findUser(id: string) {
return await this.usersRepository.findOne({ where: { socialId: id } });
}
}
// auth.controller.ts
@Get(':platform/callback')
async socialCallback(
@Param('platform') platform: string,
@Query('code') code: string,
@Res() response: Response,
) {
const access_token = await this.authService.getSocialAccessToken(
platform,
code,
);
const userInfo = await this.authService.getSocialUserInfo(
platform,
access_token,
);
const createdUser = await this.usersService.createUser(
platform,
userInfo.data,
);
console.log(createdUser);
return access_token;
}
- 사용자를 생성할 경우 콘솔에 찍어보면 아래와 같이 잘 뜬다
- 데이터베이스에도 들어가 있다
- 로그인 로직은 따로 필요하지 않다!! 데이터베이스에 존재하면 자동으로 로그인되기 때문이다~~
수고하셨습니다~~
'Nest.JS' 카테고리의 다른 글
[Nest.js] 토큰으로 로그인, 로그아웃 구현하기 (0) | 2024.11.14 |
---|---|
[Nest.js] 프로젝트 초기 설정하는 법 (0) | 2024.09.25 |
[Nest.js] 라이프사이클 정리 (0) | 2024.09.08 |
[Nest.js] 페이지네이션 백엔드 구현 (prisma) + 프론트 구현 (0) | 2024.07.04 |
[Nest.js] 카카오 로그인 API (회원가입, 로그인, 카카오 프로필 가져오기) (0) | 2024.06.23 |