Статьи

Начало работы со Scala и Scalatra — Часть III

Этот пост является третьим в серии статей, которые я пишу о скалатре. В «части I» мы создали начальную среду, а в «части II» мы создали первую часть REST API и добавили несколько тестов. В этой третьей части руководства по скалатре мы рассмотрим следующие темы:

  • Постоянство: мы используем 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 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.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.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 .