-
Notifications
You must be signed in to change notification settings - Fork 4
passport로 oAuth 로그인 회원가입 구현
[Kakao Developers](https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite)
-
https://developers.kakao.com/console/app > 애플리케이션 추가하기
-
애플리케이션 선택
-
내 애플리케이션 > 앱 설정 > 요약 정보
- 앱 키 > REST API 키 (인가 코드 요청 시 필요)
-
플랫폼 > 플랫폼 설정하기 > Web > 사이트 도메인: http://localhost:3000/
-
Redirect URI 등록하러 가기
- 활성화 설정: ON
- Redirect URI 등록 > Redirect URI: http://localhost:3000/auth/kakao/callback
-
내 애플리케이션 > 제품 설정 > 카카오 로그인 > 보안 > Client Secret > 코드 생성 (선택)
- 활성화 상태: 사용함
-
내 애플리케이션 > 제품 설정 > 카카오 로그인 > 동의항목
- 닉네임
- 프로필 사진
- 카카오계정(이메일) 설정 👈 비즈앱으로 전환해야 동의항목 설정이 가능하다.
추후 인가 코드를 받을 때 [scope](https://developers.kakao.com/docs/latest/ko/kakaologin/common#user-info-kakao-account)와 관련 있다.
[🟩 Naver Developers](https://developers.naver.com/docs/common/openapiguide/appregister.md#%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%93%B1%EB%A1%9D)
-
https://developers.naver.com/apps/#/wizard/register > 약관동의 > 계정 정보 등록 > 애플리케이션 등록
- 사용 API: 네이버 로그인
- 권한: 회원이름, 연락처 이메일 주소, 별명, 프로필 사진 ✔️
- 로그인 오픈 API 서비스 환경: PC 웹
- 서비스 URL: http://localhost:3000/
- 네이버 로그인 Callback URL: http://localhost:3000/auth/naver/callback
- 사용 API: 네이버 로그인
- 내 애플리케이션 > 개요
- 애플리케이션 정보
- Client ID
- Client Secret
- 네이버 로그인
-
개발 상태 (개발 중)
애플리케이션이 '개발 중' 상태이면 [멤버관리] 탭에서 등록한 아이디만 네이버 로그인을 이용할 수 있습니다. 개발이 완료되어 실 서비스에 적용하고자 하신다면 검수를 요청해 주세요. 검수가 승인되면 모든 아이디로 네이버 로그인을 이용할 수 있습니다.
-
- 애플리케이션 정보
- 내 애플리케이션 > 멤버관리 (애플리케이션 개설자는 등록할 필요가 없다.)
- 관리자 ID 등록
- 테스터 ID 등록
→ 사전 작업이라던데 다같이 논의해서 해야할듯? 이거 나중에 하겠음…
→ 아닙니다 테스트해보려니까 설정해둬야되네요
출처: [https://github.com/dangdangwalk/dangdang-walk/wiki#-oauth-사전-작업](https://github.com/dangdangwalk/dangdang-walk/wiki#-oauth-%EC%82%AC%EC%A0%84-%EC%9E%91%EC%97%85)
- 카카오
- 네이버
(base) ➜ auth git:(feature-be-#189) ✗ tree
.
├── auth.controller.spec.ts
├── auth.controller.ts
├── auth.module.ts
├── auth.service.spec.ts
├── auth.service.ts
├── dto
│ └── createUser.dto.ts
├── strategies
│ ├── kakao.strategy.ts
│ └── naver.strategy.ts
├── user.entity.ts
└── user.repository.ts
3 directories, 10 files
- user.entity.ts
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ unique: true })
providerId: string; // 네이버/카카오 ID
@Column()
provider: string; // 'naver' 또는 'kakao'
@Column()
email: string;
@Column({ nullable: true })
nickname: string;
@Column({ nullable: true })
profileImage: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date;
}
- user.repository.ts
// user.repository.ts
import { DataSource, Repository } from 'typeorm';
import { User } from './user.entity';
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
@Injectable()
export class UserRepository extends Repository<User> {
constructor(@InjectDataSource() private dataSource: DataSource) {
super(User, dataSource.createEntityManager());
}
}
그 전에 DTO 정리 (카카오/네이버 연동에서 제공받을 정보)
- dto/createUser.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEmail, IsIn } from 'class-validator';
export class CreateUserDto {
@IsString()
@ApiProperty({
example: 'abc1234',
description: '사용자의 카카오/네이버 아이디',
})
providerId: string;
@IsString()
@IsIn(['naver', 'kakao'], {
message: 'provider는 naver 또는 kakao 중 하나여야 합니다.',
})
@ApiProperty({
example: 'naver',
description: '연동되는 서비스: 네이버/카카오',
})
provider: string;
@IsEmail()
@ApiProperty({
example: '[email protected]',
description: '사용자의 카카오/네이버 이메일 주소',
})
email: string;
}
- auth.service.ts
→ findUser: 해당 유저 없으면 null return
→ createUser: 특정 유저 자동으로 생성
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { CreateUserDto } from './dto/createUser.dto';
@Injectable()
export class AuthService {
constructor(private readonly userRepository: UserRepository) {}
async findUser(dto: CreateUserDto): Promise<User | null> {
const { providerId, provider } = dto;
const user = await this.userRepository.findOne({
where: { providerId, provider },
});
return user;
}
async createUser(dto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(dto);
return this.userRepository.save(user);
}
}
- 먼저 auth.module.ts 코드를 완성하면
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { NaverStrategy } from './strategies/naver.strategy';
import { KakaoStrategy } from './strategies/kakao.strategy';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [AuthController],
providers: [AuthService, NaverStrategy, KakaoStrategy],
})
export class AuthModule {}
여기서 providers: [AuthService, NaverStrategy, KakaoStrategy],
를 주목하자
- 그리고 auth.controller.ts 코드를 마저 완성하면
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get('naver')
@UseGuards(AuthGuard('naver'))
async naverLogin() {
// 네이버 로그인 페이지로 리디렉션
// Passport가 리디렉션 처리
}
@Get('naver/callback')
@UseGuards(AuthGuard('naver'))
async naverCallback(@Req() req) {
// 네이버 인증 후 사용자 정보 반환
return {
message: '네이버 로그인 성공',
user: req.user,
};
}
@Get('kakao')
@UseGuards(AuthGuard('kakao'))
async kakaoLogin() {
// 카카오 로그인 페이지로 리디렉션
// Passport가 리디렉션 처리
}
@Get('kakao/callback')
@UseGuards(AuthGuard('kakao'))
async kakaoCallback(@Req() req) {
// 카카오 인증 후 사용자 정보 반환
return {
message: '카카오 로그인 성공',
user: req.user,
};
}
}
@UseGuards(AuthGuard('naver'))
, @UseGuards(AuthGuard('kakao'))
를 통해 들어온 request는 각각 NaverStrategy
, KakaoStrategy
에서 처리된다
-
NaverStrategy
와KakaoStrategy
클래스에서 Passport 전략을 작성하고'naver'
와'kakao'
라는 이름을 부여 -
AuthModule
에 해당 전략을 프로바이더로 등록했으니, 이렇게 등록된 전략은 NestJS의 DI 시스템을 통해AuthGuard
가 접근할 수 있게 된다 -
모듈에 등록된 전략은 해당 모듈에서 사용할 수 있으며, 전략에 설정한 이름(예:
'naver'
)으로 전역적으로 인식 -
AuthGuard('naver')
를 사용하면 NestJS는 컨테이너에서naver
라는 이름으로 등록된 전략을 찾아서 사용
Passport는 인증과 관련된 절차를 매우 쉽게 만들어주는 Node.js용 인증 라이브러리
Passport의 주요 역할
-
다양한 인증 전략 지원:
- Passport는 전략 기반으로 설계, 이를 통해 개발자는 필요에 따라 OAuth, JWT, Local Strategy 등 다양한 인증 방식을 쉽게 추가할 수 있다
-
인증 흐름 간소화:
- Passport는 사용자를 로그인 페이지로 리디렉션하고, 토큰을 발급받고, 사용자 정보를 가져오는 등 여러 단계를 단순화하여, 개발자는 간단한 설정과 콜백 함수만으로 인증 과정을 구현할 수 있음
-
미들웨어 기반:
- Passport는 미들웨어로 동작하기 때문에 Express나 NestJS와 같은 프레임워크에 쉽게 통합될 수 있음
-
@UseGuards(AuthGuard('naver'))
와 같은 형태로 NestJS에서 인증을 위해 쉽게 사용할 수 있음
-
Passport를 이용한 oAuth 인증과정의 흐름
-
사용자 리디렉션:
- 사용자가
/auth/naver
와 같은 경로에 접근하면 Passport는 미리 설정한authorizationURL
로 사용자를 네이버 로그인 페이지로 리디렉션함
- 사용자가
-
사용자 인증:
- 사용자가 로그인하고 동의를 완료하면 네이버는 콜백 URL로 인증 코드를 전달함
-
토큰 교환 및 사용자 정보 가져오기:
- Passport는
tokenURL
을 통해 인증 코드를 사용하여 엑세스 토큰을 요청하고, 이 토큰을 사용하여 사용자 정보를 가져옴
- Passport는
-
validate
메서드 호출:- 엑세스 토큰을 사용해 사용자 정보를 가져온 후, Passport는 전략에 정의된
validate
메서드를 호출하여 사용자 정보를 처리함
- 엑세스 토큰을 사용해 사용자 정보를 가져온 후, Passport는 전략에 정의된
-
사용자 리디렉션:
- 인증 전략의 개념
-
PassportStrategy
를 확장한NaverStrategy
와KakaoStrategy
는 OAuth2 프로토콜을 이용해 사용자의 인증 흐름을 처리하는 클래스!!!
-
Strategy
가 수행하는 구체적인 역할
2.1 OAuth 인증 흐름 관리
- 사용자가 네이버 또는 카카오 로그인 페이지로 리디렉션되고, 성공적으로 인증하면 서비스의 콜백 URL로 다시 리디렉션됨
- 이 과정에서 각 전략은 OAuth 서버와 상호작용하여 엑세스 토큰을 발급, 이를 이용해 사용자의 정보를 가져옴
2.2 사용자 정보 검증 (validate
메서드)
-
validate
메서드는 OAuth 서버로부터 사용자가 인증된 후 호출됨 - 이 메서드에서는 사용자 정보를 검증하고 필요한 경우 새로운 사용자를 생성함
- 예를 들어,
NaverStrategy
에서 인증이 완료되면validate
메서드가 호출되고, 여기서 엑세스 토큰을 사용하여 사용자 프로필을 확인!!! - 이후,
CreateUserDto
객체를 생성하여AuthService
의findOrCreateUser
메서드를 호출함으로써 사용자가 데이터베이스에 존재하지 않으면 새로운 사용자로 등록함
- 예를 들어,
2.3 사용자 정보를 req.user
에 설정
-
validate
메서드에서 반환된 사용자 정보는 NestJS의 요청 객체(req
)에req.user
로 설정 - 이렇게 하면 컨트롤러에서 사용자 정보에 쉽게 접근할 수 있음
- strategies/naver.strategy.ts
// src/auth/strategies/naver.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-naver';
import { AuthService } from '../auth.service';
import { CreateUserDto } from '../dto/createUser.dto';
@Injectable()
export class NaverStrategy extends PassportStrategy(Strategy, 'naver') {
constructor(private authService: AuthService) {
super({
// 환경 변수로 관리
clientID: process.env.NAVER_CLIENT_ID,
// 애플리케이션이 네이버 또는 카카오와 같은 외부 서비스와 통신할 때
// 해당 애플리케이션을 식별하기 위한 고유 ID
clientSecret: process.env.NAVER_CLIENT_SECRET,
// 이 값은 애플리케이션의 보안성을 높이기 위해 사용됨
// 외부 서비스와 통신할 때 애플리케이션의 신뢰성을 보장하기 위해 사용
callbackURL: process.env.NAVER_CALLBACK_URL,
// 사용자가 네이버 또는 카카오 로그인 후 리디렉션될 URL
// 이 URL을 통해 인증이 완료되었을 때 사용자 정보를 처리할 수 있음
});
}
async validate(accessToken: string, refreshToken: string, profile: Profile) {
// 네이버 인증 이후 사용자 정보 처리
const createUserDto: CreateUserDto = {
providerId: profile.id,
provider: 'naver',
email: profile._json.email,
};
let user = await this.authService.findUser(createUserDto);
if (!user) {
user = await this.authService.createUser(createUserDto);
}
return user; // req.user로 반환
}
}
-
여기서 callBackURL이란?
- 이 URL은 네이버/카카오의 개발자 콘솔에서 등록해야 하며, 애플리케이션 서버의 특정 엔드포인트를 가리켜야 함
- 사용자가 네이버/카카오로 로그인 후, 인증이 완료되면 콜백 URL로 돌아옴
- 이때 콜백 URL 엔드포인트(
naver/callback
,kakao/callback
)이 호출되며, 사용자의 인증 결과가 백엔드 서버로 전달됨
-
그러면 미룰수없다… 네이버, 카카오 개발자 콘솔에서 등록해야겠다…
→임시임!!!
→ api prefix를 추가하는걸 깜빡해서;;;; 계속 테스트 실패했음 ㅋㅋㅋㅋㅋ
- app.module.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { IoAdapter } from '@nestjs/platform-socket.io';
import * as dotenv from 'dotenv';
dotenv.config();
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new IoAdapter(app));
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api');
// -> 얘 때문에 당연히 api 앞에 붙음 (로그를 제대로 보자...)
const config = new DocumentBuilder()
.setTitle('OctoDocs')
.setDescription('OctoDocs API 명세서')
.build();
console.log(process.env.origin);
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, documentFactory);
app.enaableCors({
origin: process.env.origin,
});
await app.listen(3000);
}
bootstrap();
- redirect URL 다시 등록하기
- .env
NAVER_CLIENT_ID=***...
NAVER_CLIENT_SECRET=***...
NAVER_CALLBACK_URL=http://localhost:3000/api/auth/naver/callback
// 프런트엔드랑 연결하고 수정해야함
KAKAO_CLIENT_ID=***...
KAKAO_CLIENT_SECRET=***...
KAKAO_CALLBACK_URL=http://localhost:3000/api/auth/kakao/callback
// 프런트엔드랑 연결하고 수정해야함
→ 확인 완
→ 로그인을 미리 해두고 사용자 DB에 사용자 정보까지 들어있다? 자동으로 두번째 화면 나오는것까지 확인
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
Novel이란?
Novel 스타일링 문제
에디터 저장 및 고려 사항들
📠 실시간 협업, 통신
Yorkie와 Novel editor 연동
YJS, Websocket, React-Flow
YJS, Socket.io
WebSocket과 Socket.io에 대해 간단히 알아보기
YJS 가이드 근데 이제 Socket.io를 곁들인
🏗️ 인프라와 CI/CD
NCloud CI CD 구축
BE 개발 스택과 기술적 고민
private key로 원격 서버 접근
nCloud 서버, VPC 만들고 설정
monorepo로 변경
⌛ 캐시, 최적화
rabbit mq 사용법
🔑 인증, 인가, 보안
passport로 oAuth 로그인 회원가입 구현
FE 로그인 기능 구현
JWT로 인증 인가 구현
JWT 쿠키로 사용하기
refresh token 보완하기
🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략
🌤️ 데일리 스크럼
📑 회의록
1️⃣ 1주차
킥오프(10/25)
2일차(10/29)
3일차(10/30)
4일차(10/31)
2️⃣ 2주차
8일차(11/04)
9일차(11/05)
11일차(11/07)
13일차(11/09)
3️⃣ 3주차
3주차 주간계획(11/11)
16일차(11/12)
18일차(11/14)
4️⃣ 4주차
4주차 주간계획(11/18)
23일차(11/19)
24일차(11/20)
25일차(11/21)
5️⃣ 5주차
5주차 주간계획(11/25)
29일차(11/25)
32일차(11/28)
34일차(11/30)
6️⃣ 6주차
6주차 주간계획(12/2)
37일차(12/3)