Нарастването на приложенията от страна на клиента създаде необходимостта от защитен работен процес за удостоверяване чрез AJAX. Въпреки че ръкостискането за удостоверяване не е универсално, като се има предвид разнообразието в клиентските рамки и API от страна на сървъра, има някои най-добри практики, които могат да втвърдят комуникацията и да защитят вашите фантастични приложения с една страница от потенциални шпиони. В тази публикация ще прегледам следните практики и ще ги внедря в „демонстрационно приложение“, създадено с помощта на Backbone.JS и Node.JS:

  • CSRF заглавки в заявки за удостоверяване за предотвратяване на фалшифициране
  • Устойчивост на състоянието на удостоверяване чрез подписани бисквитки
  • Глобален (singleton) модел на сесия в клиента, чиито промени в състоянието могат да бъдат слушани
  • Валидации на модели от страна на клиента + от страна на сървъра
  • Сол/хеширане на пароли за резервно съхранение

Общ преглед на демо приложението

„Демо приложението“ се състои както от клиентски, така и от сървърни компоненти.

Сървърният слой е лек, еднофайлов HTTP сървър Express.js и база данни SQLLite3. Той отговаря на редица типични маршрути на API за удостоверяване, както и изобразява първоначалната обвивка на страницата index.html, за да постави сесийния CSRF токен. Въпреки че е добра практика да имате изолиран модел User/Auth в сървъра, който обработва валидациите и взаимодействието с базата данни, за простота тази функционалност е капсулирана в манипулатори на API (известни още като на ниво контролер).

клиентската страна е структурирана с помощта на Backbone.js (вероятно най-разпространената MV* рамка от страна на клиента по това време) и Require.js (популярна модулна рамка, базирана на зависимости). Освен това е доста оголен само с няколко изгледа, рутер с активиран pushState и модели за потребител и сесия. Клиентът комуникира със сървъра чрез Backbone.sync методи, които по същество просто обвиват стандартните AJAX CRUD операции.

CSRF

CSRF токен по същество е средство за сървъра да разпознае самоличността на искащия клиент. Това разпознаване на самоличност се използва за предотвратяване на XSS (Cross-Site Scripting) от други скриптове, които може също да се изпълняват на страницата. Рандомизираният токен се генерира и поставя от сървъра в HTML мета таговете на клиента (които не трябва да се опресняват в приложение с една страница) или вътре в скрит елемент на формуляр. След това клиентът може да избере да включи този токен в заглавките (по-конкретно заглавката „X-CSRF-Token“) на AJAX заявките като средство за идентификация и оторизация.

Повечето големи рамки от страна на сървъра, които се използват днес, предоставят средство за генериране и валидиране на CSRF токен. В демонстрационното приложение използваме CSRF Express.js мидълуер и Express.js Handlebars templating engine, за да изобразим първоначалната HTML обвивка с този токен.

<meta name=”csrf-token” content=”{{csrfToken}}”/>

В клиента SessionModel извличаме този токен от мета съдържанието с помощта на jQuery и го включваме в нашите POST заглавки за удостоверяване. След това нашият сървър може да потвърди самоличността на клиента, когато бъде получена заявка за удостоверяване.

/*
 * Abstracted fxn to make a POST request to the auth endpoint
 * This takes care of the CSRF header for security, as well as
 * updating the user and session after receiving an API response
 */
postAuth: function(opts, callback, args){
  var self = this;
  var postData = _.omit(opts, ‘method’);
  $.ajax({
    url: this.url() + ‘/’ + opts.method,
    contentType: ‘application/json’,
    dataType: ‘json’,
    type: ‘POST’,
    beforeSend: function(xhr) {
    // Set the CSRF Token in the header for security
    var token = $(‘meta[name=”csrf-token”]’).attr(‘content’);
    if (token) xhr.setRequestHeader(‘X-CSRF-Token’, token);
    },
    data: JSON.stringify( _.omit(opts, ‘method’) ),
    success: function(res){
      if( !res.error ){
        if(_.indexOf([‘login’, ‘signup’], opts.method) !== -1){
          self.updateSessionUser( res.user || {} );
          self.set({ user_id: res.user.id, logged_in: true });
        } else {
          self.set({ logged_in: false });
        }
        if(callback && ‘success’ in callback) callback.success(res);
      } else {
        if(callback && ‘error’ in callback) callback.error(res);
      }
    },
    error: function(mod, res){
      if(callback && ‘error’ in callback) callback.error(res);
    }
  }).complete( function(){
    if(callback && ‘complete’ in callback) callback.complete(res);
  });
}

За по-задълбочено гмуркане в CSRF препоръчвам да се консултирате с подробното Ръководство за сигурност на Ruby on Rails.

Подписани бисквитки

Постоянните сесии, базирани на бисквитки, стават все по-често срещани сред приложенията с една страница днес. Кой иска да трябва да влиза отново в сайт всеки път, когато го отваря отново в браузъра си? Не съм аз.

Бисквитките са просто двойки ключ-стойност, съхранявани в браузъра със специфичност на ниво домейн и изтичане. Постоянното удостоверяване често се постига чрез бисквитки, защото те могат да бъдат прочетени (в заявката) и записани (в отговора) от сървъра на притежаващия домейн. Подобно на CSRF, бисквитките могат да идентифицират клиент въз основа на тяхната стойност. Тъй като бисквитките могат да бъдат прочетени от клиентски скриптове, включително скриптове от външни източници, те трябва да бъдат криптирани/декриптирани (подписани/неподписани) от сървъра с помощта на специален ключ. Шифроването не винаги е необходимо за основните бисквитки, представляващи състоянието, но със сигурност е необходимо, когато се използва стойността на бисквитката като средство за удостоверяване на клиент.

Почти всички рамки от страна на сървъра, използвани днес, имат средство за създаване и анализиране на подписани бисквитки, но до голяма степен зависи от разработчика от страната на сървъра как да съхранява и управлява техния жизнен цикъл. Създаването и извличането на клиентски сесии в задния край може да стане по-сложно, когато един потребител може да има множество активни, постоянни сесии. Това създава необходимостта от разделяне на удостоверенията от потребителите в задната част, релационно свързване на двете чрез чужд идентификатор.

За простота в нашето демонстрационно приложение обаче ние съхраняваме този подписан токен за удостоверяване вътре в Потребителска таблица, от която можем да актуализираме и правим заявки. Мидълуерът за анализатор на бисквитки Express.js се грижи за подписването вместо нас, ако подадем таен ключ.

// Cookie config
app.use( express.cookieParser( ‘bb-login-secret’ ) ); // populates req.signedCookies
app.use( express.cookieSession( ‘bb-login-secret’ ) ); // populates req.session, needed for CSRF</pre>

Глобален модел на сесия от страна на клиента

Изграждайки модулно приложение от страна на клиента, вие бързо започвате да осъзнавате колко изгледи и основната функционалност на контролера зависят от сесията на потребителя - по-специално състоянието на влизане на потребителя и определени атрибути на потребителя. ‹a href=”http://javascript.crockford.com/code.html'›Глобалните променливи често не са добра практика в JavaScript‹/a›, но наличието на singleton SessionModel, който съхранява тази информация и може да бъде достъпен във вашето приложение е направо страхотен. Ние жертваме производителността при управлението на паметта на глобален обект, но компенсираме това с наличието на единствен източник на истина за извличане на клиентска сесия. Също така ни позволява да слушаме за промени в модела и да изобразяваме/реагираме съответно.

Демо приложението използва постоянен глобален Backbone.js SessionModel за управление на състоянието. Views и други модели могат да:

  • абонирайте се за промени в състоянието на сесията
app.session.on("change:logged_in", this.onLoginStatusChange);
  • извличане на текущи потребителски данни
"Logged in as "+ app.session.user.get(“username”)
  • задействане на събития за удостоверяване (влизане, излизане и т.н.)
app.session.logout({});

Има няколко често срещани клопки в сигурността при изграждането на глобален сесиен модел, които си струва да бъдат извадени наяве.

1. Не позволявайте на клиентския модел да запазва чувствителна информация. Backbone.js наистина обича да поддържа колкото се може повече данни от модела в клиента. Това често е полезно за пренасяне на предварително зададени свойства на модел, разширяване на всички новоизвлечени или актуализирани свойства върху себе си. Ако позволявате на модел на сесия да обработва удостоверяване, важно е незабавно да изтриете токен за удостоверяване, парола или каквато и да е чувствителна информация веднага след като бъде използвана в заявка. Или просто избягвайте да задавате чувствителни свойства на самия модел и да ги абстрахирате към функция за преминаване в модела.

// Fxn to update user attributes after recieving API response
updateSessionUser: function( userData ){
 this.user.set( _.pick( userData, _.keys(this.user.defaults) ) );
},

2. Уверете се, че цялата комуникация на сесиите със сървъра е през HTTPS. Вашият API може да не е проектиран да обработва HTTPS навсякъде, нито може да е много ефективен по този начин, но е абсолютно критичен, когато ПУБЛИКУВАТЕ пароли и друга чувствителна информация (имейл, адрес и т.н.) в обикновен текст по кабела. Тъй като бисквитките са свързани с домейн, а не с протокол, те могат да бъдат зададени и анализирани от сървъра взаимозаменяемо. HTTPS може да бъде зададен в специфичния за модела URL адрес по подразбиране или можете просто да зададете това условно, като използвате стандартен AJAX в модела вместо Backbone.sync.

3. Обвийте модела на сесията само в затваряне позволявайки достъп до другите ви изгледи/модели вътре в затварянето. „Глобално“ приложение не означава, че е достъпно през прозореца. Използването на модулна рамка на зависимост като Require.js ви позволява изрично да дефинирате кой от вашите модули се нуждае от достъп до сесията и ще приключи всичко за вас при компилиране. В нашето демонстрационно приложение ние включваме „приложение“ в дефинициите на нашите модули, когато е необходимо.

Валидации от страна на клиента и от страна на сървъра

Тъй като клиентът никога не трябва да се доверява напълно от сървъра, крайната отговорност за XSS почистването и валидирането на потребителските/удостоверяващите данни се носи от сървъра. Сървърът е вратарят на данните, които могат да влязат във вашата база данни.

Като се има предвид това, клиентът може и трябва да потвърди данните от формуляра, преди да се опита да комуникира с API на сървъра. Защо да изпращате ненужни заявки към API? Същността на приложенията с една страница е да прехвърлят толкова голяма отговорност на клиента, колкото е практично, и неща като валидиране на дължината на входа и съпоставяне на шаблони могат да се обработват лесно и ефективно в JavaScript. Въпреки че валидациите трябва да се извършват и на двете места, по-отзивчиво и по-малко натоварване на сървъра е първо да се валидират формулярите в клиента.

Една библиотека, която открих, че е особено полезна за това, е Parsley .js. Parsley се грижи за набор от общи проверки като дължина на входа, съвпадение на шаблони и равенство на съдържанието в много малко редове код. Валидациите могат дори да се намират в DOM вътре в самите тагове за въвеждане на формуляра с тагове „data-“. Едно извикване на $(‘form’).parsley(‘validate’) и вече имате списък с всички неуспешни проверки, видими в DOM. Твърде лесно.

<input class="input-medium" type="password" id="signup-password-confirm-input" placeholder="Confirm Password" name="user_password" value="" data-required="true" data-notblank="true" 
data-rangelength="[5,25]" data-equalto="#signup-password-input"/>

Осоляване/хеширане на пароли

Последната практика на криптиране на пароли не е отговорност от страна на клиента, но определено си струва да бъде прегледана. Ако клиентът изпраща пароли като обикновен текст вътре в JSON натоварванията на POST заявките към сървъра, сървърът трябва да вземе подходящите мерки, за да шифрова тези пароли, преди да ги вмъкне в базата данни.

Въпреки че има много алгоритми и практики за криптиране за наше разположение, безопасен и често срещан метод е необратимо солиране и хеширане на паролата, така че крайната й стойност да може да бъде прочетена и сравнена, но не и върната в необработената парола. Една сол може да стои отделно от хеша в базата данни или до него (или изобщо да не стои в базата данни), а една сол може да се използва за множество хеши. Докато и двете могат да бъдат извлечени, тогава те могат да бъдат сравнени за равенство с необработена парола, която е булевият отговор, необходим за ръкостискане за удостоверяване.

В нашето демонстрационно приложение използваме bcrypt Node.js модул за генериране и сравнение на сол и хеш.

if( bcrypt.compareSync( req.body.password, user.password)){
  db.run("INSERT INTO users(username, name, password, auth_token)     VALUES (?, ?, ?, ?)", [req.body.username, req.body.name, bcrypt.hashSync(req.body.password, 8), bcrypt.genSaltSync(8)], function(err, rows) {

Въпреки че можете да слезете в „заешката дупка“ на сигурността клиент-сървър, ние покрихме основните основи и общите най-добри практики за осигуряване на потока за удостоверяване на модерни уеб приложения. Вижте github repository и live demo за справка и приложение на горните концепции. Оценяваме вашите коментари/отзиви/въпроси!