Файл: Технология «клиент-сервер»(Обзор существующих решений «клиент-сервер»).pdf

ВУЗ: Не указан

Категория: Курсовая работа

Дисциплина: Не указана

Добавлен: 17.05.2023

Просмотров: 74

Скачиваний: 3

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Наконец, таблицы «oauth_access_tokens» и «oauth_refresh_tokens» используются для хранения токенов сессий пользователей, используемых для аутентификации по протоколу OAuth2.

Рисунок 3.1. Схема таблиц базы данных на сервере

Механизм SpringData существенно облегчает взаимодействие с базой данных. Например, вместо описания таблица базы данных может быть описанав виде data классов, доступных в языке Kotlin.Для этого, класс должен быть унаследован от класса Persistable, являющегося частью SpringData, и содержать аннотации из пакета «javax.persistence», примененные к самому классу и его полям.Например на схеме 3.6. приведено описание таблицы сообщений в виде класса MessageEntity.

@NoArg
@Entity
@Table(name = "messages")
class MessageEntity constructor(
givenUuid: UUID?,

@Column(nullable = false) val timestamp: Long,

@Column(nullable = true)val text: String? = null,

@Column(nullable = true) val type: Int? = null,

@Column(nullable = true) valimageUuid: String? = null,

@Column(nullable = true) valcallStatus: Int? = null,

@Column(nullable = true) valcallUuid: String? = null,

@Column(nullable = false) valisFromPatient: Boolean,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "conversationUuid", nullable = false)
val conversation: ConversationEntity

) : RandomUuidEntity(givenUuid)

Cхема 3.6. КлассMessageEntity
 

SpringData также предоставляет удобный механизм для изменения данных таблицы. Рассмотрим интерфейс DoctorsRepository (рис. 3.7), отвечающий за взаимодействие с таблицей «doctors».

@Repository
interfaceDoctorRepository :JpaRepository<DoctorEntity, String> {

funfindByEmail(email: String) : DoctorEntity?

fun findByFullNameIgnoreCaseContainingOrSpecializationIgnoreCaseContaining(
fullName: String,
specialization: String
): List<DoctorEntity>

fun existsByEmail(email: String) : Boolean

}

Схема 3.7. ИнтерфейсDoctorRepository
 

Каквиднонасхеме 3.7, интерфейсDoctorRepositoryнаследуетсяотинтерфейсаJpaRepository, являющегосячастьюSpringData. За реализацию описанного интерфейса отвечает SpringData, создающий для каждого метода реализацию на чистом SQL в соответствии с названием метода. Если для названия метода невозможно сформировать SQL запрос, SpringData скажет об этом на этапе компиляции еще до запуска программы. Более того, многие методы, такие как получение или удаление записи из таблицы, предоставляются JpaRepository по умолчанию, что еще больше сокращает количество однотипного кода в проекте.

Рисунок 3.2. Экран «Приветствие»Рисунок 3.3. Экран «Приветствие»
после изменения текущего действия

После успешного запуска программы откроется экран «Приветствие» (рис. 3.2). Пользователь имеет возможность создать новую учетную запись или войти в ранее созданную учетную запись, введя адрес электронной почты и пароль в поля ввода текста с надписями «Email» и «Пароль» и нажав на зеленую кнопку под полями ввода.


Во многих приложениях функциональности входа в аккаунт и регистрации представлены на разных экранах. Однаков нашем приложении данные, вводимые пользователем в этих двух случаях, абсолютно идентичны, поэтому было решено реализовать вход в аккаунт и регистрации на одном экране.Зеленая кнопка под полями ввода отображает действие, которое произойдет после нажатия на нее. Текущее действие может быть изменено нажатием на прозрачные кнопки с серым текстом снизу. Всего возможны 4 действия: «Войти как пациент», «Зарегистрироваться как пациент», «Войти как врач», «Зарегистрироваться как врач».

interfaceAuthRestApi {

@FormUrlEncoded
@POST("/oauth/token")
fun getFreshestTokenByRefreshToken(
@Field("refresh_token") refreshToken: String,
@Field("grant_type") grantType: String = "refresh_token"
): Call<TokenResponse>

@FormUrlEncoded
@POST("/oauth/token")
fun getFreshestTokenByPassword(
@Field("password") password: String,
@Field("username") email: String,
@Field("grant_type")grantType: String = "password"
): Single<TokenResponse>

@POST("/register")
fun register(
@Body loginData: LoginDataRequest
): Single<UserModelWrapper>

@POST("/login")
fun login(
@Body loginData: LoginDataRequest
): Single<UserModelWrapper>

}

Cхема 3.8. Интерфейс AuthRestApi, ответственный за авторизацию пользователей

Для того, чтобы пользователь получал доступ только к доступным для него данным, необходимо продумать систему регистрации и последующей авторизации пользователей. В качестве протокола авторизации был выбран OAuth 2.0 [29]. Для обеспечения безопасности передачи данных, все данные передаются между клиентской и серверной частями приложения с использованием протокола HTTPS.

Схема клиент-серверного взаимодействия авторизации пользователей состоит из трех шагов:

  1. Получение данных аккаунта;
  2. Получение access token и refresh token;
  3. Обновление access token.

Первые два шага происходят на экране «Приветствие» (рис. 3.2), когда пользователь вводит данные своего существующего или нового аккаунта.

На первом в шаге, в зависимости от того, хочет ли пользователь войти в существующий аккаунт или же создать новый, клиент вызывает методы сервера POST«/register» или POST«/login», передавая в качестве тела запроса объект JSON, хранящий в себе поля«email», «password» и «isDoctor». В ответ, сервер отправляет информацию аккаунта пользователя в формате JSONили ошибку. При ошибке, пользователю показывается соответствующее ошибке сообщение и схема завершается. Стоит отметить, что эти два метода являются единственными методами сервера, доступными без предоставления accesstoken.

На втором шаге, клиент вызывает метод сервера POST«/oauth/token», передавая с помощью типа данных «application/x-www-form-urlencoded» поля «password», «username» и «grant_type». Поле «password» – пароль пользователя, «username» – его адрес электронной почты, «grant_type» на этом шаге равен тексту «password», так как получение токена происходит по предоставлению пароля. На этом шаге, сервер проверяет, действительно ли в системе существует данный пользователь, и если он находит его, то возвращает JSON, содержащий поля «access_token», «refresh_token» и «expires_in», иначе – ошибку. В случае успеха, клиент сохраняет информацию аккаунта пользователя, accesstoken и refreshtokenв настройки приложения. Плюсом такой схемы является то, что пароль приложения не хранится на устройстве и не используется для получения доступа к методам сервера.


Наконец, на третьем шаге, происходит обновление accesstoken. Этот шаг происходит тогда, когда сервер в ответ на один из запросов клиента возвращает сообщение о том, что предоставленный accesstoken истек (код ошибки 401).Клиент вызывает метод сервера POST«/oauth/token», передавая с помощью типа данных «application/x-www-form-urlencoded» поля «refresh_token» и «grant_type», равных тексту «refresh_token». В ответ, сервер возвращает JSON, содержащий поля «access_token», «refresh_token» и «expires_in», и клиент accesstoken и refreshtoken в настройках.

Как было сказано ранее, для успешного выполнения любого запроса к серверу, кроме «/login» и «/register», требуется передача accesstoken вместе с запросом. Accesstoken передается на сервер в виде заголовка вида«Authorization: Bearer csaf41214safh214oiwuencr». На любой такой запрос сервер может ответить кодом ошибки 401, в случае чего клиент должен получить новый токен с сервера, используя refreshtoken, после чего отправить запрос заново. Однако, если делать это в месте вызова каждого запроса к серверу, количество однотипного кода в приложении существенно увеличится. Чтобы ненарушать правило «Don’trepeatyourself» [30], необходимо вынести описанную выше логику в отдельный класс.

Специально для таких целей, в библиотеке OkHttp [31],используемой в разработанном мобильном приложении, существует механизм Interceptors. Классы типа Interceptor отвечают за перехват запроса к серверу и ответа от него, дополняя запрос или обрабатывая ответ от сервера. В нашем случае, класс CredentialsInterceptor добавляет к запросу заголовок с текущим accesstoken, а в случае необходимости получает новый accesstoken сервера, используя заголовок с refreshtoken, после чего отправляет запрос с использованием обновленного accesstokenзаново.

Рисунок 3.4. Экран «Чат» Рисунок 3.5. Экран «Чат» в условиях
отсутствия интернет-соединения

Экран «Чат» содержит список всех сообщений чата, отсортированный по дате так, что последнее сообщение находится внизу списка, а наиболее ранее – вверху. Этот экран позволяет пациенту отправлять врачу текстовые сообщения и изображения. Также, существуют четыре типа системных сообщений – «Изменение пациентом настроек доступа врача к медкарте», «Отправка врачом записи в медкарту пациента», «Начало вызова», «Окончание вызова». По нажатию на сообщения первых двух типов происходит переход на экраны «Записи от врача» и «Врач», соответственно. Системные сообщения отправляются автоматически без каких-либо действий со стороны пользователей. По нажатию на имя врача происходит переход на экран «Врач».


При отсутствии интернет-соединения происходит отображение сообщений, сохраненных на устройстве. После восстановления интернет-соединения, состояние подключения будет автоматически синхронизировано с сервером без вмешательства со стороны пользователя.Под именем врача отображается текущее состояние подключения: «Подключено», «Обновление…» или «Ожидание подключения…».

После нажатия на сообщения в формате изображения, изображение открывается на весь экран. Пользователь может увеличивать или уменьшать изображение, а по нажатию на кнопку с изображением стрелочки в верхнем левом углу закрыть его.

Было сформулировано несколько требований к формату клиент-серверного взаимодействия при обмене пользователями сообщениями в чате. Во-первых, сообщения должны передаваться мгновенно, без задержки. Во-вторых, клиент и сервер должны обмениваться лишь сообщениями текущего чата, а не всех активных чатов пользователя, что позволяет избавить клиентское приложение от необходимости фильтрации получаемых сообщений и делает систему более предсказуемой. Протокол STOMP над протоколом WebSocketудовлетворяет обоим требованиями, к тому же, он хорошо поддерживается фреймворкомSpring. Однако, библиотек под Android, реализующих протокол STOMP, практически нет. В то же время, использование чистого протоколаWebSocket в мобильной разработке достаточно распространено; существует множество библиотек под Android, облегчающих работу с ним. В разработанном мобильном приложении была использована библиотека Scarlet, позволяющая описывать взаимодействие с сервером в виде интерфейсов с помощью аннотаций (схема 3.5). Поэтому было решено использовать чистый протокол WebSocket над протоколом HTTPS, при этом немного расширив его, передавая при установлении соединения в заголовкеc названием «recipient-uuid» идентификатор пользователя, с которым будет вестись общение.

Фреймворк Spring позволяет получить для текущего WebSocket соединения объект типа Principal, хранящий информацию о соединении. Унаследовав класс WebSocketPrincipal (схема 3.9) от класса Principal, можно хранить дополнительную информацию о соединении.

 

dataclassWebSocketPrincipal(
valuuid: String,valrecipientUuid: String,valisPatient: Boolean
) : Principal {
valpatientUuid = if (isPatient) uuidelserecipientUuid
valdoctorUuid = if (isPatient) recipientUuidelseuuid
override fun getName() = uuid
}

Схема 3.9. КлассWebSocketPrincipal

ПриконфигурацииWebSocketсоединения, возможно переопределить логику создания объекта Principal в методе «determineUser» (схема 3.10)для передачи в него данных, полученных из заголовков запроса на установление соединения.


override fun determineUser(
request: ServerHttpRequest,
wsHandler: WebSocketHandler,
attributes: MutableMap<String, Any>
): Principal? {
val user = super.determineUser(request, wsHandler, attributes)
?: return null
valrecipientUuid = request.headers[RECIPIENT_UUID_HEADER]?.firstOrNull()
?: return null

val doctor = doctorRepository.findByEmail(user.name)
if (doctor != null) return WebSocketPrincipal(doctor.uuid, recipientUuid, false)

val patient = patientRepository.findByEmail(user.name)
if (patient != null) return WebSocketPrincipal(patient.uuid, recipientUuid, true)

return null
}

Схема 3.10. Метод determineUser

Одной из важных функций приложения в целом является возможность общения пациента с врачом через аудио- и видеосвязь. Написание платформы для видеосвязи с нуля отняло бы непозволительно большое количество времени, поэтому было использовано SaaS решение, платформа Jitsi [32]. Она полностью бесплатна, имеет открытый исходный код и не требует наличия собственного сервера. Платформа построена так, что любой желающий может подключаться к публичным комнатам по ее названию. Однако очевидно, что наше приложение должно обеспечивать приватность и безопасность общения между врачом и пациентом. Поэтому, для создания имени комнаты используется класс UUID из пакета «java.util» позволяющий создавать такие идентификаторы, что вероятность получения доступа к комнате при таком подходе составляет [33].

Платформа Jitsi представляет SDK для операционной системы Android, позволяющая буквально в пару строк отображать интерфейс необходимой комнаты. На рисунках 3.6 и 3.7 представлен пользовательский интерфейс при взаимодействии с комнатой.


 

Рисунок 3.6. Экран «Звонок» Рисунок 3.7. Экран «Звонок»
при включенной камере
Участие пользователей в звонке должно происходить естественно и удобно с точки зрения пользовательского интерфейса. Например, в одно нажатие доктор должен иметь возможность инициировать звонок и завершить его, пациент – принять или отклонить приглашение (рис. 3.8 и 3.9). После выхода одного из пользователей из разговора необходимо закрывать экран «Звонок» на обоих устройствах.

Рисунок 3.8. Экран «Исходящий вызов» Рисунок 3.9. Экран «Входящий вызов»

Однако SDKJitsi для Androidпредоставляет лишь возможность взаимодействия пользователя с комнатой. Логика входа и выхода из комнаты должна быть реализована самостоятельно. Таким образом, необходимо было создать систему управления состоянием текущего звонка. Для этого был применен концепт StateMachine [34]. Диаграмма состояний звонка в системе представлена на рисунке 3.10.