LifeSports Application(ReactNative & Nest.js) - 8. auth-service(2)

yellow_note·2021년 9월 24일
0

#1 인증

register는 이전 포스트에서 user.service.ts를 구현하면서 완성을 했으니 auth.service.ts에서 로그인에 관한 부분을 만들어 보도록 하겠습니다.

대략적으로 다음의 흐름대로 로그인을 위한 인증이 진행됩니다.

passport 패키지를 설치하도록 하겠습니다.

npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local

passport라이브러리를 이용하여 AuthService를 작성하도록 하겠습니다.

  • ./src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Builder } from 'builder-pattern';
import { Model } from 'mongoose';
import { statusConstants } from 'src/constants/status.constant';
import { UserDto } from 'src/dto/user.dto';
import { User, UserDocument } from 'src/schema/user.schema';
import { isHashValid } from 'src/util/util.hash';

@Injectable()
export class AuthService {
    constructor(
        @InjectModel(User.name) private userModel: Model<UserDocument>
    ) {}
    
    public async loadByEmail(email: string): Promise<any> {
        return await this.userModel.findOne({ email: email });
    }

    public async validateUser(
        email: string, 
        password: string
    ): Promise<any> {
        const user = await this.loadByEmail(email);

        if(user && isHashValid(password, user.encryptedPwd)) {
            return user
        }

        return null;
    }
}

호출 순서대로 메서드를 살펴 보면 다음과 같습니다.

1) loadByEmail : email을 인자로 유저 정보를 가져오는 메서드입니다.
2) validateUser : 1)의 메서드를 이용하여 가져온 user 정보에서 패스워드를 비교하여 로그인하고자 하는 유저가 타당한 유저인지 체크를 하는 메서드입니다. 타당하다면 user의 정보를 반환하고 그렇지 않다면 null값을 반환합니다.

AuthService를 이용하여 local.strategy.ts파일을 작성하여 하나의 데코레이터로 인증에 대한 부분을 만들도록 하겠습니다.

  • ./src/strategy/local.strategy.ts
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { AuthService } from "src/auth/auth.service";
import { statusConstants } from "src/constants/status.constant";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
    constructor(readonly authService: AuthService) {
        super({
            usernameField: 'email'
        });
    }

    async validate(
        email: string,
        password: string,
    ): Promise<any> {
        const user = await this.authService.validateUser(email, password);

        if(!user) {
            return Object.assign({
                status: statusConstants.ERROR,
                payload: null,
                message: "Not valid user"
            })
        }

        return user;
    }
}

super()에서 PassportStrategy클래스의 기능을 수정하기 위해 변경을 해야할 것이 있습니다. PassportStragy에서는 기본값으로 username을 가지고 있어서 별도의 수정이 없으면 에러가 발생합니다. 저는 email을 기반으로 로그인과 회원가입을 진행해왔기 때문에 username을 email이라는 이름으로 바꿔주겠습니다.

그리고 auth.module.ts에서 PasspostModule을 imports에, LocalStrategy를 providers에 담아주도록 하겠습니다.

  • ./src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { User, UserSchema } from 'src/schema/user.schema';
import { LocalStrategy } from 'src/strategy/local.strategy';
import { UserModule } from 'src/user/user.module';
import { AuthService } from './auth.service';

@Module({
  imports: [
    UserModule, 
    PassportModule,
    MongooseModule.forFeature([{
      name: User.name,
      schema: UserSchema
    }])
  ],
  providers: [
    AuthService, 
    LocalStrategy,
  ],
  exports: [AuthService],
})
export class AuthModule {}

LocalStrategy를 AuthGuard에 담아 UseGuard에서 사용하겠습니다.

  • ./src/guard/local-auth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

AuthService에서 로그인을 위한 비즈니스 로직을 작성하겠습니다.

  • ./src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Builder } from 'builder-pattern';
import { Model } from 'mongoose';
import { statusConstants } from 'src/constants/status.constant';
import { UserDto } from 'src/dto/user.dto';
import { User, UserDocument } from 'src/schema/user.schema';
import { isHashValid } from 'src/util/util.hash';

@Injectable()
export class AuthService {
    constructor(
        @InjectModel(User.name) private userModel: Model<UserDocument>  
    ) {}
    
    ...

    public async login(userDto: UserDto): Promise<any> {
        try {
            const payloadForSign = {
                email: userDto.email 
            };

            const user = await this.userModel.findOne({ email: userDto.email });

            if(!user) {
                return Object.assign({
                    status: statusConstants.ERROR,
                    payload: null,
                    message: "Not found user"
                });
            }

            return Object.assign({
                status: statusConstants.SUCCESS,
                access_token: this.jwtService.sign(payloadForSign),
                payload: Builder(UserDto).email(user.email)
                                         .nickname(user.nickname)
                                         .phoneNumber(user.phoneNumber)
                                         .createdAt(user.createdAt)
                                         .userId(user.userId)
                                         .build(),
                message: "Successfully Login"
            });
        } catch(err) {
            console.log(err);

            return Object.assign({
                status: statusConstants.ERROR,
                payload: null,
                message: err
            });
        }
    }
}

login메서드에서는 유저를 검증하는 로직을 거치지 않습니다. 그 이유는 컨트롤러에서 사용될 LocalAuthGuard에서 1차적으로 검증에 관한 로직을 거치기 때문에 login메서드는 이 검증이 완료된 후 진행되는 유저가 데이터베이스에 존재하는지에 관한 로직만 다룹니다.

최종적으로 controller에서 사용되는 모습입니다. 인증을 위해 LocalAuthGuard를 구현했으므로 controller에서 login메서드 위에 데코레이터를 달아주도록 합니다.

  • ./src/app.controller.ts
import { Body, Controller, Get, HttpStatus, Param, Post, UseGuards } from "@nestjs/common";
import { Builder } from "builder-pattern";
import { AuthService } from "./auth/auth.service";
import { statusConstants } from "./constants/status.constant";
import { UserDto } from "./dto/user.dto";
import { LocalAuthGuard } from "./guard/local-auth.guard";
import { UserService } from "./user/user.service";
import { RequestLogin } from "./vo/request.login";
import { RequestRegister } from "./vo/request.register";
import { ResponseUser } from "./vo/response.user";

@Controller('auth-service')
export class AppController {
    constructor(
        private readonly authService: AuthService,
        private readonly userService: UserService,    
    ) {}

    ...

    @UseGuards(LocalAuthGuard)
    @Post('login')
    public async login(@Body() requestLogin: RequestLogin): Promise<any> {
        ...
    }

    ...
}

이어서 인가에 대한 부분을 구현하도록 하겠습니다.

#2 인가

유저가 로그인을 하면 해당 유저가 검증된 유저임을 알 수 있는jwt를 발급합니다. 이 토큰은 추후에 유저가 서비스를 이용할 때 header에 실어 자신이 인가된 사용자임을 알리는데 사용됩니다. jwt를 사용하기 위하여 라이브러리들을 설치하도록 하겠습니다.

$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt

위의 라이브러리를 이용하여 유저가 로그인을 할 때 토큰을 발급받을 수 있게 구현을 하겠습니다.

  • ./src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import { Builder } from 'builder-pattern';
import { Model } from 'mongoose';
import { statusConstants } from 'src/constants/status.constant';
import { UserDto } from 'src/dto/user.dto';
import { User, UserDocument } from 'src/schema/user.schema';
import { isHashValid } from 'src/util/util.hash';

@Injectable()
export class AuthService {
    constructor(
        @InjectModel(User.name) private userModel: Model<UserDocument>,
        readonly jwtService: JwtService    
    ) {}
    
    ...

    public async login(userDto: UserDto): Promise<any> {
        try {
            const payloadForSign = {
                email: userDto.email 
            };

            ...

            return Object.assign({
                status: statusConstants.SUCCESS,
                access_token: this.jwtService.sign(payloadForSign),
                ...
    }
}

JwtService.sign은 jwt를 생성하는 메서드입니다. jwt를 확인하기 위한 secret키를 만들도록 하겠습니다.

  • ./src/constants/jwt.constant.ts
export const jwtConstant = {
    secret: 'user-token'
};
  • ./src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { jwtConstant } from 'src/constants/jwt.constant';
import { User, UserSchema } from 'src/schema/user.schema';
import { JwtStrategy } from 'src/strategy/jwt.strategy';
import { LocalStrategy } from 'src/strategy/local.strategy';
import { UserModule } from 'src/user/user.module';
import { AuthService } from './auth.service';

@Module({
  imports: [
    UserModule, 
    PassportModule,
    JwtModule.register({
      secret: jwtConstant.secret,
      signOptions: { expiresIn: '43200s' },
    }),
    MongooseModule.forFeature([{
      name: User.name,
      schema: UserSchema
    }])
  ],
  providers: [
    AuthService, 
    LocalStrategy,
  ],
  exports: [AuthService],
})
export class AuthModule {}

앞서 생성했던 secret값을 JwtModule에 담아주도록 하겠습니다. 이렇게 코드를 구현하면 로그인 시에 token에 jwt가 담겨져 유저에게 반환이 됩니다. 모든 코드를 완성한 후 전체적인 테스트를 진행하도록 하겠습니다.

앞서 만들었던 LocalAuthGuard의 방법을 이용하여 토큰에 대한 권한 확인 guard를 만들도록 하겠습니다.

  • ./src/strategy/jwt.strategy.ts
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { jwtConstant } from "src/constants/jwt.constant";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: jwtConstant.secret,
        });
    }

    async validate(payload: any) {
        return { ...payload };
    }
}

헤더에서 Bearer 토큰값을 읽어와 앞서 설정했던 secret값을 이용하여 복호화를 진행합니다. 이 strategy가 호출이 되면 생성자의 과정을 거치고 payload를 반환합니다. 이 strategy를 AuthModule의 providers에 주입하고, guard를 만들도록 하겠습니다.

  • ./src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { PassportModule } from '@nestjs/passport';
import { jwtConstant } from 'src/constants/jwt.constant';
import { User, UserSchema } from 'src/schema/user.schema';
import { JwtStrategy } from 'src/strategy/jwt.strategy';
import { LocalStrategy } from 'src/strategy/local.strategy';
import { UserModule } from 'src/user/user.module';
import { AuthService } from './auth.service';

@Module({
  imports: [
    UserModule, 
    PassportModule,
    JwtModule.register({
      secret: jwtConstant.secret,
      signOptions: { expiresIn: '43200s' },
    }),
    MongooseModule.forFeature([{
      name: User.name,
      schema: UserSchema
    }])
  ],
  providers: [
    AuthService, 
    LocalStrategy,
    JwtStrategy,
  ],
  exports: [AuthService],
})
export class AuthModule {}
  • ./src/guard/jwt-auth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

jwt를 위한 guard가 완성되었으니 인가가 필요한 endpoint에 데코레이터를 달아주도록 하겠습니다.

  • ./src/app.controller.ts
import { Body, Controller, Get, HttpStatus, Param, Post, UseGuards } from "@nestjs/common";
import { Builder } from "builder-pattern";
import { AuthService } from "./auth/auth.service";
import { statusConstants } from "./constants/status.constant";
import { UserDto } from "./dto/user.dto";
import { JwtAuthGuard } from "./guard/jwt-auth.guard";
import { LocalAuthGuard } from "./guard/local-auth.guard";
import { UserService } from "./user/user.service";
import { RequestLogin } from "./vo/request.login";
import { RequestRegister } from "./vo/request.register";
import { ResponseUser } from "./vo/response.user";

@Controller('auth-service')
export class AppController {
    constructor(
        private readonly authService: AuthService,
        private readonly userService: UserService,    
    ) {}

    @UseGuards(JwtAuthGuard)
    @Get('status')
    public async status(): Promise<string> {
        return "auth-service is working successfully";
    }

    ...

    @UseGuards(JwtAuthGuard)
    @Get(':userId')
    public async getUser(@Param('userId') userId: string): Promise<any> {
        ...
    }
}

저는 status메서드와 getUser메서드에 JwtAuthGuard 데코레이터를 달아주었습니다.

이로써 인증과 인가에 대한 부분이 완성이 되었습니다. 다음 포스트에서는 이를 테스트해보도록 하겠습니다.

참고

mongodb 설치 - https://m.blog.naver.com/wideeyed/221815886721

jwt - https://velog.io/@biuea ecommerce-micro Auth-service(2) ~ (3)

0개의 댓글

관련 채용 정보