반응형
Documentation | NestJS - A progressive Node.js framework
로컬 로그인 로직 구현
passport 및 passport local 설치
pnpm add @nestjs/passport passport passport-local
pnpm add -D @types/passport-local
bcrypt 설치
pnpm add bcrypt
pnpm add -D @types/bcrypt
- bcrypt 설치 이유
- 용도
- crypto: 범용 암호화 라이브러리로 해시함수, 대칭/비대칭 암호화 등 다양한 암호화 기능 제공
- bcrypt: 비밀번호 해싱에 특화된 단방향 해시 함수
- 성능
- crypto: 일반적으로 bcrypt보다 빠름. SHA-256 등의 해시함수는 고속 처리 가능
- bcrypt: 의도적으로 느리게 설계됨. work factor 조절로 해싱 시간 조절 가능
- 이는 무차별 대입 공격을 어렵게 만드는 장점
- 보안성
- crypto:
- 빠른 처리 속도로 인해 무차별 대입 공격에 취약할 수 있음
- salt를 수동으로 관리해야 함
- bcrypt:
- 자동 salt 생성 및 관리
- 느린 처리 속도로 무차별 대입 공격 방어에 유리
- work factor 조절로 하드웨어 발전에 대응 가능
- 사용 예시:
결론:// crypto 예시 const crypto = require('crypto'); const hash = crypto.createHash('sha256').update('password').digest('hex'); // bcrypt 예시 const bcrypt = require('bcrypt'); const hash = await bcrypt.hash('password', 10); // 10은 work factor
- 비밀번호 해싱: bcrypt 권장 (보안성 우수)
- 일반 데이터 암호화/해싱: crypto 권장 (다양한 기능, 좋은 성능)
localStrategy 생성
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
- PassportStrategy 를 상속받아 localStrategy를 생성
- validate() 함수 안에 전략(검증로직)을 넣으면 됨
- 주요 검증 로직은 service layer에 구현
- 반환되는 데이터는 Request객체의 user속성으로 들어감 Request.user ⇒ 인증 후 반환해줄 타입에 대한 회의 필요!
service layer 에서 비밀번호 검증로직 실행
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { UserService } from '@/user/user.service';
@Injectable()
export class AuthService {
constructor(private userService: UserService) {}
async validateLocalLogin(username: string, inputPassword: string) {
const user = await this.userService.findUserByUsername(username);
if (!user) {
throw new UnauthorizedException('잘못된 로그인 정보');
}
const isPasswordValid = await bcrypt.compare(inputPassword, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('잘못된 로그인 정보');
}
const { password, ...result } = user;
return result;
}
}
- bcrypt 모듈을 통해 로그인 정보 검증
localLogin 함수에 UseGuards를 통해 passport 연결
//auth.controller.ts
import { LocalAuthGuard } from './local/local-auth.guard';
@UseGuards(LocalAuthGuard)
async localLogin(@Request() req) {
return {
status: 'success',
data: await this.authService.login(req.user),
};
}
//local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
- UseGuards 어노테이션을 통해 passport 전략을 넣을 수 있음.
- 공식문서에는 하단의 두 방법이 나와있음.
- @UseGuards(AuthGuard('local')) vs @UseGuards(LocalAuthGuard)
- 커스터마이징 가능성:
- AuthGuard('local'): 기본 Passport 가드를 직접 사용하며, 커스터마이징이 제한적
- LocalAuthGuard: 클래스를 확장하여 추가 로직이나 에러 핸들링을 구현할 수 있음
- 코드 재사용성:
- AuthGuard('local'): 매번 'local' 문자열을 직접 입력해야 함
- LocalAuthGuard: 재사용 가능한 클래스로, 타입 안정성이 더 높음
- 유지보수성:
- AuthGuard('local'): 변경이 필요할 때 사용된 모든 곳을 수정해야 함
- LocalAuthGuard: 한 곳에서 로직을 관리할 수 있어 유지보수가 용이
- 에러 핸들링:
- AuthGuard('local'): 기본 에러 핸들링만 가능
- LocalAuthGuard: handleRequest 메소드를 오버라이드하여 커스텀 에러 핸들링 가능
- 의존성 주입:
- LocalAuthGuard는 NestJS의 DI 시스템을 활용할 수 있어, 다른 서비스를 주입받아 사용 가능
- 커스터마이징 가능성:
- 확장성도 좋고, 공식문서의 권장도 후자이니 @UseGuards(LocalAuthGuard) 를 사용!!
⇒ 다음은 local 로그인 성공 시 일어날 로직 구현이다.
JWT 설치
pnpm add @nestjs/jwt passport-jwt
pnpm add -D @types/passport-jwt
JWT 모듈에 import, 환경변수 세팅
//auth.module.ts
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '99d' },
}),
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy],
})
- config module을 사용중이기 때문에 다음과 같은 방식으로 환경변수를 넣어주었다.
- localStrategy에서 validate를 한 이후 데이터를 받기 때문에 provider에 localStrategy를 추가해준다.
Auth.service에 createJWT 함수 추가
//auth.service.ts
import { JwtService } from '@nestjs/jwt';
constructor(
private jwtService: JwtService
) {}
async createJWT(user: Omit<User, 'password'>) {
const payload: JWTPayload = { username: user.username, sub: user.id, email: user.email };
return {
access_token: this.jwtService.sign(payload), //주입 받은 jwtService 사용
};
}
}
- 로그인 성공시 로직에 필요한 jwt토큰 생성 함수를 만든다.
localLogin함수에 로그인 성공 시 로직 추가
//auth.controller.ts
import { LocalAuthGuard } from './local/local-auth.guard';
@UseGuards(LocalAuthGuard)
async localLogin(@Request() req) {
return {
status: 'success',
data: await this.authService.createJWT(req.user),
};
}
- validate()함수의 리턴값이 Request객체의 user속성에 들어간다.
- req.user속성을 파라미터로 받아 위에서 작성한 createJWT() 함수를 통해 토큰을 생성하고 응답한다.
⇒ 이렇게 local 로그인에 대한 인증 및 로그인 로직은 끝났다.
⇒ 다음은 응답받은 jwt를 활용해 요청이 올 때 jwt를 검사하는 로직이다.
JWT
- JWT 모듈에대한 세팅은 위에서 완료해주었으니 생략
JWT 전략 생성
//auth/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload) {
return {
id: payload.sub,
username: payload.username,
email: payload.email,
};
}
}
- jwtFromRequest : JWT가 Request에서 추출되는 방법.
- 현재 코드에서는 authHeader에서 brear토큰을 제공하는 방식으로 지정
- ignoreExpiration : JWT토큰 만료 확인
- 만료된 경우 에러 전송
- secretOrKey : 토큰서명 비밀키
- 위의 세 옵션처럼 , 토큰추출→ 토큰 만료여부 확인 → 토큰서명 검증순으로 진행
- 검증실패 시 401 에러 전송
- local login과는 다르게 validate의 파라미터로 토큰안에 있는 payload가 들어온다.
- payload에 있는 내용은 변경되거나 할 경우는 거의 없기 때문에 그대로 서비스로직으로 넘겨주어도 된다.
- 추가적인 검증이 서비스로직에서 필요하다면 validate에 추가해주면된다.
auth.module에 공급자로 추가
//auth.module.ts
providers: [AuthService, LocalStrategy, JwtStrategy]
- local login과 마찬가지로 validate가 종료되면 데이터를 strategy로 받기때문에 추가해준다.
JwtAuthGuard 생성
//auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
사용
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
- local Login과 마찬가지로 @UseGuards 를 사용하여 필요한 기능에 붙인다.
반응형
'NestJS' 카테고리의 다른 글
[NestJS] Global-guard (0) | 2024.11.17 |
---|---|
[NestJS] Github OAuth 구현 (0) | 2024.11.17 |
[NestJS] TypeORMError: Entity metadata for User#applicants was not found (0) | 2024.11.10 |