NestJS - 인증

바그다드·2023년 11월 26일
0

nest g module auth
nest g controller auth --no-spec
nest g service auth --no-spec

1.유저 데이터 유효성 검사

2. 비밀번호 암호화

bcryptjs 설치

npm install --save bcrypt @types/bcrypt

비밀번호를 그대로 db에 저장하게 되면 관리자는 누구나 암호를 확인할 수 있다.
따라서 db에 저장하기 전에 암호화를 하고 이 값을 db에 담아줘야 한다.

보통 hash를 통해 암호화를 하는데, 같은 비밀번호를 사용할 경우 동일한 암호가 나오게 된다.
이렇게 되면 레인보우 테이블이라는 것을 이용해 비밀번호를 유추하게 될 위험이 생긴다.
따라서 salt라는 특정 키값을 이용해 이 값과 비밀번호를 더해 hash를 생성하고, 이 값을 db에 저장하면 된다.

예시코드

    async createUser(authCredentialDto: AuthCredentialDto): Promise<void>{
        const{username,password} = authCredentialDto;

        // 암호화 로직
        const salt = await bcrypt.genSalt(); // salt 생성
        const hashedPassword = await bcrypt.hash(password, salt); // hash암호 생성

        const user = this.userRepository.create({username, password: hashedPassword});

        try {
            await this.userRepository.save(user);    
        } catch (error) {
            if(error.code === '23505'){
                throw new ConflictException('이미 존재하는 id입니다.');
            }else{
                throw new InternalServerErrorException();
            }
        }
        
    }

JWT

json web token을 뜻한다. session의 경우 session에 유저 정보를 저장하고, session id를 쿠키로 내려주는 식으로 진행되는데 이렇게 할 경우 서버에 데이터가 누적되기 때문에 jwt를 사용하는게 로드밸런싱이나, 서버 스토리지에 유용하다.

JWT 발급

JWT 모듈 설치

npm install --save @nestjs/jwt @nestjs/passport passport passport-jwt --save

모듈 등록

  imports: [
  	// passport와 jwt모듈 추가
    PassportModule.register({ defaultStrategy: 'jwt'}),
    JwtModule.register({
      secret: 'Secret1234',
      signOptions: {
        expiresIn: 60 * 60
      }
    }),
    TypeOrmModule.forFeature([User])
  ],

JWT 발급

이제 모듈을 활용해 로그인 성공 시에 JWT를 발급해주자.

    async signIn(authCredentialDto: AuthCredentialDto): Promise<{accessToken: string}> {
        const { username, password } = authCredentialDto;
        const user = await this.userRepository.findOneBy({username});
		
        // 입력받은 password와 db에 저장된 password를 비교하여 일치한지 체크
        if(user && (await bcrypt.compare(password, user.password))){
            
            // 유저 토큰 생성 (secret + payload)
            const payload = { username };
            const accessToken = await this.jwtService.sign(payload);

            return { accessToken };
        }else{
            throw new UnauthorizedException('login Falid');
        }
    }
  • jwt의 페이로드는 단순한 인코딩만 이뤄지므로 민감한 정보를 담아서는 안된다. 따라서 일단 username만 담아서 jwt를 발급하자.

JWT를 활용한 로그인

passport-jwt 타입 정의 모듈 설치

npm install @types/passport-jwt --save

jwt.strategy.ts 생성

  • 여기서 jwt의 유효성 검사와 user 데이터 조회가 이뤄진다.
    PassportStrategy를 상속받아 구현해야 한다.
    PassportStrategy를 초기화할 때 시크릿 키와 jwt를 인자로 넘겨야 하는데, jwt는 request객체에 Authorization키에 Bearer라는 이름으로 저장되어 있다.

    이 시크릿 키와 jwt를 이용해 유효한 토큰인지 검증하고, db에서 매칭되는 사용자 정보를 가지고 온다.
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy){
    constructor(
        @InjectRepository(User)
        private userRepository:Repository<User>
    ){
        console.log("strategy ",Strategy);
        
        super({
            secretOrKey: 'Secret1234',
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
        })
    }

    async validate(payload){
        const {username} = payload;
        const user:User = await this.userRepository.findOneBy({username});

        if(!user){
            throw new UnauthorizedException();
        }

        return user;
    }
}
  • jwt활용해 유효한 토큰인지 확인하고, 유효한 토큰이라면 UserRepository에서 해당 유저 정보를 찾아 return한다.

strategy, passportmodule 등록

이제 jwtstrategy와 passportmodule을 모듈에 등록해줘야 하는데, jwtstrategy와 passportmodule의 경우 인증이 필요한 다른 모듈에서도 사용해야 하므로 exports에도 등록해주자.

@Module({
  // 생략
  providers: [AuthService, UserRepository, JwtStrategy], // JwtStrategy 등록
  // exports에 등록
  exports: [JwtStrategy, PassportModule]
})

UserGuards 데코레이터 적용

JwtStrategy에서 반환하는 user정보를 활용하기 위해 UserGuards를 사용하면 된다.

  • UserGuards의 인자로 AuthGuard()를 넘기면 request 안에 사용자 정보를 넣을 수 있다.
    @Post('/test')
    // 토큰이 유효한 토큰인지 확인하고, request에 사용자 정보를 담아주는 역할을 함
    @UseGuards(AuthGuard())
    test(@Req() req){
        console.log('Req', req);
        
    }

커스텀 데코레이터 생성

  • 현재 req를 보면 많은 키가 들어있는 것을 알 수 있다.
    그중에 우리가 사용할 값은 user인데, req.user로도 접근할 수 있지만 user정보만 반환하는 커스텀 데코레이터를 만들어보자.
import { ExecutionContext, createParamDecorator } from "@nestjs/common";
import { User } from "./user.entity";

export const GetUser = createParamDecorator((data, ctx: ExecutionContext): User => {
    
    const req = ctx.switchToHttp().getRequest();
    return req.user;
})
  • 데코레이터 적용
    @Post('/test')
    @UseGuards(AuthGuard())
    // test(@Req() req){
    test(@GetUser() user:User){
        console.log('user', user);
    }
}

권한 확인 기능 적용

이제 게시물을 가져올 때 토큰을 확인해 권한을 체크하도록 변경해주자.

@Controller('boards')
@UseGuards(AuthGuard()) // 컨트롤러 레벨로 가드 적용
export class BoardsController {
    // 생략
}

컨트롤러 단위로 가드를 적용해주면 해당 컨트롤러의 모든 핸들러에 가드가 적용되어 권한 체크를 해준다.

  • 인증 실패시
  • 인증 성공시
    업로드중..

user와 board 데이터의 관계 형성

게시물에는 게시물 관련 내용 뿐만 아니라 게시물을 작성한 사용자의 정보와도 관계를 맺게 된다. TypeORM을 이용해 관계를 맺어준다.

  • user 엔티티
@Entity()
@Unique(['username'])
export class User{
	// 생략

    // 게시물과 관계 형성하기
    @OneToMany(type => Board, board => board.user, {eager: true})
    boards: Board[];
}

eager: true는 해당 엔티티를 조회할 때 참조중인 엔티티(board)도 함께 가져오는 것이다

  • board 엔티티
@Entity()
// export class Board extends BaseEntity{
export class Board{
	// 생략
    @ManyToOne(type => User, user=> user.boards, {eager: false})
    user: User;
}

eager를 false로 줬으므로 user정보 가져오지는 않는다.

게시물 생성 시 유저 정보 넣어주기

guards로 user정보를 받아와 board생성시 담아주면 된다.

	// 컨트롤러
    @Post()
    @UsePipes(ValidationPipe)
    createBoard(
        @Body() createBoard: CreateBoardDto,
        @GetUser() user: User) : Promise<Board>{

        return this.boardService.creatBoard(createBoard,user);
    }
    // service 생략
    // 리포지토리
        async createBoard(createBoardDto: CreateBoardDto, user:User): Promise<Board>{
        const{title, description} =createBoardDto;

        const board = this.boardRepository.create({
            title,
            description,
            status: BoardStatus.PUBLIC,
            user // user정보 담아주기
        })

        await this.boardRepository.save(board);
        return board;
    }

Nestjs의 미들웨어 종류

  1. pipes
    유효성 검사 및 입력 데이터를 변환
  2. filters
    오류 처리 미들웨어
    특정 오류 처리기를 사용할 경로와 각 경로 주변의 복잡성을 관리하는 방법을 일 수 있음
  3. guards
    인증 미들웨어
    지정된 경로로 통과할 수 있는 사용자와 아닌 사용자를 서버에 알려줌
  4. interceptors
    응답 매핑 및 캐시 관리, 요청 로깅 같은 전후 미들웨어
    각 요청의 전후에 동작함

미들웨어 동작 순서
middleware - guard - interceptor - pipe - controller - service - controller - interceptor - filter - client

profile
꾸준히 하자!

0개의 댓글