Серия Node.js, част 4. Експресен уебсайт с удостоверяване и оторизация
В реална производствена среда приложението работи като услуга във фонов режим и тази услуга се управлява от мениджър на процеси. И приложението трябва да работи зад обратен прокси сървър. Този обратен прокси сървър управлява TLS криптирането, получава заявките от клиента и насочва всяка заявка към приложението, работещо във фонов режим. Така че връзката от клиента към обратния прокси сървър е TLS криптирана. Следователно данните, прехвърляни между клиента и обратния прокси, са защитени.
Как да настроите такава производствена среда ще бъде показано в първата глава на тази документация.
Самото експресно приложение също съдържа някои функции за сигурност. Приложението съдържа потребителско удостоверяване, базирано на сесия, и HTTP заглавки, които помагат за допълнителна защита на приложението. Това е обяснено във втората глава на тази документация.
И накрая експресното приложение трябва да използва шаблонната машина PUG, за да изобрази HTML вместо нас. Това е описание на третата глава на тази документация.
1. Настройте производствената среда
Инсталиране на mongodb
Командата brew tap
без никакви аргументи изброява хранилищата на GitHub, които в момента са свързани с вашата инсталация на Homebrew.
Patricks-MBP:~ patrick$ brew tap
homebrew/cask
homebrew/core
homebrew/services
Формулата mongodb е премахната от homebrew-core. Но за щастие екипът на MongoDB поддържа персонализиран Homebrew кран на GitHub. Прочетете инструкциите във файла README.md.
Добавете персонализираното докосване в терминала на Mac OS и инсталирайте mongodb.
Patricks-MBP:~ patrick$ brew tap mongodb/brew
Patricks-MBP:~ patrick$ brew tap
homebrew/cask
homebrew/core
homebrew/services
mongodb/brew
Patricks-MBP:~ patrick$ brew install [email protected]
След инсталацията са съответните пътища.
the configuration file (/usr/local/etc/mongod.conf)
the log directory path (/usr/local/var/log/mongodb)
the data directory path (/usr/local/var/mongodb)
Проверете услугите с homebrew
brew services list
Name Status User Plist
mongodb-community started patrick /Users/patrick/Library/LaunchAgents/homebrew.mxcl.mongodb-community.plist
Стартирайте и спрете mongodb.
brew services start mongodb-community
brew services stop mongodb-community
Настройте mongodb за проекта
Настройте администраторски потребител
:# mongo
> use admin
switched to db admin
> db
admin
> db.createUser({ user: "adminUser", pwd: "adminpassword", roles: [{ role: "userAdminAnyDatabase", db: "admin" }, {"role" : "readWriteAnyDatabase", "db" : "admin"}] })
> db.auth("adminUser", "adminpassword")
1
> show users
{
"_id" : "admin.adminUser",
"userId" : UUID("5cbe2fc4-1e54-4c2d-89d1-317340429571"),
"user" : "adminUser",
"db" : "admin",
"roles" : [
{
"role" : "userAdminAnyDatabase",
"db" : "admin"
},
{
"role" : "readWriteAnyDatabase",
"db" : "admin"
}
],
"mechanisms" : [
"SCRAM-SHA-1",
"SCRAM-SHA-256"
]
}
> exit
Активиране на удостоверяване с security: authorization: enabled
#> nano /usr/local/etc/mongod.conf
systemLog:
destination: file
path: /usr/local/var/log/mongodb/mongo.log
logAppend: true
storage:
dbPath: /usr/local/var/mongodb
net:
port: 27017
bindIp: 127.0.0.1
security:
authorization: enabled
Влезте и се удостоверете с администратор
#> mongo
MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("b3e7f48a-a05c-4894-87db-996cb34eb1fb") }
MongoDB server version: 4.2.3
> show dbs
> db
test
> use admin
switched to db admin
> db
admin
> show dbs
> db.auth("adminUser", "adminpassword")
1
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
>
Ако влезете, не виждате никакви бази данни, когато се обадите на show dbs
. Базата данни по подразбиране, към която сте свързани, е test
.
След това се свързвате с администраторската база данни. За администратор настройвате администраторския потребител с ролите userAdminAnyDatabase
и readWriteAnyDatabase
. С тези разрешения администраторският потребител може да управлява потребители за всяка база данни и има достъп за четене и писане на всяка база данни.
Така че, когато влезете в администраторската база данни с администраторския потребител, можете да видите всички бази данни с show dbs
.
Mongodb идва с предварително инсталирани 3 стандартни dbs:
- администратор
- конфиг
- местен
Създайте нова база данни за нашето приложение за експресна сигурност (удостоверено като администраторски потребител — вижте по-горе)
> use express-security
switched to db express-security
> db
express-security
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
>
Базата данни, която сте създали, не е посочена тук. Трябва да вмъкнем поне една колекция в него, за да покажем тази база данни в списъка.
> db
express-security
> db.createCollection("col_default")
{ "ok" : 1 }
> show dbs
admin 0.000GB
config 0.000GB
express-security 0.000GB
local 0.000GB
> exit
Създайте потребител собственик за база данни с експресна сигурност, като използвате администраторския потребител
#> mongo
MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("79f79b63-9d08-489f-9e6c-bfc10d8cc09e") }
MongoDB server version: 4.2.3
> db
test
> show dbs
> use admin
switched to db admin
> db.auth("adminUser", "adminpassword")
1
> db
admin
> show dbs
admin 0.000GB
config 0.000GB
express-security 0.000GB
local 0.000GB
> use express-security
switched to db express-security
> db.createUser({ user: "owner_express-security", pwd: "passowrd", roles: [{ role: "dbOwner", db: "express-security" }] })
Successfully added user: {
"user" : "owner_express-security",
"roles" : [
{
"role" : "dbOwner",
"db" : "express-security"
}
]
}
> db
express-security
> show users
{
"_id" : "express-security.owner_express-security",
"userId" : UUID("7a0bafb2-d2ed-4d18-9aba-e2f15a503ec5"),
"user" : "owner_express-security",
"db" : "express-security",
"roles" : [
{
"role" : "dbOwner",
"db" : "express-security"
}
],
"mechanisms" : [
"SCRAM-SHA-1",
"SCRAM-SHA-256"
]
}
> exit
Низ за свързване за свързване към express-security db с помощта на owner_express-security потребител:
mongodb://owner_express-security:password@localhost/express-security
Инсталиране на PM2
PM2 е мениджър на процеси за Node.js приложения. Може да демонизира приложения, за да ги изпълнява като услуга във фонов режим.
Инсталирам pm2 като глобален npm пакет на моя Mac.
Patricks-Macbook Pro:~ patrick$ npm install pm2 -g
След това отидете до директорията на вашия проект.
Patricks-Macbook Pro:~ patrick$ cd Software/dev/node/articles/2020-05-15-express-security/express-security
Patricks-Macbook Pro:~ patrick$ ls -l
total 112
drwxr-xr-x 5 patrick staff 160 30 Mai 05:28 database
drwxr-xr-x 115 patrick staff 3680 30 Mai 19:35 node_modules
-rw-r--r-- 1 patrick staff 34366 30 Mai 19:35 package-lock.json
-rw-r--r-- 1 patrick staff 339 30 Mai 19:35 package.json
-rw-r--r--@ 1 patrick staff 12343 30 Jun 05:00 secserver.js
drwxr-xr-x 3 patrick staff 96 30 Mai 05:03 static
Patricks-Macbook Pro:express-security patrick$
Стартирайте приложението си с pm2
Patricks-Macbook Pro:express-security patrick$ pm2 start secserver.js
Patricks-Macbook Pro:~ patrick$ pm2 list
┌─────┬──────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ secserver │ default │ 1.0.0 │ fork │ 640 │ 16h │ 0 │ online │ 0% │ 48.6mb │ patrick │ disabled │
└─────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Patricks-Macbook Pro:~ patrick$
Други команди за управление на мениджъра на процеси.
pm2 start secserver.js
pm2 start <id>
pm2 list
pm2 stop <id>
pm2 restart <id>
pm2 show <id>
Инсталиране на nginx
nginx е HTTP с отворен код и HTTP обратен прокси сървър (също прокси за поща и балансьор на натоварването и т.н.). Инсталирам nginx на моя Mac с Homebrew.
brew install nginx
Можете да изброите услугите за варене със следната команда.
Patricks-MBP:digitaldocblog-V3 patrick$ brew services list
Name Status User Plist
mongodb-community started patrick /Users/patrick/Library/LaunchAgents/homebrew.mxcl.mongodb-community.plist
nginx started patrick /Users/patrick/Library/LaunchAgents/homebrew.mxcl.nginx.plist
Patricks-MBP:digitaldocblog-V3 patrick$
Можете да стартирате и спирате услугите за варене, както следва.
brew install nginx
brew services start nginx
brew services stop nginx
Настройте nginx с TLS/SSL
SSL/TLS работи, като използва комбинация от публичен сертификат и частен ключ.
SSL ключът (личен ключ) се пази в тайна на сървъра. Използва се за криптиране на съдържание, изпратено до клиенти.
SSL сертификатът се споделя публично с всеки, който поиска съдържанието. Може да се използва за дешифриране на съдържание, подписано от свързания SSL ключ.
създайте частен ключ
Patricks-MBP:express-security patrick$ cd /usr/local/etc/nginx
Patricks-MBP:nginx patrick$ ls -l
total 144
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf.default
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params.default
-rw-r--r-- 1 patrick admin 2837 5 Apr 13:18 koi-utf
-rw-r--r-- 1 patrick admin 2223 5 Apr 13:18 koi-win
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types.default
-rw-r--r-- 1 patrick admin 3106 15 Mai 05:19 nginx.conf
-rw-r--r-- 1 patrick admin 2680 5 Apr 13:18 nginx.conf.default
-rw-r--r-- 1 patrick admin 3091 21 Jan 05:40 nginx.conf.working
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params.default
drwxr-xr-x 3 patrick admin 96 21 Jan 06:02 servers
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params.default
-rw-r--r-- 1 patrick admin 3610 5 Apr 13:18 win-utf
Patricks-MBP:nginx patrick$ mkdir ssl
Patricks-MBP:nginx patrick$ ls -l
total 152
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf.default
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params.default
-rw-r--r-- 1 patrick admin 2837 5 Apr 13:18 koi-utf
-rw-r--r-- 1 patrick admin 2223 5 Apr 13:18 koi-win
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types.default
-rw-r--r--@ 1 patrick admin 373 18 Mai 05:38 nginx.conf
-rw-r--r-- 1 patrick admin 2680 5 Apr 13:18 nginx.conf.default
-rw-r--r-- 1 patrick admin 3091 21 Jan 05:40 nginx.conf.working
-rw-r--r--@ 1 patrick admin 1390 17 Mai 05:19 nginx_old.conf
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params.default
drwxr-xr-x 5 patrick admin 160 18 Mai 05:20 servers
drwxr-xr-x 4 patrick admin 128 16 Mai 05:41 ssl
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params.default
-rw-r--r-- 1 patrick admin 3610 5 Apr 13:18 win-utf
Patricks-MBP:nginx patrick$ cd ssl
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl genrsa -out privateKey.pem 4096
Patricks-MBP:ssl patrick$ ls -l
total 16
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
създайте заявка за подписване на сертификат (CSR)
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl req -new -key privateKey.pem -out csr.pem
Patricks-MBP:ssl patrick$ ls -l
total 16
-rw-r--r-- 1 patrick admin 1740 16 Mai 05:23 csr.pem
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
В случай че искам да поискам официален сертификат, трябва да изпратя този csr на сертифициращия орган. След това този орган ще създаде подписан от органа сертификат от CSR и ще ми го изпрати обратно.
Тази стъпка се извършва от нас и това е причината да създадем самоподписан сертификат. Този самоподписан сертификат не е официален сертификат и не се доверява на нито един браузър. Не е полезно да се използва самоподписан сертификат в производството, тъй като той създава съобщения за грешка в браузърите. Но за местно развитие самоподписаният сертификат е добре.
Така че създайте самоподписания сертификат. След това файлът csr може да бъде премахнат.
създайте самоподписан сертификат
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl x509 -in csr.pem -out selfsignedcertificate.pem -req -signkey privateKey.pem -days 365
Patricks-MBP:ssl patrick$ ls -l
total 24
-rw-r--r-- 1 patrick admin 1740 16 Mai 05:23 csr.pem
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
-rw-r--r-- 1 patrick admin 1980 16 Mai 05:39 selfsignedcertificate.pem
Patricks-MBP:ssl patrick$ rm csr.pem
Patricks-MBP:ssl patrick$ ls -l
total 24
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
-rw-r--r-- 1 patrick admin 1980 16 Mai 05:39 selfsignedcertificate.pem
покажи подробности за сертификат
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl x509 -in selfsignedcertificate.pem -text -noout
Конфигуриране на nginx сървъри с SSL
В нашата конфигурация налагаме ssl. Затова създаваме уеб сървър по подразбиране, който слуша на порт 80 с име на сървъра servtest.rottlaender.lan
.
Всяка заявка до servtest.rottlaender.lan:80
се пренасочва към моя Обратен прокси сървър, който слуша servtest.rottlaender.lan:443
.
Уеб сървърът по подразбиране е конфигуриран в /usr/local/etc/nginx/nginx.conf
.
# /usr/local/etc/nginx/nginx.conf
# default Webserver
worker_processes 1;
error_log /usr/local/etc/nginx/logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
access_log /usr/local/etc/nginx/logs/access.log;
# default Webserver redirect from port 80 to port 443 ssl
server {
listen 80;
listen [::]:80;
server_name servtest.rottlaender.lan;
return 301 https://$host$request_uri;
}
include servers/*;
}
Обратният прокси сървър е конфигуриран в /usr/local/etc/nginx/servers/reverse
.
// /usr/local/etc/nginx/servers/reverse
// reverse Proxy Server
server {
listen 443 ssl;
server_name servtest.rottlaender.lan;
ssl_certificate ssl/selfsignedcertificate.pem;
ssl_certificate_key ssl/privateKey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:3300;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Името на сървъра servtest.rottlaender.lan е свързано в /private/etc/hosts към ip 192.168.178.20, което е ip на моя компютър в моята локална мрежа.
Patricks-MBP:digitaldocblog-V3 patrick$ cat /private/etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
192.168.178.20 servtest.rottlaender.lan
2. Express Secure App (HTML версия на функциите за сигурност)
Това е много просто приложение, но показва основните функции за сигурност, които трябва да използвате, когато стартирате приложение за възел в производствена среда.
Приложението е уебсайт с просто оформление и навигация.
Началната страница съдържа статична информация и може да бъде достъпна от всеки.
На страницата за регистрация потребителите могат да намерят формуляр за регистрация. Потребителските данни, въведени тук, се записват в базата данни и потребителят е вписан в същото време. Познатите потребители могат да влязат със своя имейл и парола след успешна регистрация на страницата за вход. Страницата за влизане и регистрация може да бъде достъпна само ако потребителят не е влязъл. Ако потребител е влязъл и се опита да влезе в системата за влизане или регистрация, той ще бъде пренасочен към страницата на таблото за управление.
Таблото за управление е персонализирана област на уебсайта. Тази област може да бъде достъпна само ако потребителят е влязъл. Ако потребителят не е влязъл, той ще бъде пренасочен към страницата за вход.
Изходът всъщност не е страница, а връзка, която съдържа функция за излизане. Потребителите, които са влезли, могат да излязат, като използват тази връзка. Потребителите, които все още не са влезли, ще бъдат пренасочени към страницата за вход.
Изтеглете кода от GitHub
моля отидете на моя сайт GitHub и клонирайте кода. Тук можете да намерите малко вградена документация в кода. Подробностите са обяснени в тази глава.
Създайте експресна сигурност на домашната си директория на приложението
Началната директория на приложението ми е различна от тази, която е налична, след като сте клонирали кода от GitHub.
Patricks-MBP:2020-05-15-express-security patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security
Patricks-MBP:2020-05-15-express-security patrick$ mv node-part-5-express-security-with-db-pug express-security
Patricks-MBP:2020-05-15-express-security patrick$ cd express-security
Patricks-MBP:express-security patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security/express-security
Управление на променливите на средата
За да управлявам променливите на средата за моето приложение, използвам envy
. Първо се нуждаете от файловете .env
и .env.example
в главната директория на вашия проект. В .env.example
създавате списък с всички потенциални променливи на средата без никакви стойности, а в .env
използвате дефинираните променливи и им присвоявате стойностите.
Patricks-MBP:express-security patrick$ ls -al
total 152
drwxr-xr-x 14 patrick staff 448 23 Jun 05:29 .
drwxr-xr-x 4 patrick staff 128 26 Mai 05:40 ..
-rw------- 1 patrick staff 181 23 Jun 05:59 .env
-rw-r--r-- 1 patrick staff 53 23 Jun 05:59 .env.example
....
Patricks-MBP:express-security patrick$ cat .env.example
port=
mongodbpath=
sessionsecret=
sessioncookiename=
Patricks-MBP:express-security patrick$ cat .env
port=<YOUR_PORT>
mongodbpath=<YOUR_CONNECTION_STRING>
sessionsecret=<YOUR_SESSION_SECRET>
sessioncookiename=<YOUR_SESSION_COOKIE_NAME>
Patricks-MBP:express-security patrick$
Envy трябва да се инсталира като зависимост и да се изисква в основния файл на приложението secserver.js. След това можете да зададете променливите на средата, както следва.
// secserver.js
....
// envy module to manage environment variables
const envy = require('envy');
// set the environment variables
const env = envy()
const port = env.port
const mongodbpath = env.mongodbpath
const sessionsecret = env.sessionsecret
const sessioncookiename = env.sessioncookiename
....
Стартирайте MongoDB сървъра
За да стартираме db сървъра, инсталираме mongoose
като зависимост и го изискваме в конфигурационния файл db.js. Връзката с базата данни ще бъде инициирана с mongoose.connect
и функцията StartMongoServer ще бъде експортирана, за да бъде извикана в основния файл на приложението secserver.js.
const envy = require('envy')
const env = envy()
const mongodbpath = env.mongodbpath
const mongoose = require('mongoose');
mongoose.set('useNewUrlParser', true);
mongoose.set('useUnifiedTopology', true);
const StartMongoServer = async function() {
try {
await mongoose.connect(mongodbpath)
.then(function() {
console.log(`Mongoose connection open on ${mongodbpath}`);
})
.catch(function(error) {
console.log(`Connection error message: ${error.message}`);
})
} catch(error) {
res.json( { status: "db connection error", message: error.message } );
}
};
module.exports = StartMongoServer;
Удостоверяване и оторизация
За удостоверяване на потребителя използваме модула express-session
и за съхраняване на данни за сесията в хранилището на сесията в нашата база данни използваме connect-mongodb-session
. Затова инсталираме тези модули като зависимости в нашия проект и изискваме модулите в основния файл на нашето приложение secserver.js.
След това създаваме с new MongoDBStore
хранилище за сесии в нашата MongoDB, за да съхраняваме данните за сесии в колекция col_sessions
. грешките се улавят с store.on
.
Използваме сесията в нашето приложение с app.use( session({...}) )
. С всяка заявка към нашия сайт се създава нов обект на сесия с уникален идентификатор на сесия, който включва обект на бисквитка за сесия. Обектът на сесията има опции за ключове и стойностите за всеки ключ определят как да се работи с обекта на сесията. Идентификаторът на сесията се създава и подписва с помощта на опцията secret
. Използваме name
, за да предоставим име на бисквитка за сесия и store
, за да дефинираме къде трябва да се съхранява обектът на сесията (в случай че съхраняваме сесията).
Имаме достъп до обекта на сесията с req.session
и идентификатора на сесията с req.session.id
. С всяка заявка имаме нова сесия и тази нова сесия ще бъде създадена, но не се съхранява никъде досега. Казваме, че сесията е неинициализирана. Опцията saveUninitialized
false гарантира, че сесия ще бъде записана в магазина само в случай, че е била променена. Какво означава това?
Можем да променим сесията, когато съхраняваме допълнителни данни в нея. Винаги правим това, когато потребителят влиза в системата по маршрута login
или register
. Когато публикуваме данните от login- или от registration-form към сървъра, ние извикваме loginUser или createUser модул, който е дефиниран в database/controllers/userC.js
. И двата модула правят основно едно и също нещо: те създават обект userData и съхраняват обекта userData в обекта на сесията и пренасочват потребителя към таблото за управление, когато влизането или регистрацията са успешни.
....
var userData = {
userId: user._id,
name: user.name,
lastname: user.lastname,
email: user.email,
role: user.role
}
req.session.userData = userData
res.redirect('/dashboard')
....
Ако потребителят е влязъл успешно в сесията е инициализирана (модифицирана), обектът на сесията, вкл. обектът userData се съхранява в хранилището, а бисквитката се съхранява в търсещия браузър. Съдържанието на бисквитката е само хеш на идентификатора на сесията и с всяка заявка на влязъл потребител се търси сесията на сървъра.
Бисквитката в браузъра ще живее максимум 1 седмица, както сме дефинирали в обекта на бисквитката maxAge
, зададен на 1 седмица. Поради опцията за бисквитка sameSite
true обхватът на бисквитката е ограничен до същия сайт.
Тогава опцията resave
false гарантира, че сесията няма да се актуализира с всяка заявка. Това означава, че идентификационният номер на сесията, който е създаден, когато потребителят е влязъл, ще се запази, докато потребителят не излезе отново.
// secserver.js
....
// server side session and cookie module
const session = require('express-session');
// mongodb session storage module
const connectMdbSession = require('connect-mongodb-session');
....
// Create MongoDB session storage
const MongoDBStore = connectMdbSession(session)
const store = new MongoDBStore({
uri: mongodbpath,
collection: 'col_sessions'
});
// catch errors in case store creation fails
store.on('error', function(error) {
console.log(`error store session in session store: ${error.message}`);
});
// Create the express app
const app = express();
....
// use session to create session and session cookie
app.use(session({
secret: sessionsecret,
name: sessioncookiename,
store: store,
resave: false,
saveUninitialized: false,
// set cookie to 1 week maxAge
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7,
sameSite: true
},
}));
....
Защитени HTTP заглавки
Хедърите на отговора са HTTP хедъри, които идват с HTTP отговора от сървъра към клиента. Заглавката на http отговора съдържа данни, които биха могли да навредят на целостта на клиента. Поради това е важно да защитите заглавката на отговора на вашето приложение.
За да защитя заглавките на http отговорите, използвам модула шлем. Това е сравнително лесен за използване модул, състоящ се от различни функционалности на междинния софтуер за защита на различни заглавки на http отговор.
Първо инсталираме helmet
като зависимост от нашия проект. След това изискваме каска и използваме каска веднага след като сме създали приложението.
// secserver.js
// hTTP module
const http = require('http');
// express module
const express = require('express');
// hTTP header security module
const helmet = require('helmet');
// Create the express app
const app = express();
....
// use secure HTTP headers using helmet
app.use(helmet())
Използвайки просто app.use(helmet())
, задайте защитата на http заглавката по подразбиране. След това могат да се използват следните 7 от 11 функции на каската.
- dnsPrefetchControl контролира предварителното извличане на DNS на браузъра
- frameguard за предотвратяване на clickjacking
- hidePoweredBy за премахване на заглавката X-Powered-By
- hsts за HTTP Strict Transport Security
- ieNoOpen задава X-Download-Options за IE8+
- noSniff, за да предпази клиентите от надушване на типа MIME
- xssFilter добавя някои малки XSS защити
Когато след това поискаме от нашата начална страница да извлече http заглавките, използвайки curl -k --head
в терминала, виждаме следния изход.
Patricks-MBP:express-security patrick$ curl -k --head https://servtest.rottlaender.lan
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Fri, 26 Jun 2020 16:14:14 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1734
Connection: keep-alive
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
ETag: W/"6c6-U2uWyDNyzlyBAbSI/Quxqo9RRQE"
Patricks-MBP:express-security patrick$
Маршрутизиране на приложението
Получете маршрути: Имаме следните get
маршрути и навигация.
- У дома (/)
- Вход (/вход)
- Регистрирайте се (/register)
- Табло (/dashboard)
- Изход (/изход)
get
маршрутите включват незадължителен междинен софтуер и отговарят HTML обратно на клиента.
app.get('/<route>', <optional: someMiddleware>, (req, res) => {
res.send(`<some HTML>`)
})
Няма да обяснявам подробно HTML и css. Но както всеки може да види, HTML е един и същ за всеки маршрут с изключение на <body>
. Разбира се, това не е много хубаво и става малко по-ефективно с използването на шаблонна машина, която ще обясня по-долу с помощта на PUG шаблонна машина. След това ще възстановя приложението с помощта на PUG.
Нека да разгледаме middleware
. Ако е направена заявка за маршрут и е включена функция на междинен софтуер, първо се изпълнява функцията на междинен софтуер, преди да бъде извикана следващата функция за маршрутизиране function(req, res)
. Във функцията на междинния софтуер е вградено условие, което се проверява. Моят междинен софтуер е изграден така, че в случай че условието е вярно, кодът на междинния софтуер се изпълнява директно и следващата функция за маршрутизиране се пропуска. Ако условието е невярно, се извиква следващата функция за маршрутизиране function(req, res)
.
Създадох 2 различни мидълуерни функции, всяка от които проверява
среден софтуер 1 (път за влизане и регистриране): потребителят е влязъл
// secserver.js
....
// middleware 1 to redirect authenticated users to their dashboard
const redirectDashboard = (req, res, next) => {
if (req.session.userData) {
res.redirect('/dashboard')
} else {
next()
}
}
....
Ако потребител е влязъл в системата, заявката трябва да бъде пренасочена към маршрута на таблото за управление, във всеки друг случай (потребителят не е влязъл в системата) се извиква следващата функция за маршрутизиране function(req, res)
и отговаря на HTML на браузъра. Този междинен софтуер 1 е включен в маршрута /login- и /register. Това означава, че влезлите потребители ще бъдат пренасочени към тяхното табло за управление, а невлезлите потребители ще видят формуляра за влизане и регистрация.
Middleware 2 (табло за управление и маршрут за излизане): потребителят не е влязъл.
// secserver.js
....
// middleware 2 to redirect not authenticated users to login
const redirectLogin = (req, res, next) => {
if (!req.session.userData) {
res.redirect('/login')
} else {
next()
}
}
....
Ако потребителят не е влязъл, заявката трябва да бъде пренасочена към пътя за влизане, във всеки друг случай (потребителят е влязъл) се извиква следващата функция за маршрутизиране function(req, res)
и отговаря на HTML на браузъра. Този междинен софтуер 2 е включен в маршрута /dashboard- и /logout. Това означава, че невлезлите потребители ще бъдат пренасочени към маршрута за влизане, влезлите потребители ще видят таблото за управление или могат сами да излязат.
Публикувайте маршрути: Имаме следните post
маршрути.
- /Влизам
- /регистрирам
Маршрутите за влизане и регистър get
съдържат формуляр в HTML. С тези форми потребителят предоставя данните за влизане и регистрация на потребител. Когато потребителят щракне върху бутона за изпращане, действието е да извика маршрута за влизане или регистрация post
. Това ще се случи за всички невлезли потребители. Маршрутите за влизане и регистър get
имат междинен софтуер redirectDashboard
за пренасочване на потребителя към таблото за управление, ако потребителят вече е влязъл.
// secserver.js
....
app.get('/login', redirectDashboard, (req, res) => {
....
res.send(`
....
<div class="form">
<form id='register_form' method='post' action='/register'>
......
<label for='send'>
<input class='sendbutton' type='submit' name='send' value='Send'>
</label>
</form>
</div>
`)
)}
....
app.get('/register', redirectDashboard, (req, res) => {
....
res.send(`
....
<div class="form">
<form id='login_form' method='post' action='/login'>
......
<label for='send'>
<input class='sendbutton' type='submit' name='send' value='Send'>
</label>
</form>
</div>
`)
)}
.....
Маршрутите post
съдържат функции за влизане (loginUser) или регистриране (createUser) на потребителя.
// secserver.js
....
// Post routes to manage user login and user registration
app.post('/login', userController.loginUser);
app.post('/register', userController.createUser);
....
Функцията loginUser
е дефинирана в потребителския контролер database/controllers/userC.js
. Тази функция търси потребител в базата данни въз основа на имейл адреса, предоставен от тялото на заявката. Данните, които са прикачени към тялото на заявката, са предоставени от потребителя чрез формата за влизане в приложението. Ако не може да бъде намерен потребител в базата данни, влизането не е възможно. Ако съществува потребител с дадения имейл адрес, предоставената парола ще бъде сравнена с тази, съхранена в базата данни. Ако съвпадението на паролата е неуспешно, влизането не е възможно, тъй като предоставената парола е грешна. във всеки друг случай влизането се извършва и се създава обект userData и се прикачва към обекта на сесията.
// database/controllers/userC.js
User.findOne({ email: req.body.email }, function(error, user) {
if (!user) {
res.status(400).send({ code: 400, status: 'Bad Request', message: 'No User found with this email' })
} else {
if (bcrypt.compareSync(req.body.password, user.password)) {
var userData = { userId: user._id, name: user.name, lastname: user.lastname, email: user.email, role: user.role }
req.session.userData = userData
res.redirect('/dashboard')
} else {
res.status(400).send({ code: 400, status: 'Bad Request', message: 'Wrong User password' })
}
}
})
}
Функцията createUser
също е дефинирана в потребителския контролер database/controllers/userC.js
. Тази функция създава нов потребителски обект въз основа на данните от тялото на заявката, предоставени от потребителя чрез формуляра. Предоставената парола ще бъде хеширана и съхранена заедно с всички други данни в базата данни. Накрая се създава обект userData и се прикачва към сесията и потребителят ще бъде пренасочен към таблото за управление след успешна регистрация.
// database/controllers/userC.js
createUser: async function (req, res) {
// assign input data from request body to input variables
const name = req.body.name
const lastname = req.body.lastname
const email = req.body.email
const password = req.body.password
const role = req.body.role
const newUser = new User({
name: name,
lastname: lastname,
email: email,
password: password,
role: role
})
newUser.password = await bcrypt.hash(newUser.password, saltRounds)
await newUser.save(function(err, user) {
if (err) {
// if a validation err occur end request and send response
res.status(400).send({ code: 400, status: 'Bad Request', message: err.message })
} else {
// req.session.userId = user._id
var userData = { userId: user._id, name: user.name, lastname: user.lastname, email: user.email, role: user.role }
req.session.userData = userData
res.redirect('/dashboard')
}
})
},
И имаме маршрут по подразбиране get
.
- /favicon.ico
Браузърите по подразбиране ще се опитат да поискат /favicon.ico от корена на име на хост, за да покажат икона в раздела на браузъра. Тъй като досега не използваме favicon, трябва да избягваме тези заявки да връщат 404 (не е намерено). Тук Заявката /favicon.ico ще бъде уловена и ще изпрати статус 204 Няма съдържание.
// secserver.js
....
app.get('/favicon.ico', function(req, res) {
console.log(req.url);
res.status(204).json({status: 'no favicon'});
});
....
3. Приложение Express (версия на шаблон за мопс)
От функционална гледна точка това приложение е почти същото приложение като HTML версията. Разликата е, че използваме PUG шаблони вместо HTML във всеки res.send().
Настройте отделна база данни
За PUG версията на моето приложение създадох нова база данни за управление на потребителите и сесиите.
#> mongo
MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("b3e7f48a-a05c-4894-87db-996cb34eb1fb") }
MongoDB server version: 4.2.3
> db
test
> use admin
switched to db admin
> db
admin
> db.auth("adminUser", "adminpassword")
1
> show dbs
admin 0.000GB
config 0.000GB
express-security 0.000GB
local 0.000GB
> use express-security-pug
switched to db express-security-pug
> db.createUser({ user: "owner_express-security-pug", pwd: "passowrd", roles: [{ role: "dbOwner", db: "express-security-pug" }] })
Successfully added user: {
"user" : "owner_express-security-pug",
"roles" : [
{
"role" : "dbOwner",
"db" : "express-security-pug"
}
]
}
> db
express-security-pug
> exit
Низ за връзка за свързване към express-security-pug db с помощта на потребителя owner_express-security-pug.
mongodb://owner_express-security-pug:password@localhost/express-security-pug
Изтеглете кода от GitHub
моля отидете на моя сайт GitHub и клонирайте кода. Тук ще намерите малко вградена документация в кода.
Създайте домашната си директория на приложението express-security-pug
Началната директория на приложението ми е различна от тази, която е налична, след като сте клонирали кода от GitHub.
Patricks-MBP:2020-05-15-express-security patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security
Patricks-MBP:2020-05-15-express-security patrick$ mv node-part-5-express-security-with-db-pug express-security-pug
Patricks-MBP:2020-05-15-express-security patrick$ cd express-security-pug
Patricks-MBP:express-security-pug patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security/express-security-pug
Инсталирайте PUG и го използвайте в приложението си
Първо инсталираме PUG като зависимост.
Patricks-MBP:express-security-pug patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security/express-security-pug
Patricks-MBP:express-security-pug patrick$ npm install pug --save
PUG вече е напълно интегриран в Express. моля прочетете документацията как да използвате машини за шаблони в Express.
След като инсталирате PUG, машината за изглед трябва да бъде зададена във вашия основен файл на приложение secserverpug.js.
// secserverpug.js
....
// use Pug Template Engine
app.set('view engine', 'pug')
app.set('views', './views')
....
Тези инструкции указват на приложението ви, че се използва машина за шаблони PUG и че шаблоните могат да бъдат намерени в директория /views
.
Настройка на PUG Directory
В /views
настройвам шаблоните за начало, вход, регистрация и шаблон за грешка.
В /views/includes
настройвам файловете, съдържащи HTML или JavaScript. Те могат да бъдат включени в шаблоните.
Patricks-MBP:express-security-pug patrick$ ls -l
total 128
-rw-r--r-- 1 patrick staff 771 1 Jul 06:04 README.md
drwxr-xr-x 5 patrick staff 160 29 Jun 05:26 database
drwxr-xr-x 150 patrick staff 4800 29 Jun 06:11 node_modules
-rw-r--r-- 1 patrick staff 47547 29 Jun 06:11 package-lock.json
-rw-r--r-- 1 patrick staff 367 29 Jun 06:11 package.json
-rw-r--r-- 1 patrick staff 4393 2 Jul 05:34 secserverpug.js
drwxr-xr-x 3 patrick staff 96 29 Jun 05:26 static
drwxr-xr-x 8 patrick staff 256 2 Jul 05:45 views
Patricks-MBP:express-security-pug patrick$ ls -l views
total 40
-rw-r--r-- 1 patrick staff 549 30 Jun 05:35 dashboard.pug
-rw-r--r-- 1 patrick staff 522 2 Jul 05:50 err.pug
-rw-r--r-- 1 patrick staff 420 29 Jun 05:39 home.pug
drwxr-xr-x 6 patrick staff 192 29 Jun 05:17 includes
-rw-r--r-- 1 patrick staff 735 30 Jun 05:02 login.pug
-rw-r--r-- 1 patrick staff 1067 30 Jun 05:08 register.pug
Patricks-MBP:express-security-pug patrick$ ls -l views/includes
total 32
-rw-r--r-- 1 patrick staff 76 29 Jun 05:39 foot.pug
-rw-r--r-- 1 patrick staff 167 29 Jun 05:24 head.pug
-rw-r--r-- 1 patrick staff 489 2 Jul 05:13 nav.pug
-rw-r--r-- 1 patrick staff 420 29 Jun 05:08 script.js
Patricks-MBP:express-security-pug patrick$
Отзивчив дизайн на уебсайт
Всеки сайт като начало, вход, регистрация и табло за управление има конкретен шаблон за сайт в директория /views
. Съдържанието на сайта ще бъде определено в основната секция на всеки шаблон. PUG позволява включването на файлове с HTML или JavaScript. Това прави шаблоните на сайта ясни и лесни за поддръжка. Включва се намира в директория /views/includes
.
Уебсайтът е изграден въз основа на мрежов дизайн и всеки шаблон на сайт има следната структура.
doctype html
HTML
Head
include includes/head.pug
Body
Grid-Container
Header
include includes/nav.pug
Main
... site template specific HTML ...
Footer
include includes/foot.pug
<script>
include includes/script.js
Дизайнът на уебсайта е дефиниран в css в static/css/style.css
.
Тук в css дефинираме Структурата на сайта като мрежови зони, състоящи се от горен, основен и долен колонтитул, и ги свързваме към мрежовия контейнер.
....
.header { grid-area: header; background-color: #ffffff; border-radius: 5px;}
.main { grid-area: main; background-color: #ffffff; border-radius: 5px;}
.footer { grid-area: footer; background-color: #ffffff; border-radius: 5px;}
.grid-container {
display: grid;
grid-template-areas:
"header"
"main"
"footer";
grid-gap: 5px;
background-color: #d1d1e0;
padding: 50px;
}
....
Навигацията е дефинирана в областта на заглавката на Grid-Container и HTML идва в шаблона чрез include includes/nav.pug
.
// includes/nav.pug
//(this) refers to the DOM element to which the onclick attribute belongs to
// the a DOM element will be given as parameter to the function
a(class="burgericon" onclick="myFunction(this)")
div(class='burgerline' id='bar1')
div(class='burgerline' id='bar2')
div(class='burgerline' id='bar3')
a(class='link' href='/bg/') Home
a(class='link' href='/bg/login') Login
a(class='link' href='/bg/register') Register
a(class='link' href='/bg/dashboard') Dashboard
a(class='link' href='/bg/logout') Logout
Така че навигационният дизайн след това се дефинира в css. Всеки навигационен обект е a
връзка. Имаме a
връзки с клас link
и burgericon
. Burgericon се използва за отваряне на навигационната лента при щракване, когато екранът е по-малък от 600px (като дисплеите на iphone и т.н., обяснено по-долу), състои се от 3 burgerline и тези редове се създават с помощта на 3 div обекта с клас burgerline
. Бургелините ще се трансформират със скорост 0,4 секунди, когато щракнете върху бургерикона (обяснено по-долу). Бургериконът не се вижда и е подравнен в десния край. Всички други връзки за навигация са видими и подравнени в левия край.
/* static/css/style.css */
....
/* style the navigation links with float on the left (side by side) */
.header a.link {
float: left;
display: block;
padding: 14px 16px;
text-decoration: none;
font-size: 1.4vw;
color: #28283e;
}
/* hover effect for each navigation link */
.header a.link:hover {
background-color: #28283e;
color: #ffffff;
}
/* style the burgericon link on the right */
.header a.burgericon {
float: right;
display: none;
padding: 14px 16px;
}
/* style each burgerline that create the burgericon */
.burgerline {
width: 35px;
height: 5px;
background-color: #28283e;
margin: 6px 0;
transition: 0.4s;
}
....
Когато екранът на дисплея е по-малък от 600px, връзките за навигация няма да се показват и вместо това бургериконът (от дясната страна) ще избледнява.
/* static/css/style.css */
....
/* for screens up to 600px remove the navigation links and show the burgericon instead */
@media screen and (max-width: 600px) {
.header a.link { display: none; }
.header a.burgericon { display: block; }
}
....
Когато щракнете върху иконата на бургер, линиите на бургер ще се трансформират, така че ще видите кръст вместо иконата като хамбургер. Втората бургер линия с id='bar2'
изобщо няма да бъде показана, докато другите 2 бургер линии ще бъдат завъртяни на 45 градуса обратно на часовниковата стрелка (бургер линия с id='bar1'
) и по часовниковата стрелка (бургер линия с id='bar1'
).
/* static/css/style.css */
....
/* style burgerlines after on onclick event */
/* the .change class will be added onclick with classList.toggle in the JavaScript */
/* rotate first bar */
.change #bar1 {
/* rotate -45 degrees (counterclockwise) move 15px down in Y-direction */
transform: rotate(-45deg) translateY(15px);
}
/* fade out the second bar */
.change #bar2 {
opacity: 0;
}
/* rotate third bar */
.change #bar3 {
/* rotate +45 degrees (clockwise) move 15px up in Y-direction */
transform: rotate(45deg) translateY(-15px);
}
....
След щракване върху бургерикона линиите на бургерите се трансформират, както е описано. Връзките на менюто за навигация се показват една под друга (без плаващи) и са подравнени вляво.
/* static/css/style.css */
....
/* for screens up to 600px and after onclick event the responsive class will be added to the header */
@media screen and (max-width: 600px) {
/* show navigation links left with no float (links shown among themselves) */
.header.responsive a.link {
float: none;
display: block;
text-align: left;
}
}
....
Всички функционалности на onclick се контролират от javascript, който е вграден в HTML на всеки шаблон на сайт (моля, вижте по-горе include/nav.pug). В HTML събитието onclick се инициира във връзката burgericon и функцията myFunction се извиква с onclick =" myFunction(this) "
. С параметъра this целият обект на burgericon се прехвърля към функцията на javascript.
С всяко щракване върху иконата на бургер, класът change
се добавя към всяка бургер линия или, ако е наличен, се премахва. Това се прави от функцията toggle(). Ако е зададено change
, иконата на Хамбург се трансформира в кръст според спецификацията в css (вижте по-горе). Ако change
се изтегли с ново щракване, иконата на хамбургер се показва отново.
Но това се случва още повече в javascript, когато щракнете върху иконата на хамбургер. Елементът с id responsivenav
се търси и променливатаreponsiveNavElement
се присвоява на този елемент. Е класът на responsiveNavElement header
classresponsive
се добавя след щракване върху иконата на хамбургер. Ако класът responsive
е зададен, както е описано по-горе, връзките на навигационното меню се показват една под друга (float none) и са подравнени вляво. Така че се прилага в css .header.responsive a.link {....}
Във всички останали случаи е зададен само клас header
. Така че се прилага в css .header a.link {....}
и връзките за навигация не се показват.
// includes/script.js
// the (burgerlines) parameter represent the DOM element that has been given to the function
function myFunction(burgerlines) {
burgerlines.classList.toggle('change');
var reponsiveNavElement = document.getElementById('responsivenav');
if (reponsiveNavElement.className === 'header') {
reponsiveNavElement.classList.add('responsive')
} else {
reponsiveNavElement.className = 'header';
}
}
Накрая в края на css дефинираме настройките по подразбиране за h1, за нашето текстово съдържание, формулярите, полетата за въвеждане и бутоните за изпращане.
Резюме и перспектива
В тази част 4 от моята малка серия node.js видяхме как да настроим готова за производство среда за нашето експресно приложение. Показах това с помощта на Mac OS, но по принцип тази настройка важи и за Linux, например.
Основната настройка е, казано просто, приложението да работи като услуга на сървъра във фонов режим с помощта на диспечера на процеси, но няма интерфейс към клиента. Този клиентски интерфейс регулира обратен прокси, който е нагоре по веригата на приложението и приема всички заявки и ги препраща към приложението, както и отговорите от приложението обратно към клиента. Комуникацията е SSL/TLS защитена.
В центъра на настройката е отделна локална MongoDB, която управлява всички данни на приложението. В нашия пример това са потребителите, но също и сесиите. Предпочитам да настроя своя собствена MongoDB на моя сървър, но разбира се е възможно да използвам решение, базирано на облак, или да инсталирам друга база данни локално.
Самото експресно приложение използва защитени заглавки на HTTP отговор, така че да се извършват HTTP атаки като кликджакинг, MIME тип снифинг или някои по-малки XSS атаки на клиента възможно най-трудно. Достъпът до личните области на приложението е защитен чрез удостоверяване и оторизация на базата на сесия. Данните, свързани със сесията, се съхраняват в базата данни, а не в бисквитката на браузъра, което означава допълнителна сигурност по отношение на атаки срещу клиента. Бисквитката на браузъра съдържа само хеш на идентификатора на сесията за заявка на съответните потребителски данни от базата данни.
Бих искал да завърша моята серия от node.js с част 4. Обсъдих и демонстрирах основните концепции и процедури в части от 1 до 4. Разбира се, ще има и други интересни статии по темата node.js и уеб програмирането на Digitaldocblog . Просто погледнете.