-
Notifications
You must be signed in to change notification settings - Fork 2
[BE] 로그인 기능 및 리프레시토큰
SeongHyeon edited this page Dec 2, 2024
·
1 revision
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '6000s' },
}),
],
providers: [UserRepository, AccountRepository, AuthService, JwtService],
controllers: [AuthController]
})
export class AuthModule {}
-
TypeORM으로
User
엔티티를 관리하고,JwtModule
로 JWT 토큰을 생성하고 검증하여 사용자 인증을 처리하는AuthModule
입니다. - JWT 설정은 비밀 키와 만료 시간 등을 지정하여 보안을 강화합니다.
- AuthService와 UserRepository, AccountRepository 등을 통해 사용자 데이터와 계정 데이터를 처리합니다.
- AuthController에서 인증 엔드포인트를 제공하여, 클라이언트가 로그인과 인증 요청을 수행할 수 있게 합니다.
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username);
}
- 현재는 로그인 기능만 구현
- admin 계정을 생성해서 admin계정을 이용한 비회원 체험을 할 수 있도록 합니다.
-
signIn(@Body() signInDto: Record<string, any>)
:signIn
메서드는 요청의Body
데이터를signInDto
매개변수로 받아authService.signIn
메서드를 호출합니다. -
signInDto.username
을authService.signIn
메서드에 전달하여 인증 작업을 수행하고, 인증이 성공하면 JWT 토큰 등을 반환합니다.
@Injectable()
export class AuthService {
constructor(
private userRepository: UserRepository,
private jwtService: JwtService
) {}
async signIn(
username: string
): Promise<{ access_token: string }> {
const user = await this.userRepository.findOneBy({ username })
const payload = { sub: user.id, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload, {
secret: jwtConstants.secret,
expiresIn: '6000s',
}),
};
}
}
async signIn(username: string): Promise<{ access_token: string }> {
const user = await this.userRepository.findOneBy({ username });
const payload = { sub: user.id, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload, {
secret: jwtConstants.secret,
expiresIn: '6000s',
}),
};
}
-
입력: 사용자 이름
username
을 인자로 받습니다. -
사용자 검증:
-
userRepository.findOneBy({ username })
를 사용하여 데이터베이스에서 사용자 정보를 조회합니다. -
username
에 해당하는 사용자가 존재하지 않거나 인증 실패 시UnauthorizedException
을 통해 예외를 던질 수 있습니다.
-
-
JWT 토큰 생성:
-
jwtService.signAsync()
를 사용하여 JWT 토큰을 비동기적으로 생성합니다. -
payload
: JWT 토큰에 포함할 사용자 정보입니다.sub
에는user.id
,username
에는user.username
을 포함합니다. -
secret
: JWT 서명에 사용할 비밀 키로,jwtConstants.secret
을 사용합니다. -
expiresIn
: 토큰의 유효 기간으로, 6000초로 설정되어 있습니다.
-
-
출력:
{ access_token: string }
형식으로 JWT 액세스 토큰을 반환합니다.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
request['user'] = payload;
} catch {
throw new UnauthorizedException();
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
-
요청 헤더에서 JWT 토큰을 추출하여 유효성을 검증하고, 실패 시
UnauthorizedException
을 던집니다. -
유효한 토큰인 경우
request.user
에 사용자 정보를 저장하여 이후 라우트 핸들러에서 접근할 수 있게 합니다. - 이
AuthGuard
는 JWT 토큰을 통한 인증을 구현하며, NestJS 애플리케이션에서 인증된 사용자만 접근할 수 있는 보호된 라우트를 쉽게 만들 수 있게 합니다.
-
AccessToken:
- 사용자의 인증을 위해 클라이언트가 요청마다 전송하는 토큰입니다.
- 유효 기간이 짧게 설정되므로 만약 탈취되더라도, 그 영향이 짧은 시간 내에 제한됩니다.
- 보통 API 요청 헤더에 포함하여 사용됩니다(
Authorization: Bearer <AccessToken>
).
-
RefreshToken:
- AccessToken의 만료 시간이 지나면 새로운 AccessToken을 발급하기 위해 사용됩니다.
- 유효 기간이 상대적으로 길게 설정되어 있으며, 이 토큰을 통해 클라이언트가 다시 로그인할 필요 없이 새로운 AccessToken을 받을 수 있습니다.
- RefreshToken은 클라이언트의 안전한 저장소(예:
HttpOnly
쿠키)에 저장되어 서버와의 통신 중에만 사용되도록 관리하는 것이 좋습니다.
-
보안 강화:
- AccessToken을 탈취당하더라도, 짧은 유효 기간 덕분에 만료 시점이 빨리 도래합니다.
- 탈취된 토큰이 만료되면 더 이상 사용할 수 없기 때문에 공격자가 장시간 AccessToken을 악용하지 못하게 됩니다.
-
사용자 경험 향상:
- 유효 기간이 긴 RefreshToken을 사용하여 사용자는 계속해서 로그인 상태를 유지할 수 있습니다.
- AccessToken이 만료될 때마다 새로 로그인할 필요가 없으며, 자동으로 새로운 AccessToken을 발급받습니다.
-
서버의 무상태 인증 유지:
- AccessToken은 JWT의 장점인 서버의 무상태 인증을 유지하면서도 보안을 높일 수 있습니다.
- 서버는 AccessToken의 유효성만 검증하므로 별도의 세션 저장소가 필요하지 않으며, 서버 부하가 줄어듭니다.
- accessToken 유효기간을 6000초로 설정했기 때문에 재발급을 했다고 하더라도 이전 토큰이 사라지지는 않았던 것.
- accessToken의 유효기간을 짧게하고 refreshToken을 발급하여 redis나 다른 db에 저장하여 관리해야 한다.
서버는 DB에 RefreshToken만 보관하고 있다가, 필요한 경우에만 가져다 사용하면 된다.
그래서 RDBMS를 사용할 수 있다.
하지만 RefreshToken 특성상, 반복적이고 빠른 읽기 요구, RefreshToken의 만료시간 관리, 로그아웃시에 AccessToken의 블랙리스트 관리를 복합적으로 생각해 봤을 때
위에서의 요구조건인 NoSQL + In-Memory + expiretime
을 종합적으로 가지는 Redis가 가장 적합하다.
async refreshTokens(
refreshToken: string,
): Promise<{ access_token: string; refresh_token: string }> {
try {
const payload = await this.verifyRefreshToken(refreshToken);
const userId = payload.userId;
await this.validateStoredToken(userId, refreshToken);
const user = await this.findUserById(userId);
return this.generateTokens(user.id, user.username);
} catch {
throw new UnauthorizedException('Failed to refresh tokens');
}
}
private async generateTokens(
userId: number,
username: string,
): Promise<{ access_token: string; refresh_token: string }> {
const payload = { userId, userName: username };
const accessToken = await this.jwtService.signAsync(payload, {
secret: jwtConstants.secret,
expiresIn: ACCESS_TOKEN_TTL,
});
const refreshToken = await this.jwtService.signAsync(
{ userId },
{
secret: jwtConstants.refreshSecret,
expiresIn: REFRESH_TOKEN_TTL,
},
);
await this.redisRepository.setAuthData(
`refresh:${userId}`,
refreshToken,
REFRESH_TOKEN_TTL,
);
return {
access_token: accessToken,
refresh_token: refreshToken,
};
}
-
jwtService.signAsync
:- JSON Web Token(JWT)을 생성합니다.
- 각각 액세스 토큰과 리프레시 토큰의 비밀 키와 만료 시간을 분리하여 보안을 강화합니다.
-
redisRepository.setAuthData
:- 리프레시 토큰을 Redis에 저장합니다.
- 키를 사용자 ID별로 구분하여 관리하며, 만료 시간을 설정해 자동 삭제되도록 합니다.
-
액세스 토큰:
- 클라이언트가 서버에 요청할 때 인증 정보를 포함하여 전달하는 데 사용됩니다.
- 상대적으로 짧은 만료 시간(예: 15분)을 가지며, 만료 시 리프레시 토큰을 통해 갱신됩니다.
-
리프레시 토큰:
- 액세스 토큰이 만료되었을 때, 클라이언트가 새 액세스 토큰을 요청할 때 사용됩니다.
- 상대적으로 긴 만료 시간(예: 7일)을 가지며, 안전한 저장소에 저장됩니다.
클라이언트가 제공한 리프레시 토큰의 유효성을 검증한 뒤, 저장된 리프레시 토큰과 대조하여 탈취 여부를 확인하고, 유효한 경우 새로운 액세스 토큰과 리프레시 토큰을 생성합니다.
생성된 리프레시 토큰은 Redis에 저장해 만료 시간을 설정하고, 필요 시 무효화할 수 있도록 관리합니다.
액세스 토큰은 짧은 만료 시간을 가져 요청 인증에 사용되고, 리프레시 토큰은 긴 만료 시간으로 세션 연장과 새로운 토큰 발급에 사용됩니다.
보안을 강화하기 위해 액세스 토큰과 리프레시 토큰의 비밀 키를 분리하고 Redis를 활용해 안전하게 관리하며, 이를 통해 사용자가 재로그인 없이도 인증 세션을 유지할 수 있도록 설계되었습니다.
- [FE] TailwindCSS @apply
- [FE] 캐러셀 구현
- [FE] 사이드 바 상태관리 도전기
- [FE] axios interceptor로 로그인 필요한 api 개선하기
- [FE] Tanstack Query API 최적화 도전기
- [FE] Tanstack Query로 구현하는 무한 스크롤 차트 도전기
- [FE] 차트 무한 스크롤링 최적화 도전기
- [FE] 차트 실시간 등락 구현 도전기
- [FE] 검색 구현 및 검색 API 호출 최적화 도전기
- [FE] 고차 컴포넌트를 활용한 인증 접근 제어
- [FE] 코드 스플릿팅으로 최적화 도전기
- [BE] Server 생성
- [BE] CI/CD
- [BE] GitAction 학습 정리
- [BE] ssh터널링으로 db연결
- [BE] 배포환경에서 DB 연결 및 테스트 완료
- [BE] https 적용
- [BE] upbit api 연결 및 SSE api
- [BE] SSE 구현
- [BE] SSE 에러
- [BE] redis 설치 및 연동
- [BE] 트랜잭션 락 구현과 최적화
- [BE] Oauth CORS
- [BE] QueryRunner 사용 시 발생한 문제점과 해결방안
- [BE] Git Action 학습 정리
- [BE] NestJS 학습 정리
- [BE] 로그인 기능 및 리프레시토큰
- [BE] 비회원 체험 기능
- [BE] Nginx 학습 정리
- [BE] Mixed Content와 HTTPS 보안 구현하기
- [BE] 매수/매도 로직 구현 및 개선 과정
- [BE] Queue, Load Balancing, Redis