As a front-end chicken, I have to deal with HTTP a lot these days. Well, it’s the browser that tells the server what the current Web application needs over HTTP, and the server sends the resources back over HTTP. In short, most of the time it’s the browser that’s hitting on the conversation, and the server that’s responding, without saying “crap.” Surely time is long, the former also can be tired.

However, in fact, it is quite different. In this process, there is always a “dog drama” like “EITHER I reject others, or others reject me”.

When I was a front-end geek, I took the HTML and CSS from the Head First series of books, typed the following code, and opened it in a browser:


      
<html lang="en">
<head>
  <title>Document</title>
</head>
<body>
  <div>helloworld</div>
</body>
</html>

Copy the code

I think, the browser with the server “evil fate” from now on in my world staged it.

At this point, the browser just picked up the page from nearby (local) and showed it to me. I pressed F12 to switch to the Network panel and saw no hidden secrets. The file protocol is used.

Browser and GET familiar to according to this address: http://127.0.0.1:5500/index.html, has sent a message to the server (GET request), expressed the hope to GET the index. The aspirations of the HTML page. Pretty soon, the server gave it the page so readily that it seemed like it was no different than picking up a page locally. I pressed F12 to switch to the Network panel:

A lot of strange information. The browser was probably designed that way because of its “genius”. It made me realize that there were a lot of hidden secrets waiting to be discovered in browsers.

Submit a form

Later, I made a login page that needed to pass the username and password to the server, thus opening up more content for “privileged” users.


      
<html lang="en">
<head>
  <title>Document</title>
</head>
<body>
  <form action="/login" method="post">
    <input type="text" name="usernmae" placeholder="username"><br>
    <input type="password" name="password" placeholder="password"><br>
    <input type="submit" value="login">
  </form>
</body>
</html>

Copy the code

Start by simply making a Web server with ExpressJS for observation, which probably no one has actually written (including numerous examples since then) 🙂 :

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/static', express.static("static"));

app.post('/login', (req, res) => {
  const name = req.body.username;
  const pwd = req.body.password;
  if(name === 'helloworld' && pwd === 'form') {
    res.send('success! ');
  }else {
    res.send(`  `); }}); app.listen(1024);

console.log(` ` Server listening on http://127.0.0.1:1024);


Copy the code

So cheerfully to access “http://127.0.0.1:1024/static/form.html” fill in the user name “helloworld,” password “form”, and click the login button.

The browser tells me you logged in successfully! But, we can find that the address bar of the browser has also changed to “127.0.0.1:1024/login”.

Anyway, I knew that since the login was successful and the browser took me to a new page, everything looked harmonious and interesting. Until one time, I typed in from.

Caught off guard, I received a pop-up from my browser that said “Forbidden Login!” I had no choice but to make sure. Then the browser’s progress bar flashes and the page is still the same, but the information you filled in is gone. Imagine that I’m a real user with 20 form items instead of 2, and I’m going to be devastated.

So I said to the browser, can we not be so disgusting? Browser: You can use XMLHttprequest-based Ajax techniques.

AJAX and XMLHttpRequest

At first listen to my heart a surprise, this is what fairy technology? Never heard of it, so I had to ask for science popularization.

Ajax(Asynchronous JavaScript And XML), Asynchronous JavaScript And XML. Today, however, XML doesn’t have much of a presence in the Web development world, thanks to JSON(JavaScript Object Notation), a data interaction format better suited to JS. So, it really refers more to the technique of communicating with the server via an XMLHttpRequest object. At this point, the page does not jump or refresh, but you can get the data you want from the server and change the state of the page, which naturally solves the problem in the example above.

From now on, JS can also participate in HTTP communication with the server.

There is such a fairy operation in the world, a self-science, immediate action. The Web server then provides a new address for JavaScript students to use: /ajaxform.

app.post('/ajaxform', (req, res) => {
  const name = req.body.username;
  const pwd = req.body.password;
  if(name === 'helloworld' && pwd === 'form') {
    res.status(200).json({
      msg: 'success! '}); }else {
    res.status(400).json({
      msg: 'fail! '}); }});Copy the code

In the meantime, modify form.html as follows:


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <input 
    id="username"
    type="text" 
    name="usernmae" 
    placeholder="username"
  ><br>
  <input 
    id="pwd"
    type="password" 
    name="password" 
    placeholder="password"
  ><br>
  <input 
    id="submit"
    type="submit" 
    value="login"
    onclick="submit()"
  >
</body>
<script>
  function submit(e) {
    const params = {
      username: document.getElementById('username').value,
      password: document.getElementById('pwd').value,
    }
    
    if(! params.username || ! params.password) { alert('invalid request! ');
      return;
    }

    ajax({
      method: 'POST'.url: '/ajaxform',
      params,
      success(res) {
        alert('success! ');
      },
      fail(err) {
        alert('fail! '); }}); }function ajax(options) {
    const params = options.params 
      ? JSON.stringify(options.params)
      : null;
    const method = options.method || 'GET';
    const xhr = new XMLHttpRequest();

    xhr.open(method, options.url);
    params && xhr.setRequestHeader('Content-Type'.'application/json');
    xhr.send(params);

    xhr.onreadystatechange = function(){
      if(xhr.readyState === 4) {
        if(xhr.status === 200) {
          const res = JSON.parse(xhr.responseText);
          options.success(res);
        } else {
          conststatusMsg = xhr.statusText; options.fail(statusMsg); }}}}</script>
</html>

Copy the code

Enter your username and password, click login, and the following request appears in the network panel of your browser:

And even if you type the wrong password, you won’t refresh your browser to erase it.

From there, you can happily expand your application capabilities. Thanks to the features of Ajax technology, the browser can participate in user interaction and view changes without refreshing/changing the page, and the boundaries of javascript capabilities have been greatly expanded, thus beginning to flourish

And so, over time, web applications become richer and richer, until one day you even have to interface with other applications.

  • The page address: http://127.0.0.1:5500/static/list.html
  • The interface address: http://127.0.0.1:8080/member

With excitement, I sent a request, as I always did. But something unexpected happened, and the browser told me on its Console panel: Access to the XMLHttpRequest at ‘http://127.0.0.1:8080/member’ from origin ‘http://127.0.0.1:5500’ has had been blocked by CORS Policy: No ‘access-Control-allow-origin’ header is present on the requested resource. But its Network panel told me that the request was successful and that it got the data.

All I could do was ask the browser, what the hell happened?

The browser was unfathomable: This is what I call the same-Origin Policy.

The same-origin policy

The same origin policy was introduced into the browser by Netscape in 1995 to protect user privacy and data security. Since then, more and more companies have implemented it in their browser products, and it has become the security foundation for building Web applications on top of the browser. For example, the cookies under website A are not allowed to be obtained when the user visits website B. For example, in the example above, cross-source Ajax requests are not allowed.

A source indicates the same protocol, domain name, and port number. If any of the three sources do not meet the requirements, the source is considered to be cross-source. Obviously, in the example above, the two urls have different port numbers, which triggers the browser’s same-origin policy. Although the request is successful and the data is retrieved, the browser still “rejects” the request.

Is a popular science of self, browser really has too many hidden secrets!

But knowing why, I still had no idea how to break through the curse. Didn’t I tell you that in the Console panel? Let the server kid on port 8080 learn the CORS strategy and come back to me!

CORS cross-domain resource sharing

Cross-domain resource sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers that Web applications running on one Origin (domain) are granted access to specified resources from different source servers. When a resource requests a resource from a different domain, protocol, or port than the server on which the resource itself resides, the resource makes a cross-domain HTTP request. So, the example above makes a cross-domain HTTP request.

The way it uses extra HTTP headers to tell the browser that the browser is not in the driver’s seat when sharing resources across domains, but is waiting for a third party to tell it: Hey, here’s my message, open the door!

The browser checks that there is no problem with the “clearance message” and allows the response message to “enter”. Obviously, the third party using the extra HTTP header is the outdomain server. So what are these additional HTTP headers?

  • Access-Control-Allow-OriginThe server uses this field to set a URI (the same as the web application that the current browser is accessing) or wildcard * to tell the browser that you can allow that web application to use my resources. Open the door.
  • Access-Control-Expose-HeadersThe server allows the browser to access headers across domains that it should not have access to.
  • Access-Control-Allow-CredentialsWhen the “credentials” set to true for an application request, the server uses this field to tell the browser whether the Web application can get the response information. If false, the browser intercepts the response; If true, not only can the browser carry cookies to the server, but the server can also set cookies for the current Web application. This ensures information security to a large extent.
  • So this can be a way to carry credentials through cookies, but at this point the Access-Control-Allow-Origin field must specify the URI and can no longer use the wildcard (*).

  • Access-Control-Max-AgeThe server tells the browser that your last preflight request will be valid for a number of seconds and that until then, don’t send me any more OPTIONS requests.
  • Access-Control-Allow-Methods: When an application makes a request using a method other than GET, POST, and HEAD, the server uses thePreview the requestThe response tells the Web application that the actual subsequent request must be made using the HTTP methods I listed for you, or the browser will intercept my response.
  • Access-Control-Allow-Headers: Indicates that a request initiated by an application does not belong toCORS specification security header collection field/valueThe server will be in the header fieldPreview the requestThe response tells the browser that the actual request can only be made with the header fields I listed for you, otherwise the browser will block my response again.

When I saw the first header field, I was like, No ‘access-Control-allow-origin’ header is present on the requested resource. Although I was still confused about the pre-inspection request mentioned above, it could not prevent my impulse to see the dawn. So excitedly quickly let the server add it.

app.post('/member', (req, res) => {
  res.append('Access-Control-Allow-Origin'.'http://127.0.0.1:5500');
  res.status(200).json([
    {user: 'li lei'},
    {user: 'Han Meimei'},
    {user: 'Tom'},]); });Copy the code

Then click the button to send the request. However, the Network panel does not display a POST request that was blocked by the browser but responded successfully. Instead, there is a strange OPTIONS request.

At the same time, the browser says again in its Console panel: Access to the XMLHttpRequest at ‘http://127.0.0.1:8080/member’ from origin ‘http://127.0.0.1:5500’ has had 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. Finally, I tracked down the Preflight request.

I asked the browser why? The browser says: “FOR every cross-domain request and response, I check it according to the CORS specification. This request is not simple, so I have to” take the initiative “to send an OPTIONS request to the server to ask if it will allow this cross-domain request.

Precheck requests and simple requests

In other words, when I just clicked the button, I sent a non-simple request. What kind of request is considered simple?

A Request can be considered a “Simple Request” if all of the following conditions are met:

  1. Use one of the following methods:
  • GET
  • POST
  • HEAD
  1. A header field outside the CORS security header field set is not set artificially. The collection is as follows:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  1. The value of the content-type is limited to one of the following:
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded
  1. None of the XMLHttpRequestUpload objects in the request are registered with any event listeners (the XMLHttpRequestUpload object can be accessed using the xmlHttprequest.upload attribute).

  2. No ReadableStream object is used in the request.

If a Request does Not meet the above conditions, it is considered a not-so-simple Request. The browser must first issue a precheck request to the server using the OPTIONS method to know if the server will allow the actual request. The use of precheck requests prevents cross-domain requests from having unexpected impacts on the server’s user data.

Given the above theory, it is easy to see that the request made in the above example uses “application/json” as the “content-Type” value, so it is treated as a non-simple request, and the browser “takes the initiative” to send a precheck request using the OPTIONS method. Asks the server if the cross-domain request is allowed. OK, now that I understand the problem, let me take a look at cross-domain requests. Modify the server code as follows:

const memberLs = (req, res) = > {
  res.append('Access-Control-Allow-Origin'.'http://127.0.0.1:5500');
  if(req.method === 'OPTIONS') {
    res.append('Access-Control-Allow-Methods'.'GET,POST,OPTIONS');
    res.append('Access-Control-Allow-Headers'.'Content-Type');
    res.status(200).end();
  }else {
    res.status(200).json([
      {user: 'li lei'},
      {user: 'Han Meimei'},
      {user: 'Tom'},]); } } app.post('/member', memberLs);

Copy the code

Click the button again and you’re done. You can see the records of the two HTTP requests in the Network panel of your browser.

HTTP/1.1 200 OK X-powered-by: Express Access-Control-allow-Origin: http://127.0.0.1:5500 access-control-allow-methods: GET,POST,OPTIONS Access-Control-Allow-Headers: Content-Type Date: Tue, 26 Nov 2019 11:49:52 GMT Connection: Keep alive - Content - Length: 0 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the OPTIONS/member HTTP / 1.1 Host: 127.0.0.1:8080 Connection: keep-alive access-control-request-method: POST Origin: http://127.0.0.1:5500 user-agent: Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, Like Gecko) Chrome/78.0.3904.108 Safari/537.36 Access-Control-request-headers: Content-type Accept: */* sec-fetch -Site: The same - the site of the Sec - Fetch - Mode: cors Referer: http://127.0.0.1:5500/static/list.html Accept - Encoding: gzip, deflate, br Accept-Language: zh-CN,zh; Q = 0.9, en. Q = 0.8Copy the code
HTTP/1.1 200 OK X-powered-by: Express Access-Control-allow-Origin: http://127.0.0.1:5500 set-cookie: name=tobi; HTTP/1.1 200 OK X-powered-by: Express Access-Control-allow-Origin: http://127.0.0.1:5500 set-cookie: name=tobi; Path=/ Content-Type: application/json; charset=utf-8 Content-Length: 55 ETag: W/"37-30cJYOjzuCqPVyovsDoOf7e+UBQ"Date: Tue, 26 Nov 2019 11:49:52 GMT Connection: Keep - the alive -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the POST HTTP / 1.1 / member Host: 127.0.0.1:8080 Connection: Keep-alive Content-Length: 26 Origin: http://127.0.0.1:5500 user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36 Content-Type: application/json Accept: */* Sec-Fetch-Site: same-site Sec-Fetch-Mode: cors Referer: http://127.0.0.1:5500/static/list.html Accept - Encoding: gzip, deflate, br Accept - Language: useful - CN, useful; Q = 0.9, en. Q = 0.8Copy the code

And finally, the view is updated as desired:

So far, as the CORS specification describes, “the years are quiet,” as if this had solved the cross-domain problem once and for all. Other issues such as carrying credentials and passing custom header fields will be a piece of cake

But, if you send an additional OPTIONS request for each request, it will also cause a huge consumption of network resources. Although OPTIONS request itself is small and thanks to keep-alive mechanism, although the server can set the access-Control-max-age response header to greatly reduce the disadvantages caused by additional preflight request, the disadvantages are still there.

Again, though, for the request in the example above, the Web application script could be modified to send data in the “Application/X-www-form-urlencoded” format to be considered a “simple request”. But there’s always an exception, and this is just the exception. It’s a pain in the neck.

At this time, the browser said: it is said that in the beginning of cross-domain, there is a pioneer named JSONP broke through the “same origin policy” contained in my body, making communication between foreign countries possible, you might want to know.

JSONP

JSONP(JSON with Padding), a “usage mode” for JSON data formats. It makes use of the “vulnerability” that

This also means that this method can only use the GET method, which is quite limited.

I’ve heard of JSNOP before, so take your browser’s advice and see how it works.

First, the server provides an interface:

app.get('/memberjsonp', (req, res) => {
  res.jsonp([
    {user: 'li lei'},
    {user: 'Han Meimei'},
    {user: 'Tom'},]); })Copy the code

Then, the Web application:

<body id="body">
  <button onclick="memberlsJonp()">jsonp</button>
</body>
<script>
  const $body = document.getElementsByTagName('body') [0];
  function memberlsJonp() {
    jsonp({
      url: 'http://127.0.0.1:8080/memberjsonp'.query: 'page=1&type=member&callback=resCallback'}); }function jsonp(options) {
    const url = options.url + '? ' + options.query;
    const $script = document.createElement('script');
    $script.src = url;
    $body.append(script);
  }

  function resCallback(res) {
    for(let item of res) {
      const $p = document.createElement('p'); $p.innerText = item.user; $body.append($p); }}</script>

Copy the code

OK, click the button and get the desired view update. At the same time, the Network panel truthfully records the js script loading traces, and the server returns a script like this:

typeof resCallback === 'function' 
  && resCallback([{"user":"Li lei"}, {"user":"Han Meimei"}, {"user":"Tom"}]);

Copy the code

The above, whether CORS or JSONP, is provided that the server of the foreign resource we want to access can cooperate to implement support for both specifications. Their own server is easy to say, but if other servers, presumably to find someone else to do docking desire can only be in vain. So I looked expectantly at the browser again. But this time, it was silent.

After a while, the server hosting the Web application sneaks up and says, hey, to tell you a secret, the same-origin policy restriction doesn’t exist on our server. So, don’t worry about the browser, just let it come to my domain to request the resources I want, as long as I forward the corresponding foreign request, get the required resources and then throw to the browser. In this way, it may fool itself into thinking that it is always in the same domain, which may “fool” it, but life is really good when everyone is good. This saves everyone a lot of trouble.

I heard, greatly moved. According to the server, this is the legendary reverse proxy.

Reverse Proxy

A reverse proxy lets a client think it is getting resources directly from the proxy server, without knowing that there is a real server behind the proxy server.

Action is better than action. Start doing it now.

Typically, production environments use tools such as nginx to start a service, so here’s an example. Add the following configuration to nginx.conf:

server { listen 8080; Server_name 127.0.0.1; Location ^~ /apis/ {proxy_pass http://127.0.0.1:3000/; }}Copy the code

You can then make a request in list.html like this:

<script>
ajax({
  method: 'POST'.url: '/apis/member',
  params,
  success(res) {
    const $body = document.getElementById('body');
    for(item of res) {
      const $p = document.createElement('p');
      $p.innerHTML = item.user
      $body.append($p);
    }
  },
  fail(err) {
    console.log('failed')}});</script>

Copy the code

At this time, the web application to access the “http://127.0.0.1:8080/apis/member”, will be nginx forwarded to http://127.0.0.0:3000/member.

Take a final look at the request record in the Network panel

HTTP/1.1 200 OK Server: nginx/1.17.3 Date: Tue, 26 Nov 2019 14:32:32 GMT Content-Type: Application /json; HTTP/1.1 200 OK Server: nginx/1.17.3 Date: Tue, 26 Nov 2019 14:32:32 GMT Content-Type: Application /json; charset=utf-8 Content-Length: 55 Connection: keep-alive X-Powered-By: Express Set-Cookie: name=tobi; Path=/ ETag: W/"37-30cJYOjzuCqPVyovsDoOf7e+UBQ"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the POST HTTP / 1.1 / apis/member Host: 127.0.0.1:8080 Connection: Keep-alive Content-Length: 26 Origin: http://127.0.0.1:8080 user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36 Content-Type: application/json Accept: */* Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Referer: http://127.0.0.1:8080/ accept-encoding: gzip, deflate, BR Accept-language: zh-cn,zh; Q = 0.9, en. Q = 0.8 cookies: LiveWSBYT63791127 = b02bfbcd8cba468299b726e9b17fe161; NBYT63791127fistvisitetime=1567320685477; UM_distinctid=16ceb97bc4cd02-0257ef8249e93c-38637701-13c680-16ceb97bc4d31b; _ga = GA1.1.2056984704.1570163359; CNZZDATA1275303586=1208628234-1567319233-%7C1572869768; NBYT63791127visitecounts=3; NBYT63791127lastvisitetime=1572874605770; NBYT63791127visitepages=8; Hm_lvt_35442676cb689f804c3274d59b25d5a9 = 1572873197157651, 216Copy the code

Everything is like a request in the same domain, a perfect Reverse Proxy.


The above. For solutions to cross-domain problems, CORS, JSONP, and reverse proxy have their own advantages and disadvantages and application scenarios, which need to be selected according to specific business scenarios. However, CORS and reverse proxies are undisputed mainstream solutions.