Security & Privacy
Создание безопасных приложений с Parse и Back4App
22 мин
как создать безопасное приложение с использованием parse введение привет, сообщество back4app! это гостевой урок от йорена винге на startup soul http //startup soul com/ мы помогаем стартапам быстро разрабатывать и запускать их продукты наши друзья из back4app попросили нас показать вам, как создать безопасное приложение на основе back4app в этом посте мы проведем вас через шаги по созданию безопасного приложения to do на back4app безопасность важна если ваше приложение станет популярным, вам нужно будет убедиться, что данные вашего приложения защищены и что вашу систему нельзя взломать функции безопасности в parse сначала давайте поговорим о первом уровне безопасности, acl (список контроля доступа) acl это, по сути, просто правила, которые вы устанавливаете при создании объекта допустим, вы создаете элемент списка дел, в момент создания вы можете указать, кто может его читать и кто может его записывать вы можете назначить определенных пользователей, которые смогут читать этот элемент или записывать в этот элемент, или вы можете установить доступ для всех, что позволяет доступ любому но acl не всегда работают есть случаи, когда вам может понадобиться более сложная логика вместо простого acl иногда вы также можете оказаться в ситуации, когда вам нужно предоставить кому то доступ к объекту на условной основе, а не на абсолютной основе, как с acl поэтому давайте пропустим использование acl они слишком жесткие и в то же время позволяют слишком много доступа к данным итак, я собираюсь рассказать вам секрет создания безопасного приложения на back4app и parse server вы готовы? разрешения на уровне классов! да, мы установим разрешения на уровне классов для каждой таблицы в нашей базе данных и уровень разрешений, который мы установим, будет без разрешений вообще мы заблокируем каждую таблицу так, чтобы никто не имел доступа на чтение или запись! звучит экстремально, я знаю, но это первый шаг к созданию безопасного приложения единственное разрешение, которое мы разрешим, будет на таблице пользователей, которое будет предназначено для создания новых объектов пользователей и для того, чтобы пользователь мог просматривать свои собственные данные, что необходимо для обновления текущего пользователя мы защитим пользователя от возможности просмотра данных других пользователей, используя acl это единственный раз, когда мы будем использовать acl, так что, я думаю, они не совсем бесполезны их хорошо иметь, но не полагайтесь на них, чтобы делать всё но как мы будем получать доступ к данным, спрашиваете вы? хороший вопрос, рад, что вы об этом думаете! секрет в том, чтобы позволить клиентам получать доступ к данным контролируемым образом, заключается в том, чтобы каждое взаимодействие между клиентом и базой данных фильтровалось через функцию облачного кода да, каждый раз, когда вы что то делаете с вашим приложением, это будет происходить через пользовательскую функцию облачного кода больше никаких клиентских pfqueries вы практически пропускаете использование всего клиентского sdk parse, за исключением функций регистрации, входа, восстановления пароля и выхода для этих функций мы всё равно будем использовать нативные клиентские sdk это просто проще вы когда нибудь писали облачный код раньше? нет, вы говорите? ну, это довольно просто это просто javascript, и он использует parse javascript sdk, но внутренне на сервере вашего приложения на самом деле, поскольку parse server основан на node js, это довольно похоже на написание маршрутов с express, но даже проще, поскольку ваш язык запросов уже установлен, а функции облачного кода гораздо легче писать, чем целое приложение node js express итак, вот что мы сделаем мы будем использовать ios приложение для задач, которое я уже создал мы не будем беспокоить вас тем, как я его создал вместо этого мы сосредоточимся на написании облачного кода и защите базы данных приложение для задач будет безопасным приложением, где вы можете получить доступ только к своим задачам и можете писать только свои задачи данные будут защищены на сервере от злонамеренных клиентов я также покажу вам, как написать безопасную фоновую задачу parse по сути, то же самое, что и cron задача чтобы вы могли иметь автоматизированные службы, которые манипулируют вашими данными по расписанию звучит сложно, но это не так просто представьте себе маленьких серверных роботов, которые делают всё, что вы хотите, по автоматизированному расписанию звучит круто, правда? ладно, поехали!!!!!! давайте настроим безопасное приложение todo на back4app 1\) создайте приложение на back4app создайте новое приложение на back4app назовите приложение ‘secure todo app’ примечание следуйте за учебником по созданию нового parse приложения чтобы узнать, как создать приложение на back4app перейдите на страницу основных настроек приложения и затем нажмите на изменить детали приложения отключите флажок под названием ‘разрешить создание клиентских классов’, чтобы отключить создание клиентских классов, и нажмите сохранить мы хотим ограничить то, что клиент может делать по умолчанию 2\) установите разрешения на уровень класса для класса пользователь далее мы установим разрешения для класса user перейдите в панель управления базой данных back4app и нажмите на класс user затем нажмите на вкладку безопасность, затем нажмите на значок шестеренки в правом верхнем углу вы должны увидеть меню, которое говорит простое/расширенное переключите ползунок на расширенное затем вы должны увидеть полные разрешения на уровне класса для этого класса отключите флажок найти отключите флажки обновить и удалить наконец, отключите флажок добавить поле затем нажмите сохранить ваши настройки безопасности должны выглядеть так 3\) создайте класс todo нажмите создать класс и назовите его todo установите тип класса как пользовательский 4\) установите разрешения на уровне класса для класса todo далее мы установим разрешения для класса todo перейдите в панель управления базой данных back4app и нажмите на класс todo затем нажмите на вкладку безопасность, затем нажмите на значок шестеренки в правом верхнем углу вы должны увидеть меню, которое говорит простое/расширенное переключите ползунок на расширенное затем вы должны увидеть полные разрешения на уровне класса для этого класса отключите все, затем нажмите сохранить ваши настройки безопасности должны выглядеть так 5\) давайте добавим несколько колонок в класс todo сначала давайте объединим класс todo с классом user мы сделаем это, добавив 2 столбца первый столбец будет называться ‘user’ и будет указателем на класс пользователя далее давайте создадим столбец для идентификатора объекта пользователя, который его создал он будет строкового типа и будет называться ‘userobjectid’ далее давайте создадим столбец для хранения нашей фактической информации о задачах он также будет строковым и будет называться ‘tododescription’ давайте создадим логическое значение для хранения состояния задачи назовем его ‘finished’ наконец, давайте добавим еще один столбец для хранения даты, когда вы завершили свою задачу назовем его ‘finisheddate’ и установим его как тип даты ваш класс todo должен выглядеть так 6\) давайте рассмотрим клиент клиент — это довольно простое приложение для задач оно использует встроенные функции parse для входа в систему, создания нового пользователя и сброса пароля кроме того, все основано на облачном коде и безопасно acl пользователя также устанавливаются сразу после входа в систему или регистрации, чтобы быть на 100% уверенным, что система безопасна давайте начнем с написания функции облачного кода для установки acl пользователя при входе в систему или регистрации в любое время вы можете получить доступ к полному проекту ios, созданному с помощью этого руководства, по следующему репозиторию github вы также можете получить доступ к файлу cloud code main js, созданному для этого руководства, по следующему репозиторию github 1 в клиенте перейдите в todocontroller swift и найдите функцию setusersaclsnow эта функция вызывается, когда вы входите в систему или просматриваете loggedinviewcontroller swift функция проверяет, вошли ли вы в систему, и если да, то вызывает облачную функцию для настройки ваших личных пользовательских acl todocontroller swift 1 func setusersaclsnow(){ 2 if pfuser current() != nil{ 3 let cloudparams \[anyhashable\ string] = \["test" "test"] 4 pfcloud callfunction(inbackground setusersacls, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 //print(error debugdescription) 8 if let descrip = error? localizeddescription{ 9 print(descrip) 10 } 11 }else{ 12 print(result as! string) 13 } 14 }) 15 } 16 } 2 теперь давайте напишем функцию облачного кода parse server 3 x 1 parse cloud define('setusersacls', async(request) => { 2 let currentuser = request user; 3 currentuser setacl(new parse acl(currentuser)); 4 return await currentuser save(null, { usemasterkey true }); 5 }); parse server 2 x 1 parse cloud define('setusersacls', function (request, response) { 2 var currentuser = request user; 3 currentuser setacl(new parse acl(currentuser)); 4 currentuser save(null, { 5 usemasterkey true, 6 success function (object) { 7 response success("acls updated"); 8 }, 9 error function (object, error) { 10 response error("got an error " + error code + " " + error description); 11 } 12 }); 13 }); 3 этот облачный код использует две ключевые функции для обеспечения безопасности вашего приложения request user и masterkey request user позволяет вам получить доступ к пользователю, который вызывает облачный код, и позволяет ограничить доступ для этого пользователя в данном случае мы используем его, чтобы установить acl пользователя, ограничивая доступ на чтение только для текущего пользователя таким образом, только пользователь может читать свою собственную информацию разрешения на уровне класса предотвращают запись даже для текущего пользователя таким образом, пользователи не могут изменять свою собственную информацию они могут изменять только информацию о своем собственном пользователе через облачный код возможно импортировать ложную информацию, когда пользователь впервые регистрируется, но я бы рекомендовал написать функцию облачного кода для проверки информации пользователя после создания нового пользователя встроенная функция parse для создания нового пользователя действительно полезна, поэтому я думаю, что это разумный компромисс, но вы всегда можете установить значения по умолчанию для пользователя через облачный код сразу после его регистрации существует множество защитных мер, которые вы также можете написать в облачном коде и заставить их работать автоматически и непрерывно, используя фоновые задания для обнаружения любой злонамеренной информации о пользователе, которая была импортирована при первом создании пользователя если вы хотите быть действительно безопасным, вы можете хранить любую конфиденциальную информацию, такую как статус членства или платежную информацию, в отдельной таблице от таблицы пользователей таким образом, пользователь не может подделать какую либо конфиденциальную информацию при создании пользователя 4\ далее давайте создадим задачу в клиенте перейдите в todocontroller swift и найдите функцию savetodo эта функция вызывается, когда вы создаете новую задачу функция принимает строку, которая описывает задачу, и сохраняет ее в базе данных todocontroller swift 1 func savetodo(todostring\ string, completion @escaping ( result bool, message\ string, todoarray \[todo]) >()){ 2 var resulttodoarray \[todo] = \[] 3 let cloudparams \[anyhashable\ any] = \["todostring"\ todostring] 4 pfcloud callfunction(inbackground createtodosforuser, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 if let descrip = error? localizeddescription{ 8 completion(false, descrip, resulttodoarray) 9 } 10 }else{ 11 resulttodoarray = result as! \[todo] 12 completion(true, "success", resulttodoarray) 13 } 14 }) 15 } 5\ теперь давайте напишем функцию облачного кода для сохранения задачи в базе данных parse server 3 x 1 parse cloud define("createtodosforuser", async(request) => { 2 let currentuser = request user; 3 let todostring = request params todostring; 4 let todo = parse object extend("todo"); 5 let todo = new todo(); 6 todo set("user", currentuser); 7 todo set("userobjectid", currentuser id); 8 todo set("tododescription", todostring); 9 todo set("finished", false); 10 return await todo save(null, { usemasterkey true }); 11 }); parse server 2 x 1 parse cloud define("createtodosforuser", function(request, response) { 2 var currentuser = request user; 3 var todostring = request params todostring; 4 var todo = parse object extend("todo"); 5 var todo = new todo(); 6 todo set("user", currentuser); 7 todo set("userobjectid", currentuser id); 8 todo set("tododescription", todostring); 9 todo set("finished", false); 10 todo save(null, { 11 usemasterkey true, 12 success function (object) { 13 response success(\[todo]); 14 }, 15 error function (object, error) { 16 response error("got an error " + error code + " " + error description); 17 } 18 }); 19 }); 6 эта функция облачного кода создает объект todo и устанавливает текущего пользователя в качестве владельца объекта это важно, чтобы только пользователь, который его создал, мог его найти или изменить запрещая создание todo на клиенте, мы заставляем объект todo соответствовать нашим стандартам и гарантируем, что todos принадлежат пользователю, который их создал 7\ далее давайте рассмотрим получение задач, которые вы создали на сервере в клиенте перейдите в todocontroller swift и найдите функцию gettodosfordate эта функция вызывается, когда вы получаете свои задачи функция принимает дату в качестве параметра и использует ее для получения списка задач, которые были созданы вами до этой даты в порядке убывания использование даты — отличный способ написать запрос с ленивой загрузкой, который не использует пропуски пропуск может иногда не сработать на большом наборе данных todocontroller swift 1 func savetodo(todostring\ string, completion @escaping ( result bool, message\ string, todoarray \[todo]) >()){ 2 var resulttodoarray \[todo] = \[] 3 let cloudparams \[anyhashable\ any] = \["date"\ date] 4 pfcloud callfunction(inbackground gettodosforuser, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 if let descrip = error? localizeddescription{ 8 completion(false, descrip, resulttodoarray) 9 } 10 }else{ 11 resulttodoarray = result as! \[todo] 12 completion(true, "success", resulttodoarray) 13 } 14 }) 15 } 8\ теперь давайте напишем функцию облачного кода для получения задач из базы данных на основе начальной даты мы запрашиваем задачи, которые были созданы до параметра даты, поэтому мы используем ‘query lessthan’, потому что даты по сути являются числами, которые становятся больше, чем дальше вы находитесь в будущем я также включил здесь немного хитрого кода скажем, мы включаем объект пользователя, который создал задачу, но не хотим делиться конфиденциальной информацией об этом пользователе с другими пользователями, нам нужно удалить его из json ответа поэтому у нас есть цикл for, в котором мы извлекаем объект пользователя из задачи, удаляем электронную почту и имя пользователя из json, а затем возвращаем его обратно в задачу это удобно для удаления конфиденциальных данных из вызова api в ситуациях, когда вы не можете контролировать, какие поля вы возвращаете например, включенный объект пользователя в этом случае нам это не очень нужно, потому что эта функция будет возвращать только задачи, которые вы создали сами мы делаем это, используя currentuser снова, чтобы запрашивать только задачи, созданные currentuser, который был прикреплен к запросу результаты возвращаются в порядке убывания, чтобы последние задачи отображались первыми когда вам нужно лениво загрузить другую партию задач, вы берете дату createdat из последней задачи и используете ее в качестве параметра даты для следующего запроса parse server 3 x 1 parse cloud define("gettodosforuser", async(request) => { 2 let currentuser = request user; 3 let date = request params date; 4 let query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query lessthan("createdat", date); 7 query descending("createdat"); 8 query limit(100); 9 query include("user"); 10 let results = await query find({ usemasterkey true }); 11 if(results length === 0) throw new error('no results found!'); 12 13 let resultsarray = \[]; 14 for (let i = 0; i < results length; i++) { 15 let todo = results\[i]; 16 let tempuser = todo get("user"); 17 let jsonuser = tempuser tojson(); 18 delete jsonuser email; 19 delete jsonuser username; 20 21 jsonuser type = "object"; 22 jsonuser classname = " user"; 23 24 let cleanedtodo = todo tojson(); 25 cleanedtodo user = jsonuser; 26 cleanedtodo type = "object"; 27 cleanedtodo classname = "todo"; 28 resultsarray push(cleanedtodo); 29 } 30 return resultsarray; 31 }); parse server 2 x 1 parse cloud define("gettodosforuser", function(request, response) { 2 var currentuser = request user; 3 var date = request params date; 4 var query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query lessthan("createdat", date); 7 query descending("createdat"); 8 query limit(100); 9 query include("user"); 10 query find({ 11 usemasterkey true, 12 success function (results) { 13 var resultsarray = \[]; 14 for (var i = 0; i < results length; i++) { 15 var todo = results\[i]; 16 var tempuser = todo get("user"); 17 var jsonuser = tempuser tojson(); 18 delete jsonuser email; 19 delete jsonuser username; 20 21 jsonuser type = "object"; 22 jsonuser classname = " user"; 23 24 var cleanedtodo = todo tojson(); 25 cleanedtodo user = jsonuser; 26 cleanedtodo type = "object"; 27 cleanedtodo classname = "todo"; 28 resultsarray push(cleanedtodo); 29 } 30 response success(resultsarray); 31 }, 32 error function (error) { 33 response error(" error " + error code + " " + error message); 34 } 35 }); 36 }); 9 теперь, когда у нас есть дела, мы можем видеть их в приложении и отмечать как выполненные, если захотим давайте обсудим это дальше 10\ чтобы отметить задачу как завершенную, просто нажмите кнопку «отметить как завершенную» на любой из созданных вами задач это вызовет метод в todocontroller swift под названием «marktodosascompletedfor», который принимает выбранную вами задачу в качестве параметра он отправляет todo objectid на сервер в качестве параметра, а затем возвращает обновленную задачу в результате todocontroller swift 1 func marktodosascompletedfor(todo\ todo, completion @escaping ( result bool, message\ string, todoarray \[todo]) >()){ 2 var resulttodoarray \[todo] = \[] 3 let cloudparams \[anyhashable\ any] = \["todoid"\ todo objectid ?? ""] 4 pfcloud callfunction(inbackground marktodoascompletedforuser, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 if let descrip = error? localizeddescription{ 8 completion(false, descrip, resulttodoarray) 9 } 10 }else{ 11 resulttodoarray = result as! \[todo] 12 completion(true, "success", resulttodoarray) 13 } 14 }) 15 } 11\ теперь мы напишем облачный код для обновления этого дела он ищет дело для обновления на основе objectid, но также использует currentuser, чтобы убедиться, что дело, связанное с objectid, было создано пользователем, выполняющим запрос это гарантирует, что вы можете просматривать только дела, которые вы создали, и, следовательно, это безопасно мы включаем ограничение в 1 результат, чтобы убедиться, что сервер не продолжает поиск после нахождения дела существует другой метод для поиска объекта на основе objectid, но мне не нравится его использовать, так как он может возвращать странные результаты, если не найдет объект, связанный с objectid мы также устанавливаем 'finisheddate' с текущей датой, когда объект был обновлен установив finisheddate только с помощью этой функции, мы убедились, что finisheddate безопасен и не может быть подделан или изменен мы также использовали 'query equalto( parse server 3 x 1 parse cloud define("marktodoascompletedforuser", async(request) => { 2 let currentuser = request user; 3 let todoid = request params todoid; 4 let query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query equalto("objectid", todoid); 7 query equalto("finished", false); 8 let todo = await query first({ usemasterkey true }); 9 if(object keys(todo) length === 0) throw new error('no results found!'); 10 todo set("finished", true); 11 let date = new date(); 12 todo set("finisheddate", date); 13 try { 14 await todo save(null, { usemasterkey true}); 15 return todo; 16 } catch (error){ 17 return("getnewstore error " + error code + " " + error message); 18 } 19 }); parse server 2 x 1 parse cloud define("marktodoascompletedforuser", function(request, response) { 2 var currentuser = request user; 3 var todoid = request params todoid; 4 var query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query equalto("objectid", todoid); 7 query equalto("finished", false); 8 query limit(1); 9 query find({ 10 usemasterkey true, 11 success function (results) { 12 if (results length > 0) { 13 var todo = results\[0]; 14 todo set("finished", true); 15 var date = new date(); 16 todo set("finisheddate", date); 17 todo save(null, { 18 usemasterkey true, 19 success function (object) { 20 response success(\[todo]); 21 }, 22 error function (object, error) { 23 response error("got an error " + error code + " " + error description); 24 } 25 }); 26 } else { 27 response error("todo not found to update"); 28 } 29 30 }, 31 error function (error) { 32 response error(" error " + error code + " " + error message); 33 } 34 }); 35 }); 7\) завершение! и это всё вы создали безопасное приложение для задач снова, ключ к созданию безопасного приложения на сервере parse это отключение всех разрешений на уровне классов для всех классов, кроме класса пользователь в классе пользователь вы отключаете все разрешения, кроме создания и получения также убедитесь, что все acl пользователей настроены так, чтобы пользователь мог только получать свои собственные данные затем все ваши взаимодействия проходят через облачный код и фильтруются с использованием request user, то есть текущегопользователя так что вот, теперь вы можете строить безопасные системы на основе сервера parse и back4app но подождите, вы скажете? что насчет фоновых заданий и живых запросов? ну, у вас есть хорошая точка, так что я расскажу об этом в двух бонусных разделах далее 8\) бонусные разделы 1\ фоновые задания иногда вам нужно создать фоновое задание, которое будет выполняться каждый час, каждый день или каждую неделю если вы работаете с отключенными всеми разрешениями на уровне классов, ваше фоновое задание не сможет запрашивать базу данных, если оно не настроено правильно это довольно сложно сделать, поэтому я хочу включить пример здесь в этом случае мы создадим фоновое задание, которое проверяет базу данных на незавершенные задачи, которые старше 1 года, и затем автоматически помечает их как завершенные секрет здесь в правильном использовании ‘usemasterkey’ его нужно добавить к запросу перед обещанием then просто следуйте этому шаблону, и вы сможете легко писать безопасные фоновые задания вы всегда начинаете с написания запроса, который вы хотите пройти по всей базе данных, а затем убедитесь, что включили status error, если произошла ошибка, и завершите его status success, чтобы убедиться, что оно завершено вы можете следить за журналами на back4app, чтобы увидеть, как работает фоновое задание, пока вы его запускаете parse server 3 x 1 parse cloud job("markunfinishedtodosolderthan1yearasfinished", async(request) => { 2 let date = new date(); 3 let intyear = date getfullyear() 1; 4 let query = new parse query("todo"); 5 query equalto("finished", intyear); 6 query lessthan("createdat", date); 7 8 let todo = await query find({ usemasterkey true }); 9 for (let i = 0; i < results length; i++) { 10 let todo = results\[i]; 11 todo set("finished", true); 12 todo set("finisheddate", date); 13 try { 14 await todo save(null, { usemasterkey true}); 15 } catch (error){ 16 console log("getnewstore error " + error code + " " + error message); 17 } 18 } 19 return "migration completed successfully "; 20 }); parse server 2 x 1 parse cloud define("marktodoascompletedforuser", function(request, response) { 2 var currentuser = request user; 3 var todoid = request params todoid; 4 var query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query equalto("objectid", todoid); 7 query equalto("finished", false); 8 query limit(1); 9 query find({ 10 usemasterkey true, 11 success function (results) { 12 if (results length > 0) { 13 var todo = results\[0]; 14 todo set("finished", true); 15 var date = new date(); 16 todo set("finisheddate", date); 17 todo save(null, { 18 usemasterkey true, 19 success function (object) { 20 response success(\[todo]); 21 }, 22 error function (object, error) { 23 response error("got an error " + error code + " " + error description); 24 } 25 }); 26 } else { 27 response error("todo not found to update"); 28 } 29 30 }, 31 error function (error) { 32 response error(" error " + error code + " " + error message); 33 } 34 }); 35 }); 2\ живые запросы иногда вам нужно использовать функцию живых запросов parse для чего то вроде приложения для живого чата вы захотите использовать живой запрос, чтобы увидеть, когда создаются новые сообщения для вашего пользователя живой запрос это, по сути, способ parse использовать сокеты для получения обновлений в реальном времени это довольно удобно, но это не будет работать с классом, у которого отключены разрешения find поэтому в этом случае мы снова включим разрешения find для класса сообщений, а вместо этого мы назначим acl для этого сообщения напрямую acl должен быть установлен так, чтобы только получатель мог использовать find, чтобы получить сообщение с сервера затем вы запускаете свой pf live query на клиенте, ищущем сообщения для вашего пользователя, и это будет работать безупречно если вы имеете дело с групповыми сообщениями, это немного иначе вы можете назначить несколько человек в acl, но это действительно не масштабируется вместо этого есть лучший способ вы устанавливаете acl на основе роли parse role и затем любому пользователю, которому вы хотите предоставить доступ к этому сообщению, просто назначаете его на эту parse role если вы хотите остановить их от чтения сообщений для этой группы, вы удаляете их из этой роли это гораздо проще, чем удалять их из acl каждого отдельного сообщения, и это масштабируется для очень больших групп это правильный способ сделать это я не собираюсь оставлять пример кода для этого, так как это слишком сложно для этого учебника, но, возможно, я объясню, как это сделать в следующем спасибо за чтение этого учебника по безопасности с parse и back4app если у вас есть вопросы, не стесняйтесь связаться со мной и я буду рад на них ответить спасибо! йорен