问题
I am trying to understand how CORS Filter works and how can one benefit from it especially when we are talking about the Origin
option.
I am working on a project where i need to provide access for the client to add lead into the API using Ajax requests.
I am using yii\rest\Controller
for my controller. I have an API already in place and working, I have a base controller where I set my behavior and other common methods and extend all my controllers from this base controller.
The code for the base controller is below
class ApiController extends yii\rest\Controller
{
/**
* @var array
*/
protected $_logs = [];
/**
* Implemented CORS and HTTP Basic Auth for API
*
* @return string
*/
public function behaviors()
{
$behaviors = parent::behaviors();
// remove authentication filter
$auth = $behaviors['authenticator'];
// remove authentication filter necessary because we need to
// add CORS filter and it should be added after the CORS
unset($behaviors['authenticator']);
// add CORS filter
$behaviors['corsFilter'] = [
'class' => Cors::class,
'cors' => [
'Origin' => ['http://www.my-sms-business.local', 'http://localhost'],
'Access-Control-Request-Method' => ['GET', 'POST'],
],
];
// re-add authentication filter
$behaviors['authenticator'] = $auth;
// avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
$behaviors['authenticator']['except'] = ['options'];
return $behaviors;
}
}
As you can see I have the CORS defined with http://localhost
added into the Origin
, now as per Yii it should allow the requests from these domains and it should work smoothly, Yes it does work correctly when I use a simple Html page with a form and javascript code to submit the lead to the API, the code looks like below.
$(document).ready(function() {
$("#my-form").submit(function(e) {
e.preventDefault();
//serialize form input data
let data = $(this).serialize();
//get the form action
let url = $(this).attr('action');
$.ajax({
url: url,
method: 'POST',
data: data,
success: function(data) {
if (data.status == 200) {
alert(data.message);
} else {
alert("An error occoured see details,\n " + data.message.join(
"\n"));
}
},
})
return false;
});
});
<form name="my-form" id="my-form" method="POST" action="http://www.my-sms-business.local/api/v1/lead/add?access-token=MY-ACCESS-TOKEN">
<label>Phone</label>
<input type="text" name="phone_number" id="phone_number" />
<label>Campaign Id</label>
<input type="text" name="campaign_id" />
<label>user ID</label>
<input type="text" name="user_id" />
<label>name</label>
<input type="text" name="name" />
<input type="submit" value="Submit" />
</form>
The LeadController
uses QueryParamAuth
as i send the access-token
via query string in the url of the ajax call.
class LeadController extends ApiController
{
/**
* @return mixed
*/
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => QueryParamAuth::class,
];
return $behaviors;
}
/**
* Add the subscriber to the campaign
*
* @return mixed
*/
public function actionAdd()
{
if (Yii::$app->request->isPost) {
//start db transaction
$transaction = Yii::$app->db->beginTransaction();
//post data
$post['SubscribersForm'] = Yii::$app->request->post();
$model = new SubscribersForm();
$model->active = SubscribersForm::ACTIVE_YES;
$model->type = SubscribersForm::TYPE_SINGLE;
try {
if ($model->load($post)) {
if ($model->validate()) {
//if subscriber already exists in the database
$subscriberExists = Subscriber::findOne(['phone_number' => $model->phone_number]) !== null;
// call add subscriber so that the subscriber is added to the
// specified campaign and also to other campaigns under the
// same number the current campaign is associated to.
$model->saveSubscribers();
//commit the transaction
$transaction->commit();
if ($subscriberExists) {
return $this->formatMessage(['The Phone exsts in our database, we have added the number to the campaign that was provided.'], 200);
} else {
return $this->formatMessage(['Lead added successfully'], 200);
}
} else {
return $this->formatMessage($model->getErrorSummary(true), 401);
}
}
} catch (Exception $ex) {
//roll back the transaction
$transaction->rollBack();
//return the error response
return $this->formatMessage([$ex->getMessage()], $ex->getCode());
}
}
}
}
Now I wanted to test that if I remove http://localhost
from the Origin
it should not allow the request from that origin. When I remove it and hit the submit button on the form to send the ajax call I see that the console is throwing the warning
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://www.my-sms-business.local/api/v1/lead/add?access-token=MY-ACCESS-TOKEN. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
And the alerts that are inside the success callback of the ajax call does not trigger, so far so good.
BUT when I click on the request that was generated and look into the details BOOM, it still goes ahead to the actionAdd()
in the LeadController
and attempts to add the lead? See the image below
Is it how it is going to work or works? I don't think so, because i thought if the allowed domain is not present in the Origin
the request would terminate. Currently, the way it is behaving it becomes a useless option in this way.
SO where am I doing it wrong here? What config am i missing. We are planning to provide an option on the frontend of the site where users can generate their API key and add their domains to whitelist from where they will be sending the requeststo add leads.
UPDATE
To my surprise if i use the HttpBasicAuth
and add headrs via my ajax call using beforeSend
like below
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization",
"Basic " + btoa('MY_ACCESS_TOKEN:'));
xhr.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
},
and remove the QueryParamAuth
from my LeadController
. And update the behaviors()
method in the ApiController
to the following
public function behaviors()
{
$behaviors = parent::behaviors();
// remove authentication filter
$auth = $behaviors['authenticator'];
// remove authentication filter necessary because we need to
// add CORS filter and it should be added after the CORS
unset($behaviors['authenticator']);
// add CORS filter
$behaviors['corsFilter'] = [
'class' => Cors::class,
'cors' => [
'Origin' => ['http://www.my-sms-business.local', 'http://localhost'],
'Access-Control-Request-Method' => ['GET', 'POST', 'OPTIONS'],
'Access-Control-Request-Headers' => ['Authorization'],
'Access-Control-Allow-Credentials' => true,
],
];
// re-add authentication filter
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::class,
];
// avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
$behaviors['authenticator']['except'] = ['OPTIONS'];
return $behaviors;
}
It starts working as expected if i remove a domain from my whitelist or Origin it does not add the lead, as the OPTIONS
request is sent before the actual request is sent and when it fails it does not initiate the POST
request.
So What is all this about, why it moves on to the action even if the domain is not in the whitelist when using the QueryParamAuth
?.
来源:https://stackoverflow.com/questions/55865764/using-cors-filter-to-block-ajax-requests