[http] SOP(Same Origin Policy) / CORS > Flutter/Dart/Node

본문 바로가기
사이트 내 전체검색

Flutter/Dart/Node

[http] SOP(Same Origin Policy) / CORS

페이지 정보

작성자 sbLAB 댓글 0건 조회 3,138회 작성일 22-12-19 23:07

본문

Flutter(크롬/엣지 브라우저) 에서 WepApi 호출할 때 CORS 에러(XMLHttpRequest error.) 


Cross-Origin Resource Sharing (CORS)

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS


PHP 소스에서 CORS 설정(권장)

접속 클라이언트가 웹브라우저인 경우(크롬 또는 웹게시 플러터앱)에 서버에 HTTP_ORIGIN 전달됨, 

순수어플에서 프로그래밍되어 호출될때 기본적으로 HTTP_ORIGIN 전달 없음 -> 전달없을 때는 CORS 과정 비실행하려고 하면..

바로 아래 소스 비슷하게 하면 원하는 동작이 되는듯 하지만 사용할수 없는 부족한 소스임(하단 개선된 소스사용). 

    <?php

    $httpOrigin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : null;
if($httpOrigin){
    if (in_array($httpOrigin, [
        'http://localhost:5743',
        'http://select.domain.com'
    ])){
    header("Access-Control-Allow-Origin: ${httpOrigin}"); //header("Access-Control-Allow-Origin: *");
    header('Access-Control-Allow-Credentials: true');
    header("Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS");
    header("Access-Control-Allow-Headers: Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization");
    header("Content-type:text/html;charset=utf-8");
}
}
 



아파치 웹서버 httpd.conf 에서 CORS 설정 

예) 서버 웹 디렉토리가 D:/xampp/htdocs/jp 인 경우 (http://localhost/jp/)


    <Directory "D:/xampp/htdocs/jp">
      <IfModule mod_rewrite.c>
      RewriteEngine On
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteRule ^(.*)$ index.php [NC,L]
      </IfModule>

  <IfModule mod_headers.c>
    Header always set Access-Control-Allow-Origin "*"
    Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS"
    Header always set Access-Control-Max-Age "1800"
    Header always set Access-Control-Allow-Headers "x-requested-with, Content-Type, origin, authorization, accept, client-security-token"
  </IfModule>

      Options Indexes FollowSymLinks Includes ExecCGI
      AllowOverride None
      Require all granted
    </Directory>
   



[아파치 서버, PHP소스 모두 CORS 설정을 하지 않은 경우 크롬 에러]

Access to XMLHttpRequest at 'http://192.168.0.3/jp/memberselect' from origin 'http://localhost:2120' has been blocked by CORS policy: 

No 'Access-Control-Allow-Origin' header is present on the requested resource.

browser_client.dart:72 

POST http://192.168.0.3/jp/memberselect net::ERR_FAILED 200

  


[아파치 서버에서 CORS 설정한 상태에서 PHP소스에서도 CORS 설정을 중복한 경우 크롬 에러]

Access to XMLHttpRequest at 'http://192.168.0.3/jp/memberselect' from origin 'http://localhost:2120' has been blocked by CORS policy: 

The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:2120', but only one is allowed.

browser_client.dart:72          

POST http://192.168.0.3/jp/memberselect net::ERR_FAILED 200



[크롬,엣지 웹브라우저에서 WebApi 호출할때 클라이언트 HTTP_ORIGIN 값(http://localhost:5743) 전달됨]

a26083313ee4c20c54d34e5a2d9c9b44_1671583314_7456.jpg
 

[순수 스마트폰 어플에서 http.post로 WebApi 호출할때(내장 웹브라우저 호출 아님) 클라이언트의 HTTP_ORIGIN 전달 없음] 

a26083313ee4c20c54d34e5a2d9c9b44_1671583559_7413.jpg
 


  

[개선된 소스]

<?php

//CORS CHECK
$httpOrigin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : null;
if ($httpOrigin) {
    $allowed_domains = [
        'http://localhost:18410',
        'http://localhost:5743',
        'http://you.domain.kr',
    ];

    if (in_array($httpOrigin, $allowed_domains)) {
        header('Access-Control-Allow-Origin:' . $httpOrigin);
        header('Access-Control-Allow-Credentials: true');
    }

    if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { //1: OPTIONS -> 2: POST
        header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS');
        header('Access-Control-Allow-Headers: origin, content-type, accept, API');
        header('Access-Control-Max-Age: 600'); //cache for 10 minutes(Chromium)
        header('content-type: application/json; charset=utf-8');

        http_response_code(200);
        exit;
    }
}

/**
 * DB connection -> $dbconnection
 */
...
 


header('Access-Control-Max-Age: 600'); //cache for 10 minutes(Chromium) 

위 헤더를 넣어주면, 처음 1번만 Preflight(OPTIONS 콜 -> POST 호출, 즉 2번 콜) 동작하여 검증하고, 

이후 부터는 바로 이미 캐시된 정보가 있는 상황이면 처음부터 POST 로 바로 서버 콜함.(권장헤더)

여기에서는 600(10분간 유효),  캐시 유효시간은 아래참고. 


Access-Control-Max-Age: <delta-seconds>

<delta-seconds> 

Maximum number of seconds the results can be cached, as an unsigned non-negative integer. 

Firefox caps this at 24 hours (86400 seconds). 

Chromium (prior to v76) caps at 10 minutes (600 seconds). Chromium (starting in v76) caps at 2 hours (7200 seconds).

The default value is 5 seconds.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age


[위 소스 설명]

플러터(크롬게시) 에서 아래처럼 WebApi를 호출함.

var response = await http.post(url,headers: {"Content-Type": "application/json"}, body: body);

[참고]

프로그램(웹브라우저)에서 호출할 때 아래 3개의 Content-Type 은 ​Preflight 작동안함. 

REQUEST_METHOD: POST 로 전송됨(처음부터)

application/x-www-form-urlencoded

multipart/form-data

text/plain


프로그램(웹브라우저)에서 호출할 때 아래 Content-Type  ​Preflight 으로 자동 작동됨(WebApi 를 연속 2회(왕복) 호출함)

REQUEST_METHOD: OPTIONS 로 전송됨(OPTIONS -> POST 순)

application/json

text/xml


[과정]

1) 웹브라우저에서 RestApi(WebApi) "Content-Type": "application/json" 으로 post 호출하면 

아래 헤더값들이 웹브라우저에서 서버로 전달. 


HTTP_ORIGIN:"http://localhost:3767" 

HTTP_ACCESS_CONTROL_REQUEST_METHOD:"POST"

REQUEST_METHOD:"OPTIONS"


2) HTTP_ORIGIN 값(http://localhost:3767)이 서버 $allowed_domains 에 등록되어 있다면,

   header('Access-Control-Allow-Origin:' . $httpOrigin);

   header('Access-Control-Allow-Credentials: true')  등록하고,


맨 처음 호출될때 REQUEST_METHOD 가 'OPTIONS' 이므로, 아래처럼 OPTIONS 헤더가 등록되고, CORS 체크로직을 종료(exit;)시킴. 

    if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { //1: OPTIONS -> 2: POST

        header('Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS');

        header('Access-Control-Allow-Headers: origin, content-type, accept, API');

        header('content-type: application/json; charset=utf-8');


       http_response_code(200);  // 처음 OPTIONS 요청메서드로 Access-Control-Allow-Origin 사전 검증받는 역할만 하므로,
       exit;                         //200 리턴해주고, exit; 로 아래 이하 php 스크립트 실행 종료 

    }


3)  위 1)2)번에서 완성된 헤더를 서버로 부터 웹브라우저가 받음. 

    이제부터 웹브라우저에서 서버로 실제 데이타 전송 가능해짐


4)  웹브라우저로 부터 HTTP_ORIGIN  헤더를 서버가 받아 보고 서버 검증($allowed_domains)과 일치한 상태이므로 

웹브라우저는 아래 헤더들 + 실제 전송데이타와 함께 서버에 post 전송되어 서버 로직을 실행.

HTTP_ORIGIN:"http://localhost:3767"

REQUEST_METHOD:"POST"

CONTENT_TYPE:"application/json; charset=utf-8" 


--------------------

 ​Preflight 에서 1차 전송될때 REQUEST_METHOD 가 'OPTIONS' 이고, 2차 전송될때는 REQUEST_METHOD 가 'POST'이므로,

 if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') 구문으로 2차 전송때는 불필요한 OPTIONS 헤더 등록 로직실행을 하지 않음.


 -웹브라우저로 부터 HTTP_ORIGIN 헤더를 받아 본 후 서버 검증($allowed_domains 에 등록되지 않은 경우)과 일치하지 않는 경우는

 위 2)번의 아래 헤더 등록 작업을 하지 못한채로, Access-Control-Allow-Origin 값이 없는 미완의 헤더를 웹브라우저가 받음. 

 header('Access-Control-Allow-Origin:' . $httpOrigin); 

 header('Access-Control-Allow-Credentials: true') 


 Access-Control-Allow-Origin 값이 없는 미완의 헤더를 받은 웹브라우저는 CORS policy​ 를 충족하지 못하여 오류가 나고, 서버 접근이 불가능.

 Access to XMLHttpRequest at 'http://192.168.0.3/jp/memberselect' from origin 'http://localhost:3767' has been blocked by CORS policy: Response to preflight   request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.


 5) 위 소스와 같이 아래 로직이 필요하다.

http_response_code(200); exit;   
 

 ​Preflight 는 서버에 검증받기 위해 먼저 물어보고, OK되면 실제 post 데이타를 전송하게된다.(왕복 2번)

 그런데, 위 로직을 넣지 않으면, 1회 호출때 CORS 체크 로직을 벗어나 빈 데이타를 접근하며 하위 소스를 계속실행하게 됨.

 ​Preflight 는 서버에서 미리 검증하여 미검증 된 HTTP_ORIGIN 접근은 사전에 실행을 차단하는 목적이므로, 

 CORS  체크로직 아래가 실행될수 없도록  http_response_code(200); exit;  로직을 넣어줌.


[참고 - Slim4 프레임워크 사용시는 index.php 에 아래와 같이 구성하면 됨] 


    ......

// 6) Accept all routes for options
   ① $app->options('/{routes:.+}', function ($request, $response, $args) {
        return $response;
    });

    // 7) CORS Pre-Flight OPTIONS Request Handler
    $app->add(function ($request, $handler) {
        $response = $handler->handle($request);
        return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
    });

  // Register routes
    $routes = require __DIR__ . '/../routes/routes.php';
    $routes($app);

    /**
     * 9) Catch-all route to serve a 404 Not Found page if none of the routes match
     * NOTE: make sure this route is defined last
     */
  $app->map(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], '/{routes:.+}', function ($request, $response) {
        throw new HttpNotFoundException($request);
    });

    //Run App
    $app->run();
 



​Preflight 위소스 흐름도  

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 

Diagram of a request that is preflighted 



[참고]

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F 

php에서 CORS설정

https://blog.mydepot.kr/20220323_543#gsc.tab=0 

https://stex.tistory.com/46 

Apache 에서 CORS설정 

https://pjc91.tistory.com/55 

https://sarc.io/index.php/httpd/1278-apache-how-to-cors

https://youtu.be/6QV_JpabO7g 


댓글목록

등록된 댓글이 없습니다.

회원로그인

접속자집계

오늘
178
어제
407
최대
1,279
전체
211,948

그누보드5
Copyright © sebom.com All rights reserved.