Scala和Scalatra入门–第三部分

这篇文章是我在scalatra上撰写的一系列文章中的第三篇。 在“第一部分”中,我们创建了初始环境,在“第二部分”中,我们创建了REST API的第一部分,并添加了一些测试。 在scalatra教程的第三部分中,我们将研究以下主题:
  • 持久性:我们使用scalaquery来持久化模型中的元素。
  • 安全性:处理包含API密钥的安全标头。

首先,我们将讨论持久性部分。 对于这一部分,我们将使用scalaquery 。 请注意,我们在这里显示的代码与scalaquery的后继代码非常相似。 但是, Slick需要scala 2.10.0-M7,这意味着我们必须更改完整的scala设置。 因此,在此示例中,我们将仅使用scalaquery(其语法与slick相同)。 如果您尚未这样做, 请安装JRebel,这样您的更改将立即反映出来,而无需重新启动服务。

坚持不懈

在本示例中,我使用了postgresql,但是可以使用scalaquery支持的任何数据库。 我使用的数据库模型非常简单:

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中添加以下依赖项

'org.scalaquery' %% 'scalaquery' % '0.10.0-M1',
  'postgresql' % 'postgresql' % '9.1-901.jdbc4'

更新后,您将拥有所需的scalaquery和postgres jar。 让我们看一下其中一个存储库:bidrepository和RepositoryBase特性。

// 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的工作原理,我们就可以了。 使用scalaquery,您可以创建表映射。 在此映射中,您可以指定所需的字段类型。 在此示例中,我们的映射表如下所示:

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 *”是默认的构造函数,我们事先拥有所有字段,“ def noID”是我们首次创建出价时将使用的字段,但尚无ID。 请记住,ID由数据库自动生成。
通过此映射,我们可以开始编写存储库功能。 让我们从第一个开始:

/**
   * 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]。 请注意,这可以写得更简洁,但这很好地解释了您需要采取的步骤。

下一个功能是创建

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;
    }

现在,我们使用自定义的BidMapping'constructor'noID生成插入语句。 如果未指定noID,则必须已经指定一个ID。 现在,我们已经在数据库中插入了一个新的Bid对象,我们需要将刚创建的Bid和新的ID返回给用户。 为此,我们需要执行一个名为“ LASTVAL”的简单查询,该查询返回最后一个自动生成的值。 在我们的示例中,这是创建的出价的ID。 根据这些信息,我们创建一个新的出价,然后将其返回。

我们存储库的最后一个操作是删除功能。 此功能首先检查指定的出价是否存在,如果存在,则将其删除。

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保持持久性的最基本用途。 如果您需要更复杂的操作(例如联接),请查看scalaquery.org网站上的示例。

现在,我们具有创建和删除出价的功能。 因此,如果我们能够通过某种方式对用户进行身份验证,那也将非常不错。 在本教程中,我们将创建一个非常简单的基于API密钥的身份验证方案。 对于每个请求,用户都必须添加带有其API密钥的特定标头。 然后,我们可以使用该密钥中的信息来确定该用户是谁,以及该用户是否可以删除或访问特定信息。

安全

我们将从密钥生成部分开始。 当有人要使用我们的API时,我们要求他们指定一个应用程序名称和一个主机名,从该主机名开始发出请求。 我们将使用此信息来生成在每个请求中必须使用的密钥。 该密钥只是一个简单的HMAC哈希。

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()函数来做到这一点:

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);
  }
 
}

我们包含在我们的主要scalatra servlet中的此特征从请求中获取正确的信息,并检查所提供的哈希是否与您先前看到的代码生成的哈希相对应。 如果是这种情况,则传递请求,否则,我们将停止请求的处理并发送回401,以说明如何使用此API进行身份验证。

如果客户省略了这些标头,他将得到以下响应:

如果客户端发送正确的标头,他将得到以下响应:

这部分就是这样。 在下一部分中,我们将研究Depdency Injection,CQRS,Akka并在云中运行此代码。

参考: 教程:scala和scalatra入门–第三部分,来自我们的JCG合作伙伴 Jos Dirksen,来自Smart Java博客。


翻译自: https://www.javacodegeeks.com/2012/09/getting-started-with-scala-and-scalatra_24.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值