Запросы, параметры, куки, сессии ================================ Запросы ------- Типы параметров ~~~~~~~~~~~~~~~ Доступны два вида параметров запроса: текстовые параметры и параметры файлы (file upload, бинарные данные) Текстовые параметры делятся на три вида, каждый имеет тип ``scala.collection.mutable.Map[String, Seq[String]]``: 1. ``queryParams``: параметры после символа ? в ссылке, например: http://example.com/blah?x=1&y=2 2. ``bodyTextParams``: параметры в теле POST запроса 3. ``pathParams``: параметры в пути запроса, например: ``GET("articles/:id/:title")`` Параметры собираются воедино в переменной ``textParams`` в следующем порядке (от 1 к 3, более поздние перекрывают более ранние). ``bodyFileParams`` имеет тип scala.collection.mutable.Map[String, Seq[`FileUpload `_]]. Доступ к параметрам ~~~~~~~~~~~~~~~~~~~ Из контроллера в можете получить доступ к параметрам напрямую, или вы можете использовать методы доступа. Для доступа к ``textParams``: * ``param("x")``: возвращает ``String``, выбрасывает исключение если x не существует * ``paramo("x")``: возвращает ``Option[String]`` * ``params("x")``: возвращает ``Seq[String]`` Вы можете преобразовывать их к другим типам (Int, Long, Fload, Double) автоматически используя ``param[Int]("x")``, ``params[Int]("x")`` и пр. Для преобразования текстовых параметров к другим типам, перекройте метод `convertTextParam `_. Для параметров файлов: ``param[FileUpload]("x")``, ``params[FileUpload]("x")`` и пр. Более подробно, смотри :doc:`Загрузка файлов `. "at" ~~~~ Для передачи данных из контроллера в представление вы можете использовать ``at``. Тип ``at`` - ``scala.collection.mutable.HashMap[String, Any]``. Если вы знакомы с Rails, ``at`` это аналог ``@`` из Rails. Articles.scala :: @GET("articles/:id") class ArticlesShow extends AppAction { def execute() { val (title, body) = ... // Например, получаем из базы данных at("title") = title respondInlineView(body) } } AppAction.scala :: import xitrum.Action import xitrum.view.DocType trait AppAction extends Action { override def layout = DocType.html5( {antiCsrfMeta} {xitrumCss} {jsDefaults} {if (at.isDefinedAt("title")) "My Site - " + at("title") else "My Site"} {renderedView} {jsForView} ) } "atJson" ~~~~~~~~ ``atJson`` - утильный метод который автоматически конвертирует ``at("key")`` в JSON. Метод может быть полезен для передачи моделей напрямую из Scala в JavaScript. ``atJson("key")`` эквивалент ``xitrum.util.SeriDeseri.toJson(at("key"))``: Action.scala :: case class User(login: String, name: String) ... def execute() { at("user") = User("admin", "Admin") respondView() } Action.ssp :: RequestVar ~~~~~~~~~~ У ``at`` есть недостаток, он не безопасен относительно типов, т.к. основан на не типизированной коллекции. Если вам нужна большая безопасность, можно использовать идею RequestVar, которая оборачивает ``at``. RVar.scala :: import xitrum.RequestVar object RVar { object title extends RequestVar[String] } Articles.scala :: @GET("articles/:id") class ArticlesShow extends AppAction { def execute() { val (title, body) = ... // Get from DB RVar.title.set(title) respondInlineView(body) } } AppAction.scala :: import xitrum.Action import xitrum.view.DocType trait AppAction extends Action { override def layout = DocType.html5( {antiCsrfMeta} {xitrumCss} {jsDefaults} {if (RVar.title.isDefined) "My Site - " + RVar.title.get else "My Site"} {renderedView} {jsForView} ) } Куки ---- Подробнее о `куки `_. Внутри контроллера, используйте ``requestCookies``, для чтения кук отправленных браузером (тип ``Map[String, String]``). :: requestCookies.get("myCookie") match { case None => ... case Some(string) => ... } Для отправки куки браузеру, создайте экземпляр `DefaultCookie `_ и добавьте его к массиву ``responseCookies`` который хранит все `куки `_. :: val cookie = new DefaultCookie("name", "value") cookie.setHttpOnly(true) // true: JavaScript не может получить доступ к куки responseCookies.append(cookie) Если вы не укажите путь для через метод ``cookie.setPath(cookiePath)``, то будет использован корень сайта как путь (``xitrum.Config.withBaseUrl("/")``). Это позволяет избежать случайного дублирования кук. Что бы удалить куку отправленную браузером, отправить куку с тем же именем и с временем жизни 0. Браузер посчитает ее истекшей. Для того что бы создать куку удаляемую при закрытии браузере, установите время жизни в ``Long.MinValue``: :: cookie.setMaxAge(Long.MinValue) `Internet Explorer не поддерживает "max-age" `_, но Netty умеет это определять и устанавливает "max-age" и "expires" должны образом. Не беспокойтесь! Браузер не отправляет атрибуты куки обратно на сервер. Браузер отправляет `только пары имя-значение `_. Если вы хотите подписать ваши куки, что бы защититься от подделки, используйте ``xitrum.util.SeriDeseri.toSecureUrlSafeBase64`` и ``xitrum.util.SeriDeseri.fromSecureUrlSafeBase64``. Подробнее смотри :doc:`Как шифровать данные `. Допустимые символы в куки ~~~~~~~~~~~~~~~~~~~~~~~~~ Вы можете использовать только `ограниченный набор символов в куки `_. Например, если вам нужно передать UTF-8 символы, вы должны закодировать их. Можно использовать, например, ``xitrum.utill.UrlSafeBase64`` или ``xitrum.util.SeriDeseri``. Пример записи куки: :: import io.netty.util.CharsetUtil import xitrum.util.UrlSafeBase64 val value = """{"identity":"example@gmail.com","first_name":"Alexander"}""" val encoded = UrlSafeBase64.noPaddingEncode(value.getBytes(CharsetUtil.UTF_8)) val cookie = new DefaultCookie("profile", encoded) responseCookies.append(cookie) Чтение куки: :: requestCookies.get("profile").foreach { encoded => UrlSafeBase64.autoPaddingDecode(encoded).foreach { bytes => val value = new String(bytes, CharsetUtil.UTF_8) println("profile: " + value) } } Сессии ------ Хранение сессии, восстановление, шифрование и прочее выполняются автоматически. В контроллере, вы можете использовать переменную ``session``, которая имеет тип ``scala.collection.mutable.Map[String, Any]``. Значения в ``session`` должны быть сериализуемые. Например, что бы сохранить что пользователь прошел авторизацию, вы можете сохранить его имя в сессии: :: session("userId") = userId Позднее, если вы хотите убедиться что пользователь авторизован, вы просто проверяете есть ли его имя в сессии: :: if (session.isDefinedAt("userId")) println("This user has logged in") Хранение идентификатора пользователя и загрузка его из базы данных при каждом запросе обычно является не плохим решением. В этом случае информация о пользователе обновляется при каждым запросе (включая изменения в правах доступа). session.clear() ~~~~~~~~~~~~~~~ `Одна строчка кода позволяет защититься от фиксации сессии `_. Прочитайте статью по ссылке выше что бы узнать подробнее про эту атаку. Для защиты от атаки, в контроллере который использует логин пользователя, вызовете ``session.clear()``. :: @GET("login") class LoginAction extends Action { def execute() { ... session.clear() // Сброс сессии прежде чем выполнять какие либо дейтсвияthe session session("userId") = userId } } Это касается так же контроллера, который выполняет "выход пользователя" (log out). SessionVar ~~~~~~~~~~ SessionVar, как и RequestVar, это способ сделать сессию более безопасной. Например, вы хотите хранить имя пользователя в сессии после того как он прошел авторизацию: Объявите session var: :: import xitrum.SessionVar object SVar { object username extends SessionVar[String] } Присвойте значение во время авторизации: :: SVar.username.set(username) Отобразите имя пользователя: :: if (SVar.username.isDefined) {SVar.username.get} else Login * Для удаления используйте: ``SVar.username.remove()`` * Для сброса всей сессии используйте: ``session.clear()`` Хранилище сессии ~~~~~~~~~~~~~~~~ Из коробки Xitrum предоставляет 3 простых хранилища. В файле `config/xitrum.conf `_ есть возможность настроить хранилище сессии: CookieSessionStore: :: # Хранение сессии на стороне клиента в куках store = xitrum.scope.session.CookieSessionStore LruSessionStore: :: # Простое хранилище на стороне сервера store { "xitrum.local.LruSessionStore" { maxElems = 10000 } } Если вы запускаете несколько серверов, вы можете использовать `Hazelcast для хранения кластеризованных сессии `_. Важно, если вы используете CookieSessionStore или Hazelcast, ваши данные должны быть сериализуемыми. Если ваши данные не подлежат сериализации используйте LruSessionStore. При использовании LruSessionStore вы можете кластеризовать сессии используя load balancer и sticky sessions. Эти три типа хранилища сессии обычно покрывают все необходимые случаи. Существует возможность определить свою реализацию хранилища сессии, используйте наследование от `SessionStore `_ или `ServerSessionStore `_ и реализуйте абстрактные методы. Хранилище может быть объявлено в двух видах: :: store = my.session.StoreClassName Или: :: store { "my.session.StoreClassName" { option1 = value1 option2 = value2 } } Используйте куки когда это возможно, поскольку они более масштабируемы (сериализуемым и меньше 4KB). Храните сессии на сервере (в памяти или базе данных) если это необходимо. Дальнейшее чтение: `Web Based Session Management - Best practices in managing HTTP-based client sessions `_. object vs. val -------------- Пожалуйста, используйте ``object`` вместо ``val``. **Не делайте так**: :: object RVar { val title = new RequestVar[String] val category = new RequestVar[String] } object SVar { val username = new SessionVar[String] val isAdmin = new SessionVar[Boolean] } Приведенный код компилируется но не работает корректно, потому что Vars внутри себя используют имена классов что бы выполнять поиск. При использовании ``val``, ``title`` и ``category`` мы имеем тоже самое имя класса "xitrum.RequestVar". Одно и тоже как и для ``username`` и ``isAdmin``.