PortOne API 로 계좌 검증하기 (feat. CORS)

아현·2024년 4월 28일
0

🗂️Archive

목록 보기
4/5
post-thumbnail

개요

팀 프로젝트를 진행하며 결제, 정산 구현을 담당하게 되었다.

글을 사고 팔 수 있는 중개 플랫폼으로,

  • 포인트를 충전하고 사용자 간 거래가 발생하면 판매자에게 수익이 적립된다.
  • 적립된 수익은 정산 요청을 통해 출금할 수 있다.
  • 정산 요청을 하기 위해서는 정산 정보 등록(=계좌 정보)이 필요하다는 시나리오였다.

위와 같은 기능을 구현하기 위해 포트원(PortOne) 에서 제공하는 1) 결제, 2) 예금주 조회 기능을 사용하고자 했다. 그 중에서도 결제 기능은 다소 순조롭게 구현할 수 있었지만.. 정산 정보 등록시 계좌 검증을 하기 위해 예금주 조회 API를 이용하는 것이 마음대로 되지 않았다.

이를 해결하였던 과정을 정리해볼 겸 글을 작성해 보려고 한다. ✍🏻

💡 예금주 조회 API (V1)
https://developers.portone.io/api/rest-v1/vbank#get%20%2Fvbanks%2Fholder

예금주 조회 API 호출 순서

  1. access token 발급 (/users/getToken)
  2. 발급 받은 access token을 이용해 예금주 조회 API 호출 (/vbank/holder)

예금주 조회 API 사용하기

Access Token 발급

POST /users/getToken
Request Body imp_key, imp_secret

포트원 API를 사용하기 위해서는 인증 토큰(access token)을 매 요청시 마다 Authroization 헤더에 포함하여 사용자 인증을 진행해야 한다. 발급 받은 인증 토큰(access token)은 30분간 유효하며 그 이후에는 재발급이 필요하다.

토큰 발급을 위해서 /user/getToken을 호출해야 하는데, 이 때 필요한 imp_secret, imp_key 값을 얻기 위해서 아래 경로로 이동한다.

REST APPI KEY, SECRET 발급 경로

  • [포트원 관리자] - [연동 관리]- [식별 코드 · API Keys]
  • REST API Key 와 Secret 키를 통해 인증 토큰(access token)을 발급 받는다.
    • imp_key : REST API Key
    • imp_secret : REST API Secret
  • 인증 토큰(access token)은 발급 후 30분간 유효한 토큰 (만료 시 재발급)

토큰 발급에 필요한 요청 정보인 Secret과 Key는 외부에 노출되면 안되는 값이므로 보안을 위해 서버측에서 처리하기로 했다.

🔗iamport-rest-client-java

build.gradle

// 포트원에서 제공하는 SDK를 의존성에 추가

repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }  // 추가
}

dependencies {
  // 추가
  implementation 'com.github.iamport:iamport-rest-client-java:0.1.6'
}

application.yml

portone: 
  api:
    key: {REST_API_KEY}
    secret: {REST_API_SECRET}
  • 포트원에서 발급 받은 REST API Key, REST API Secret 값을 yml 파일에 추가
    (단, Github에는 올라가지 않도록 yml 파일 구분해서 관리하기!)

PortOneService.java

@Service
public class PortOneService {

    private final IamportClient iamportClient;

    @Autowired
    public PortOneService(@Value("${portone.api.key}") String key,
                          @Value("${portone.api.secret}") String secret) {
        this.iamportClient = new IamportClient(key, secret);
    }

    public String issueToken() {
        return iamportClient.getAuth().getResponse().getToken();
    }

PortOneApiController.java

@Controller
@RequestMapping("/api/portone")
@RequiredArgsConstructor
public class PortOneApiController {

    private final PortOneService portOneService;
    
    @GetMapping("/issueToken")
    public ResponseEntity<String> issueToken() {
        return ResponseEntity.ok(portOneService.issueToken());
    }

예금주 조회 API

GET /vbanks/holder
Request Header Authorization: Bearer {ACCESS_TOKEN}
Request Query bank_code, bank_num

문제는 여기서 발생했다. javascript fetch를 통해 access token을 발급 받고, 다시 한번 fetch로 아래의 코드를 이용해 예금주 조회 API를 호출했지만 돌아오는 것은 빈 화면뿐..

   fetch('https://api.iamport.kr/vbanks/holder?bank_code={은행코드}&bank_num={계좌번호}', {
            method: 'get',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'bearer {ACCESS_TOKEN}'
            }
        })
            .then(response => response.json())
            .then(json => console.log(json));

개발자 도구를 확인해봐도 설정을 제대로 안해놨었는지 (특히 콘솔 탭 설정?) 오류가 제대로 뜨지 않아서 뭐지.. 뭐가 잘못된거지? 고민하다가 Postman으로 호출해보았다.
Postman

근데 잘만 된다. ㅠ__ㅠ
이래 저래 헛짓을 하다가 콘솔 설정을 다시 하고 보니 오류 메시지가 아래와 같이 나타났다.
DevTools

CORS..? 그게 뭔데..? 왜 내 앞길을 막아
이에 관련해서 좀 더 찾아보던 찰나에 포트원 Github Issue에서 아래와 같은 문구를 발견하게 된다.

가상계좌시 계좌번호를 알 수 있는 방법이 없나요?? #63


결국, 자바스크립트로 호출하면 안된다는 것 이었다. Secret과 Key를 클라이언트를 통해 전달하면 노출될 수 있으니 클라이언트에서 호출하는 것을 막아 놓은 것 같다.
하지만 ... 나의 삽질은 계속 되었다 ^___^ㅎ..

  • 아 ! 그럼 Access Token 발급만 서버단에서 진행하고, 계좌 검증 API는 Access Token을 이용해서 클라이언트에서 하면 되는거 아니야?
    • 지금 생각하면 무진장 바보 같은 생각이었다..ㅠㅠ
      계좌 검증 API를 호출해 놓고 CORS 에러를 봤으면서 왜 이런 생각을 했을까?;;
  • 근데 또 이렇게 헷갈렸던 건 예금주 조회 API에는 Secret, Key를 넘기는 것이 아니라 인증 토큰(access token) 넘겨주기 때문에 상관 없다고 생각해서 계속 클라이언트 호출을 시도 했던 것 같은데.
  • 계좌 검증 후 넘어오는 이름이나 그 외 API에 사용자의 개인정보가 포함되어 있으니 서버 측에서 호출하도록 설정이 되어있었던 것이 아닐까 싶음! (아마도...)

아무튼 이번에는 서버측에서 예금주 조회 API를 호출해보자!
아쉽게도 포트원 SDK(IamportClient)에서 예금주 조회 API 기능을 지원하지 않는 듯 하여, RestTemplate 을 이용해 요청하는 방식을 이용하였다.

PortOneService.java

@Service
public class PortOneService {

    private final RestTemplate restTemplate;

    private final IamportClient iamportClient;

    @Autowired
    public PortOneService(@Value("${portone.api.key}") String key,
                          @Value("${portone.api.secret}") String secret,
                          RestTemplateBuilder restTemplateBuilder) {
        this.iamportClient = new IamportClient(key, secret);
        this.restTemplate = restTemplateBuilder.build();
    }

    public String issueToken() {
        return iamportClient.getAuth().getResponse().getToken();
    }

    public CommonResponse getAccountHolder(String bankCode, String bankNum) throws URISyntaxException {

        try {
            String accessToken = issueToken();
            String fullUrl = UriComponentsBuilder.fromHttpUrl(IamportClient.API_URL + "/vbanks/holder")
                                                 .queryParam("bank_code", bankCode)
                                                 .queryParam("bank_num", bankNum)
                                                 .toUriString();

            HttpHeaders headers = new HttpHeaders();
            headers.setBearerAuth(accessToken);
            headers.setContentType(MediaType.APPLICATION_JSON);

            RequestEntity<Object> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, new URI(fullUrl));
            IamportResponse iamportResponse = restTemplate.exchange(requestEntity, IamportResponse.class).getBody();

            return new CommonResponse(true, null, iamportResponse.getResponse());

        } catch (HttpClientErrorException e) {
            return new CommonResponse(false, "계좌 검증 실패", null);
        }
    }
}
  • RestTemplate 을 생성자 주입
  • getAccountHolder(): 검증 정보를 받아 올바른 계좌 정보인 경우 계좌 소유자를 반환함
    • 이 때, bankCode는 금융결제원 표준코드 3자리

PortOneApiController.java

@RestController
@RequestMapping("/api/portone")
@RequiredArgsConstructor
public class PortOneApiController {

    private final PortOneService portOneService;

    @RequestMapping("/vbank")
    public ResponseEntity<CommonResponse> isAccountValid(@RequestBody UserSettlementDTO userInfo) throws URISyntaxException {
        return ResponseEntity.ok(portOneService.getAccountHolder(userInfo.getBankCode(), userInfo.getAccountNumber()));
    }
}
  • isAccountValid() : Service의 return 값을 응답

CommonResponse.java

@Getter
@Setter
public class CommonResponse {

    private Boolean success;
    private String message;
    private Object response;

    public CommonResponse(Boolean success) {
        this(success, null, null);
    }

    public CommonResponse(Boolean success, String message) {
        this(success, message, null);
    }

    public CommonResponse(Boolean success, Object response) {
        this(success, null, response);
    }

    public CommonResponse(Boolean success, String message, Object response) {
        this.success = success;
        this.message = message;
        this.response = response;
    }
}
  • 응답 형태를 예쁘게 만들고 싶어서 만들었던 공통 응답용 객체

결과적으로..

RestTemplate을 이용하여 서버 측에서 직접 HTTP 요청을 날려서 예금주 조회 API를 응답받아 해당 결과를 다시 클라이언트 측에 전달하는 방식으로 구현하게 되었다.
더 나은 코드나 예제가 있을 수도 있겠지만 우선 다시 한번 정리하고자 하는 마음으로 기록한다!🤣

profile
💻🤦🏻‍♀️

0개의 댓글