E-commerce Application(Nest js Microservice) - 4. Auth-Service(3)

yellow_note·2021년 8월 20일
0

#0 bcryptpassword

이전 포스트에서 패스워드 암호화를 작성하지 못하여 패스워드 암호화에 관한 부분을 작성하고 인증에 관한 부분을 처리하도록 하겠습니다.

npm install --save bcrypt
npm install --save-dev @types/bcrypt

그리고 util폴더를 만들어 이 hash에 관한 부분을 처리하도록 하겠습니다.

  • util.hash.ts
import * as bcrypt from 'bcrypt';

export const hash = async (password: string): Promise<string> => {
    const saltOrRounds = 10;

    return await bcrypt.hash(password, saltOrRounds);
}

export const isHashValid = async (password, encryptedPwd): Promise<boolean> => {
    return await bcrypt.compare(password, encryptedPwd);
}

이 util을 이용하여 user.service.ts에서 user의 password를 암호화 하겠습니다.

  • user.controller.ts -> register()
public async register(userDto: UserDto) : Promise<ResponseUser> {  
    try {
        const user = new UserEntity();

        user.email = userDto.email;
        user.encryptedPwd = await hash(userDto.password);
        user.nickname = userDto.nickname;
        user.userId = uuid();

	await this.userRepository.save(user);
            
        const responseUser = new ResponseUser();

        responseUser.email = user.email;
        responseUser.encryptedPwd = user.encryptedPwd;
        responseUser.nickname = user.nickname;
        responseUser.userId = user.userId;

	return responseUser;
    } catch(err) {
        throw new HttpException(err, HttpStatus.BAD_REQUEST);
    }
}
  • 암호화 확인

postman으로 요청한 결과와 데이터베이스에 저장된 값을 본 결과 암호화가 잘 된 모습을 볼 수 있습니다.

#1 Passport

Authentication(인증)은 대부분의 애플리케이션에서 아주 중요한 부분입니다. 이런 인증에 관한 부분은 nest js에서는 passport라는 라이브러리를 이용하여 처리합니다.

우선 nestjs에서 이 passport라이브러리를 사용하기 위해 다음과 같은 명령어로 설치를 진행하겠습니다.

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

그리고 auth에 관한 service와 module을 만들도록 하겠습니다.

nest g module auth
nest g service auth

authService는 패스워드를 검증하고 유저에 관한 정보를 불러오는 서비스입니다. 그렇기 때문에 validateUser()라는 메서드를 만들고 검증에 대한 부분을 구현하도록 하겠습니다. 그 전에 user.service.ts파일에서 loadByEmail()이라는 메서드를 만들어 email을 인자로 유저를 찾도록 하겠습니다.

user.service.ts - loadByEmail()

public async loadByEmail(email: string) : Promise<any> {
        return await this.userRepository
                         .findOne({ where: { email: email }});
    }
}

그리고 user.module.ts에서 추가할 설정이 있습니다. exports라는 설정인데 여기에 UserService클래스를 넣어서 향후 AuthService에서 UserService의 메서드들을 사용할 수 있게 만듭니다.

user.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from 'src/entity/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}

이제 AuthService에서 validateUser를 구현해보도록 하겠습니다.

  • auth.service.ts -> validateUser
import { Injectable } from '@nestjs/common';
import { UserService } from 'src/user/user.service';
import { isHashValid } from 'src/util/util.hash';

@Injectable()
export class AuthService {
    constructor(private readonly userService: UserService) {}

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

        if(user && isHashValid(password, user.encryptedPwd)) {
            const { ...result } = user;

            return result;
        }

        return null;
    }
}

validateUser메서드를 살펴보면 email, password를 인자값으로 받아 이를 바탕으로 user 객체를 생성합니다. 이 user객체는 앞서 UserService에서 만든 loadByUsername(email)을 이용합니다. 그리고 user가 존재하고 isHashValid(password, user.encryptedPwd)가 타당할 경우에만 result에 user값을 담아 반환합니다. isHashValid는 로그인 시 요청받은 password값과 mariadb에 저장되어있는 encryptedPwd를 비교해주는 메서드입니다.

그리고 local.strategy.ts라는 파일을 auth폴더에 만들어 Passport local authentication strategy를 implement하도록 하겠습니다.

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

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

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

        return user;
    }
}

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

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

  • auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UserModule, PassportModule],
  providers: [AuthService, LocalStrategy]
})
export class AuthModule {}

추후에 여러 guard를 만들 수 있으므로 독자적인 guard를 만들어 주겠습니다.

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

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

#2 중간결과 확인

AppController에 UseGuards를 이용하여 LocalStrategy를 이용한 검증을 하도록 하겠습니다.

  • app.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/strategy/local-auth.guard';

@Controller()
export class AppController {
  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }  
}

loadByEmail를 이용해 user를 가져왔으므로 예상되는 결과값은 UserEntity에 대한 값들일 것입니다. 중간 결과를 확인해보겠습니다.

예상한 것처럼 UserEntity에 대한 값들이 반환이 된 모습을 볼 수 있습니다.

#3 jwt

jwt를 연동하기 위하여 라이브러리들을 설치하도록 하겠습니다.

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

그리고 AuthService에 이 jwt관련 코드들을 입력해보고 살펴보도록 하겠습니다.

import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserDto } from 'src/dto/user.dto';
import { UserService } from 'src/user/user.service';
import { isHashValid } from 'src/util/util.hash';

@Injectable()
export class AuthService {
    constructor(
        private readonly userService: UserService,
        private readonly jwtService: JwtService,  
    ) {}

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

        if(user && isHashValid(password, user.encryptedPwd)) {
            const { ...result } = user;

            return result;
        }

        return null;
    }

    async login(userDto: UserDto) {
        const payload = { 
            email:  userDto.email,
            sub: userDto.userId,
        };
        
        return {
            access_token: this.jwtService.sign(payload),
        }
    }
}

AuthService에서는 login 메서드를 만들었습니다. userDto의 값을 전달받아 이를 이용해 payload를 만들고 이를 이용해 jwt token을 생성합니다.

jwt 토큰을 생성하고 토큰 복호화를 위해서는 plain text가 필요합니다. 이 plain text를 auth폴더에 contants.ts라는 파일로 만들어 보겠습니다.

  • constants.ts
export const jwtConstants = {
    secret: 'user_token',
};

그리고 이 jwt관련 코드들을 auth.module.ts에 추가하도록 하겠습니다.

  • auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { LocalStrategy } from './strategy/local.strategy';

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '43200s' }
    })
  ],
  providers: [
    AuthService,
    LocalStrategy,
  ],
  exports: [AuthService],
})
export class AuthModule {}

jwtModule.register를 살펴보면 secret 부분에 앞서 설정해두었던 jwtConstants의 plain text를 이용하였고, signOptions는 토큰의 만료시간입니다. 저는 12시간을 만료시간으로 할 것이기 때문에 12 * 3600의 값을 넣어주었습니다.

그러면 토큰이 잘 발급되었는지 AppController에서 authService.login을 호출하고 postman을 통해 확인을 해보겠습니다.

  • AppController
import { Body, Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { LocalAuthGuard } from './auth/strategy/local-auth.guard';
import { UserDto } from './dto/user.dto';
import { RequestLogin } from './vo/request.login';

@Controller()
export class AppController {
  constructor(private readonly authService: AuthService) { }
  
  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Body() requestLogin: RequestLogin) {
    const userDto = new UserDto();

    userDto.email = requestLogin.email;
    userDto.password = requestLogin.password;
    
    return this.authService.login(userDto);
  }  
}

postman확인 결과 토큰이 잘 발급되는 모습을 볼 수 있습니다.

#4 Authorization

토큰이 잘 발급되었으니 이 토큰을 이용하여 권한을 확인하는 과정이 필요합니다. 권한 확인을 위한 JwtStrategy, jwt.auth.guard를 만들어보도록 하겠습니다.

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

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

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

JwtStrategy는 생성하면서 헤더파일에서 jwt토큰을 읽어옵니다. 그리고 앞서 설정해두었던 plain text로 복호화를 진행하죠. 생성자에서 이러한 설정을 해두고 validate라는 메서드를 통해 payload값(유저 정보)을 반환합니다.

auth.module.ts에 JwtStrategy를 providers에 넣어주도록 하겠습니다.

  • auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { JwtStrategy } from './strategy/jwt.strategy';
import { LocalStrategy } from './strategy/local.strategy';

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '43200s' }
    })
  ],
  providers: [
    AuthService,
    LocalStrategy,
    JwtStrategy,
  ],
  exports: [AuthService],
})
export class AuthModule {}

그리고 guard폴더와 strategy폴더를 따로 나누어 strategy, guard들을 관리하도록 하겠습니다.

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

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

인가를 위한 준비가 거의 다 되었습니다. 처음에 user컨트롤러를 만들면서 uri값들을 UserContoller에 위치시켰습니다. 이제부터는 UserContoller를 삭제하고 uri들을 AppContoller에 위치시키도록 하겠습니다. 또한 로그인을 하게 되면 token정보와 로그인한 유저의 정보를 얻고싶으니 AuthService의 login 메서드 또한 수정하도록 하겠습니다.

  • AppContoller
import { Body, Controller, Get, Param, Patch, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/guard/jwt-auth.guard';
import { LocalAuthGuard } from './auth/guard/local-auth.guard';
import { UserDto } from './dto/user.dto';
import { UserService } from './user/user.service';
import { RequestLogin } from './vo/request.login';
import { RequestRegister } from './vo/request.register';
import { RequestUpdate } from './vo/request.update';
import { ResponseUser } from './vo/response.user';

@Controller('users')
export class AppController {
    constructor(
        private readonly authService: AuthService,
        private readonly userService: UserService
    ) { }
    
    @UseGuards(LocalAuthGuard)
    @Post('login')
    async login(@Body() requestLogin: RequestLogin) {
        const userDto = new UserDto();

        userDto.email = requestLogin.email;
        userDto.password = requestLogin.password;
        
        return this.authService.login(userDto);
    }
    
    @UseGuards(JwtAuthGuard)
    @Get('status')
    public async getStatus() {
        return "auth-serivce is working successfully";
    }

    @Post('register')
    public async register(@Body() requestRegister: RequestRegister): Promise<ResponseUser> {
        const userDto = new UserDto();

        userDto.email = requestRegister.email;
        userDto.password = requestRegister.password;
        userDto.nickname = requestRegister.nickname;

        return await this.userService.register(userDto);
    }

    @UseGuards(JwtAuthGuard)
    @Get(':userId')
    public async getUser(@Param('userId') userId: string) {
        return this.userService.getUser(userId);
    }

    @UseGuards(JwtAuthGuard)
    @Patch(":userId")
    public async updateUser(
        @Param('userId') userId: string,
        @Body() requestUpdate: RequestUpdate
    ) {
        const userDto = new UserDto();

        userDto.userId = userId;
        userDto.nickname = requestUpdate.nickname;
        
        return this.userService.updateUser(userDto);
    }
}
  • AuthService.login
public async login(userDto: UserDto) {
    const user = await this.userService.loadByEmail(userDto.email);
    const payload = { 
        email:  user.email,
        sub: user.userId,
    };
        
    userDto.encryptedPwd = user.encryptedPwd;
    userDto.id = user.id;
    userDto.nickname = user.nickname;

    return {
        access_token: this.jwtService.sign(payload),
        user: userDto,    
    }
}

register POST 메서드를 제외하고 전부 인가를 위해 UseGuard(JwtAuthGuard)데코레이션을 넣었습니다. 즉 GET, PATCH 요청을 위해서는 앞으로 유효한 JWT토큰을 필요로 한다는 뜻입니다. 한번 토큰을 발급받고 이 토큰을 헤더에 담아서 인가가 되는지 확인해보도록 하겠습니다.

로그인시 token, user정보가 잘 얻어져 오는 모습을 볼 수 있습니다.

토큰을 헤더에 실지 않고 GET요청시 401 Unauthorized 메시지가 오는 모습을 볼 수 있습니다.

토큰을 넣고 GET요청을 한 결과 성공 메시지가 반환되는 모습을 볼 수 있습니다.

이렇게 auth에 관한 서비스들을 대략적으로 완성을 했습니다. 다음 포스트에서는 catalog 서비스를 만들어 상품재고에 관한 서비스를 만들어보도록 하겠습니다.

참고

0개의 댓글

관련 채용 정보