احراز هویت در Node.js (ثبت‌ نام کاربر)

همان‌طور که احراز هویت درAPIها مهم است، در بعضی از برنامه‌های وب هم ویژگی مهمی محسوب می‌شود، مانند برنامه‌هایی که دارای صفحات و امکاناتی هستند که می‌خواهند فقط برای کاربران ثبت‌نام شده و تصدیق شده قابل رؤیت باشند.

احراز هویت در Node.js (ثبت‌ نام کاربر)

در این آموزش یک برنامه وب ساده می‌سازید تا نحوه ایجاد ثبت‌نام کاربر را یاد بگیرید.

تنظیمات برنامه

یک پوشه جدید ایجاد کنید. چون پروژه ما یک پروژه آموزشی است،نام آن را (site-auth) می‌گذاریم. npm در پوشه جدید شما فقط create را ایجاد می‌کند. در کد زیر نحوه تولید npm را می‌بینید.

npm init –y

y- به npm می‌گوید که از گزینه‌های پیش‌فرض استفاده کند.

بخش dependencies که ما آن را در زیر نوشته‌ایم را در فایل package.json خود  ویرایش کنید.

#package.json
 
{
  "name": "site-auth",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "izuchukwu1",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "body-parser": "^1.17.1",
    "connect-flash": "^0.1.1",
    "cookie-parser": "^1.4.3",
    "express": "^4.15.2",
    "express-handlebars": "^3.0.0",
    "express-messages": "^1.0.1",
    "express-session": "^1.15.2",
    "joi": "^13.0.1",
    "mongoose": "^4.11.12",
    "morgan": "^1.8.1",
    "passport": "^0.4.0",
    "passport-local": "^1.0.0"
  }
}

با انجام این کار، دستورات نصب dependencies اجرا می‌شوند.

npm install

فایلی به نام app.js را در پوشه‌ای که با آن کار می‌کنید،‌ ایجاد کنید.

با require کردن dependenciesای که نصب کرده‌اید و فایل‌های ضروری آن شروع کنید.

#app.js
 
const express = require('express');
const morgan = require('morgan')
const path = require('path');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const expressHandlebars = require('express-handlebars');
const flash = require('connect-flash');
const session = require('express-session');
const mongoose = require('mongoose')
const passport = require('passport')
 
require('./config/passport')

این dependencies وقتی که دستور npm install را اجرا کردید نصب شد. برای استفاده از آن‌ها در برنامه خود، باید آن‌ها را require کرده و در متغیرهای مخصوص خود ذخیره کنید.

در این آموزش از MongoDB به عنوان پایگاه داده استفاده خواهید کرد. اطلاعات کاربر را باید در پایگاه داده ذخیره کنید. برای کار با MongoDB، از Mongoose، ابزار مدل‌سازی MongoDB برای Node.js، استفاده خواهید کرد.

#app.js
 
mongoose.Promise = global.Promise
mongoose.connect('mongodb://localhost:27017/site-auth')

بیاید در این مرحله، میان‌افزارها (middleware) را راه‌اندازی کنیم.

// 1
const app = express()
app.use(morgan('dev'))
 
// 2
app.set('views', path.join(__dirname, 'views'))
app.engine('handlebars', expressHandlebars({ defaultLayout: 'layout' }))
app.set('view engine', 'handlebars')
 
// 3
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public')))
app.use(session({
  cookie: { maxAge: 60000 },
  secret: 'codeworkrsecret',
  saveUninitialized: false,
  resave: false
}));
 
app.use(passport.initialize())
app.use(passport.session())
 
// 4
app.use(flash())
app.use((req, res, next) => {
  res.locals.success_mesages = req.flash('success')
  res.locals.error_messages = req.flash('error')
  next()
})
 
// 5
app.use('/', require('./routes/index'))
app.use('/users', require('./routes/users'))
 
// 6
// catch 404 and forward to error handler
app.use((req, res, next) => {
  res.render('notFound')
});
 
// 7
app.listen(5000, () => console.log('Server started listening on port 5000!'))

1. Express تولید شده و به app تخصیص داده شده است.

2. میان‌افزاری که viewها را مدیریت می‌کند، راه‌اندازی شده است.

3. شما میان‌افزاری برای bodyparser، cookie، session و passport راه‌اندازی کرده‌اید. وقتی که کاربران می‌خواهند لاگین کنند، از Passport استفاده می‌شود.

4. در بعضی قسمت‌ها پیام‌های flash نمایش داده خواهد شد. بنابراین نیاز دارید میان‌افزارهایی هم برای آن‌ها راه‌اندازی کنید، همچنین نوع flash messages هایی که می‌خواهید را ایجاد کنید.

5. میان‌افزارهای مسیرها (Routes)، که هر درخواستی که برای مسیر URL ساخته می‌شود را مدیریت می‌کند. مسیرهای URL مشخص شده در اینجا برای مسیر کاربران و ایندکس هستند.

6. میان‌افزاری برای مدیریت ارور 404. وقتی که درخواست‌ها با هیچ کدام از میان‌افزارهای ایجاد شده در بالا map نباشد، این میان‌افزار اجرا می‌شود.

7. سرور برای گوش دادن به پورت 5000 تنظیم شده است.

تنظیمات Viewها

یک پوشه به نام views ایجاد کنید. درون پوشه views، دو پوشه دیگر نیز بسازید و آن‌ها را layouts و partials نامگذاری کنید. باید یک ساختار درختی مثل تصویر زیر برای views خود داشته باشید. بنابراین، فایل‌های لازم را در دایرکتوری‌های مربوطه ایجاد کنید.

├── dashboard.handlebars
├── index.handlebars
├── layouts
│   └── layout.handlebars
├── login.handlebars
├── notFound.handlebars
├── partials
│   └── navbar.handlebars
└── register.handlebars

بعد از انجام این کار به مرحله بعد می‌رویم.

#dashboard.handlebars
 
<!-- Jumbotron -->
<div class="jumbotron">
  <h1>User DashBoard</h1>
</div>

این داشبوردی است که باید فقط برای کاربران ثبت‌نام شده قابل رؤیت باشد. در این آموزش، این صفحه، صفحه محرمانه شماست.

در حال حاضر صفحه index برنامه‌یتان باید مثل دستور زیر باشد.

#index.handlebars
 
<!-- Jumbotron -->
<div class="jumbotron">
  <h1>Site Authentication!</h1>
  <p class="lead">Welcome aboard.</p>
</div>

برنامه نیاز به یک layout دارد تا از آن استفاده کند. در زیر layoutای که قرار است مورد استفاده قرار دهید را ببینید.

#layout/layout.handlebars
 
<!DOCTYPE html>
<html>
  <head>
    <title>Site Authentication</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous">
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
          integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
          crossorigin="anonymous">
    <link rel="stylesheet"
          href="/css/style.css">
  </head>
  <body>
    {{#if success_messages }}
      <div class="alert alert-success">{{success_messages}}</div>
    {{/if}}
    {{#if error_messages }}
      <div class="alert alert-danger">{{error_messages}}</div>
    {{/if}}
    <div class="container">
      {{> navbar}}
      {{{body}}}
    </div>
 
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
            crossorigin="anonymous"></script>
  </body>
</html>

شما نیاز به یک صفحه login برای ثبت‌نام کاربران دارید.

#views/login.handlebars
 
<form class="form-signin" action="/users/login" method="POST">
  <h2 class="form-signin-heading">Please sign in</h2>
 
  <label for="inputEmail" class="sr-only">Email address</label>
  <input type="email" id="inputEmail" name="email" class="form-control" placeholder="Email address" required autofocus>
 
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required>
 
  <br/>
   
  <button class="btn btn-lg btn-default btn-block" type="submit">Sign in</button>
</form>

فایل notFound.handlebars به عنوان صفحه error استفاده خواهد شد.

#views/notFound.handlebars
 
<!-- Jumbotron -->
<div class="jumbotron">
  <h1>Error</h1>
</div>

صفحه ثبت‌نام‌تان همانند تصویر زیر است.

<form class="form-signin" action="/users/register" method="POST">
  <h2 class="form-signin-heading">Please sign up</h2>
 
  <label for="inputEmail" class="sr-only">Email address</label>
  <input type="email" id="inputEmail" name="email" class="form-control" placeholder="Email address" required autofocus>
 
  <label for="inputUsername" class="sr-only">Username</label>
  <input type="text" id="inputUsername" name="username" class="form-control" placeholder="Username" required>
 
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required>
 
  <label for="inputConfirmPassword" class="sr-only">Confirm Password</label>
  <input type="password" id="inputConfirmPassword" name="confirmationPassword" class="form-control" placeholder="Confirm Password" required>
 
  <br/>
 
  <button class="btn btn-lg btn-default btn-block" type="submit">Sign up</button>
</form>

در نهایت navigation bar برای viewهای شما اینگونه خواهد بود.

#partials/navbar.handlebars
 
<div class="masthead">
  <h3 class="text-muted">Site Authentication</h3>
  <nav>
    <ul class="nav nav-justified">
      <li class="active"><a href="/">Home</a></li>
      {{#if isAuthenticated}}
        <li><a href="/users/dashboard">Dashboard</a></li>
        <li><a href="/users/logout">Logout</a></li>
      {{else}}
        <li><a href="/users/register">Sign Up</a></li>
        <li><a href="/users/login">Sign In</a></li>
      {{/if}}
    </ul>
  </nav>
</div>

بعد از انجام این کار، خوب است که حالا به بخش‌های حرفه‌ای‌تر بپردازید.

اعتبارسنجی داده‌ها

شما به مدلی برای User نیاز دارید. از کدهای views بالا، می‌توان نتیجه گرفت که ویژگی‌هایی که برای این مدل نیاز دارید عبارتند از، ایمیل، نام کاربری و رمز عبور. یک پوشه به نام models بسازید و یک فایل در آن به نام user.js ایجاد کنید.

#models/user.js
 
// 1
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const bcrypt = require('bcryptjs')
 
// 2
const userSchema = new Schema({
  email: String,
  username: String,
  password: String
}, {
 
  // 3
  timestamps: {
    createdAt: 'createdAt',
    updatedAt: 'updatedAt'
  }
})
 
// 4
const User = mongoose.model('user', userSchema)
module.exports = User

1. dependencies را ایمپورت کرده و در متغیرها ذخیره کنید.

2. Schema را new کنید. نیاز دارید تا ایمیل،‌ رمز عبور و نام کاربری هر کاربر را در پایگاه داده ذخیره کنید. Schema نشان می‌دهد که چگونه مدل برای هر سند ساخته می‌شود. در اینجا تمام این ویژگی ها از نوع رشته هستند.

3. هر کاربر در پایگاه داده ذخیره می‌شود، همچنین لازم است تا یک timestamps ایجاد کنید. Mongoose برای به دست آوردن createdAt و updatedAt مورد استفاده قرار می‌گیرد و سپس در پایگاه داده ذخیره می‌شود.

4. مدل تعریف شده و به متغیری به نام User اختصاص داده می‌شود،‌ که پس از آن به عنوان یک ماژول اکسپورت می‌شود، بنابراین می‌تواند در سایر قسمت‌های برنامه استفاده شود.

Salting و Hashing رمز عبور

شما نمی‌خواهید رمز عبور کاربر را مانند یک متن ساده ذخیره کنید. چیزی که می‌خواهید اتفاق بیفتد این است که وقتی کاربر رمز عبور خود را با کاراکترهای ساده وارد کرد، این رمز ساده با استفاده از salt که توسط برنامه شما تولید می‌شود (با استفاده از bcryptjs) hash شود. سپس این رمز عبور هش‌شده در پایگاه داده ذخیره شود.

این عملکرد عالی است، نه؟ بیایید آن را در فایل user.js پیاده کنیم.

#models/user.js
 
module.exports.hashPassword = async (password) => {
  try {
    const salt = await bcrypt.genSalt(10)
    return await bcrypt.hash(password, salt)
  } catch(error) {
    throw new Error('Hashing failed', error)
  }
}

Index و مسیرهای  کاربران

یک پوشه به نام routes ایجاد کنید. در این پوشه دو فایل جدید ایجاد کنید: index.js و users.js.

فایل index.js بسیار ساده خواهد بود. این فایل به ایندکس برنامه شما map می‌شود. به یاد دارید زمانی که این کار را انجام دادید، میان‌افزار را برای routes خود در js file راه‌اندازی کردید.

app.use('/', require('./routes/index'))
app.use('/users', require('./routes/users'))

بنابراین routes ایندکس شما، که به سادگی صفحه ایندکس را ارائه می‌دهند، باید اینگونه باشند.

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

حالا نوبت routes کاربران است. در حال حاضر این فایل route، 4 کار را انجام می‌دهد.

1. require کردن dependencies. شما باید dependenciesای که با استفاده از npm نصب کرده‌اید را require کنید.

2. ورودی‌های معتبر. شما می‌خواهید مطمئن شوید که کاربر یک فرم خالی را ارسال نمی‌کند. همه ورودی‌ها لازم هستند و باید از نوع رشته باشند. ایمیل یک اعتبارسنجی خاص به نام .email() دارد که تضمین می‌کند چیزی که وارد شده است با فرمت ایمیل مطابقت دارد. رمز عبور هم با regular expression مورد تأیید قرار می‌گیرد. این اعتبارسنجی‌ها با استفاده از Joi انجام می‌شوند.

تنظیمات router. درخواست‌های رسیده شده صفحه ثبت نام را ارائه می‌دهند. وقتی کاربر روی دکمه ارسال فرم کلیک می‌کند در خواست POST اجرا می‌شود.

Router به عنوان یک ماژول ارسال می‌شود.

#routes/users.js
 
const express = require('express');
const router = express.Router()
const Joi = require('joi')
const passport = require('passport')
 
const User = require('../models/user')
 
 
//validation schema
 
const userSchema = Joi.object().keys({
  email: Joi.string().email().required(),
  username: Joi.string().required(),
  password: Joi.string().regex(/^[a-zA-Z0-9]{6,30}$/).required(),
  confirmationPassword: Joi.any().valid(Joi.ref('password')).required()
})
 
router.route('/register')
  .get((req, res) => {
    res.render('register')
  })
  .post(async (req, res, next) => {
    try {
      const result = Joi.validate(req.body, userSchema)
      if (result.error) {
        req.flash('error', 'Data entered is not valid. Please try again.')
        res.redirect('/users/register')
        return
      }
 
      const user = await User.findOne({ 'email': result.value.email })
      if (user) {
        req.flash('error', 'Email is already in use.')
        res.redirect('/users/register')
        return
      }
 
      const hash = await User.hashPassword(result.value.password)
 
      delete result.value.confirmationPassword
      result.value.password = hash
 
      const newUser = await new User(result.value)
      await newUser.save()
 
      req.flash('success', 'Registration successfully, go ahead and login.')
      res.redirect('/users/login')
 
    } catch(error) {
      next(error)
    }
  })
 
  module.exports = router

بیایید نگاه عمیق‌تری به عملکرد درخواست POST بیندازیم.

مقادیر وارد شده در فرم ثبت‌نام از طریق req.body قابل دسترس هستند و مقادیر شبیه دستورات زیر است.

value: 
   { email: 'chineduizuchkwu1@gmail.com',
     username: 'izu',
     password: 'chinedu',
     confirmationPassword: 'chinedu' },

این اعتبارسنجی مورد استفاده در userSchema است که در بالا آن را ساختید،‌ و مقادیر وارد شده توسط کاربر به متغیری به نام result اختصاص داده می‌شود.

اگر به دلیل اعتبارسنجی خطایی رخ دهد، پیغام خطایی به کاربر نمایش داده می‌شود و کاربر به صفحه ثبت‌نام بازگردانده می‌شود.
درغیر این صورت، آدرس ایمیل کاربر را چک می‌کنیم تا وجود نداشته باشد چون نمی‌خواهیم ایمیل تکراری ثبت شود. اگر آدرس ایمیل وارد شده یافت شود به کاربر گفته می‌شود که این ایمیل تکراری است.

اگر ایمیل تکراری نباشد به مرحله بعد می‌رود و باید رمز عبورش Hash شود. این جایی است که متد hashPassword را که در فایل user.js ساخته بودید را فراخوانی می‌کنید. بنابراین رمز عبور هش شده و به متغیری به نام hash اختصاص داده می‌شود.

نیازی به ذخیره‌سازی confirmationPassword در پایگاه داده نیست. بنابراین این مورد حذف شده است. رمز عبور در دسترس، هنوز همان رمز عبور ساده است. از آنجا که نمی‌خواهید رمز عبور ساده را در پایگاه داده ذخیره کنید، مهم است که مقدار هش‌شده را مجددا به رمز عبور اختصاص دهید. این کار با یک خط کد انجام می‌شود.

result.value.password = hash

کاربر جدید در پایگاه داده ذخیره می‌شود. پیغامی حاکی از موفقیت‌آمیز بودن ثبت‌نام نشان داده می‌شود و کاربر به صفحه ورود هدایت می‌شود.

با اجرای ترمینال، سرور خود را راه‌اندازی کنید:

node app.js

مرورگر خود را به این آدرس http://localhost:5000 بفرستید، حالا باید برنامه جدید خود را ببینید.

نتیجه‌گیری

حالا شما می‌دانید که چگونه ویژگی‌های ثبت‌نام را در برنامه وب Node پیاده کنید. اهمیت اعتبارسنجی ورودی‌های کاربر و نحوه انجام این کار با استفاده از Joi را یاد گرفتید. همچنین یاد گرفتید که با استفاده از bcryptjs رمز عبور را salt و hash کنید.