테스트 사이트 - 개발 중인 베타 버전입니다

https에서 http와 ajax 통신시 proxy 프로그램 만들기

QA 게시판에서 제목과 같은 질문이 나와서 생각난김에 정리 해서 올립니다.

 

3422125589_1655359006.3347.png

 

위 그림을 보시면 https://aaa.com 에서 javascript ajax로 http://bbb.com의 api를 호출하려고 합니다.

두 서버간 프로토콜이 다르므로 브라우저에서 보안상 통신을 허용하지 않게됩니다.

혹은 두 프로토콜이 https로 통일되더라도 bbb.com에 CORS(크로스도메인설정) 가 설정되지 않았다면 aaa.com에서는 ajax로 통신할 방법이 없죠. 

물론 프론트 기반에서 얘기 입니다. 서버 프로그램간 얘기는 아니구요.

즉 프론트 javascript에서 다른 서버의 api를 호출할때로 선 조건을 제시해야 겠군요.

 

간혹 저도 경험을 해봤는데 bbb.com에 기술적 협조요청이되지 않는 경우가 있습니다. 지금 상황에서는 bbb.com에 ssl을 설정하고 CORS도 설정하면되지만 제 경험에서는 bbb.com에서 콘텐츠는 줄께 니 알아서 해라~~라고 그냥 사뿐히 무시하는 경우를 몇번 겪었습니다.

 

서버 프로그램에서는 그냥 사뿐히 가져오지만 문제는 프론트딴에선 못가져오죠. 여기 많은 분들이 full 스택을 다루시겠지만 많은 회사에서는 엄연히 프론트 개발 영역과 서버 개발 영역을 분리하고 각 개발자의 서버 접근 권한이 제한 됩니다. 즉 내가 프론트 개발자 인데 서버 코딩해서 뷰를 뿌려 줄래~~! 이건 허용되지 않는 문제죠. 이때는 정중히 서버 개발자와 업무 협조 요청을 해야합니다. 

 

우회기법으로 내 서버내에 proxy 프로그램을 개발하고 그 proxy와 bbb.com과 통신을 시도할 수 있습니다.

 

3422125589_1655359406.7853.png

 

위 그림처럼 client.php는 ajax로 proxy.php과 통신합니다. proxy.php는 bbb.com의 api.php php 라이브러리인 curl로 통신하는 구조이죠.

 

여하튼 

client.php 소스는 대충 임의로 짰습니다.

[code]

<?php 
    include_once('./common.php');
    include_once('./head.sub.php');
?>
<div style="padding: 10px;">
    <div style="font-size: 17px; width: 350px; text-align: center;">
        <form id="myForm" action="" class="form-example" onsubmit="return false;">
            <div class="form-example">
                <label for="name">Enter your name : </label> 
                <input type="text" name="name" id="name" required value="test">
            </div>
            <br>
            <div class="form-example">
                <label for="email">Enter your email : </label> 
                <input type="email" name="email" id="email" required value="test@gmail.com">
            </div>
            <br><br>
            <div class="form-example">
                <input type="submit" class="submit" value="get">
                <input type="submit" class="submit" value="post">
                <input type="submit" class="submit" value="put">
                <input type="submit" class="submit" value="delete">
            </div>
        </form>
    </div>
    <br><br>
    <div id="result" style="font-size: 17px;"></div>
</div>

<script>
$(function(){
    $("#myForm").submit(function(){
        var url = 'https://54.hull.kr/proxy.php';
        var formData = $(this).serialize();
        var method = $(document.activeElement).val();
        $.ajax({
            url : url,
            type : method,
            data : formData,
             cache : false,
             dataType:'json',
            error : function(jqXHR, textStatus, errorThrown) {        
                alert(textStatus);
            },
            success : function(data, jqXHR, textStatus) {
                $("#result").html(JSON.stringify(data));
                setTimeout(function(){
                    $("#result").html("");
                }, 5000);
            }
        });
        return false;
    }); 
});

</script>
<?php
include_once ('./tail.sub.php');
?>

[/code] 

 

위코드에서 sumbt 버튼에 따라 get,post,put,delete 방식으로 method를 설정할 수 있게 했습니다. resetful api와 규격을 맞추기 위해서죠. 내부에 proxy.php ajax로 통신하구요.

 

proxy.php는 순수하게 php로만 짯습니다. 함수와 class는 스텍오버플로우에서 가져와서 잘 안되는 오류를 수정 했습니다. 출처는 하두 정신없이 오래전에 검색해 둔거라 어딘지는 모르겠습니다.

[code]

    class Params {
        private $params = Array();
        
        public function __construct() {
            $this->_parseParams();
        }
        
        /**
         * @brief Lookup request params
         * @param string $name Name of the argument to lookup
         * @param mixed $default Default value to return if argument is missing
         * @returns The value from the GET/POST/PUT/DELETE value, or $default if not set
         */
        public function get($name, $default = null) {
            if (isset($this->params[$name])) {
                return $this->params[$name];
            } else {
                return $default;
            }
        }
        
        private function _parseParams() {
            $method = $_SERVER['REQUEST_METHOD'];
            if ($method == "PUT" || $method == "DELETE") {
                parse_str(file_get_contents('php://input'), $this->params);
                $GLOBALS["_{$method}"] = $this->params;
                // Add these request vars into _REQUEST, mimicing default behavior, PUT/DELETE will override existing COOKIE/GET vars
                $_REQUEST = $this->params + $_REQUEST;
            } else{
                $this->params = $_REQUEST;
            }
        }
    }

    function integralCurl(string $url, string $method = "GET", array $sendData = [], array $header = [], bool $isSsl = false, array $option = []) : array{
        // $certificateLoc = $_SERVER['DOCUMENT_ROOT'] . "/inc/cacert.pem";
        $certificateLoc = "";
        $method = strtoupper($method);
        
        $defaultOptions = array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 10
        );
        
        $ch = curl_init();
        curl_setopt_array($ch, $defaultOptions);
        
        curl_setopt($ch, CURLOPT_POST, $method === "POST");
        
        if ($method === "POST") {
            /*
             $sendData 샘플
             [
             "a" => 1,
             "b" => "22"
             ]
             */
            if (count($sendData) >= 1) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $sendData);
            }
        } elseif ($method === "GET") {
            if (count($sendData) >= 1) {
                $paramsUrl = http_build_query($sendData);
                $url .= "?" . $paramsUrl;
            }
        } elseif ($method === "PUT") {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
            
            if (count($sendData) >= 1) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($sendData));
            }
        } elseif ($method === "DELETE") {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
            
            if (count($sendData) >= 1) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($sendData));
            }
        }
        
        curl_setopt($ch, CURLOPT_URL, $url);
        
        if (count($option) >= 1) {
            /*
             $option 샘플
             [
             CURLOPT_HEADER => false,
             CURLOPT_USERAGENT => "test"
             ]
             */
            curl_setopt_array($ch, $option);
        }
        
        if (count($header) >= 1) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        }
        
        if ($isSsl === true && $certificateLoc != "") {
            curl_setopt($ch, CURLOPT_CAINFO, $certificateLoc);
        }
        
        $returnData = curl_exec($ch);
        $returnState = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        
        if ($returnData === false) {
            $returnErr = "CURL ERROR: " . curl_error($ch);
        } else {
            $returnErr = "success";
        }
        
        curl_close($ch);
        if($returnData){
            $returnData = json_decode($returnData,true);
        }
        
        return [
            "data" => $returnData,
            "code" => $returnState,
            "msg" => $returnErr
        ];
    }

    
    $method = $_SERVER["REQUEST_METHOD"];
    $param = new Params();
    $result = $_REQUEST;
    $result['method'] = $method;
    //$result['name'] = $param->get("name","");
    //$result['email'] = $param->get("email","");

    $headers = [
        'accept: application/json,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'sec-ch-ua: "Google Chrome";v="87", " Not;A Brand";v="99", "Chromium";v="87"',
        'sec-ch-ua-mobile: ?0',
        'user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
    ];
    echo json_encode(integralCurl("http://test.hull.kr/api.php", $method , $result, $headers));

[/code]

 

new Params(); 으로 객체화를 진행하면 $_REQUEST 에 get,post,put,delete 상관없이 파싱되어 저장됩니다.

그걸 그대로 bbb.com/api.php 전송하는 것이죠. 전송할때도 기존 client.php에서 전송한 method 형식에 맞게 전송하기 위해 integralCurl() 함수를 사용합니다. integralCurl("http://test.hull.kr/api.php", $method , $result, $headers) 코드를 통해 http 서버에 기존 client.php에소 요청한 method 방식으로 데이터, 그리고 해더 값을 전송합니다.

이후 http://bbb.com/api.php 는 우리 영역이 아니므로 사실 고민할 필요는 없겠죠. 그곳에서 전송해주는 데이터만 echo 해주면 되니깐요. 

 

가상의 http://bbb.com/api.php 를 개발해 봤습니다. 별 내용은 없습니다.

 

http://test.hull.kr/api.php 내용

[code]
    class Params {
        private $params = Array();
        
        public function __construct() {
            $this->_parseParams();
        }
        
        /**
         * @brief Lookup request params
         * @param string $name Name of the argument to lookup
         * @param mixed $default Default value to return if argument is missing
         * @returns The value from the GET/POST/PUT/DELETE value, or $default if not set
         */
        public function get($name, $default = null) {
            if (isset($this->params[$name])) {
                return $this->params[$name];
            } else {
                return $default;
            }
        }
        
        private function _parseParams() {
            $method = $_SERVER['REQUEST_METHOD'];
            if ($method == "PUT" || $method == "DELETE") {
                parse_str(file_get_contents('php://input'), $this->params);
                $GLOBALS["_{$method}"] = $this->params;
                // Add these request vars into _REQUEST, mimicing default behavior, PUT/DELETE will override existing COOKIE/GET vars
                $_REQUEST = $this->params + $_REQUEST;
            } else{
                $this->params = $_REQUEST;
            }
        }
    }
    
    
    $method = $_SERVER["REQUEST_METHOD"];
    $param = new Params();
    $result = $_REQUEST;
    $result['method'] = $method;
    //$result['name'] = $param->get("name","");
    //$result['email'] = $param->get("email","");
    
    echo json_encode($result);

[/code]

 

그냥 api.php 입장에서는 GET/POST/PUT/DELETE 방식으로 전송받은 데이터를 그대로 ceho 해주는 프로그램입니다. 정상적 통신이 되었다는 의미로 mehod 방식도 확인하여 같이 ceho 해줍니다.

 

결과 입니다.

 

 get

3422125589_1655360513.4694.png

 

 

post

3422125589_1655360557.5261.png

 

put

3422125589_1655360606.5899.png

 

delete

3422125589_1655360665.8617.png

 

 

다 이상 없이 통신이 가능했습니다.

 

간혹 aaa.com의 서버 개발 권한이 없으시면 ccc.com을 하나 세팅하시고 거기다 proxy.php 파일을 만들고 크로스도메인을 설정하시면되겠죠. ssl도 적용하시고

 

이상 허접한 팁이였습니다.

 

ps. 또 다른 방법으로는 php curl이 아니라 아예 php를 이용해서 os(리눅스 shell)에게 api 서버와 통신하게 하는 방법도 있습니다.  다음에 시간 나면 또 정리 해서 올리겠습니다.

 

댓글 작성

댓글을 작성하시려면 로그인이 필요합니다.

로그인하기

댓글 10개

그냥 guzzle 쓰네요
@나리스 넵 선택사항이죠. ^^ 질문에서는 선제조건이 프론트에서 통신이 안되서 해결책을 제시했던거구요. 서버사이드에선 아무꺼나 써도되죠. 요즘은 프론트, 서버 개발로 나뉘니깐 프론트 개발자에게 api 던져 주는 개념으로 생각하시면 될 듯 합니다.
강좌글이나 소개글 쓸 때 스샷이미지가 깔끔하면 최고입니다.
좋은 정보 얻어갑니다.
@비타주리 감사합니다. 별거 아닌 팁인데...
뭔지 잘 모르겠지만 상세한 설명 감사합니다.
좋은 정보 감사합니다.당신은 최고~~
좋은 제안 감사 합니다. 멋지네요.
소중한 시간내서 설명해 주셔서 너무 감사합니다.
건강이 좋지 않아 병원에 있다가 이제서야 몸 추스리고 확인하네요.
행복한 하루되세요.
좋은 정보 감사합니다.
감사합니다~

게시판 목록

개발자팁

개발과 관련된 유용한 정보를 공유하세요.
질문은 QA에서 해주시기 바랍니다.
글쓰기
🐛 버그신고