- Постоянство: мы используем scalaquery для сохранения элементов нашей модели.
- Безопасность: обрабатывать заголовок безопасности, содержащий ключ API.
Фрист, мы посмотрим на постоянство части. Для этой части мы будем использовать scalaquery . Обратите внимание, что код, который мы здесь показываем, в значительной степени такой же, как и для преемника scalaquery. Slick , однако, требует Scala 2.10.0-M7, и это будет означать, что мы должны изменить нашу полную настройку Scala. Так что для этого примера мы будем просто использовать scalaquery (чей синтаксис такой же, как у slick). Если вы еще этого не сделали, установите JRebel, чтобы ваши изменения были отражены мгновенно без перезапуска службы.
живучесть
Я использовал postgresql для этого примера, но можно использовать любую из баз данных, поддерживаемых scalaquery. Модель базы данных, которую я использовал, очень проста:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
CREATE TABLE sc_bid( id integer NOT NULL DEFAULT nextval('sc_bid_id_seq1'::regclass), 'for' integer, min numeric, max numeric, currency text, bidder integer, date numeric, CONSTRAINT sc_bid_pkey1 PRIMARY KEY (id ), CONSTRAINT sc_bid_bidder_fkey FOREIGN KEY (bidder) REFERENCES sc_user (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT sc_bid_for_fkey FOREIGN KEY ('for') REFERENCES sc_item (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION) CREATE TABLE sc_item( id integer NOT NULL DEFAULT nextval('sc_bid_id_seq'::regclass), name text, price numeric, currency text, description text, owner integer, CONSTRAINT sc_bid_pkey PRIMARY KEY (id ), CONSTRAINT sc_bid_owner_fkey FOREIGN KEY (owner) REFERENCES sc_user (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION) CREATE TABLE sc_user( id serial NOT NULL, username text, firstname text, lastname text, CONSTRAINT sc_user_pkey PRIMARY KEY (id )) |
Как вы можете простую модель, с парой внешних ключей и первичных ключей, которые генерируются автоматически. Мы определяем таблицу для пользователей, для товаров и предложений. Обратите внимание, что это зависит от базы данных, так что это будет работать только для postgresql. Дополнительная заметка о postgresql и scalaquery. Scalaquery не поддерживает схемы. Это означает, что мы должны определить таблицы в «публичной» схеме.
Прежде чем мы сможем начать работу с scalaquery, мы должны сначала добавить его в наш проект. В build.sbt добавьте следующие зависимости
|
1
2
|
'org.scalaquery' %% 'scalaquery' % '0.10.0-M1','postgresql' % 'postgresql' % '9.1-901.jdbc4' |
После обновления у вас будут необходимые фляги для scalaquery и postgres. Давайте посмотрим на один из репозиториев: bidrepository и признак RepositoryBase.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
// the traitimport org.scalaquery.session.Database trait RepositoryBase { val db = Database.forURL('jdbc:postgresql://localhost/dutch_gis?user=jos&password=secret', driver = 'org.postgresql.Driver')} // simple implementation of the bidrepositorypackage org.smartjava.scalatra.repositoryimport org.smartjava.scalatra.model.Bidimport org.scalaquery.session._import org.scalaquery.ql.basic.{BasicTable => Table}import org.scalaquery.ql.TypeMapper._import org.scalaquery.ql._import org.scalaquery.ql.extended.PostgresDriver.Implicit._import org.scalaquery.session.Database.threadLocalSession class BidRepository extends RepositoryBase { object BidMapping extends Table[(Option[Long], Long, Double, Double, String, Long, Long)]('sc_bid') { def id = column[Option[Long]]('id', O PrimaryKey) def forItem = column[Long]('for', O NotNull) def min = column[Double]('min', O NotNull) def max = column[Double]('max', O NotNull) def currency = column[String]('currency') def bidder = column[Long]('bidder', O NotNull) def date = column[Long]('date', O NotNull) def noID = forItem ~ min ~ max ~ currency ~ bidder ~ date def * = id ~ forItem ~ min ~ max ~ currency ~ bidder ~ date } /** * Return a Option[Bid] if found or None otherwise */ def get(bid: Long, user: String) : Option[Bid] = { var result:Option[Bid] = None; db withSession { // define the query and what we want as result val query = for (u <-BidMapping if u.id === bid) yield u.id ~ u.forItem ~ u.min ~ u.max ~ u.currency ~ u.bidder ~ u.date // map the results to a Bid object val inter = query mapResult { case(id,forItem,min,max,currency,bidder,date) => Option(new Bid(id,forItem, min, max, currency, bidder, date)); } // check if there is one in the list and return it, or None otherwise result = inter.list match { case _ :: tail => inter.first case Nil => None } } // return the found bid result } /** * Create a bid using scala query. This will always create a new bid */ def create(bid: Bid): Bid = { var id: Long = -1; // start a db session db withSession { // create a new bid val res = BidMapping.noID insert (bid.forItem.longValue, bid.minimum.doubleValue, bid.maximum.doubleValue, bid.currency, bid.bidder.toLong, System.currentTimeMillis()); // get the autogenerated bid val idQuery = Query(SimpleFunction.nullary[Long]('LASTVAL')); id = idQuery.list().head; } // create a bid to return val createdBid = new Bid(Option(id), bid.forItem, bid.minimum, bid.maximum, bid.currency, bid.bidder, bid.date); createdBid; } /** * Delete a bid */ def delete(user:String, bid: Long) : Option[Bid] = { // get the bid we're deleting val result = get(bid,user); // delete the bid val toDelete = BidMapping where (_.id === bid) db withSession { toDelete.delete } // return deleted bid result }} |
Выглядит сложно, верно? Мы сделаем это не раз, когда вы поймете, как работает скаляр. С помощью scalaquery вы создаете отображение таблицы. В этом отображении вы указываете тип полей, которые вы ожидаете. В этом примере наша таблица сопоставления выглядит следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
object BidMapping extends Table[(Option[Long], Long, Double, Double, String, Long, Long)]('sc_bid') { def id = column[Option[Long]]('id', O PrimaryKey) def forItem = column[Long]('for', O NotNull) def min = column[Double]('min', O NotNull) def max = column[Double]('max', O NotNull) def currency = column[String]('currency') def bidder = column[Long]('bidder', O NotNull) def date = column[Long]('date', O NotNull) def noID = forItem ~ min ~ max ~ currency ~ bidder ~ date def * = id ~ forItem ~ min ~ max ~ currency ~ bidder ~ date} |
Здесь мы определяем отображение таблицы ‘sc_bid’. Для каждого поля мы определяем имя столбца и его тип. Если мы хотим, мы можем добавить конкретные параметры, которые учитываются при создании вашего ddl из этого (не то, что я использовал для этого примера). Последние два определения определяют «конструкторы» для этого отображения. ‘Def *’ — это конструктор по умолчанию, где у нас есть все поля заранее, а ‘def noID’ — это тот, который мы будем использовать, когда создадим ставку в первый раз, и у нас пока нет идентификатора. Помните, что идентификаторы автоматически генерируются базой данных.
С этим отображением мы можем начать писать наши функции репозитория. Начнем с первого: получить
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/** * Return a Option[Bid] if found or None otherwise */def get(bid: Long, user: String) : Option[Bid] = { var result:Option[Bid] = None; db withSession { // define the query and what we want as result val query = for (u <-BidMapping if u.id === bid) yield u.id ~ u.forItem ~ u.min ~ u.max ~ u.currency ~ u.bidder ~ u.date // map the results to a Bid object val inter = query mapResult { case(id,forItem,min,max,currency,bidder,date) => Option(new Bid(id,forItem, min, max, currency, bidder, date)); } // check if there is one in the list and return it, or None otherwise result = inter.list match { case _ :: tail => inter.first case Nil => None } } // return the found bid result } |
Здесь вы можете видеть, что мы используем стандартную конструкцию scala для создания итерации запроса по таблице, отображенной с помощью BidMapping. Чтобы убедиться, что мы получаем только то поле, которое нам нужно, мы применяем фильтр, используя оператор «if u.id === bid». В выражении yield мы указываем поля, которые мы хотим вернуть. Используя mapResult в запросе, мы можем обработать результаты запроса и преобразовать его в наш объект case и добавить его в список. Затем мы проверяем, действительно ли что-то есть в списке и возвращаем Option [Bid]. Обратите внимание, что это может быть написано более кратко, но это хорошо объясняет шаги, которые вы должны предпринять.
Следующая функция создать
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
def create(bid: Bid): Bid = { var id: Long = -1; // start a db session db withSession { // create a new bid val res = BidMapping.noID insert (bid.forItem.longValue, bid.minimum.doubleValue, bid.maximum.doubleValue, bid.currency, bid.bidder.toLong, System.currentTimeMillis()); // get the autogenerated bid val idQuery = Query(SimpleFunction.nullary[Long]('LASTVAL')); id = idQuery.list().head; } // create a bid to return val createdBid = new Bid(Option(id), bid.forItem, bid.minimum, bid.maximum, bid.currency, bid.bidder, bid.date); createdBid; } |
Теперь мы используем пользовательский noid BidMapping для конструктора, чтобы сгенерировать оператор вставки. Если мы не указали noID, мы должны уже указать id. Теперь, когда мы вставили новый объект Bid в базу данных, нам нужно вернуть только что созданный Bid с новым идентификатором пользователю. Для этого нам нужно выполнить простой запрос LASTVAL, который возвращает последнее автоматически сгенерированное значение. В нашем случае это идентификатор созданной ставки. Из этой информации мы создаем новую ставку, которую мы возвращаем.
Последняя операция для нашего репозитория — это функция удаления. Эта функция сначала проверяет наличие указанной ставки и, если она есть, удаляет ее.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
def delete(user:String, bid: Long) : Option[Bid] = { // get the bid we're deleting val result = get(bid,user); // delete the bid val toDelete = BidMapping where (_.id === bid) db withSession { toDelete.delete } // return deleted bid result} |
Здесь мы используем фильтр ‘where’ для создания запроса, который мы хотим выполнить. Когда мы вызываем delete в этом фильтре, все соответствующие элементы удаляются. И это самое простое использование скалябства для настойчивости. Если вам нужны более сложные операции (например, объединения), посмотрите примеры на сайте scalaquery.org.
Теперь у нас есть функциональность для создания и удаления ставок. Поэтому было бы неплохо, если бы у нас был какой-то способ аутентификации наших пользователей. Для этого урока мы собираемся создать очень простую схему аутентификации на основе API-ключей. Для каждого запроса пользователь должен добавить определенный заголовок со своим ключом API. Затем мы можем использовать информацию из этого ключа, чтобы определить, кто этот пользователь, и может ли он удалить или получить доступ к конкретной информации.
Безопасность
Начнем с части генерации ключей. Когда кто-то хочет использовать наш API, мы требуем от него указать имя приложения и имя хоста, с которого будет сделан запрос. Эту информацию мы будем использовать для генерации ключа, который они должны использовать в каждом запросе. Этот ключ — простой хэш HMAC.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package org.smartjava.scalatra.util import javax.crypto.spec.SecretKeySpecimport javax.crypto.Macimport org.apache.commons.codec.binary.Base64 object SecurityUtil { def calculateHMAC(secret: String, applicationName: String , hostname: String ) : String = { val signingKey = new SecretKeySpec(secret.getBytes(),'HmacSHA1'); val mac = Mac.getInstance('HmacSHA1'); mac.init(signingKey); val rawHmac = mac.doFinal((applicationName + '|' + hostname).getBytes()); new String(Base64.encodeBase64(rawHmac)); } def checkHMAC(secret: String, applicationName: String, hostname: String, hmac: String) : Boolean = { return calculateHMAC(secret, applicationName, hostname) == hmac; } def main(args: Array[String]) { val hmac = SecurityUtil.calculateHMAC('The passphrase to calculate the secret with','App 1','localhost'); println(hmac); println(SecurityUtil.checkHMAC('The passphrase to calculate the secret with','App 1','localhost',hmac)); }} |
Приведенный выше вспомогательный объект используется для вычисления начального хэша, который мы отправляем пользователю, и может использоваться для проверки входящего хэша. Чтобы использовать это в нашем REST API, нам нужно перехватить все входящие запросы и проверить эти заголовки перед вызовом определенного маршрута. С помощью scalatra мы можем сделать это с помощью функции before ():
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package org.smartjava.scalatra.routesimport org.scalatra.ScalatraBaseimport org.smartjava.scalatra.repository.KeyRepository /** * When this trait is used, the incoming request * is checked for authentication based on the * X-API-Key header. */trait Authentication extends ScalatraBase { val ApiHeader = 'X-API-Key'; val AppHeader = 'X-API-Application'; val KeyChecker = new KeyRepository; /** * A simple interceptor that checks for the existence * of the correct headers */ before() { // we check the host where the request is made val servername = request.serverName; val header = Option(request.getHeader(ApiHeader)); val app = Option(request.getHeader(AppHeader)); List(header,app) match { case List(Some(x),Some(y)) => isValidHost(servername,x,y); case _ => halt(status=401, headers=Map('WWW-Authenticate' -> 'API-Key')); } } /** * Check whether the host is valid. This is done by checking the host against * a database with keys. */ private def isValidHost(hostName: String, apiKey: String, appName: String): Boolean = { KeyChecker.validateKey(apiKey, appName, hostName); } } |
Эта черта, которую мы включили в наш основной сервлет скалатры, получает правильную информацию из запроса и проверяет, соответствует ли предоставленный хеш сгенерированным кодом, который вы видели ранее. Если это так, запрос передается, если нет, мы останавливаем обработку запроса и отправляем обратно 401, объясняющий, как пройти аутентификацию с помощью этого API.
Если клиент пропустит эти заголовки, он получит это в ответ:
Если клиент отправит правильные заголовки, он получит такой ответ:
Вот именно для этой части. В следующей части мы рассмотрим внедрение Indency, CQRS, Akka и запуск этого кода в облаке.
Ссылка: Учебное пособие: Начало работы со scala и scalatra — часть III от нашего партнера JCG Йоса Дирксена из блога Smart Java .

