- Постоянство: мы используем 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 _ pkey 1 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 trait import 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 bidrepository package org.smartjava.scalatra.repository import org.smartjava.scalatra.model.Bid import 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.SecretKeySpec import javax.crypto.Mac import org.apache.commons.codec.binary.Base 64 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(Base 64 .encodeBase 64 (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.routes import org.scalatra.ScalatraBase import 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 .