JWT 인증 인가 (accessToken)

JBoB·2023년 2월 13일
0
post-custom-banner

전에 한번 정리했던 것을 복습하자면

JWT

JWT 구조

이처럼 Nest.Js를 통해 JWT를 구현해보는 시간을 가졌다.

이것을 다시 한번 복습해보자.

🐧 JWT accessToken 구현

시작하기 전에 앞서 패키지들을 설치해줘야한다.

yarn add @nestjs/jwt passport-jwt
yarn add @nestjs/passport
yarn add --dev @types/passport-jwt
yarn add bcrypt
yarn add --dev @types/bcrypt

설치후 auth 폴더 생성 후 module.ts & resolver.ts & service.ts 파일 생성해준다.

기본 뼈대 만들어주기

//auth.module.ts

@Module({
  imports: [
  ],

  providers: [
  ],
})
export class AuthModule {}

//auth.reslover.ts

@Resolver()
export class AuthResolver {
  constructor(
    private readonly authService: AuthService, //
  ) {}

  @Mutation(() => String)
  login(
		@Args('email') email: string, //
    @Args('password') password: string,
): {}
}

// auth.servcie.ts

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

    private readonly usersService: UsersService, //
  ) {}

  login({ email, password }: IAuthServiceLogin): {}

  getAccessToken(): {}
}

@Args 인자로 이메일과 비번을 받을 걸 정했으니 그에 따라 login API 작성하기

//auth.reslover.ts

import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { AuthService } from './auth.service';

@Resolver()
export class AuthResolver {
  constructor(
    private readonly authService: AuthService, //의존성주입
  ) {}

  @Mutation(() => String)
  async login(
    @Args('email') email: string, // @Args()데코레이터 사용하여 데이터 지정
    @Args('password') password: string,
  ): Promise<string> { //accessToken를 받아야하므로 string
   // Promise 기다려주는 타입 !
    return this.authService.login({ email, password });
  }
}

login 하기 위해 검증로직을 짜야하므로 아까 만들었던 service.ts 만들어주기

// auth.service.ts
import { JwtService } from '@nestjs/jwt'; //앞에 설치하였던 Jwt 사용
import * as bcrypt from 'bcrypt'; // 앞에 설치하였던 bcrypt
																	// 전체 사용을 위한 *

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService, // 의존성 주입 사용

    private readonly usersService: UsersService, // 의존성 주입 사용
  ) {}

  async login({ email, password }: IAuthServiceLogin): Promise<string> {
    // 1. 이메일이 일치하는 유저를 DB에서 찾기
    const user = await this.usersService.findOneByEmail({ email });

    // 2. 일치하는 유저가 없으면?! 에러 던지기!!!
    if (!user) throw new UnprocessableEntityException('이메일이 없습니다.');

    // 3. 일치하는 유저가 있지만, 비밀번호가 틀렸다면?!
		// 아래 로직 설명 유저안에있는 비밀번호! 순서틀리지 말기
    const isAuth = await bcrypt.compare(password, user.password);
    if (!isAuth) throw new UnprocessableEntityException('암호가 틀렸습니다.');

    // 4. 일치하는 유저도 있고, 비밀번호도 맞았다면?!
    //    => accessToken(=JWT)을 만들어서 브라우저에 전달하기
    return this.getAccessToken({ user });
  }

// JwtSercive 의존성 주입 사용하여 토큰 생성후 4번에 전달 
// AccessToken 은 바로 생성되기에 async~awiat 사용 x 
  getAccessToken({ user }: IAuthServiceGetAccessToken): string {
    return this.jwtService.sign(
      { sub: user.id },
      { secret: process.env.JWT_ACCESS_KEY, expiresIn: '1h' },
    );
  }
}

// auth-service.interface.ts

export interface IAuthServiceLogin {
  email: string;
  password: string;
}

// users.service.ts

 findOneByEmail({ email }: IUsersServiceFindOneByEmail) {
    return this.usersRepository.findOne({ where: { email } });
  } // 추가
    // userDB에 존재하는 email과 login시 작성한 email이 동일한지 확인하는것.

resolver 와 service 합치기

// auth.module.ts

@Module({
  imports: [
    JwtModule.register({}), // register({}) 사이에는 토큰을 만들때 필요한 설정들을 넣어줄수 있음 
    TypeOrmModule.forFeature([ // **user table을 조회하기 위해 사용**
      User, //
    ]),
  ],
  providers: [
    AuthResolver, //
    AuthService,
    UsersService,
  ],
})
export class AuthModule {}

🐤Authorization(인가) Flow Chart

Resource Owner(사용자) 가 인증이 필요한 경우 Client(애플리케이션 서버) 는 발급받은 JWT를

Request Header 에 보내준다.

Backend는 JWT를 받고 Guard를 통해 JWT Strategy를 실행하고,

Secret Key를 통해 JWT를 Decoding 합니다.

JWT를 복호화 한 후에 원하는 API의 Business Logic이 수행된 후, Response 됩니다.

양식은 항상 다음과 같은 형식으로 보내줍니다.

{"Authorization":"Bearer accesstoken정보"} 
// Bearer : 토큰을 통해 인증할 때 Bearer 용어를 붙여서 사용하는 약속으로 큰 의미가 없는 문자열

보내주게 되면 브라우저 네트워크에 해당 토큰 정보가 들어옵니다.

🐧Passport Module 구현

Passport

: 인기 있는 node.js 인증 라이브러리로서 자격 증명(JWT, 사용자 이름/암호)을 확인하여 사용자를 인증하고, 인증 상태를 관리하고, 인증된 사용자에 대한 정보를 Route Handler에서 사용할 수 있도록 Request 객체에 첨부해 줍니다.

시작하기 전 설치해야 할 패키지

yarn add @nestjs/jwt passport-jwt
yarn add --dev @types/passport-jwt
yarn add @nestjs/passport
yarn add passport

로그인 한 사람의 정보를 가지고 오는 API를 만들어 준다. 그 전에 만들어둔 fetchUser API는 누구나 사용할 수 있는 API 이기에

로그인을 했든,안했든 누구나 다 사용 할 수있는 API이다.

// users.resolver.ts

@UseGuards(AuthGuard('access'))
  @Query(() => String)
  fetchLoginUser(): string {
    console.log('인가에 성공했습니다.');
    return '인가에 성공했습니다.';
  }

여기서 로그인을 했는지 안했는지 검증을 하기 위한 방어막을 씌어준다.방어막은 UseGuard() 이다.

이 부분은 GraphQl 구현으로 이따가 설명!

// user.resolver.ts

import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import * as bcrypt from 'bcrypt';
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Resolver()
export class UsersResolver {
  constructor(
    private readonly usersService: UsersService, //
  ) {}

	@UseGuards(AuthGuard('access'))
  @Query(() => String)
  fetchUser(): string {
		console.log('인가에 성공하였습니다')
		return '인가에 성공하였습니다.'
  }

  @Mutation(() => User)
  async createUser(
    @Args('email') email: string,
    @Args('password') password: string,
    @Args('name') name: string,
    @Args({ name: 'age', type: () => Int }) age: number,
  ): Promise<User> {
    const hashedPassword = await bcrypt.hash(password, 10);
    return this.usersService.create({ email, hashedPassword, name, age });
  }
}

이제 방어막을 사용할 수 있게 검증 로직을 만들어 준다. 1차 구현 rest-api만을 위한 구현

// jwt-access.strategy.ts

import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access'){
	constructor(){
		//자식이 부모한테 값을 던져주고 싶을 때 super 함수를 쓴다.
		//자식 passprt 부모 JwtAccessStrategy
		super({
			jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
			// bearer을 제외한 문자열 추출
			secretOrKey: process.env.JWT_ACCESS_KEY
			// 복호화할 키
		})
	}
		validate(payload) {	// 인증 성공 시 payload 열람하기 위한 validate 
    return { //검증 실패시 에러가 반환!
      email: payload.email, 
      id: payload.sub,	// payload.sub 는 인증할 때 sub : id 로 담았던 id
    };
  }
}

🐙 검증 로직 정리

  1. 프론트에서 fetchUser API 요청
  2. Query(fetchUser) 를 설치하기 전 먼저 ‘access’ 검증
  3. jwt-access.strategy.ts 에서 super를 통해 JWT 옵션값들이 PassportStrtegy 로 넘겨져 jwt 토큰 방식으로 검증을 시작
  4. 검증이 완료되면 토큰을 복호화되었을때 나오는 id와 email 값을 payload 형태로 반환
  5. 검증이 실패하면 에러 반환. 통과되면 API 실행 , 실패하면 API가 실행되지 않음.

🐤fetchUser 2차 구현 ( UseGuards 실습 )

GraphQl에서는 Guards를 사용하기 위해 추가적인 로직이 필요하다.

즉, GraphQL에서는 @UseGuards(AuthGuard('access'))를 사용할 수 없습니다.

GraphQL에 사용 하기 위해선 graphql 에서 필요한 부분을 뽑아서 AuthGuard로 보내주어야 합니다.

// gql-auth.guard.ts

import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';

// GqlAuthAccessGuard 는 AuthGuard('access')를 상속받음
// GqlAuthAccessGuard 실행 후 AuthGuard 실행 (오버라이딩)
// 오버라이딩: 부모클래스가 실행후 자식클래스 실행 즉,재할당?
export class GqlAuthAccessGuard extends AuthGuard('access') {
  // 반드시 getRequest 함수로 해야한다.
	getRequest(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context);
      // rest-api 용으로 들어오는 context 이기에 GraphQL 용으로 다시 만들어줌 (오버라이딩)
    return gqlContext.getContext().req;// user.resolver.ts 파일의 
																			 // GqlAuthAccessGuard 로 리턴
  }
}

AuthGuard('access')GqlAuthAccessGuard 로 상속시켜 주었기에 users.resolver.ts 파일을 수정해주자

// users.resolver.ts

// @UseGuards(AuthGuard('access'))
@UseGuards(GqlAuthAccessGuard) // 수정
  @Query(() => String)
  fetchLoginUser(): string {
    console.log('인가에 성공했습니다.');
    return '인가에 성공했습니다.';
  }

로그인 하여 accessToken 복사해주자.

Jwt에서 받은 accessToken를 실어서 실행하면 fetchUser API가 성공적으로 실행했다는걸 볼수 있다.

🐤fetchUser 3차 구현 ( User 정보 가지고 오기 )

유저정보를 가져오기 위해 resolver 를 수정해준다.

// user.resolver.ts

  @UseGuards(GqlAuthAccessGuard)
  @Query(() => String)
  fetchUser(
    @Context() context: IContext, //
//context : 모든 resolver 함수에 전달되며, 
//					현재 로그인한 사용자 / DB access 와 같은 중요 정보를 담는다.
//context 는 Request / Response / header / payload 등 에 대한 정보들을 담고 있다.
  ): string {
    // 유저 정보 꺼내오기
    console.log('================');
    console.log(context.req.user);
    console.log('================');
    return '인가에 성공하였습니다.';
  }

context 타입을 지정해주는 IContext를 만들어주기 위해 새로운 파일을 생성한다.

// context.ts
// context 타입을 지정해주는 IContext를 
import { Request, Response } from 'express';

export interface IAuthUser {
  user?: { // 검증이 실패할 수도 있어서 값이 필수적이지 않을때는 ?써준다.
    email: string;
    id: string;
  };
}

export interface IContext {
  req: Request & IAuthUser; // validate를 통해 보내주는 payload 정보인 로그인한 user 정보까지
  res: Response;            // 담아준다 : IAuthUser
}

3차 구현 결과

validate에 존재하는 payload값과

fetchUser에 존재하는 유저의 정보도 받아 올수 있다.

profile
간절하고 치열하게 살자
post-custom-banner

0개의 댓글