-
Nestjs로 구현하는 Authentication & Authorization(3)개발이야기 2021. 4. 11. 15:05
이 포스트는 NestJS를 사용하여 로그인 API를 구현하는 방법을 정리한 글이다. NestJS 의 공식문서를 참고하였으며 passport, typeorm 을 사용한다. 아래와 같이 4개의 글로 나누어 작성하였으며 이 글에서는 세번째로 passport를 사용하여 로그인 인증을 구현하는 방법을 정리하였다.
전체 코드는 github에 업로드 되어 있다.
1. Auth 생성
로그인 기능을 구현하기 위해 auth와 관련된 파일을 생성한다.
nest generate module auth nest generate controller auth nest generate provider auth
2. Auth Module
// auth.module.ts @Module({ imports: [ UserModule, PassportModule, JwtModule.register({ secret: jwtConstants.secret, signOptions: { expiresIn: '60s' }, }), TypeOrmModule.forFeature([User])], providers: [AuthService, LocalStrategy, JwtStrategy], controllers: [AuthController], }) export class AuthModule {}
auth.module.ts에 auth에서 사용할 다른 모듈을 정의한다. passport를 사용하기 위해 PassportModule와 JwtModule을 import한다. 관련된 passport package는 다음 명령어로 설치한다.
npm install --save @nestjs/passport passport passport-local npm install --save @nestjs/jwt passport-jwt
Passport는 여러가지 strategy를 제공한다. 이중에서 id/password 기반으로 동작하는 LocalStrategy을 사용하여 로그인 기능을 구현하고, 로그인을 할 때 토큰을 생성하여 이후 요청을 관리할 수 있는 JwtStrategy를 사용한다.
3. Auth Controller
// auth.controller.ts @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @UseGuards(LocalAuthGuard) @Post('login') async login(@Req() req) { return this.authService.login(req.user); } @UseGuards(JwtAuthGuard) @Get('profile') getProfile(@Req() req) { return req.user; } }
auth.controller.ts에서 로그인과 토큰에 저장된 정보를 확인하기 위한 route 처리를 수행한다. 이때 Guard를 사용하여 해당 함수를 실행하기 전에 필요한 작업을 수행할 수 있다.
4. Guard
4.1 AuthLocalGuard
passport에서 제공하는 local-strategy를 사용하여 login 함수를 수행하기 전에 id와 password를 검증한다. LocalAuthGuard는 Passport의 local-strategy를 수행하는 Guard로 다음과 같이 정의한다.
// local-auth.guard.ts @Injectable() export class LocalAuthGuard extends AuthGuard('local') {}
LocalAuthGuard가 실행하는 실제 함수는 local.strategy.ts에 있는 validate함수로 다음과 같이 정의한다.
//local.strategy.ts @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super({ usernameField: 'userId' }); } async validate(userId: string, password: string): Promise<any> { let loginUserDto: LoginUserDto = { userId: userId, password: password, } const user = await this.authService.validateUser(loginUserDto); if(!user) { throw new UnauthorizedException(); } return user; } }
생성자에서 usernameField를 원하는대로 지정할 수 있다. default로 username, password에 각각 id와 password가 매핑되며 생성자에서 원하는 이름으로 변경할 수 있다. validate 함수는 id와 password를 인자로 받아서 유효성을 검증하는 함수이다. 검증에 성공하면 사용자 객체를 반환하고 실패하면 예외를 던진다.
4.2 JWTAuthGuard
토큰에 저장된 정보를 확인하기 전에 passport의 jwt-strategy를 사용하여 토큰을 검증한다.
JWTAuthGuard는 Passport의 jwt-strategy를 수행하는 Guard로 다음과 같이 정의한다.// jwt-auth.guard.ts @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}
LocalAuthGuard가 실행하는 실제 함수는 jwt.strategy.ts에 있는 validate함수로 다음과 같이 정의한다.
// jwt.strategy.ts @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: jwtConstants.secret, }); } async validate(payload: any) { return { userId: payload.userId, userName: payload.userName, seq: payload.seq, role:payload.role }; } }
생성자에서 검증에 사용할 토큰의 종류를 선택하고 만료된 토큰은 사용할 수 없도록 ignoreExpiration을 true로 저장한다. secretOrKey는 토큰을 생성할 때 필요한 정보로 auth.module.ts에서 사용한 값과 동일한 상수를 지정한다.
실습을 위해 코드 상에서 secret을 다음과 같이 지정하였다.
실제로 사용할때는 이 값이 노출되지 않도록 안전한 곳에 보관하여야 한다.// constants.ts export const jwtConstants = { secret: 'secretKey', };
5. Auth Service
// auth.service.ts @Injectable() export class AuthService { constructor( @InjectRepository(User) private userRepository: Repository<User>, private jwtService: JwtService ) {} async validateUser(loginUserDto: LoginUserDto): Promise<any> { const user = await this.userRepository.findOne({userId: loginUserDto.userId}); if (!user) { throw new ForbiddenException({ statusCode: HttpStatus.FORBIDDEN, message: [`등록되지 않은 사용자입니다.`], error: 'Forbidden' }) } const isMatch = await bcrypt.compare(loginUserDto.password, user.password); if (isMatch) { const { password, ...result } = user; return result; } else { throw new ForbiddenException({ statusCode: HttpStatus.FORBIDDEN, message: [`사용자 정보가 일치하지 않습니다.`], error: 'Forbidden' }) } } async login(user: any) { const payload = { userId: user.userId, userName: user.userName, seq: user.seq, role:user.role }; return { accessToken: this.jwtService.sign(payload) } } }
auth.service.ts에서는 db에 있는 데이터를 불러와서 id, password를 검증한다. validateUser함수에서는 id로 사용자 정보를 db에서 조회한 후 password가 일치하는지 비교한다. password가 일치한다면 사용자 정보에서 password를 제외한 user 객체를 반환한다. 검증에 성공하면 login 함수가 실행된다. login함수에서는 인자로 받은 user객체를 사용하여 토큰을 생성한다. 토큰에 들어가는 정보는 payload 객체이며 여기에 원하는 값을 넣으면 된다. 생성된 토큰은 accessToken이라는 이름으로 json 형태로 반환된다.
POST http://localhost:3000/auth/login Content-Type: application/json { "userId": "admin", "password": "admin" }
전체적인 프로세스를 살펴보면 위와 같은 요청이 들어왔을 때 LocalAuthGuard의 validation 함수가 실행되어 유효성을 검증하고 검증에 성공하면 user 객체를 반환한다. 반환된 user객체는 login 함수의 req.user에 들어가고 서비스에 저장된 login함수를 실행한다. login함수에서는 user객체의 정보를 통해 토큰을 생성하고 accessToken이라는 이름으로 json 형태로 토큰을 반환한다.
GET http://localhost:3000/auth/profile Authorization: 'Bearer {accessToken}'
토큰에 저장된 정보를 확인하기 위해서 위와 같은 GET 요청을 수행하면 저장된 정보를 확인할 수 있다.
'개발이야기' 카테고리의 다른 글
Apache Httpd Configuration 정리 (0) 2021.08.16 docker를 활용한 apache-tomcat 이중화 서버 구현 (0) 2021.05.13 Nestjs로 구현하는 Authentication & Authorization(4) (0) 2021.04.11 Nestjs로 구현하는 Authentication & Authorization(2) (1) 2021.03.26 NestJS로 구현하는 Authentication & Authorization(1) (0) 2021.03.26