티스토리 뷰
[Passport + NestJS] passport와 guard 사용 정리 - 로그인 인증 로직
dv_jamie 2022. 6. 16. 16:21Passport is Express-compatible authentication middleware for Node.js.
- Passport는 미들웨어라는 것을 기억하자!
- 공식 사이트 : https://docs.nestjs.com/security/authentication
설치
npm i @nestjs/passport passport passport-local
디렉토리 만들기
간단한 로그인 로직을 구현한 후에 게시글을 회원만 조회할 수 있도록 할 예정
// auth
nest g mo auth
nest g s auth
// users
nest g mo users
nest g s users
임의의 회원 정보 데이터 만들기
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'maria',
password: 'guess',
}
]
}
getUser 로직 추가
유저가 입력한 username과 일치하는 값을 반환하는 로직
// users.srevice.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
// ... 생략
async getUser(username: string): Promise<object | undefined> {
return this.users.find(user => user.username === username)
}
}
AuthService에서 UsersService 사용하기
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
}
(공식 사이트에는 함수명을 validateUser라고 했는데, 바꿔도 되는지 확인해보려고 checkUserData로 작성해봤다.)
UsersService를 import 하려고 하면 다음과 같은 에러가 나올 것이다.
AuthService에서 UsersService를 사용했기 때문이다!
UsersService를 exports하고, AuthService가 포함된 AuthModule에서 imports하면 된다.
이렇게 외부 모듈에서 사용하려면 exports를 해줘야 한다.
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
exports: [UsersService], // UsersService 내보내기
providers: [UsersService]
})
export class UsersModule {}
// auth.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
@Module({
imports: [UsersModule], // UsersModule 가져오기
providers: [AuthService]
})
export class AuthModule {}
AuthService에 유저 네임과 패스워드가 맞는지 체크하는 로직 추가
유저가 입력한 username과 password 데이터를 인자로 받는다.
- user 데이터베이스에서 username과 일치하는 데이터를 가져옴
- 가져온 데이터 내의 password가 입력값과 일치하는지 체크
import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async checkUserData(username: string, password: string): Promise<any> {
// username을 기반으로 user 찾기
const user = await this.usersService.getUser(username)
// user가 있으면서 그 패스워드도 일치하면
if(user && user.password === password) {
const user = await this.usersService.getUser(username)
return user
}
// username에 해당하는 데이터가 없거나 패스워드 일치하지 않으면
return null
}
}
(타입은 우선 모두 any로 했다.)
user가 없을 경우 null을 반환하는데 에러 처리는 passport 구현하면서 할 예정!
Controller에 경로 작성
먼저 controllers.ts 파일을 만든다.
nest g co auth
로그인 경로를 추가한다.
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(
@Body() userData: any
): Promise<any> {
// 아래 코드는 임시 - 나중에 local.strategy.ts에 추가할 예정
return await this.authService.checkUserData(userData.username, userData.password)
}
}
여기까지 테스트
- controller에서 받은 userData
- service의 checkUserData 함수 인자로 전달
- 결과를 다시 controller에서 받아 반환
여기부터 본격적으로 Passport & Guard 이용
Passport & Guard를 이용하게 되면 아래와 같은 순서가 된다.
- Guard에 의해 LocalStrategy의 validate 내부로 감
- validate 내부 로직을 수행 (나의 로직 같은 경우 AuthService.checkUserData를 수행)
- AuthService.checkUSerData에서 결과값을 받음
- null이 아닐 경우 결과값을 Request 객체 내 user 키의 값으로 할당 + @Post Route Handler 실행
- null일 경우 { "statusCode": 401, "message": "Unauthorized" } 반환하고 중지
- @Post Route Handler 실행
LocalStrategy 파일 생성
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.checkUserData(username, password)
if(!user) {
throw new UnauthorizedException()
}
return user
}
}
여기서 주의해야할 점
(1) passport는 기본으로 username, password 라는 이름을 키 값으로 사용한다.
커스텀하게 변경을 원할 경우에는 아래와 같이 super() 내에서 따로 지정해주어야 한다.
constructor(private authService: AuthService) {
super({
usernameField: 'uid',
passwordField: 'upw'
});
}
우선은 username, 과 password를 그대로 사용할 예정
(2) validate라는 메서드명도 그대로 적어주어야 한다.
validate 말고 다른 이름을 적으면 아래와 같은 에러가 뜬다.
AuthModule 수정
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule], // PassportModule 추가
controllers: [AuthController],
providers: [AuthService, LocalStrategy], // LocalStrategy 추가
})
export class AuthModule {}
@UseGuard 사용
controller에서 @UseGuard를 사용하기 전에 인자로 사용할 LocalAuthGuard 파일을 만든다.
@UseGuards(AuthGuard('local'))
이런 식으로 바로 사용이 가능하기는 하지만, 추후에 인가를 위한 로직을 처리할 때는 Jwt 사용할 예정이기 때문에
둘을 명확하게 구분하기 위해서 파일로 따로 만드는 것!
('local'이라는 이름은 꼭 지켜주어야 한다. 나중에는 JwtAuthGuard 파일을 만들어야 한다.)
// local-auth.guard.ts 파일 생성 후 아래 로직 추가
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
위와 같이 LocalAuthGuard 파일을 만든 후 AuthController 파일에 아래와 같이 추가한다.
import { Body, Controller, Post, Req, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(
@Request() req // req.user 객체에 LocalAuthGuard.validate의 반환값이 할당됨
// @Body() userData: any // 삭제
): Promise<any> {
return req.user
// return await this.authService.checkUserData(userData.username, userData.password) // 삭제
}
}
현재는 user 객체를 반환하고 있지만, 나중에 jwt 를 이용하게 되면 token을 반환하면 된다.