-
Notifications
You must be signed in to change notification settings - Fork 2
[준섭] 1121(화) 개발기록 ‐ 커스텀 AuthGuard 작성
@UseGuards(AuthGuard())
와 같은 경우, AuthGuard()
는 기본적으로 클라이언트 요청의 Authorization
헤더에 있는 토큰은 읽어서 내부적으로 등록된 Strategy
를 활용하여 요청을 인증한다.
그러나 우리는 Authorization
헤더를 사용하지 않고, 브라우저 쿠키에 토큰을 저장하여 요청 시 쿠키 헤더에 토큰이 담겨 들어온다.
나는 AuthGuard
를 커스텀하여 Authorization
헤더가 아닌 쿠키
에서 토큰을 읽게 하고, Strategy
또한 커스텀하여 원하는대로 인증을 진행하기로 계획했다.
위에서 말했듯 AuthGuard
는 Passport
에서 설정하여 내부적으로 등록된 Strategy
를 읽어 인증을 진행한다.
PassportStrategy
를 상속하면 커스텀 Strategy
를 만들 수 있다.
// src/auth/jwt.strategy.ts
import ...
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private userRepository: UserRepository) {
super({
secretOrKey: process.env.JWT_SECRET_KEY,
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}
// 생략 ..
}
이렇게 커스텀을 한다고 하면 생성자 super()안 jwtFromRequest
부분에 jwt를 넘겨줘야 한다.
위처럼 fromAuthHeaderAsBearerToken()
메서드로 Authorization 헤더의 Bearer 토큰을 넘겨줄 수도 있지만, 우리는 쿠키에 저장된 토큰을 넘겨야 한다.
jwtFromRequest: (req) => {
let token = null;
if (req && req.cookies) {
token = req.cookies['accessToken']; // 쿠키에서 accessToken 가져오기
}
return token;
},
위와 같이 req에서 쿠키를 넘길 수도 있지만, 생각해보니 accessToken
뿐 아니라 refreshToken
도 함께 넘겨야 accessToken이 유효하지 않은 경우 refreshToken을 검사할텐데 어떻게 하지? 라는 생각이 들었다.
그래서 일단 AuthGuard
를 커스텀 해보자! 라고 생각하고 넘어갔다.
AuthGuard도 jwt를 사용하는 경우 AuthGuard('jwt')
를 상속받아서 커스텀할 수 있다고 한다.
import {
Injectable,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class CookieAuthGuard extends AuthGuard('jwt') {
constructor(private readonly jwtService: JwtService) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
try {
const accessToken = request.cookies['accessToken'];
const { username, id } = this.jwtService.verify(accessToken);
request.user = { username, userId: id };
return true;
} catch (error) {
throw new UnauthorizedException('로그인이 필요합니다.');
}
}
}
JwtService의 verify 메서드는 토큰을 검사하고 유효하지 않으면 에러를 발생시킨다.
일단 refreshToken은 생각하지 않고, 위처럼 accessToken만 검사하고 유효하지 않으면 UnauthorizedException을 발생시켰다.
그리고 유효하다면 요청에 user로 username과 userId를 넘겨주었다.
이렇게 하고 확인을 위해 테스트 api를 만들어서 테스트 해보았다.
// auth.controller.ts
@Get('test')
@UseGuards(CookieAuthGuard)
test(@Req() req) {
return req.user; // 응답 body에 CookieAuthGuard에서 추가해준 req.user 정보 반환
}
쿠키가 없는 경우 다음과 같이 Unauthorized 에러가 발생하였다.
쿠키에 JWT 토큰을 넣은 채로 로그인을 하니 다음과 같이 user 정보가 잘 담겨서 출력되었다!
인가 과정은 다음과 같다.
- 쿠키가 없다면 로그인 하지 않았다고 판단, 인가 X → Unauthorized 에러 발생
- accessToken이 유효하다면 인가, request.user에 user정보를 담아 서비스 로직에 활용할 수 있도록 함
- accessToken이 유효하지 않다면 refreshToken을 검사
- refreshToken이 유효하지 않다면 브라우저의 쿠키를 지우고 인가 X → Unauthorized 에러 발생
- 유효하다면 Redis에 저장된 토큰과 비교
- 일치하지 않다면 브라우저의 쿠키를 지우고 인가 X → Unauthorized 에러 발생
- 일치하다면 인가 → 새로운 accesToken 발급
import ...
@Injectable()
export class CookieAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly jwtService: JwtService,
private readonly redisRepository: RedisRepository,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
if (!request.cookies) { // 쿠키가 없다면 로그인을 하지 않았다고 판단
throw new UnauthorizedException('로그인이 필요합니다.');
}
const accessToken = request.cookies['accessToken'];
try { // accessToken이 유효하다면 request.user에 user 정보를 담고 인가
const { userId, username, nickname } =
this.jwtService.verify(accessToken);
request.user = { userId, username, nickname };
return true;
} catch (error) {} // 유효하지 않다면 아래 로직으로
const refreshToken = request.cookies['refreshToken'];
try { // refreshToken이 유효하다면 일단 request.user에 user 정보 담고 다음 로직으로
const { userId, username, nickname } =
this.jwtService.verify(refreshToken);
request.user = { userId, username, nickname };
} catch (error) { // 유효하지 않다면 Unauthorized 에러 반환
response.clearCookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME);
response.clearCookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME);
throw new UnauthorizedException('로그인이 필요합니다.');
}
if ( // Redis에 저장된 refreshToken과 일치하지 않는다면
!(await this.redisRepository.checkRefreshToken(
request.user.username,
refreshToken,
))
) { // 브라우저 쿠키를 지우고 Unauthorized 에러 반환
response.clearCookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME);
response.clearCookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME);
throw new UnauthorizedException('로그인이 필요합니다.');
}
// refreshToken이 유효하면서 Redis에 저장된 토큰과도 같다면 새로운 accessToken 반환
const newAccessToken = await createJwt(
request.user,
JwtEnum.ACCESS_TOKEN_TYPE,
this.jwtService,
);
response.cookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME, newAccessToken, {
path: '/',
httpOnly: true,
});
return true;
}
}
위처럼 로직이 굉장히 복잡하다..
코드를 짜면서 이게 맞나? 라는 생각이 들었다.
AuthGuard가 너무 무거워 지는 느낌?
다음에 함수를 분리해서 가독성을 높여봐야겠다.
어쨌든 동작은 잘 된다!
상황을 나눠서 확인해보겠다.
- 쿠키가 없는 경우
당연히 Unauthorized 에러가 발생한다.
인가가 잘 된 모습을 보인다.
accessToken과 refreshToken의 payload에서 유효 기간을 만료한 상태로 쿠키에 저장하여 테스트 해보았다.
Unauthorized 에러가 발생한다.
쿠키도 사라진 모습을 보인다.
인가가 잘 된 모습을 보인다.
또한 accessToken이 현재 시간 기준으로 재발급 되었다!
로그인을 한 후 Redis의 해당 정보를 삭제하고 확인해보았다.
Unauthorized 에러가 발생한다.
역시 쿠키도 사라진 모습을 보인다.
후.. 이게 맞나 싶지만 일단 커스텀 AuthGuard를 만들었다.
가드가 필요한 곳에 @UseGuard(CookieAuthGuard)
와 같은 식으로 사용하면 된다!
© 2023 debussysanjang
- 🐙 [가은] Three.js와의 설레는 첫만남
- 🐙 [가은] JS로 자전과 공전을 구현할 수 있다고?
- ⚽️ [준섭] NestJS 강의 정리본
- 🐧 [동민] R3F Material 간단 정리
- 👾 [재하] 만들면서 배우는 NestJS 기초
- 👾 [재하] GitHub Actions을 이용한 자동 배포
- ⚽️ [준섭] 테스트 코드 작성 이유
- ⚽️ [준섭] TypeScript의 type? interface?
- 🐙 [가은] 우리 팀이 Zustand를 쓰는 이유
- 👾 [재하] NestJS, TDD로 개발하기
- 👾 [재하] AWS와 NCP의 주요 서비스
- 🐰 [백범] Emotion 선택시 고려사항
- 🐧 [동민] Yarn berry로 모노레포 구성하기
- 🐧 [동민] Vite, 왜 쓰는거지?
- ⚽️ [준섭] 동시성 제어
- 👾 [재하] NestJS에 Swagger 적용하기
- 🐙 [가은] 너와의 추억을 우주의 별로 띄울게
- 🐧 [동민] React로 멋진 3D 은하 만들기(feat. R3F)
- ⚽️ [준섭] NGINX 설정
- 👾 [재하] Transaction (트랜잭션)
- 👾 [재하] SSH 보안: Key Forwarding, Tunneling, 포트 변경
- ⚽️ [준섭] MySQL의 검색 - LIKE, FULLTEXT SEARCH(전문검색)
- 👾 [재하] Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- 👾 [재하] NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
- 2주차(화) - git, monorepo, yarn berry, TDD
- 2주차(수) - TDD, e2e 테스트
- 2주차(목) - git merge, TDD
- 2주차(일) - NCP 배포환경 구성, MySQL, nginx, docker, docker-compose
- 3주차(화) - Redis, Multer 파일 업로드, Validation
- 3주차(수) - AES 암복호화, TypeORM Entity Relation
- 3주차(목) - NCP Object Storage, HTTPS, GitHub Actions
- 3주차(토) - Sharp(이미지 최적화)
- 3주차(일) - MongoDB
- 4주차(화) - 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- 4주차(수) - 코드 개선, 트랜잭션 제어
- 4주차(목) - 트랜잭션 제어
- 4주차(일) - docker 이미지 최적화
- 5주차(화) - 어드민 페이지(전체 글, 시스템 정보)
- 5주차(목) - 감정분석 API, e2e 테스트
- 5주차(토) - 유닛 테스트(+ mocking), e2e 테스트(+ 파일 첨부)
- 6주차(화) - ERD
- 2주차(화) - auth, board 모듈 생성 및 테스트 코드 환경 설정
- 2주차(목) - Board, Auth 테스트 코드 작성 및 API 완성
- 3주차(월) - Redis 연결 후 RedisRepository 작성
- 3주차(화) - SignUpUserDto에 ClassValidator 적용
- 3주차(화) - SignIn시 RefreshToken 발급 및 Redis에 저장
- 3주차(화) - 커스텀 AuthGuard 작성
- 3주차(수) - SignOut시 토큰 제거
- 3주차(수) - 깃헙 로그인 구현
- 3주차(토) - OAuth 코드 통합 및 재사용
- 4주차(수) - NestJS + TypeORM으로 MySQL 전문검색 구현
- 4주차(목) - NestJS Interceptor와 로거
- [전체] 10/12(목)
- [전체] 10/15(일)
- [전체] 10/30(월)
- [FE] 11/01(수)~11/03(금)
- [전체] 11/06(월)
- [전체] 11/07(화)
- [전체] 11/09(목)
- [전체] 11/11(토)
- [전체] 11/13(월)
- [BE] 11/14(화)
- [BE] 11/15(수)
- [FE] 11/16(목)
- [FE] 11/19(일)
- [BE] 11/19(일)
- [FE] 11/20(월)
- [BE] 11/20(월)
- [BE] 11/27(월)
- [FE] 12/04(월)
- [BE] 12/04(월)
- [FE] 12/09(금)
- [전체] 12/10(일)
- [FE] 12/11(월)
- [전체] 12/11(월)
- [전체] 12/12(화)