Node.JS學習筆記(九)_Restful API的swagger文檔_304 not modified狀態碼異常_ETag介紹

 
後端寫好的API之後前端要如何知道有哪些路由可以存取?
因此Swagger 文檔這套API文件生成工具就可方便讓我們提供給前端參考。
Swagger 強大的地方在於,只需要寫一個 json 檔,就可以動產生出 API document 在網頁上瀏覽,還可以直接發送 http request 來測試 API。



以Foreca 此網站來看,就有提供 API 供開發者串接,
所以 API  document 就需要十分清楚透明
https://developer.foreca.com/#Forecasts



新建一個NodeJs Express專案


我們這裡的進入點是index.js
cd. > index.js
npm install express
npm install nodemon

多下載此套件來直接將我們的 json 轉換為 API document
npm install swagger-ui-express





code1:

1
2
3
4
5
6
7
8
const express = require("express");
const app = express();

const PORT = 3000;

app.listen(PORT,()=>{
    console.log('Server is listen on port ' + PORT);
});

那預設express專案配置起來會預設安置好的port -> 3000,這裡是可自行定義的。
就類似對應的鑰匙門房號或是密鑰,知道正確結果的就可以進行存取。
那port 連接埠、協定埠 (protocol port)就好比如電腦的各式各樣服務的門,也是一種識別應用服務的代號,範圍從0~65535。

但預設有已經有相應功能的port
以下的就要避免用到專用port

80: HTTP服務
443:HTTPS服務
21:FTP服務
23:SSH服務
3306:MySQL預設port (若有兩個以上會額外定義)
27017:MongoDB預設port(若有兩個以上會額外定義)

編寫get請求方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const express = require("express");
const app = express();

const PORT = 3000;

app.listen(PORT,()=>{
    console.log('Server is listen on port ' + PORT);
});


app.get('/',(request,response) =>{
    //status code
    response.status(200);
    response.send("Hi this is get test");
});


這裡在進行get請求時候
可觀察的到回傳的狀態代碼和一些header資訊
(比方X-Powered-by就是指說這個網站應用是採取捨麼程式碼或是框架去撰寫的,通常因應資安會到IIS,Apache,Nginx等web server 去把一些敏感資訊隱藏起來。)
statuscode有時不如預期顯示304 not modified
狀態碼異常
可能是因為瀏覽器有緩存
此時也可用一種方式就是在get 網址後面去寫一個隨機數或時間戳
即可防止緩存問題


這時我們可在express app當中呼叫
app.disable('etag');

304 not modified狀態碼異常


解決方案.將HTTP ETag給關閉
但這樣也會導致後續緩存機制判讀會全域被關閉
 app.disable()可以用來關閉或隱藏類似header資訊

比方
app.disable("x-powered-by");
app.disable("strict routing");
app.disable("case sensitive routing");

比較完整的表可參考官網(裡面的字串欄位不是隨便打的!!)
https://expressjs.com/en/api.html#app.settings.table




 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const express = require("express");
const app = express();

const PORT = 3000;

app.listen(PORT,()=>{
    console.log('Server is listen on port ' + PORT);
});

app.disable('etag');

app.get('/',(request,response) =>{
    //status code
    response.status(200);
    response.send("Hi this is get test");
});


瀏覽器快取與緩存
Last-Modified/Etag標記都是瀏覽器快取緩存的一種機制
伺服器可在後續請求用其來判斷頁面是否已經被修改,有無必要重新要取資源。

請求流程如下:
1.客戶端請求一個頁面(A)。
2.伺服器返回頁面A,並在給A加上一個Last-Modified/ETag。
3.客戶端展現該頁面,並將頁面連同Last-Modified/ETag一起快取。
4.客戶再次請求頁面A,並將上次請求時伺服器返回的Last-Modified/ETag一起傳遞給伺服器。
5.伺服器檢查該Last-Modified或ETag,並判斷出該頁面自上次客戶端請求之後還未被修改,直接返迴響應304和一個空的Response Body。



HTTP協定規格說明定義ETag為“被請求變數的實體值”。
Etag(Entity Tags).Etag僅僅是一個和檔案相關的標記,就好比如這份檔案內容一種像是 hash 的token,類似就是一樣的內容會產生一樣的 token,不一樣的會產生不一樣的 token。

Etag 主要為了解決 Last-Modified 無法解決的一些問題。
一些檔案也許會周期性的更改,但是他的內容並不改變(僅僅改變的修改時間),這個時候我們並不希望客戶端認為這個檔案被修改了,而重新GET

某些檔案修改非常頻繁,比如在秒以下(毫秒級別)的時間內進行修改,(比方說1s內修改了N次),If-Modified-Since能檢查到的粒度是s級的,這種修改無法判斷(或者說UNIX記錄MTIME只能精確到秒)


Etag由伺服器端生成,客戶端通過If-Match或者說If-None-Match等條件判斷
請求來驗證資源是否修改

====第一次請求===
1.客戶端發起 HTTP GET 請求一個檔案;
2.伺服器處理請求,返回檔案內容和一堆Header,當然包括Etag(例如"2e681a-6-5d044840")(假設伺服器支持Etag生成和已經開啟了Etag).狀態碼200

====第二次請求===
1.客戶端發起 HTTP GET 請求一個檔案,注意這個時候客戶端同時傳送一個If-None-Match頭,這個頭的內容就是第一次請求時伺服器返回的Etag:2e681a-6-5d044840
2.伺服器判斷發送過來的Etag和計算出來的Etag匹配,因此If-None-Match為False,不返回200,返回304,客戶端繼續使用本地快取;







Restful API
路由設計基本上會將endpoint進行統一接口
只用http method來區分其相應的行為
Response回傳會採用Json格式進行回傳
此外相應的Status Code也有相關回傳列表





專案目錄配置




所需套件配置下載

npm install express
npm install ejs
npm install nodemon
npm install mysql
npm install swagger-ui-express

MySQL準備




index.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const express = require("express");
const bodyParser = require('body-parser');
const swaggerUi = require('swagger-ui-express');
const studentsRouter = require('./routes/students.js')
const swaggerSetting = require('./config/swagger')
const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use('/students',studentsRouter);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSetting))

app.listen(PORT,()=>{
    console.log('Server is listening on port:'+ PORT);
})



db.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const mysql = require('mysql');
const dbConnection = mysql.createConnection({
    host: 'localhost',
    user:'root',
    password:'',
    port:3307,
    database:'course'
})
dbConnection.connect((err)=>{
    if(err) throw err;
    console.log("Database Connected!")
})
module.exports =dbConnection;




student.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const express = require("express");
const router = express.Router();
const dbConnect = require("../config/db");

// R - read
router.get("/", (req, res, next) => {
    // query('sql',(err,result))
    dbConnect.query("SELECT * FROM students", (err, result) => {
        if (err) {
            throw err;
        }
        const returnObj = {'results':result};
        // 設定statusCode
        res.status(200)
        res.json(returnObj);
    });
});

// C - create
router.post("/", (req, res, next) => {
    dbConnect.query(
        "INSERT INTO students(name,age,email,department) VALUES(?,?,?,?)",
        [req.body.name, req.body.age, req.body.email, req.body.department],(err,result)=>{
            if(err){
                throw err;
            } else{
                const resutnObj = {'message':"insert data successfully!"}
                res.status(201);
                res.json(resutnObj)
            }
        }
    );
});

// U - update
router.patch('/:id',(req,res,next)=>{
    const studentId = req.params.id;
    dbConnect.query("update students set age=? where id= ?",[req.body.age,studentId],(err,result)=>{
        if(err){
            throw err;
        }
        else{
            const resutnObj = {'message':"update data successfully!"}
            res.status(200);
            res.json(resutnObj)
        }
    })
})

// D -delete
router.delete('/:id',(req,res,next)=>{
    const studentId = req.params.id;
    dbConnect.query("delete FROM students where id=?",studentId,(err,result)=>{
        if(err){
            throw err
        }else{
            const resutnObj = {'message':"deleted data successfully!"}
            res.status(204);
            res.json(resutnObj)
        }
    })
})

module.exports = router;



依據OpenAPI 3.0規範


準備好swagger.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
const apiDoc = {
    "openapi": "3.0.0",
    "info": {
      "version": "1.0.0", //版本號
      "title": "Test Student API", //swagger文件的標頭
      "description": "This is a student API for CRUD." //swagger描述
    },
    "tags": [   //API分類用可收闔
      {
        "name": "學生API", //Tag的title
        "description": "這是測試"
      }
    ],
    "consumes": [
      "application/json"
    ],
    "produces": [
      "application/json"
    ],
    "paths": {      //API 路由長捨麼樣子,依照HTTP Verbs(Methods)來分類
      "/students": {
        "get": {
          "tags": [
            "Students"
          ],
          "summary": "Get all students",    //每個API後面功能描述
          "produces": ["application/json"],
          "responses": {    //回應方式
            "200": { //用statuCode做分類
              "description": "OK",
              "content": { //內容
                "application/json": { //response格式
                  "schema": { //response要吃什麼樣的model,definitions定義於下方
                    "$ref": "#/definitions/Students"
                  }
                }
              }
            }
          }
        },
        "post": {
          "tags": [
            "Students"
          ],
          "summary": "Create a new student",
          "requestBody": {
            "description": "Students Object",
            "required": true, //告訴前端一定要帶以下數值
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/definitions/Students"
                }
              }
            }
          },
          "produces": [
            "application/json"
          ],
          "responses": {
            "201": {
              "description": "OK",
              "schema": {
                "$ref": "#/definitions/Students"
              }
            },
          }
        }
      },
      "/students/{id}": {
        "patch": {
          "tags": ["Students"],
          "summary": "Update students age",
          "parameters": [ //更新的參數要怎麼帶?
            {
              "name": "id",
              "in": "path",
              "required": true,
              "description": "Student id"
            }
          ],
          "requestBody": {
            "description": "update student for age",
            "required": true,
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "age": {
                      "type": "integer"
                    }
                  }
                }
              }
            },
          },
          "responses": {
            "200": {
              "description": "OK",
              "content": {
                "application/json": {
                  "example": {
                    "message": "update successfully"
                  }
                }
              }
            },
          }
        },
        "delete": {
          "tags": ["Students"],
          "summary": "Delete students",
          "parameters": [
            {
              "name": "id",
              "in": "path",
              "required": true,
              "description": "Student id"
            }
          ],
          "responses": {
            "204": {
              "description": "OK",
              "content": {
                "application/json": {
                  "example": {
                    "message": "delete successfully"
                  }
                }
              }
            },
          }
        }
      }
    },
    "definitions": {
      "Students": {//Table名稱
        "type": "object", //資料表物件
        "properties": { //資料表屬性
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "age": {
            "type": "integer"
          },
          "email": {
            "type": "string"
          },
          "department": {
            "type": "string"
          }
        }
      }
    },
  }
  
  module.exports = apiDoc











Ref:
304 not modified 缓存问题解决




[express/connect.static] Set ‘Last-Modified’ to now to avoid 304 Not Modified

The Node Express server is returning "304 Not Modified" responses, but only after querying the database

NodeJS/express: Cache and 304 status code




如何解決”SyntaxError: Cannot use import statement outside a module”問題?









留言

這個網誌中的熱門文章

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header

經得起原始碼資安弱點掃描的程式設計習慣培養(三)_7.Cross Site Scripting(XSS)_Stored XSS_Reflected XSS All Clients

(2021年度)駕訓學科筆試準備題庫歸納分析_法規是非題