DAY22

yejichoi·2022년 12월 14일
0
post-thumbnail

Algorithm Study

Backend Class

토큰 기반 인증 시스템의 장점

무상태(Stateless) & 확장성(Scalability)
Stateful Server의 경우 클라이언트에게 요청을 받을 때마다 상태를 유지하고 정보를 서비스 제공에 이용
Stateless Server에서는 상태정보를 저장하지 않고, 서버는 클라이언트의 요청만으로 작업을 처리하며 세션을 사용x => 토큰을 사용하면 클라이언트와 서버의 연결고리가 없어 서버를 확장하기에 매우 적합한 환경을 제공
확장성(Extensibility)
로그인 정보가 사용되는 분야를 확장

서버 : 백엔드 / 클라이언트: 프론트엔드

Json Web Token

JWT는 웹표준으로서 C, Java, Python, JS등 대부분의 주류 프로그래밍 언어에서 지원
필요한 모든 정보를 자체적으로 가지고 있어 자가 수용적(Self-contained)이며 그렇기에 두 개체 사이에서 쉽게 전달될 수 있다는 장점

  • Header

Header는 토큰의 타입과 해싱 알고리즘이라는 두가지 정보를 담고 있음
{"alg":"HS256","typ":"JWT"}

  • Payload

Payload에는 토큰에 담을 정보가 들어가며, 담는 정보의 한 조각은 name/value의 한 쌍으로 이루어진 Claim이라고 부름
Claim은 Registered, Public, Private의 세 분류로 나뉘어져 있으며 Registered Claim은 토큰 발급자, 토큰 제목, 토큰 만료시간, 토큰 발급시간 등 토큰에 대한 정보를 담기 위해 이미 이름이 정해진 Claim

  • Signature

JWT의 마지막 부분은 서명으로, Header의 인코딩값과 Payload의 인코딩값을 합친 후 주어진 비밀키로 해싱하여 생성

login API

authentication과 authorization
authentication(인증) : 로그인을 하는 것(로그인을해서 토큰을 받아오는 과정)
authorization(인가) : 로그인 한 후, 로그인이 필요한 서비스들을 사용할 때 해당 유저임을 확인하는 것(리소스에 접근 할 수 있도록 토큰을 확인하는 과정)

// auth.resolver.ts
@Resolver()
export class AuthResolver {
  constructor(
    private readonly userService: UserService, //
    private readonly authService: AuthService,
  ) {}

  @Mutation(() => String)
  async login(
    @Args('email') email: string, //
    @Args('password') password: string,
  ) {
    // 1. 로그인(이메일이 일치하는 유저를 DB에서 찾기)
    const user = await this.userService.findOne({ 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.authService.getAccessToken({ user });
  }
}

조회한 유저의 정보를 가지고 와서 회원가입 시 bcrypt를 사용해 암호화한 비밀번호와 로그인 시 입력한 비밀번호가 일치하는지 확인하기 위해서 bcrypt의 compare 메서드를 사용해 검사했으며, 만약 일치하지 않으면 에러를 반환
bcrypt.compare(로그인 시 입력하는 비밀번호, DB에 저장되어 있는 암호화된 비밀번호)

//user.service.ts
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async findOne({ email }) {
    return await this.userRepository.findOne({ email });
  }
// auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

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

  getAccessToken({ user }) {
    return this.jwtService.sign( //토큰발급
      { email: user.email, sub: user.id },
      { secret: 'myAccessKey', expiresIn: '1h' },
      //JWT은 누구든지 열어볼 수 있기에 많은 데이터를 저장하지 않는게 좋음
    );
  }
}
  • secret : accessToken 키를 작성하는 부분으로 원하는 비밀번호를 문자열 형태로 작성할 수 있음

    • 만든 토큰을 무제한 사용 가능하다면 토큰 키를 탈취당했을 때 해당 사람 이름(철수)으로 토큰을 계속 사용 => 토큰 만료시간(expiresIn)을 가급적 짧게 해야 안전성이 보장
      토큰 만료 시간은 1s(1초), 1m(1분), 1h(1시간), 1w(1주)로 작성하는 것이며, 만료시간이 지나면 해당 토큰은 무효화가 되어 사용 불가능
    • jwtService를 사용하여 토큰이 바로 생성되기에 async ~ await는 사용x

  • json data : 유저의 정보가 담긴 payload를 의미
  • sercretKey : 서명된 JWT를 생성할 때 사용하는 키(암호화와 복호화에서 사용되는 키)
  • option : 해싱 알고리즘(기본적으로 HS256 해싱 알고리즘을 사용), 토큰 유효기간 및 발행자 지정 가능
// auth.module.ts

@Module({
  imports: [
    JwtModule.register({}), // jwtService의 주입
    TypeOrmModule.forFeature([User]), //user table을 조회
  ],
  providers: [
    AuthResolver, //
    AuthService,
    UserService,
  ],
})

회원 조회 API 인가

유저 프로필 정보 조회하기 위해서는 로그인 한 사람만 본인의 프로필을 조회할 수 있기에 accesstoken을 통해 인가를 받아야 함

사용자의 인증이 필요한 경우 Client는 발급받은 JWT를 Requet Header(HTTP Header)에 실어 같이 보내줌 -> Backend는 JWT를 받고 Guard를 통해 JWT Strategy를 실행하고, Secret Key를 통해 JWT를 Decoding -> JWT를 복호화한 후에 원하는 API의 Business Logic이 수행된 후, Response

//HTTP Headers라는 부분을 graphql playground에 적기 
{"Authorization":"Bearer accesstoken정보"}

Passport module
자격증명(JWT, 사용자 이름/암호)을 확인하여 사용자를 인증하고, 인증 상태를 관리하고, 인증된 사용자에 대한 정보를 Route Handler에서 사용할 수 있도록 Request 객체에 첨부

// user.resolver.ts
	@UseGuards(AuthGuard('myGuard'))
//로그인을 한 유저면 fetchUser API를 실행시키고
//로그인을 하지 않은 유저면 fetchUser API가 실행되지 못하게함
  @Query(() => String)
  fetchUser() {
    console.log('fetchUser 실행 완료!!!');
		return 'fetchUser 실행 완료!!!'
  }
}
// jwt-access.strategy.ts
//토큰을 인증해 줄 Guard를 제작
//rest-api
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';

export class JwtAccessStrategy extends PassportStrategy(Strategy, 'myGuard') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // req.headers.Authorization... 
      //(Header)에 존재하는 jwt token을(fromAuthHeaderAsBearerToken()) 추출
      secretOrKey: 'myAccessKey', //payload 정보 뽑아옴 
    });
  }

  validate(payload) { //검증에 성공(인가에 성공)한다면 payload를 열어서 사용자의 정보를 반환
    console.log(payload); // { email: c@c.com, sub: qkwefuasdij-012093sd }
    return { //context 안의 req에 user라는 이름으로 email과 id 정보가 담긴 객체를 user 안으로 return 
      email: payload.email,
      id: payload.sub,
    };
  }
}
  • PassportStrategy(인가를 처리할 방식, 나만의 인증 방식 이름) : 해당 secret키가 맞는지 복호화를 시도해보며, 만료기간 남았는지 등을 확인
  • 나만의 인증 방식 이름을 설정해주는 것은 어떤 문자열을 사용해도 상관 없지만, user.resolver.ts 파일에서 작성한 AuthGuard 이름과 같아야함
  • context 안의 req에 user라는 이름으로 email과 id 정보가 담긴 객체를 user 안으로 return (passport에서 user를 자동으로 만들어 주기에, 바꿀 수 없습니다). context는 요청 정보이기에 API 중간중간 어디서든 뽑아서 사용할 수 있음
>검증로직
1. 프론트에서 fetchUser API를 요청
2. Query(fetchUser)를 실행하기전에 먼저 ‘myGuard’ 검증을 실행 
3. strategy 파일에서 ‘myGuard’의 이름을 가진 검증 로직을 찾음
4. 해당 검증 로직을 찾아 super을 통해 JWT 옵션값들이 `PassportStrtegy` 로 넘겨져 jwt 토큰 방식으로 검증을 시작 
5. 검증이 완료되면 토큰을 복호화되었을때 나오는 id와 email 값을 payload 형태로 받을 수 있음
    
    검증이 완료되지 않으면 프론트로 에러가 반환
    
    따라서, 검증에서 통과가 되면 fetchUser API가 실행 되고, 통과되지 않으면 API 가 실행되지 않음.
// gql-auth.guard.ts

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

export class GqlAuthAccessGuard extends AuthGuard('myGuard') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}
  • getRequest 검증 함수 : rest-api 용도의 함수를 graphql 용도의 함수로 바꿔줌(= overriding).
    • context : request 요청에 포함된 Headers 등의 내용들이 담겨져 있습니다.
    • GqlExecutionContext.create(context) : rest-api 용으로 들어오는 context이기에 GraphQL용 context로 다시 만들어 줌
    • getContext() : GraphQL용 context를 가지고 와서 안에 들어있는 req 정보만 뽑아줌
    • return : user.resolver.ts 파일의 GqlAuthAccessGuard 로 리턴
//User 정보 가지고 오기
// user.resolver.ts
@UseGuards(GqlAuthAccessGuard)
  @Query(() => String)
  fetchUser(
    @Context() context: IContext //현재 로그인한 사람의 정보를 받아오기
  ) {
    // 유저 정보 꺼내오기
    console.log(context.req.user)
    console.log('fetchUser 실행 완료!!!');
    return 'fetchUser 실행 완료!!!';
  }
  • GraphQL에서는 req, res를 담고있는 context 객체가 존재
    • context 내부에는 req, res(header, body) 뿐만 아니라, validate를 통해 보내주는 payload 정보까지 존재
// context.ts

export interface IUser {
  // 타입 내 존재하는 인가를 거쳐야지만 user 정보가 있기에 user의 정보가 존재할 수도 있고, 없을 수도 있음
    user?: {
        email: string
        id: string
    }
}
  
export interface IContext {
    req: Request & IUser
  //IContext 타입 내부에서는 Request 뿐 아니라, validate를 통해 보내주는 payload 정보인 로그인한 user 정보까지 담아줌
  //두 개의 타입을 합칠때는 & 연산자를 사용
    res: Response
}

HW

0개의 댓글