«phpClub» — архив тем ("тредов"), посвящённых изучению PHP и веб-технологий.
Crypter someApprentice 2019/02/25 14:14:56  №1354910 1
https://github.com/someApprentice/Crypter

Для начала реализовал сервис авторизации. Можно сказать, что это выполненная задача Студентов https://github.com/codedokode/pasta/blob/master/student-list.md выполненная на JavaScript, с Server Side Rendering и SPA, поэтому если новичкам интересно они могут что-то подсмотреть.

Если у новичков есть какие-то вопросы - чувствуйте себя свободно задавать их.


Всё сделано "как по учебнику" и интересного здесь мало чего, за исключением пару вещей.

1. При первом заходе на сайт, пользователя встречает форма Приветствия, в которую нужно заполнить email и, в зависимости от того зарегистрирован ли пользователь или нет, перенаправляет на страницу регистрации или логина, а соответствующий импут email'а автозаполняется.

Это реализовано с помощью пары строчек, которые добавляют для определенного роута поле data с этим email'ом:

https://github.com/someApprentice/Crypter/blob/master/src/app/auth/welcome/welcome.component.ts#L46-L49

https://github.com/someApprentice/Crypter/blob/master/src/app/auth/registration/registration.component.ts#L75-L77
https://github.com/someApprentice/Crypter/blob/master/src/app/auth/login/login.component.ts#L35-L37

Но получив некий опыт на Ангуляре, я понял что более шаблонным и следовательно простым для понимания было бы обернуть все эти компоненты в отдельный компонент AuthComponent и в нём уже сделать свойство email (а лучше объект user/form) и передавать это значение с помощью "Взаимодействий Компонентов" https://angular.io/guide/component-interaction.

2. Поскольку приложение пререндерится на сервере а так же работает в браузере, ему нужен общий "источник информации" о пользовательском хранилище. Так как, для сервера используются лучшие пользовательское хранилище это Кукисы по сути кукисы не на что не влияют кроме как условий отображения пререндеренной страницы, а для браузера localStorage, то для обоих этих хранилищ написан сервис-обёртка.

https://github.com/someApprentice/Crypter/blob/master/src/app/storage.service.ts#L18-L20
https://github.com/someApprentice/Crypter/blob/master/src/app/storage.service.ts#L22-L26

https://github.com/someApprentice/Crypter/blob/master/src/app/models/StorageWrapper.ts


Небольшие вопросы, которые не критичны, но всё же знать их следует чтобы довести до идеала:

https://github.com/someApprentice/Crypter/blob/master/api/api.ts#L31
Позволяет ли Bearer token защититься от XSRF?

Я детально изучил эту уязвимость и касаемо этого вопроса, критический момент зависит от того, что может ли злоумышленник "обмануть" браузер отправить автоматически запрос с этим токеном. Он может, например, сделать XMLHttpRequests с ним (со своего сайта), но для начала ему нужно узнать этот токен, что практически тоже самое что добавлять csrf-token, чтобы бороться с этой уязвимостью.

Способ с Bearer token'ом технически идентичен с методом Cookie-to-header token ( https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-header_token ) за исключением семантики заголовка X-Csrf-Token заменённого на Bearer token.
И в моём случае, токен хранится в Кукисах, которые защищены с помощью HttpOnly и Secure флагов и в localStorage.

Приходит ли вам на ум какая-нибудь слабая точка которую можно эксплуатировать?

https://github.com/someApprentice/Crypter/blob/master/src/app/storage.service.spec.ts#L32-L33
Достаточно ли это строгая проверка на то является ли сущность экземпляром объекта localStorage?

https://github.com/someApprentice/Crypter/blob/master/src/app/models/StorageWrapper.ts
В правильной ли директории находится эта обёртка? Это не совсем сущность, например как User, но места по лучше я не могу придумать для неё. Куда следует помещать обёртки?

https://github.com/someApprentice/Crypter/blob/master/src/app/auth/registration/registration.component.spec.ts#L67
https://github.com/someApprentice/Crypter/blob/master/src/app/auth/login/login.component.spec.ts#L66
Нужно ли делать проверку на каждую валидацию? Например на встроенные в Ангуляр валидаторы или на валидацию по регулярному выражению? https://github.com/someApprentice/Crypter/blob/master/src/app/auth/registration/registration.component.ts#L21-L24


Следующий шаг написания сервиса сообщений.

Он будет реализован с помощь протокола WAMP и на платформе от https://crossbar.io/ , которая написана на Питоне, и для которой для аутентификации клиентов нужно тоже написать код на нём.

https://github.com/crossbario/crossbar-examples/blob/master/authentication/ticket/dynamic/authenticator.py

Поэтому я сейчас буду изучать его, и возможно у меня появиться небольшие вопросы по нему. Могу я задать их в этом треде?

Заодно и реактивное программирование подучу.


Буду признателен за проверку моего завершающего задания на JavaScript.

Это очень много значит для меня. Большое спасибо.
Ответы: >>1356149 >>1361814 >>1361815
Аноним 2019/03/10 06:03:16  №1361815 2
>>1354910

По тесту:

https://github.com/someApprentice/Crypter/blob/master/api/api.test.ts#L31

Тут код теста почти повторяет код обработчика. Это не годится, так как ты можешь сделать одинаковые ошибки и там, и там. Если мы тестируем функцию вычисления корня, то ее проверяют возведением в квадрат, а не аналогичным вычислением.

В твоем случае надо сформулировать требования к функции регистрации. На мой взгляд, они такие:

- она должна создавать аккаунт на сервере, под которым можно залогиниться
- она должна возвращать токен, с которым можно использовать защищенное API
- нельзя дважды зарегистрироваться с одинаковым email
- нельзя регистрироваться без указания обязательных аргументов
- и, может, еще какие-то требования по валидации. Например, что при ошибке возвращается объект определенного вида

Для каждого требования мы придумываем свой способ проверки. Создание аккаунта на сервере можно проверить, попробовав залогиниться в него. Токен можно проверить, попробовав вызвать защищенное API, например "получить сведения о себе".

В общем, тесты пока сделаны неправильно.

Так как ты тестируешь код, использующий БД, тебе надо предусмотреть, чтобы база была перед тестом в каком-то определенном состоянии (полностью пустая, например, или с какими-то тестовыми данными). Один из вариантов - в самом начале почистить БД/загрузить дамп, а каждый тест обернуть в транзакцию, которая откатывается. Это может быть быстрее, чем загружать дамп каждый раз.

На практике, может быть полезно разместить тестовую БД на ramfs, чтобы избежать вообще обращений к диску и ускорить прогон тестов.

Можно также попробовать добавить тест для обработчика ошибок (вызвать специальный URL, который выбрасывает исключение). Иначе ты можешь его отключить/сломать и не заметить.

> Is there any way to declare all classes at once for a webpack output file?

Я думаю, это не имеет отношения к webpack, так как ты должен в sequelize передать список классов. А для этого их надо импортировать. И вебпак увидит импорты.

По клиентской стороне:

Ты в API при логине ставишь куки вручную, но разве это правильно? По идее, это должно работать так:

- пользователь заходит на HTML-версию и его браузер отправляет запрос на сервер
- на сервере создается "песочница", в которой выполняется Angular-приложение и рендерит страницу
- оно добавляет записи в StorageService
- серверный код видит эти изменения и формирует из них куки

Если ты сделал куки аналогом localStorage для серверного рендеринга, то, наверно, логично абстрагировать это от приложения. Оно работает с StorageService, а куда потом сохраняются данные - не его забота.

Тут конечно мы упираемся в проблему, что объем кук ограничен, в то время как в браузере мы можем кучу информации сохранить в localStorage. Решением наверно тут будет как можно меньше сохранять в storage, когда мы выполняемся в режиме рендера на сервере. В идеале - хранить там только JWT.

Реализовать разную логику для работы на клиенте и на сервере можно, например, через интерфейс:

interface Storage {
getJwtToken(): Promise<string>
setJwtToken(token)
getUserData(): Promise<User>
setUserData()
}

И 2 реализации: клиентская берет данные из localStorage, а серверная - берет из БД или кук. Но это, конечно, требует писать два варианта кода.

Есть еще альтернатива, но она довольно сложная. Мы могли бы сделать что-то вроде "сессий" и делать для каждой "сессии" свой серверный аналог localStorage. То есть пользователь заходит на сайт, на сервере создается некий serverStorage, который не очищается между запросами. И приложение Angular, работая на сервере, работает с ним, как будто это localStorage. Ключом сессии может быть токен JWT. Данные serverStorage могут храниться в памяти Node-приложения или сбрасываться в HASH внутри редис.

То есть, когда приложение работает на сервере, в итоге данные пишутся в redis с ключом на основе JWT. Там конечно еще можно подумать над оптимальным по производительности алгоримом. И опять же, стараться при работе на сервере меньше сохранять данные в serverStorage, если они есть в БД.

https://github.com/someApprentice/Crypter/blob/master/src/app/app.component.html

Тут мне не кажется 100% надежной проверка залогиненности. Тут же нет проверки, что это валидный токен.

И еще, я не очень понял, как будет работать форма регистрации в режиме серверного рендеринга. Как и куда она будет отправлять свои данные?

> let route = this.router.config.find(r => r.path === redirect);
> route.data['email'] = email;

А это передает email только один раз, или ты этот email навсегда в конфиг роутинга вписал? По моем, не очень красиво получается. Разве нельзя передавать email внутри route parameters: https://angular.io/guide/router#route-parameters

Тогда можно сделать так: router.navigate(['/register', { email: email }])

Причем, я бы хранил email не в path, а в query: /register?email=xyz@example.com

> Позволяет ли Bearer token защититься от XSRF?
> Приходит ли вам на ум какая-нибудь слабая точка которую можно эксплуатировать?

Да. У тебя используются куки, и в них тоже есть токен. Предположим, у нас есть HTML версия, работающая без ангулара. Не получится ли, что злоумышленник сделает форму, которая будет имитировать отправку запроса этой HTML версией, и возникнет XSRF?

> Достаточно ли это строгая проверка на то является ли сущность экземпляром объекта localStorage?

Проверка достаточная, но непонятен ее смысл. Тест по идее должен проверять, что данные сохраняются и загружаются, а не какой именно класс там испольуется для хранения.

> В правильной ли директории находится эта обёртка? Это не совсем сущность, например как User, но места по лучше я не могу придумать для неё. Куда следует помещать обёртки?

Это сервис. Помещать - не знаю, в папку для сервисов? Но у меня есть ощущение, что в клиентском приложении файлы и так довольно хаотично разбросаны.

> Нужно ли делать проверку на каждую валидацию? Например на встроенные в Ангуляр валидаторы или на валидацию по регулярному выражению?

Нет смысла тестировать протестированное. Валидатор если и тестировать, то юнит-тестами, и они уже есть в сторонней библиотеке. Но надо протестировать, что:

- эти валидаторы действительно вызываются и проверяют данные
- их результат работы отображается

Потому, я думаю, нужно проверять не каждую валидацию, а просто вариант, когда поле заполнено неправильно. Можно даже сделать тест, где все поля заполнены неправильно, и проверить, что каждое из них помечено как ошибочное. Ну и тест, что есть какой-то минимум полей, без заполнения которых нельзя пройти регистрацию.

> Следующий шаг написания сервиса сообщений.

> Он будет реализован с помощь протокола WAMP и на платформе от https://crossbar.io/ , которая написана на Питоне, и для которой для аутентификации клиентов нужно тоже написать код на нём.

А чем Node не годится, кстати? Или ты решил изучить Питон и микросервисы? Ок, но на практике такой маленький проект может быть невыгодно разбивать на микросервисы - у них ведь есть и накладные расходы.

> Поэтому я сейчас буду изучать его, и возможно у меня появиться небольшие вопросы по нему. Могу я задать их в этом треде?

> Заодно и реактивное программирование подучу.

Конечно. Только пиши тогда, что именно ты добавил в проект и на что стоит посмотреть, а то он большой и с ходу это будет не очевидно.

Я пока все не успел проверить, увы, давай сначала с тем, что я написал, разберемся, а потом остальное глянем.

Ответы: >>1366103 >>1366104
someApprentice 2019/03/17 14:55:51  №1366103 3
>>1361814
>Еще, кстати, мне интересно, можно ли как-то использовать описания моделей (например, User) для автоматической генерации документации о формате JSON-ответа, и можно ли использовать описания для автоматического тестирования (тест берет Swagger-описание, проходится по всем методам и пробует их вызывать и проверяет, что ответ соответствует описанию).
Я ничего из этого не знаю, но постараюсь разобраться.


>>1361815
>Тут код теста почти повторяет код обработчика. Это не годится, так как ты можешь сделать одинаковые ошибки и там, и там.
А где у меня код повторяется? То что я токен генерирую? А как я узнаю какой токен будет иначе?

>Если мы тестируем функцию вычисления корня, то ее проверяют возведением в квадрат, а не аналогичным вычислением.
Не понимаю сравнение.

var n = 5
expect(sqrt(n^2)).toEqual(n)

Я же тоже "возвожу" данные пользователя в токен и проверяю совпадает ли возвращённый ответ, а именно кукисы, с ним.

var user = { ... }
token(user, (jwt) => {
expect(cookies['jwt']).toEqual(jwt)
});

Ну да, теперь вижу причину. Нужно проверить что пришедший токен в дешифрованном виде равен данным пользователя?

>В твоем случае надо сформулировать требования к функции регистрации. На мой взгляд, они такие:
>
>- она должна создавать аккаунт на сервере, под которым можно залогиниться
Я забыл сделать проверку, что пользователь добавился в БД и данные правильные. Это достаточное условие? Залогинивание проверяется в предназначенном для неё тесте, и там как раз сначала заполняется запись в БД.

>- она должна возвращать токен, с которым можно использовать защищенное API
Нужно сделать проверку как я писал выше, что пришедший токен в дешифрованном виде равен данным пользователя или действительно нужно проверять каким-то методом API?

>- нельзя дважды зарегистрироваться с одинаковым email
Сделаю. то нужно сделать прописав ассоциацию Sequelize на уникальность emai'а. Тогда при повтороном имейле будет вбрасываться ошибка и тест провалится.

>- нельзя регистрироваться без указания обязательных аргументов
Сделаю.

>- и, может, еще какие-то требования по валидации. Например, что при ошибке возвращается объект определенного вида
Сделаю.


>Для каждого требования мы придумываем свой способ проверки. Создание аккаунта на сервере можно проверить, попробовав залогиниться в него. Токен можно проверить, попробовав вызвать защищенное API, например "получить сведения о себе".
>Создание аккаунта на сервере можно проверить, попробовав залогиниться в него.
>Токен можно проверить, попробовав вызвать защищенное API, например "получить сведения о себе".
Почему? Разве не достаточно проверить что запись добавилась в базу? Разве не залогинивание не должно проверяться в своём собственном тесте?
А токен не достаточно проверить просто дешифровав его (как раз метод и называется verify(token))?


>Так как ты тестируешь код, использующий БД, тебе надо предусмотреть, чтобы база была перед тестом в каком-то определенном состоянии (полностью пустая, например, или с какими-то тестовыми данными). Один из вариантов - в самом начале почистить БД/загрузить дамп, а каждый тест обернуть в транзакцию, которая откатывается. Это может быть быстрее, чем загружать дамп каждый раз.
В данный момент для каждого теста БД должна быть в чистом состоянии и после каждого теста база очищается это можно улучшить добавив перед каждым тестом очистку (методо beforeEach()).

>На практике, может быть полезно разместить тестовую БД на ramfs, чтобы избежать вообще обращений к диску и ускорить прогон тестов.
Тяжело понять. Я с таким не сталкивался. Вижу что это как бы "эмулирует" жесткий диск в оперативной памяти. Как для этого настроить psql? Как нужно будет для это настраивать найстройки соеденения к БД в Ноде? Слишком много вопросов для такой маленькой задачи. Возможно позже, с ростом приложения это будет актуально. Если что взял этот подход на вооружение и вернусь к нему, когда тесты будут делаться медленно.


>Можно также попробовать добавить тест для обработчика ошибок (вызвать специальный URL, который выбрасывает исключение). Иначе ты можешь его отключить/сломать и не заметить.
Сделаю.

>> Is there any way to declare all classes at once for a webpack output file?
>
>Я думаю, это не имеет отношения к webpack, так как ты должен в sequelize передать список классов. А для этого их надо импортировать. И вебпак увидит импорты.
В sequlize-typescript можно указать директорию с моделями, но webpack транспилирует всё в один файл, и это ломается.

https://www.npmjs.com/package/sequelize-typescript#configuration
https://github.com/someApprentice/Crypter/issues/7


>По клиентской стороне:
>
>Ты в API при логине ставишь куки вручную, но разве это правильно? По идее, это должно работать так:
Ставлю куки вручную в клиентской стороне? Нет, у меня такого нет. В клиентской стороне куки только берутся из реквеста, чтобы пререндерить страницу с залогиненым пользователем или нет. Для этого они и нужны. localStorage ставиться в ручную(?) в клиентской стороне при залогинивании https://github.com/someApprentice/Crypter/blob/master/src/app/auth/login/login.component.ts#L48-L51 . Не могли бы вы уточнить строку где я это делаю, я не могу понять последующие описание проблемы.


>https://github.com/someApprentice/Crypter/blob/master/src/app/app.component.html
>
>Тут мне не кажется 100% надежной проверка залогиненности. Тут же нет проверки, что это валидный токен.
Так это же отображение. Если что-то не так, то при запросе к API выдастся соответствующая ошибка. Вряд ли, пользователь сможет что-то сломать не лазия в хранилище.


>И еще, я не очень понял, как будет работать форма регистрации в режиме серверного рендеринга. Как и куда она будет отправлять свои данные?
>
>> let route = this.router.config.find(r => r.path === redirect);
>> route.data['email'] = email;
Имеете ввиду версию страницы работающей с отключенным JS? Я решил отказаться от этой идеи. Она бесполезная, по крайней мере для части чата, потому что сообщения будут шифроваться на стороне клиента. Может быть позже можно что-то с этим придумать, но пока этого не будет. Я считаю, что это вопрос доверия пользователя, оно есть в каких-то границах, и эти границы могут как и широки так и малы, т.е. я пытаюсь сказать что зависит от пользователя и его доверия. Я думаю, что для приложения с открытом кодом можно даже задуматься и о вайтлисте js.

>А это передает email только один раз, или ты этот email навсегда в конфиг роутинга вписал? По моем, не очень красиво получается. Разве нельзя передавать email внутри route parameters: https://angular.io/guide/router#route-parameters
>
>Тогда можно сделать так: router.navigate(['/register', { email: email }])
>
>Причем, я бы хранил email не в path, а в query: /register?email=xyz@example.com
Можно, но мне наоборот кажется это не очень красивым, когда в URL что-то появляется. Хочу чтобы было так.
Этот email удаляется из роутинга после уничтожения связанного компонента:

https://github.com/someApprentice/Crypter/blob/master/src/app/auth/login/login.component.ts#L63-L68


>> Позволяет ли Bearer token защититься от XSRF?
>> Приходит ли вам на ум какая-нибудь слабая точка которую можно эксплуатировать?
>
>Да. У тебя используются куки, и в них тоже есть токен. Предположим, у нас есть HTML версия, работающая без ангулара. Не получится ли, что злоумышленник сделает форму, которая будет имитировать отправку запроса этой HTML версией, и возникнет XSRF?
Как я говорил ранее, я принял решение отказаться от версии без ангуляра, что значит что не будет такого обработчика запроса, который будет работать с куками повторюсь в очередной раз, что они нужны только для условий в отоброжении (токен наверно и удалить из кук)


>> Достаточно ли это строгая проверка на то является ли сущность экземпляром объекта localStorage?
>
>Проверка достаточная, но непонятен ее смысл. Тест по идее должен проверять, что данные сохраняются и загружаются, а не какой именно класс там испольуется для хранения.
Проверить... что, например, вот для серверной части есть проверка, что вернулся StorageWrapper и всё работает, а для браузерной тоже аналогично. Это не нужно?
someApprentice 2019/03/17 14:56:40  №1366104 4
>>1361815
>> Следующий шаг написания сервиса сообщений.
>
>> Он будет реализован с помощь протокола WAMP и на платформе от https://crossbar.io/ , которая написана на Питоне, и для которой для аутентификации клиентов нужно тоже написать код на нём.
>
>А чем Node не годится, кстати? Или ты решил изучить Питон и микросервисы? Ок, но на практике такой маленький проект может быть невыгодно разбивать на микросервисы - у них ведь есть и накладные расходы.
У роутеров WAMP'а на Node, нету возможностей для аутентифекации и авторизации соединения, и документации. Вышеупомянутая платформа развита лучше других.


>> Поэтому я сейчас буду изучать его, и возможно у меня появиться небольшие вопросы по нему. Могу я задать их в этом треде?
>
>> Заодно и реактивное программирование подучу.
>
>Конечно. Только пиши тогда, что именно ты добавил в проект и на что стоит посмотреть, а то он большой и с ходу это будет не очевидно.
Хорошо, я буду всё подробно описывать.


>Я пока все не успел проверить, увы, давай сначала с тем, что я написал, разберемся, а потом остальное глянем.
Хорошо, спасибо большое! Я не успеваю ответить в тот же день. Не понимаю почему - занимаюсь каждый день, и всё больше и больше нужно изучить. Я так хочу просто писать код. Я никогда не знал что разработка на JS может быть такой сложной, и приложений вообще.
Я ещё серьёзно задумываюсь о приложениях для устройств, а там либо медленный и кривой JS, либо изучать Java/Swift.

Я так хочу хотя бы браузернное приложение к лету закончить, а приложения к осени.


Спасибо большое, у меня нет слов как мне всё это важно.