개발새발 박스/Nest.js

Nest.js 로 NICE API 처리하기!

업폰 2022. 12. 5. 08:56
반응형

일단 여러분들에게 제가 했던 고생을 하지 않아도 되도록 설명을 드릴 예정인데요.

다들 준비 되셨죠?

 

물론.

 


 

먼저 왜 이 작업을 했는가? 라는 부분을 설명해야할 것 같다.

이번 클라이언트의 작업은 Nice 본인인증을 이용하기 위해 해당 모듈을 연결해달라는 내용이었습니다.

 

작업 일정은 7일로 잡았는데, 정말 별에 별 상황이 거듭 막아서고, 그걸 해결하는 일정이었습니다.

 

넌, 못 지나간다.

 

덕분에 + 1일이 되어, 총 8일을 작업했죠.

 

이는 제가 기존에 작업기간을 산정하는 것을 생각해보자면, 크게 벗어난 스케쥴입니다.

 

저는 테스트 / 수정에 훠어어얼씬 더 많은 시간을 할애합니다.

그래서 되도록 1.5배로 스케쥴을 잡고, 한참 먼저 개발을 마무리하고, 클리이언트의 테스트 후 추가 수정사항을 처리하는 식으로 작업을 진행하죠.

 

아무튼 이렇게 된 이유부터 해결 방안 코드까지 여러분께 공유하려고 글을 씁니다.

 

나, 멋있지?

 

좋았으면, 댓글 좀 달아주라? 나 외로워.

 


 

그래서 진행되어진 순서는 크x를 통해 수주를 받았습니다.

어쨌던, 받았으니 일을 시작해야하는데, 채팅을 통해 나눴던 대화와 현재 클라이언트 서버환경이 완전히 다름.

 

여기서 1차, 빡침이 옵니다.

꾹꾹 눌러 참고,

 

서버 정보를 받았습니다.

서버는 별도의 서버가 있고, 작업자체는 Git을 통한 작업으로 Merge 하여 배포한다고 하네요?

아마 AWS를 쓰는 것 같았습니다.

 

그리고 Nice 모듈 받았는데, 모듈을 받기까지 이틀이 걸렸지요.

 

금요일날 수주받지 마세요.

 

뭐, 그래도 앞선 설명처럼 일정에는 차질이 없을거라 생각하고, 모듈을 확인하고, 서버확인해도 일정문제는 없었을 겁니다.

 

그리고 받은 모듈을 확인해보았는데, 아... 이게 왠걸 .exe 파일을 이용해 암호화를 대신하는 처리가 필요한 상황이었습니다.

서버 환경은 Nest.js면서...

 

아무튼 저는 급하게 Nice 관련 정보를 검색하고, Nice API가 있는 것을 확인하고, 즉시 문의를 진행합니다.

문의는 다행히 당일 돌아왔고, Nice 본인인증 서비스 계약에는 Nice API가 포함되더군요.

 

덕분에 Nice API를 통해 개발을 진행하게 되었습니다.

다만, Nice API에는 아주 큰 단점이 있었는데, IP를 체크하기 때문에 입력된 IP 외의 Request는 IP가 다르다는 Response를 보낸 다는 것.

클라이언트 측은 보안에 신경을 많이 써서 관리 계정 공유도 해주지 않고, 내부 담당자가 서로 다른지 개인 IP 추가가 안된다고 하더이다.

 

그런데, 마지막 날 IP를 추가해줌, ㄹㄱㄴ

 

아무튼 그런 일이 있어, 개발과 테스트는 아래와 같은 방식으로 진행 되었습니다.

 

개발자 개발 머지     테스트
담당자         배포  

 

4일째까지만 해도 담당자가 직접 배포해야하는 상황이었고,

5일째가 되는 날, 서버에 자동 코드 수정시 자동 배포 기능을 추가 해주어서, 새벽 늦게까지도 혼자 테스트가 가능해졌습니다.

 

다만, 역시나 한번 테스트에 배포까지 약 10분가량의 시간이 소모되었고, 더욱 늦어지는 이유가 되었다는 것은 안비밀.

 

거듭 말하지만 그냥 개발자의 IP를 추가하면 됩니다?

(위와 같은 상황이 일반적으로는 발생하지 않습니다.)

 

아무튼 NICE API는 참 보안에 신경을 많이 쓴 API인 것 같습니다.

옆동네 Toss 결제 API도 이렇게까지 보안적인 기능을 요구하지는 않았는데 말이죠.

 

Nice API Flow

 

자, 보면 알겠지만 모든 처리 후 Nice API 표준창을 호출하고, 결과 값을 암호화된 내용으로 받습니다.

여기서 결과값은 이름, 성별, 휴대폰 번호 등의 정보로 계약시 어떤 내용을 받을지에 따라 금액이 변동되는 것 같습니다.

 

일단, 해당 내용을 본 클라이언트는 회의를 통해 완전히 Backend 측에서 동작하도록 처리되길 바랬습니다.

다만, API 호출은 이러니 저러니 Frontend 측 내용이다보니 1차 불가능을 표했으나, 어떻게든 해달라는 요구가 와버렸고,그래서 꾀를 써서 처리 하였는데, 그 내용은 코드를 안내하며 설명하겠습니다.

(별건 아님.)

 

별도의 module로 controller부터 service까지 모두 따로 만들었다면, 그냥 전체 모듈을 올려드리는 게 편하지만...

클라이언트 측 요청사항이 있다보니, 다른 모듈 위에 인증관련 내용이 추가되었다. Nice에 관한 내용만 하게되는 점 양해바랍니다.

 

service의 class 아래로 들어가는 함수 형태이며, 위치나 구성자체는 각자 잘 하실 수 있으리라 믿습니다.

 

1. 기관 Token 요청

더보기
async getNiceToken() {
       // api 관련 변수
      const urlNiceApi = 'https://svc.niceapi.co.kr:22001'; // www.niceapi.co.kr
      const pathGetToken = '/digital/niceid/oauth/oauth/token'; // 토큰 생성 url

      // 사용하는 클라이언트 내용
      const client_id = ''; // 클라이언트 ID
      const client_secret = ''; // 클라이언트 시크릿 키

      // 먼저, 컬럼 탐색하여, 키값 있는지 확인
      let findToken: any = false;
      const where = {
        where: {
          deleted_at: undefined,
          key: 'tokenNiceAPI',
        },
      };
      findToken = await this.dataSource.manager.findOne('DB_table', where);

      // 검색되어 false가 아니라면, 해당 키를 return;
      if (findToken) {
        return findToken.value;
      } else {
        const authorization =
          'Basic ' +
          Buffer.from(client_id + ':' + client_secret, 'utf8').toString(
            'base64',
          ); // base64 인코딩 하여, 시크릿키와 조합
        const url = urlNiceApi + pathGetToken; // url

        // 요구사항에 맞는 해더 작성
        const headersRequest = {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization: authorization,
        };

        const tokenVerify = this.httpService
          .post(
            url,
            'grant_type=client_credentials&scope=default', // 추가 요청 값
            { headers: headersRequest }, // header
          )
          .pipe(map((response) => response.data));

        // response 값 사용할 수 있도록 lastValueFrom 처리.
        // 해당 처리 이후부터 이전에 response 데이터였던 내용을 클래스 변수처럼 사용가능
        const formData = await lastValueFrom(tokenVerify);

        if (formData.dataHeader.GW_RSLT_CD != 1200) return false; // 정상 처리가 아니라면, false 반환

        // body에는 아래와 같은 내용이 실려옴
        formData.dataBody.access_token; // 토큰값
        formData.dataBody.expires_in; // 만료까지 남은 시간
        formData.dataBody.token_type; // 토큰의 타입 :: bearer로 고정
        formData.dataBody.scope; // 요청한 스코프값

        // DB 저장
        await this.dataSource.manager.save('DB_table', {
          key: 'tokenNiceAPI',
          value: formData.dataBody.access_token,
        });

        return formData.dataBody.access_token; // 완료되면, 토큰값을 반환
      }
  }

DB에 저장해둔 Token이 있는지 검색 후 → 없다면, 생성하여 저장 / 있다면, 해당 값을 반환 하는 함수로 위쪽 보면 알겠지만, 50년 인증이기에 단 한번 사용되면, 대부분 DB에서 꺼내다 쓰게 됩니다.

 


 

2. 기관 Token 폐기

더보기
async destroyNiceToken() {
      // api 관련 변수
      const urlNiceApi = 'https://svc.niceapi.co.kr:22001'; // www.niceapi.co.kr
      const pathDestroyToken = '/digital/niceid/oauth/oauth/token/revokeById'; // 토큰 삭제 url

      // 사용하는 클라이언트 내용
      const client_id = ''; // 클라이언트 ID
      const client_secret = ''; // 클라이언트 시크릿 키, 폐기에선 사용x

      findToken = this.getNiceToken();

      if (!findToken) return false; // 저장된 토큰이 없으면, return

      const dateCurrent = new Date();
      const current_timestamp = dateCurrent.getTime() / 1000;

      const authorization =
        'Basic ' + // bearer 일수도 있음, 폐기의 API 규약이 서로 다름
        Buffer.from(
          findToken.value + ':' + current_timestamp + ':' + client_id,
          'utf8',
        ).toString('base64'); // base64 인코딩 하여, 시크릿키와 조합
      const url = urlNiceApi + pathDestroyToken; // url

      // 요구사항에 맞는 해더 작성
      const headersRequest = {
        'Content-Type': 'application/x-www-form-urlencoded',
        Authorization: authorization,
      };

      const tokenVerify = this.httpService
        .post(
          url,
          null,
          { headers: headersRequest }, // header
        )
        .pipe(map((response) => response.data));

      // response 값 사용할 수 있도록 lastValueFrom 처리.
      // 해당 처리 이후부터 이전에 response 데이터였던 내용을 클래스 변수처럼 사용가능
      const formData = await lastValueFrom(tokenVerify);

      if (formData.dataHeader.GW_RSLT_CD != 1200) return false; // 정상 처리가 아니라면, false 반환

      // body에는 아래와 같은 내용이 실려옴
      formData.dataBody.result; // result > ture

      if (formData.dataBody.result == true) {
        await this.dataSource.manager.softDelete('DB_table', {
          key: 'tokenNiceAPI',
        });
      }

      return formData.dataBody.result; // 완료되면, result값 반환 (ture일 것임)
  }

토큰 관련 오류가 발생했을 때 사용할 수 있도록 폐기관련 내용도 만들어두기는 했는데, 실제로는 사용하도록 처리하지는 않았습니다.

실제로 연동규약 내에서는 토큰 잔여시간 체크 후 재발급 받을 수 있도록 권장하고 있습니다.

 


 

3. 암호화 토큰 요청

더보기
async getNiceEncryptionToken() {
      // api 관련 변수
      const urlNiceApi = 'https://svc.niceapi.co.kr:22001'; // www.niceapi.co.kr
      const pathGetApiToken = '/digital/niceid/api/v1.0/common/crypto/token'; // API 토큰 (암호화키) 발급 url

      // 사용하는 클라이언트 내용
      const client_id = ''; // 클라이언트 ID
      const client_secret = ''; // 클라이언트 시크릿 키, 암호화키 사용 x

      const findToken = await this.getNiceToken(); // 토큰은 이미 알고 있음, 없다면 생성

      if (!findToken) return false; // 토큰이 없다면, 오류

      const dateCurrent = new Date();
      const current_timestamp = dateCurrent.getTime();

      const authorization =
        'bearer ' +
        Buffer.from(
          findToken + ':' + current_timestamp + ':' + client_id,
          'utf8',
        ).toString('base64'); // base64 인코딩 하여, 시크릿키와 조합
      const url = urlNiceApi + pathGetApiToken; // url

      // 요구사항에 맞는 해더 작성
      const headersRequest = {
        'Content-Type': 'application/json',
        Authorization: authorization,
        client_id: client_id,
        productID: '2101979031', // 고정값 혹시 필요하다면, 해당 부분 또한 env 처리
      };

      // 요청 필수 값 정리
      // date format 기능이 없어, 직접 YYYYMMDDHH24MISS 형태 만들기
      const rd_YYYY = dateCurrent.getFullYear();
      let rd_MM: any = dateCurrent.getMonth() + 1;
      if (Number(rd_MM) < 10) rd_MM = '0' + String(rd_MM);
      let rd_DD: any = dateCurrent.getDay();
      if (Number(rd_DD) < 10) rd_DD = '0' + String(rd_DD);
      let rd_HH: any = dateCurrent.getHours();
      if (Number(rd_HH) < 10) rd_HH = '0' + String(rd_HH);
      let rd_MI: any = dateCurrent.getMinutes();
      if (Number(rd_MI) < 10) rd_MI = '0' + String(rd_MI);
      let rd_SS: any = dateCurrent.getSeconds();
      if (Number(rd_SS) < 10) rd_SS = '0' + String(rd_SS);
      const req_dtim =
        String(rd_YYYY) +
        String(rd_MM) +
        String(rd_DD) +
        String(rd_HH) +
        String(rd_MI) +
        String(rd_SS);
      
      // 요청고유번호
      // 그냥 getTime으로 하긴 했는데, 해당 값이 어떻게 쓰이는지 알 수 없음.
      const req_no = String(dateCurrent.getTime());

      const dataRequest = {
        dataHeader: { CNTY_CD: 'ko' }, // ko 한글
        dataBody: {
          req_dtim: req_dtim, // 요청일시
          req_no: req_no, // 요청고유번호
          enc_mode: '1', // 암복호화구분 (1:AES128/CBC/PKCS7)
          key_make: findToken + ':' + current_timestamp + ':' + client_id,
        },
      };

      const tokenVerify = this.httpService
        .post(
          url,
          dataRequest,
          { headers: headersRequest }, // header
        )
        .pipe(map((response) => response.data));

      // response 값 사용할 수 있도록 lastValueFrom 처리.
      // 해당 처리 이후부터 이전에 response 데이터였던 내용을 클래스 변수처럼 사용가능
      const formData = await lastValueFrom(tokenVerify);

      if (formData.dataHeader.GW_RSLT_CD != 1200) return false; // 정상 처리가 아니라면, false 반환
      if (formData.dataBody.rsp_cd != 'P000') return false; // P000이 정상코드
      if (formData.dataBody.result_cd != '0000') return false; // 0000이 정상코드

      const dataResult = {
        // body에는 아래와 같은 내용이 실려옴
        rsp_cd: formData.dataBody.rsp_cd, // 정상처리 여부
        res_msg: formData.dataBody.res_msg, // 오류 메세지 > 써먹는다면 윗쪽에서 사용할 수 있음
        result_cd: formData.dataBody.result_cd, // rsp_cd P000 일 때 상세결과 코드 0000만 정상
        site_code: formData.dataBody.site_code, // 사이트코드
        token_version_id: formData.dataBody.token_version_id, // 서버 토큰 버전
        token_val: formData.dataBody.token_val, // 암복호화를 위한 서버 토큰값
        period: formData.dataBody.period, // 토큰의 만료까지 남은 시간

        // formData 전체를 반환해야하는 상황으로 대칭키 생산에 필요한 데이터 추가
        req_dtim: req_dtim,
        req_no: req_no,
      };

      return dataResult; // 하위 값들을 사용할 수 있도록 반환
  }

암호화를 하기 위해 매개값으로 쓰일 암호화 토큰을 발급 받습니다.

이 녀석은 갈기갈기 분해되어, 이곳 저곳 쓰입니다.

 

위쪽의 2가지 함수를 이해했다면, 해당 함수를 이해하는데, 별 문제 없을 것이라 생각합니다.

[..]

      // 요청 필수 값 정리
      // date format 기능이 없어, 직접 YYYYMMDDHH24MISS 형태 만들기
      const rd_YYYY = dateCurrent.getFullYear();
      let rd_MM: any = dateCurrent.getMonth() + 1;
      if (Number(rd_MM) < 10) rd_MM = '0' + String(rd_MM);
      let rd_DD: any = dateCurrent.getDay();
      if (Number(rd_DD) < 10) rd_DD = '0' + String(rd_DD);
      let rd_HH: any = dateCurrent.getHours();
      if (Number(rd_HH) < 10) rd_HH = '0' + String(rd_HH);
      let rd_MI: any = dateCurrent.getMinutes();
      if (Number(rd_MI) < 10) rd_MI = '0' + String(rd_MI);
      let rd_SS: any = dateCurrent.getSeconds();
      if (Number(rd_SS) < 10) rd_SS = '0' + String(rd_SS);
      const req_dtim =
        String(rd_YYYY) +
        String(rd_MM) +
        String(rd_DD) +
        String(rd_HH) +
        String(rd_MI) +
        String(rd_SS);
      
      // 요청고유번호
      // 그냥 getTime으로 하긴 했는데, 해당 값이 어떻게 쓰이는지 알 수 없음.
      const req_no = String(dateCurrent.getTime());
 
 [..]

 

위 부분은 'YYYYMMDDHH24MISS' 형태의 Datetime 값을 만들기 위한 처리인데, PHP의 data같은 함수가 분명히 라이브러리 안에 있을텐데...

아는 사람은 해당 함수를 이용해 처리해도 무관합니다.

또한 req_no 값의 경우, 도대체 어떤식으로 이용된다는 내용이 명시 되어있지 않아, 확률상 유니크한 time으로 처리했습니다.

이 또한 고유한 값이 서비스 내 존재한다면, 그 값을 이용해도 됩니다.

 


 

4. 요청정보 암호화 + Nice 표준창 Open

더보기
async openNiceAPI(data1: any, data2: any, data3: any) {
      // 암호화 토큰 가져오기
      const tokens: any = await this.getNiceEncryptionToken();

      if (!token.rsp_cd) return false;

      const strOriginKey =
        String(tokens.req_dtim).trim() +
        String(tokens.req_no).trim() +
        String(tokens.token_val).trim();

      const hash = createHash('sha256').update(strOriginKey);
      const resultVal = hash.digest().toString('base64');

      const key = resultVal.substring(0, 16); // 대칭키
      const iv = resultVal.substring(resultVal.length - 16, resultVal.length); // iv
      const hmac_key = resultVal.substring(0, 32); // 위변조 체크

      // 전달 데이터
      // 회원 정보 수정을 위해 키값과 리다이렉션 url을 가진다.
      const receivedata = {
        data1: data1,
        data2: data2,
        data3: data3,
      };
      const edcodeData = Buffer.from(
        JSON.stringify(receivedata),
        'utf8',
      ).toString('base64'); // json str 화 이후 base64인코딩

      const dataRequest = {
        requestno: token.req_no,
        returnurl: '', // 인증 결과를 받을 url
        sitecode: token.site_code,
        methodtype: 'post',
        popupyn: 'Y',
        receivedata: edcodeData, // 인증 결과 url에 전달받을 데이터 (요청값을 그대로 해당 url에 전달) > 아무래도 배열형태의 값은 불가능한 듯하다.
      };

      // 대칭키를 통한 암호화해야함
      const dataRequest_string = JSON.stringify(dataRequest);

      const cipher = createCipheriv('aes-128-cbc', key, iv);

      let enc_data = cipher.update(dataRequest_string, 'utf8', 'base64');
      enc_data += cipher.final('base64');

      // 무결성 키
      const hmac = createHmac('sha256', hmac_key);
      const integrity_value = hmac.update(enc_data).digest().toString('base64');

      // token_version_id 를 key로 하여 key, iv 를 저장해둔다.
      // 복호화에 필요함
      await this.dataSource.manager.save('DB_table', {
        key: token.token_version_id + '-KEY',
        value: key,
      });

      await this.dataSource.manager.save('DB_table', {
        key: token.token_version_id + '-IV',
        value: iv,
      });

      // html 처리
      return (
        '<form name="openNiceAPIform" id="openNiceAPIform" method="get" action = "https://nice.checkplus.co.kr/CheckPlusSafeModel/service.cb">' + // method 지정이 안되어 있어, post로 발송되는 경우 발생 > get 지정
        '<input type="hidden" id="m" name="m" value="service" />' +
        '<input type="hidden" id="token_version_id" name="token_version_id" value="' +
        token.token_version_id +
        '" />' +
        '<input type="hidden" id="enc_data" name="enc_data" value="' +
        enc_data +
        '" />' +
        '<input type="hidden" id="integrity_value" name="integrity_value" value="' +
        integrity_value +
        '" />' +
        //'<button type="submit">실행</button>' +
        '</form>' +
        '<script>' +
        'document.openNiceAPIform.submit()' +
        '</script>'
      );
  }

해당 코드가 다 이해가 된다면, 정말 좋겠지만 현재 코드를 볼 때, 일단, 매개값이 있는 함수라는 것을 알 수 있습니다.

async openNiceAPI(data1: any, data2: any, data3: any) {

[...]

      // 전달 데이터
      // 회원 정보 수정을 위해 키값과 리다이렉션 url을 가진다.
      const receivedata = {
        data1: data1,
        data2: data2,
        data3: data3,
      };
      const edcodeData = Buffer.from(
        JSON.stringify(receivedata),
        'utf8',
      ).toString('base64'); // json str 화 이후 base64인코딩
      
[...]

해당 함수는 Controller 에서 호출될 때, 매개값을 넣게 되는데, 이는 receivedata 에 사이트 내 동작을 각종 데이터를 넘기기 위함으로 여러분의 서비스 상황에 따라 이 부분은 매개 변수가 없어도 되기에 이부분 또한 상황에 맞게 수정하시기 바랍니다.

 

그리고 굉장히 중요한 내용이 있는데, 이 receivedata는 반드시 string 형태로 입력되어야 합니다. (연동 규약에 이 내용이 빠져있어요.)

때문에 json str 처리하여, 변조를 막기위해 인코딩하여 Request에 실립니다.

[..]

      const strOriginKey =
        String(tokens.req_dtim).trim() +
        String(tokens.req_no).trim() +
        String(tokens.token_val).trim();

      const hash = createHash('sha256').update(strOriginKey);
      const resultVal = hash.digest().toString('base64');

      const key = resultVal.substring(0, 16); // 대칭키
      const iv = resultVal.substring(resultVal.length - 16, resultVal.length); // iv
      const hmac_key = resultVal.substring(0, 32); // 위변조 체크

[..]

      const cipher = createCipheriv('aes-128-cbc', key, iv);

      let enc_data = cipher.update(dataRequest_string, 'utf8', 'base64');
      enc_data += cipher.final('base64');

      // 무결성 키
      const hmac = createHmac('sha256', hmac_key);
      const integrity_value = hmac.update(enc_data).digest().toString('base64');

      // token_version_id 를 key로 하여 key, iv 를 저장해둔다.
      // 복호화에 필요함
      await this.dataSource.manager.save('DB_table', {
        key: token.token_version_id + '-KEY',
        value: key,
      });

      await this.dataSource.manager.save('DB_table', {
        key: token.token_version_id + '-IV',
        value: iv,
      });
      
[..]

또한 암호화 과정에서 Key, Iv, Hmac_Key를 생성하여 이를 이용해 hash, createCipheriv 암호화하고, 무결성값을 생성하고 Key, Iv 값은 이후 복호화에 필요하기에 DB에 저장합니다.

[..]

      // html 처리
      return (
        '<form name="openNiceAPIform" id="openNiceAPIform" method="get" action = "https://nice.checkplus.co.kr/CheckPlusSafeModel/service.cb">' + // method 지정이 안되어 있어, post로 발송되는 경우 발생 > get 지정
        '<input type="hidden" id="m" name="m" value="service" />' +
        '<input type="hidden" id="token_version_id" name="token_version_id" value="' +
        token.token_version_id +
        '" />' +
        '<input type="hidden" id="enc_data" name="enc_data" value="' +
        enc_data +
        '" />' +
        '<input type="hidden" id="integrity_value" name="integrity_value" value="' +
        integrity_value +
        '" />' +
        //'<button type="submit">실행</button>' +
        '</form>' +
        '<script>' +
        'document.openNiceAPIform.submit()' +
        '</script>'
      );
      
[..]

마지막으로 이 부분은 Frontend 에서 처리하기를 권장드립니다.

클라이언트의 요청으로 부득이하게 위와 같이 진행하였지만, 결국 Html 코드를 통해 Form을 통한 데이터 전송입니다.

 


 

5. 데이터 수신 + 수신 정보 복호화

더보기
async decryptionData(data: string, token_version_id: string) {
      // 먼저 key와 iv를 찾는다.
      const key: any = await this.dataSource.manager.findOne('DB_table', {
        where: {
          deleted_at: undefined,
          key: token_version_id + '-KEY',
        },
      });

      const iv: any = await this.dataSource.manager.findOne('DB_table', {
        where: {
          deleted_at: undefined,
          key: token_version_id + '-IV',
        },
      });

      // 기존 데이터 Key 삭제 필요
      await this.dataSource.manager.Delete('DB_table', {
        key: token_version_id + '-KEY',
      });

      await this.dataSource.manager.Delete('DB_table', {
        key: token_version_id + '-IV',
      });
      
      const cipher = createDecipheriv('aes-128-cbc', key, iv);

      let strDecoded = cipher.update(data, 'base64', 'utf8');
      strDecoded += cipher.final('utf8');

      // > 여기까지 진행시 json 배열로 디코딩 된다.
      const objDecode = JSON.parse(strDecoded); // object로 변환

      const result = {
        resultcode: objDecode.resultcode, // 결과코드
        requestno: objDecode.requestno, // 요청 고유 번호(회원사에서 전달보낸 값)
        enctime: objDecode.enctime, // 요청 고유 번호
        sitecode: objDecode.sitecode, // 사이트코드
        responseno: objDecode.responseno, // 응답 고유번호
        authtype: objDecode.authtype, // 인증수단 M:휴대폰인증, C:카드본인확인, X:공동인증서, F:금융인증서, S:PASS인증서
        name: objDecode.name, // 이름
        utf8_name: decodeURI(objDecode.utf8_name), // GET으로 받았기 때문에 %EC%9D%B4%EC%83%81%ED%98%81 형태로 받았기 때문에 decode 필요
        birthdate: objDecode.birthdate, // 생년월일
        gender: objDecode.gender, // 성별 0:남성, 1:여성
        nationalinfo: objDecode.nationalinfo, // 내외국인 코드 0:내국인, 1:외국인
        mobile_co: objDecode.mobileco, // 휴대폰 통신사 1:SKT, 2:KT, 3:LGU+, 5:SKT 알뜰폰, 6:KT 알뜰폰, 7:LGU+ 알뜰폰 > 원래는 mobile_co / mobile_no 통신규약 문서 내 항목과 다름;
        mobile_no: objDecode.mobileno, // 휴대폰 번호
        ci: objDecode.ci, // 개인 식별 코드 (CI)
        di: objDecode.di, // 개인 식별 코드 (DI)
        businessno: objDecode.businessno, // 사업자번호 (법인인증서 인증시에만)
        receivedata: JSON.parse(
          Buffer.from(objDecode.receivedata, 'base64').toString('utf8'),
        ), // 보낸 데이터 (json str 후 base64로 보냈기 때문에 변환처리)
      };

      return result;
  }

복호화의 경우, 앞서 설명했던 암호화와 같은 일을 반대로 한 내용이기에 보고 이해할 수 있으리라 생각하고,

 

Response 데이터는 Controller를 통해 받으며, 이전 API 호출 함수에서 전송한 data의 returnurl을 해당 Controller 로 지정하면 됩니다.

예를 들어, returnurl 이 [ https://up4n.tistory.com/success ] 라면, @POST('/success') 로 받아내면되는데, 견본 코드는 아래와 같습니다.

/* ------.controller.ts 파일 예시 */

[..]

@Post('/success')
  async sucessNice(
    @Body('token_version_id') token_version_id: string,
    @Body('enc_data') enc_data: string,
    @Body('integrity_value') integrity_value: string,
    @Res() res: Response,
  ) {
    const data: any = await this.authService.decodeVerifyToTVI(
      enc_data,
      token_version_id,
    );
  }
  
[..]

response 데이터는 Post data로 오기 때문에 body를 통해 받아야하고, 이렇게 받은 인증데이터를 어떻게 처리할지는 각 서비스에 맞춰 이용하시면 됩니다. return 값으로 처리하였는데, 함수자체를 수정하여 DB에 저장해도 좋고, 이 견본처럼 컨트롤러에서 데이터를 모두 받아, 필요한 값만 이용할 수도 있습니다.

 


 

이상 여기까지가 여러분을 위한... 제 경험담 겸, Nest.js 로 Nice API 처리하기 입니다.

 

글로 작성하니깐, 별 이슈가 아니었던 것 같기도 한데, 실제로는 암호화, 복호화관련 테스트로 엄청난 시간을 보냈고, 연동규약에 euc.kr 로 반환되기 때문에 인코딩이 필요하다는 내용도 있어서 마지막 result data 체크에도 엄청난 시간을 보냈습니다.

 

이게 다 IP 추가 안해줘서 고통스러운 머지 → 배포 후 테스트 때문인 것 같아요.

 


 

연동규약은 사기꾼임 ㄹㅇ

그리고 NICE API 연동규약은 좀 고칠 필요가 있을 것 같습니다.

실제 연동값과 틀린 게 너무 많아요.

 

 

반응형

'개발새발 박스 > Nest.js' 카테고리의 다른 글

httpService Request 처리 중 콘솔에 왠 로그가?  (1) 2022.11.18
Nest.js 가 왜 안되지?  (0) 2022.11.18