[camp] 5주차 정리 (4.11~15)

36666 단어 campcamp

4.11

암호화

암호화는 전송하고, 수신하고, 저장하는 정보를 해독할 수 없도록 정보를 비밀 코드로 변환하는 기술적 프로세스
암호화되지 않은 파일에 포함된 변환되지 않은 메시지는 '평문'으로 불리고, 암호화된 형식의 메시지는 '암호문'으로 불린다.

단방향 - 양방향 암호화 비교

JWT

JWT(Json Web Token)란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다. 주로 회원 인증이나 정보 전달에 사용되는 JWT는 아래의 로직을 따라서 처리된다.

생성된 토큰은 HTTP 통신을할때

{ "Authorization": "Bearer {생성된 토큰 값}", }와 같은 방식으로 사용된다.

Nest.js Guard

API의 엔드포인트를 아무나 실행 못하게 막아주는 방어막 비스무리한거..

@nestjs/passport를 활용해서
@UseGuards(AuthGuard)와 같이 사용한다.

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

export class JWtAccessStrategy extends PassportStrategy(Strategy, 'aaa') {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'myAccessKey',
    }); // 검증부
  }

  // 검증이 완료되면 해당부분이 실행된다.
  validate(payload) {
    console.log(payload);
  }
}

4.12

RefreshToken

브라우저에서 백엔드서버로 로그인요청을 보내게되면 JWT토큰을 2개보내준다

  1. payload(AccessToken) 자동으로 쿠키안으로 돌아간다
  2. cookie(RefreshToken)
  3. 토큰을 재발급해준다
  4. 재발급받은 토큰으로 재차 엑세스요청을보낸다.

만약 현재 엑세스토큰이 죽었다면
restore로 해당토큰을 되살려주는데 cookie에있는 RefreshToken을 활용한다.

Cookie & Session (Storage)

Cookie

즉 서버를 대신해서 웹 브라우저에 저장을하고 요청할때 서버로 보내 사용자를 구분할수있게해주는것
서버가 사용자의 웹브라우저(Local)에 저장시키며 브라우저마다 저장되는 쿠키가 다르다

    1. 웹 서버가 웹 브라우저에게 보내어 저장했다가, 서버의 요청이 있을때 서버로 보내주는 문자열
    1. 웹페이지 방문시에 방문을한 기록등등... 브라우저의 정보가 담긴 텍스트파일

Session

쿠키는 웹브라우저에 노출이 되기때문에 보안문제가있다, 그런 보안 문제를 해결하기 위해 인증정보에 해당하는것을 세션에 저장시킨다

Session의 동작원리
1. Client -> Request(Server)
2. Server -> Client로 Cookie값을 체크후 Session이 없다면 새로 생성해서 Response값을 보내준다
3. Client -> 전달받은 Session값을 매 Request마다 Cookie에 담아서 Request를보낸다
4. Server -> Session으로 사용자를 식별
5. Client -> 로그인 요청시에 Session을 갱신시키고, 새로운 Session을 Response
6. Client -> 종료(브라우저 종료)시에 Session을제거하고 서버에서도 Session을 제거

소셜로그인(OAuth)이란?

OAuth는
Naver로 로그인.. Kakao로 로그인 등등.. 다른 외부 서비스에서 인증을 요청이 가능하게 제공하는것

소셜로그인의 순서

  1. Clinet에서 서버에 로그인 요청을 보낸다

  2. Server에서 해당 소셜서비스로 Request를보낸후 Response로 토큰을 받는다

  3. Server에서 받은 토큰의 정보로 Access권한을 주며 로그인 및 유저의 정보를 저장해준다

  4. Clinet로 로그인이 완료됐으니 다른곳으로 리다이렉트 시켜준다

4.13

소셜로그인 구현(메인프로젝트)

해당깃

Controller

import { User } from '../users/entities/user.entity';
import { AuthService } from './auth.service';
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { AuthGuard } from '@nestjs/passport';

interface IOAuthUser {
  user: Pick<User, 'email' | 'password' | 'name' | 'phone'>;
}

@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Get('/login/google')
  @UseGuards(AuthGuard('google'))
  async loginGoogle(
    @Req() req: Request & IOAuthUser, //
    @Res() res: Response,
  ) {
    this.authService.socialLogin(req, res);
  }
  @Get('/login/naver')
  @UseGuards(AuthGuard('naver'))
  async loginNaver(
    @Req() req: Request & IOAuthUser, //
    @Res() res: Response,
  ) {
    this.authService.socialLogin(req, res);
  }
  @Get('/login/kakao')
  @UseGuards(AuthGuard('kakao'))
  async loginKakao(
    @Req() req: Request & IOAuthUser, //
    @Res() res: Response,
  ) {
    this.authService.socialLogin(req, res);
  }
}

로그인엔드포인트로 요청이오면 해당 소셜로그인에 맞는 api로 요청을 보낼수 있도록 service단에서 처리한다.

Service

  async socialLogin(req, res) {
    let user = await this.userService.findOne({ email: req.user.email });
    if (!user) {
      const createUser = new CreateUserInput();
      createUser.email = req.user.email;
      createUser.name = req.user.name;
      createUser.password = req.user.password;
      createUser.phone = req.user.phone;

      // 1:1관계의 결제테이블
      // id값을 직접생성해서 넣어준다
      const paymentId = v4();
      createUser.payment = { id: paymentId, name: 'none' };

      user = await this.userService.create({ createUserInput: createUser });
    }
    // 로그인
    this.setRefreshToken({ user, res });
    res.redirect(
      302,
      'http://localhost:5500/main-project/frontend/login/index.html',
    );
  }

소셜로그인 요청이들어오면,
1. 현재 해당유저가 존재한다면 리프레쉬토큰을 세팅하고, 로그인을 시켜주고 리다이렉팅을 한다

  1. 유저가 존재하지않는다면, 소셜로그인 api에서 제공하는 정보를 바탕으로 새로운 유저를 생성한후 위의 로직처럼 리프레쉬 토큰을 세팅후, 로그인 및 리다이렉팅을 진행해준다.

4.14

결제의 종류와 구현

위의 그림과같이 어떤 결제인지 대해서 분기와 기능을 구현해야하기때문에 직접 결제 API를 만드는것은 매우 어렵다

그래서 이런 결제 프로세스를 프로젝트에 직접 적용하는것은 매우 어려운데 외부 API를 사용한다면 간단하게 구현이 가능하다.

IamPort API

아임포트 API의 흐름

  1. 결제하기 버튼을누르면 프론트엔드에서 아임포트사의 API로 결제를 요청한다

  2. 아임포트 API에선 PG(결제대행사)에서 결제를 요청한다

  3. PG에서는 카드사쪽으로 결제를 요청한다

  4. 결제가 마무리되면, 프론트엔드에선 백엔드서버로 결제건에대한 응답을 보내준다

  5. 백엔드는 DB에 결제정보를 바탕으로 디비에 저장한다.

아임포트 연동

Resolver

  @Mutation(() => Order)
  @UseGuards(GqlAuthAccessGuard)
  async createOrder(
    @Args('impUid') impUid: string,
    @Args('merchantUid') merchantUid: string,
    @Args('subscribeId')
    subscribeId: string,
    @CurrentUser() currentUser: ICurrentUser,
  ) {
    //
    return await this.orderService.create({
      impUid,
      merchantUid,
      subscribeId,
      currentUser,
    });
  }

Service

  async create({ impUid, merchantUid, subscribeId, currentUser }) {
    // 토큰가져오기 + 결제정보가져오기도 서비스로 나눈다

    // 1. 아임포트 토큰을 가져온다.
    const token = await this.iamportService.createIamPortToken();

    // 2. 결제정보를 찾는다
    const iamPortResult = await this.iamportService.searchIamPort({ impUid });

    const amount = iamPortResult.data.response.amount;

    // 3. 프론트에서 넘겨준 제품과 검증이 완료된 유저 정보로 해당 리포지토리로 검색을한다.
    const user = await this.userRepository.findOne({
      id: currentUser.id,
    });
    const subscribe = await this.subscribeRepository.findOne({
      id: subscribeId,
    });

    // 4. 결제정보 (amonut)와 subscribe()의 가격이 맞는지 확인
    if (!(amount == subscribe.price)) {
      throw { status: 'forgery', message: '위조된 결제시도입니다!!!' };
    } // FIXME 결제취소 완성되면 추가해야한다

    // 결제정보가 맞다면 존재하는지 검증한다.
    const checkimpUid = await this.orderRepository.findOne({
      where: { impUid: impUid },
    });

    // 존재하면에러
    if (checkimpUid) {
      throw new ConflictException('이미 등록된 결제입니다.');
    }

    const userSubs = await this.userSubscribeRepository.save({
      subscribe: subscribe,
      user: user,
    });

    console.log(userSubs);

    return await this.orderRepository.save({
      impUid: impUid,
      merchantUid: merchantUid,
      userSubscribe: userSubs,
    });
  }

해당하는 방식으로 검증 및 결제건에대한 정보를 기반으로 DB에 저장한다

4.15

결제검증과 취소

결제검증과 취소를 배운것을 기반으로 아임포트 API로 직접구현했다,

구현도중에 취소관련해서 참고를해보니 좋은글이있엇다

해당 상황에서 아임포트 측에 문의를 했고, 답변은 우리 상황에 따라 다르다는 답변을 받았다.
상대가 일부러 금액을 바꿔서 우리에게 피해를 입히려고 한 것이기 때문에 물건을 보내지 않고, 환불처리도 안해준다면
당연히 환불 때문에 우리 고객 센터에 연락이 올 것이라는 것이다. 또 그게 아니라면 금액이 다를 경우 자동으로 환불처리 되도록 코드를 진행하면
된다고 한다.

위에서의 해당 상황은, 결제시에 상대방이 위변조를시켜서 결제가됐지만 DB에 저장이안됨 (위변조의상황)인경우에는 어떻게든 결제는 완료됐지만 서버쪽에서 어떻게 대응하냐에 따라서 방법이 달라진다는걸 보여준다

나는 결제시에 위변조했다면 자동환불이 되도록 FIX

  // 허위로 결제를 만들거나 유저가 취소를 요청했을경우의 아임포트서비스 취소
  async cancelIamPort({ impUid, reason = '', currentUser }) {
    // 1. 아임포트 토큰을 가져온다.
    const token = await this.iamportService.createIamPortToken();

    // 2. 결제정보를 찾는다
    const iamPortResult = await this.iamportService.searchIamPort({ impUid });
    if (iamPortResult.data.response.status === 'canclled') {
      throw new UnprocessableEntityException('이미 취소가된 결제입니다..');
    }

    const currentOrder = await this.orderRepository.findOne({
      relations: [
        'userSubscribe',
        'userSubscribe.user',
        'userSubscribe.subscribe',
      ],
      where: { impUid: impUid },
    });

    const checkUser = await this.userRepository.findOne({
      where: { id: currentOrder.userSubscribe.user.id },
    });

    // 현재유저의 아이디와 현재주문정보의  유저 아이디를 체크한다.
    if (currentUser.id != checkUser.id) {
      throw new UnprocessableEntityException(
        '취소하려는 결제의 유저정보와 현재 유저가 맞지 않습니다.',
      );
    }

    const merchant_uid = currentOrder.merchantUid;
    const checksum = currentOrder.userSubscribe.subscribe.price;

    const cancelResult = await this.iamportService.cancelIamPort({
      reason,
      impUid,
      merchant_uid,
      checksum,
    });
    return cancelResult.data.message;
  }

이런식으로 취소가 됐는지, 주문정보가 맞는지를 체크후 최종적으로 주문을 취소시켜준다.

좋은 웹페이지 즐겨찾기