- 持久性:我们使用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