Forms, File Uploads và Security với NodeJS- phần 1


1054

Lời giới thiệu

Nếu bạn đang xây dựng một ứng dụng web NodeJS, bạn có thể gặp phải nhu cầu xây dựng các form HTML vào ngày đầu tiên. Chúng là một phần quan trọng của trải nghiệm web và chúng có thể phức tạp.

Thông thường, quá trình xử lý form bao gồm:

  • hiển thị một form HTML trống để đáp ứng request GET ban đầu
  • người dùng gửi form với data trong request POST
  • xác thực trên cả máy khách và máy chủ
  • hiển thị lại biểu mẫu được điền dữ liệu và thông báo lỗi nếu không hợp lệ
  • làm điều gì đó với dữ liệu trên máy chủ nếu tất cả đều hợp lệ
  • chuyển hướng người dùng hoặc hiển thị thông báo thành công sau khi dữ liệu được xử lý.

Xử lý dữ liệu form cũng đi kèm với các cân nhắc bảo mật bổ sung.

Chúng ta sẽ xem xét tất cả những điều này và giải thích cách xây dựng chúng với NodeJS và Express – framework web phổ biến nhất cho NodeJS. Trước tiên, chúng ta sẽ tạo một form liên hệ đơn giản để mọi người có thể gửi tin nhắn và địa chỉ email một cách an toàn, sau đó xem xét những gì liên quan đến việc xử lý tải tệp lên.

A contact form with email and message with validation errors

Như mọi khi, code hoàn chỉnh có thể được tìm thấy trong repo GitHub repo 

Setup

Đảm bảo rằng bạn đã cài đặt phiên bản NodeJS gần đây. node -v phải trả về 8.9.0 hoặc cao hơn.

Tải xuống code khởi động từ đây với Git:

git clone -b starter https://github.com/sitepoint-editors/node-forms.git node-forms-starter
cd node-forms-starter
npm install
npm start

Lưu ý: repo có hai nhánh, starter và master. Nhánh starter chứa thiết lập tối thiểu bạn cần làm theo bài viết này. Nhánh master chứa một bản demo đầy đủ, đang hoạt động (liên kết ở trên).

Không có quá nhiều code trong đó. Đó chỉ là một thiết lập Express đơn giản với  EJS template và trình xử lý lỗi:

const path = require('path');
const express = require('express');
const layout = require('express-layout');
const routes = require('./routes');
const app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
const middlewares = [
  layout(),
  express.static(path.join(__dirname, 'public')),
];
app.use(middlewares);
app.use('/', routes);
app.use((req, res, next) => {
  res.status(404).send("Sorry can't find that!");
});
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
app.listen(3000, () => {
  console.log('App running at http://localhost:3000');
});

Url root / chỉ hiển thị chế độ xem index.ejs:

// routes.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
  res.render('index');
});
module.exports = router;

Hiển thị form

Khi mọi người request GET tới /contact, chúng ta muốn hiển thị một contact.ejs ở chế độ view mới:

router.get('/contact', (req, res) => {
  res.render('contact');
});

Form contact sẽ cho phép gửi tin nhắn và địa chỉ email của họ:

<!-- views/contact.ejs -->
<div class="form-header">
  <h2>Send us a message</h2>
</div>
<form method="post" action="/contact" novalidate>
  <div class="form-field">
    <label for="message">Message</label>
    <textarea class="input" id="message" name="message" rows="4" autofocus></textarea>
  </div>
  <div class="form-field">
    <label for="email">Email</label>
    <input class="input" id="email" name="email" type="email" value="" />
  </div>
  <div class="form-actions">
    <button class="btn" type="submit">Send</button>
  </div>
</form>

Xem nó trông như thế nào tại http: // localhost: 3000 / contact.

Form Submission

Để nhận các giá trị POST trong Express, trước tiên bạn cần bao gồm middleware body-parser, phần mềm này hiển thị các giá trị form đã gửi trên req.body trong trình xử lý route của bạn. Thêm nó vào cuối mảng middlewares:

// server.js
const bodyParser = require('body-parser');
const middlewares = [
  // ...
  bodyParser.urlencoded({ extended: true }),
];

Đó là quy ước chung cho các form POST dữ liệu trở lại cùng một URL như được sử dụng trong yêu cầu GET ban đầu. Hãy làm điều đó tại đây và xử lý POST / contact để xử lý thông tin người dùng nhập.

Trước tiên, hãy xem xét nội dung gửi không hợp lệ. Nếu không hợp lệ, chúng ta cần trả lại các giá trị đã gửi cho chế độ xem (vì vậy người dùng không cần nhập lại chúng) cùng với bất kỳ thông báo lỗi nào mà chúng ta muốn hiển thị:

router.get('/contact', (req, res) => {
  res.render('contact', {
    data: {},
    errors: {}
  });
});
router.post('/contact', (req, res) => {
  res.render('contact', {
    data: req.body, // { message, email }
    errors: {
      message: {
        msg: 'A message is required'
      },
      email: {
        msg: 'That email doesn‘t look right'
      }
    }
  });
});

Nếu có bất kỳ lỗi xác thực nào, chúng ta sẽ thực hiện như sau:

  • hiển thị các lỗi ở đầu form
  • đặt các giá trị đầu vào thành những gì đã được gửi đến máy chủ
  • hiển thị lỗi nội dòng bên dưới đầu vào
  • thêm class form-field-invalid hợp lệ vào các trường có lỗi

<div class="form-header">
  <% if (Object.keys(errors).length === 0) { %>
    <h2>Send us a message</h2>
  <% } else { %>
    <h2 class="errors-heading">Oops, please correct the following:</h2>
    <ul class="errors-list">
      <% Object.values(errors).forEach(error => { %>
        <li><%= error.msg %></li>
      <% }) %>
    </ul>
  <% } %>
</div>
<form method="post" action="/contact" novalidate>
  <div class="form-field <%= errors.message ? 'form-field-invalid' : '' %>">
    <label for="message">Message</label>
    <textarea class="input" id="message" name="message" rows="4" autofocus><%= data.message %></textarea>
    <% if (errors.message) { %>
      <div class="error"><%= errors.message.msg %></div>
    <% } %>
  </div>
  <div class="form-field <%= errors.email ? 'form-field-invalid' : '' %>">
    <label for="email">Email</label>
    <input class="input" id="email" name="email" type="email" value="<%= data.email %>" />
    <% if (errors.email) { %>
      <div class="error"><%= errors.email.msg %></div>
    <% } %>
  </div>
  <div class="form-actions">
    <button class="btn" type="submit">Send</button>
  </div>
</form>

Gửi form tại http: // localhost: 3000 / contact để xem form này hoạt động. Đó là mọi thứ chúng ta cần ở view.

Validation và Sanitization

Có một middleware tiện dụng được gọi là express-validator để xác thực và làm sạch dữ liệu bằng cách sử dụng thư viện validator.js. Hãy thêm nó vào ứng dụng của chúng ta.

Validation

Với validators được cung cấp, chúng ta có thể dễ dàng kiểm tra xem thư và địa chỉ email hợp lệ đã được cung cấp hay chưa:

const { check, validationResult, matchedData } = require('express-validator');
router.post('/contact', [
  check('message')
    .isLength({ min: 1 })
    .withMessage('Message is required'),
  check('email')
    .isEmail()
    .withMessage('That email doesn‘t look right')
], (req, res) => {
  const errors = validationResult(req);
  res.render('contact', {
    data: req.body,
    errors: errors.mapped()
  });
});

Sanitization

Với sanitizers được cung cấp, chúng ta có thể cắt bỏ khoảng trắng từ đầu và cuối giá trị, đồng thời chuẩn hóa địa chỉ email thành một mẫu nhất quán. Điều này có thể giúp loại bỏ các liên hệ trùng lặp được tạo bởi các đầu vào hơi khác nhau. Ví dụ: ‘Mark@gmail.com’ và’mark@gmail.com ‘đều sẽ được chuyển thành’ mark@gmail.com ‘.:

Sanitizers có thể đơn giản được gắn vào phần cuối của trình xác nhận

router.post('/contact', [
  check('message')
    .isLength({ min: 1 })
    .withMessage('Message is required')
    .trim(),
  check('email')
    .isEmail()
    .withMessage('That email doesn‘t look right')
    .bail()
    .trim()
    .normalizeEmail()
], (req, res) => {
  const errors = validationResult(req);
  res.render('contact', {
    data: req.body,
    errors: errors.mapped()
  });
  const data = matchedData(req);
  console.log('Sanitized:', data);
});

Function matchData trả về kết quả đầu ra của sanitizers trên đầu vào của chúng ta.

Ngoài ra, hãy lưu ý việc chúng ta sử dụng method bail, method này sẽ ngừng chạy các xác thực nếu bất kỳ điều gì trước đó không thành công. Chúng ta cần điều này vì nếu người dùng gửi form mà không nhập giá trị vào trường email, normalizeEmail sẽ cố gắng chuẩn hóa một chuỗi trống và chuyển nó thành @. Điều này sau đó sẽ được chèn vào trường email của chúng ta khi render lại form.

Nếu có lỗi xảy ra, chúng ta cần render lại view. Nếu không, chúng ta cần làm điều gì đó hữu ích với dữ liệu và sau đó cho thấy rằng quá trình gửi đã thành công. Thông thường, một người được chuyển hướng đến trang thành công và hiển thị một thông báo.

Lưu ý

HTTP là stateless, vì vậy bạn không thể chuyển hướng đến một trang khác và chuyển các thông báo mà không có sự trợ giúp của session cookie để duy trì thông báo đó giữa các yêu cầu HTTP. “Tin nhắn flash” là tên được đặt cho loại tin nhắn chỉ sử dụng một lần này mà chúng ta muốn duy trì qua chuyển hướng và sau đó biến mất.

Có ba middlewares  mà chúng ta cần include  để kết nối điều này:

const cookieParser = require('cookie-parser');
const session = require('express-session');
const flash = require('express-flash');
const middlewares = [
  // ...
  cookieParser(),
  session({
    secret: 'super-secret-key',
    key: 'super-secret-cookie',
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 60000 }
  }),
  flash(),
];

Middleware  express-flash thêm req.flash(type, message),mà chúng ta có thể sử dụng trong trình xử lý route  của mình:

router.post('/contact', [
  // validation ...
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.render('contact', {
      data: req.body,
      errors: errors.mapped()
    });
  }
  const data = matchedData(req);
  console.log('Sanitized: ', data);
  // Homework: send sanitized data in an email or persist to a db
  req.flash('success', 'Thanks for the message! I‘ll be in touch :)');
  res.redirect('/');
});

middleware express-flash thêm messages vào req.locals mà tất cả view đều có quyền truy cập:

<% if (messages.success) { %>
  <div class="flash flash-success"><%= messages.success %></div>
<% } %>
<h1>Working With Forms in Node.js</h1>

Bây giờ bạn sẽ được chuyển hướng đến chế độ view index  và thấy thông báo thành công khi form được gửi với dữ liệu hợp lệ. Huzzah! Bây giờ chúng ta có thể deploy app NodeJS và được gửi tin nhắn bởi hoàng tử Nigeria.

Gửi Email với NodeJS

Bạn có thể nhận thấy rằng việc gửi thư thực sự để cho người đọc làm bài tập về nhà. Điều này không quá khó vì nghe có vẻ thực hiện được bằng cách sử dụng gói Nodemailer. Bạn có thể tìm thấy hướng dẫn rõ ràng về cách thiết lập điều này tại đây

Kết luận

Qua phần này, bạn đã có thể tạo ra form và valid một cách tỉ mỉ.

Tham khảo thêm về NodeJS tại đây: Hướng dẫn sử dụng AWS Lambda với NodeJS – phần 1

Tham khảo thêm về React tại đây Tự học React JS trọn bộ kiến thức cơ bản – phần 1


Like it? Share with your friends!

1054