-
Notifications
You must be signed in to change notification settings - Fork 3
JWT로 인증 인가 구현
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로 반환
}
이 코드에서의 accessToken, refreshToken은 네이버, 카카오와 같은 OAuth 제공자가 발급해주는 토큰으로, 사용자가 네이버/카카오에 로그인한 세션을 유지 + 서비스 데이터에 접근할 수 있게 해줌
하지만 이제 우리는 네이버/카카오로 인증을 끝낸 후, 사용자 식별을 위해 로그인 세션을 유지하고 인가된 요청을 처리할 거임.
- JWT는 어떤 흐름으로 쓰이고 처리되는가?
-
클라이언트: JWT를 받아 저장(localStroage / sessionStroage)한 후 모든 요청의 헤더에 이 JWT를
Authorization: Bearer <JWT_TOKEN>
형식으로 포함시킬 것 -
서버: 서버는 클라이언트의 요청이 들어올 때 이 JWT가 (1) 유효한지 (2) 변조되지 않았는지 확인해야하며, 만약 둘을 확인했다면 사용자의 정보를 request에 req.user 형태로 설정해 둘것
→ 어떻게? 이 인증을 할 AuthGaurd를 만들어서!!
-
JWT의 장점은?
a. 한번의 인증으로 여러번 요청을 인증된 상태로 보낼 수 있음 → 사용자가 세션을 유지할 수 있음
b. 사용자가 인증된 후에도 특정 권한을 가지고 있는지 확인할 수 있음
→ 이걸로 워크스페이스 편집 권한 관리해야겠다….
c. 상태를 서버에 저장하지 않기 때문에, 분산 시스템에서 확장이 유이함
-
JWT의 구성은?
-
헤더: 토큰의 타입(JWT)과 사용된 알고리즘(예:
HS256
)이 포함 -
페이로드: 사용자의 고유 식별자, 인증 제공자, 그리고 만료 시간과 같은 인증 관련 정보가 포함, 여기서 사용자의 ID와 제공자(
naver
또는kakao
) 정보가 포함 -
서명: 토큰이 변조되지 않았음을 증명하는 서명 부분, 서버가 가지고 있는 비밀 키 (
JWT_SECRET
)로 생성
-
헤더: 토큰의 타입(JWT)과 사용된 알고리즘(예:
- 일단 acessToken, refeshToken으로 인증을 성공하면 서버에서는 사용자에게 JWT를 발급해주도록 하자.
-
auth.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; import { JwtService } from '@nestjs/jwt'; // import { JwtAuthGuard } from './guards/jwt-auth.guard'; @Controller('auth') export class AuthController { constructor( private readonly authService: AuthService, private readonly jwtService: JwtService, ) {} ... @Get('naver/callback') @UseGuards(AuthGuard('naver')) async naverCallback(@Req() req) { // 네이버 인증 후 사용자 정보 반환 const user = req.user; // TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가 const payload = { sub: user.id, provider: user.provider }; const token = this.jwtService.sign(payload); return { message: '네이버 로그인 성공', user, accessToken: token, }; } ... @Get('kakao/callback') @UseGuards(AuthGuard('kakao')) async kakaoCallback(@Req() req) { // 카카오 인증 후 사용자 정보 반환 const user = req.user; // TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가 const payload = { sub: user.id, provider: user.provider }; const token = this.jwtService.sign(payload); return { message: '카카오 로그인 성공', user, accessToken: token, }; }
- 이제 유저의 권한이 필요한 컨트롤러 함수를 하나 만들어보자.
@Get('profile')
@UseGuards(JwtAuthGuard) // JWT 인증 검사
async getProfile(@Req() req) {
// JWT 토큰을 검증하고 사용자 정보 반환
return {
message: '인증된 사용자 정보',
user: req.user,
};
}
→ 컨트롤러에 사용자 정보를 조회할 수 있는 profile 엔드포인트를 만들어보자
→ 로그인한 사용자가 본인의 프로필을 조회할 수 있는 엔드포인트
// Example: 로그인한 사용자만 접근할 수 있는 엔드포인트
// auth/profile
@Get('profile')
@UseGuards(JwtAuthGuard) // JWT 인증 검사
async getProfile(@Req() req) {
// JWT 토큰을 검증하고 사용자 정보 반환
return {
message: '인증된 사용자 정보',
user: req.user,
};
}
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { LoginRequiredException } from '../../exception/login.exception';
import { InvalidTokenException } from '../../exception/invalid.exception';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authorizationHeader = request.headers['authorization'];
// 토크가 있는지 확인
if (!authorizationHeader) {
// console.log('Authorization header missing');
throw new LoginRequiredException();
}
const token = authorizationHeader.split(' ')[1];
// 유효한 토큰인지 확인
try {
const decodedToken = this.jwtService.verify(token, {
secret: process.env.JWT_SECRET,
});
request.user = decodedToken;
return true;
} catch (error) {
// console.log('Invalid token');
throw new InvalidTokenException();
}
}
}
- auth.module.ts
→ 자 여기서 트러블 슈팅 과정이 있었음!!!
- 원래 코드
import { Module } from '@nestjs/common';
import { UserRepository } from '../user/user.repository';
import { UserModule } from 'src/user/user.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { NaverStrategy } from './strategies/naver.strategy';
import { KakaoStrategy } from './strategies/kakao.strategy';
import { JwtModule } from '@nestjs/jwt';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
@Module({
imports: [
UserModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
controllers: [AuthController],
providers: [
AuthService,
NaverStrategy,
KakaoStrategy,
UserRepository,
JwtAuthGuard,
],
})
export class AuthModule {}
→ 여기서 process.env.JWT_SECRET
를 읽지 못하는 문제가 터짐!!!!
→ why??
→ 즉, 모듈에서 등록하는 환경변수는 동적으로 로드되게, 따로 설정을 해야하는 것...
- useFactory를 사용하여 비동기적으로 코드를 수정하면....
import { Module } from '@nestjs/common';
import { UserRepository } from '../user/user.repository';
import { UserModule } from 'src/user/user.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { NaverStrategy } from './strategies/naver.strategy';
import { KakaoStrategy } from './strategies/kakao.strategy';
import { JwtModule } from '@nestjs/jwt';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UserModule,
ConfigModule.forRoot({ isGlobal: true }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
controllers: [AuthController],
providers: [
AuthService,
NaverStrategy,
KakaoStrategy,
UserRepository,
JwtAuthGuard,
],
})
export class AuthModule {}
코드보다는 부여받은 토큰으로 제대로 인증과정이 처리되는지 확인해보겠다
→ 여기서 또 헤맴....
Value에 "Bearer " 실어서 보내야됨 !!!!!
- 현재 access token 주기: 한시간
- 일단 refresh token 주기 => 일주일로 해두면
- controller에서 콜백함수에서 refreshToken을 발급받게 추가해준다
@Get('naver/callback')
@UseGuards(AuthGuard('naver'))
async naverCallback(@Req() req) {
// 네이버 인증 후 사용자 정보 반환
const user = req.user;
// TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가
const payload = { sub: user.id, provider: user.provider };
const accessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
return {
message: '네이버 로그인 성공',
user,
accessToken,
refreshToken,
};
}
- 모듈 기능에 있던 expire 옵션도 없애준다
signOptions: { expiresIn: '1h' },
- refreshToken을 인증하고 인증이 성공적으로 마무리될 시, accessToken을 다시 발급받는 엔드포인트를 만든다
@Post('refresh')
async refreshAccessToken(@Req() req) {
const { refreshToken } = req.body;
const decoded = this.jwtService.verify(refreshToken, {
secret: process.env.JWT_SECRET,
});
const payload = { sub: decoded.sub, provider: decoded.provider };
const newAccessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
return {
message: '새로운 Access Token 발급 성공',
accessToken: newAccessToken,
};
}
-
하지만 여기서 refreshToken을 인증하는데 실패할 때는 총 두가지 경우가 있다.
(a) refreshToken이 조작된 경우, 형식이 잘못된 경우 (토큰을 도입하는 가장 큰 이유)
(b) refreshToken이 만료돼서 사용자가 재인증 (로그인) 을 거쳐 다시 accessToken, refreshToken을 발급받아야 하는 경우
이 경우를 나눠서 처리하기 위해 에외처리를 상세하게 해준다
import { TokenExpiredError } from 'jsonwebtoken';
...
catch (error) {
if (error instanceof TokenExpiredError) {
// Refresh Token이 만료됨 -> 사용자의 인증 필요
throw new ExpireException();
} else {
// 잘못된 Token임 -> 형식 오류 / 조작된 토큰일 것
throw new RefreshTokenException();
}
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
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)