hmac hmac.new_使用HMAC(Play 2.0)保护REST服务

hmac hmac.new

我们有HTTPS,还需要什么?

当您谈论基于REST的API的安全性时,人们通常会指向HTTPS。 借助HTTPS,您可以使用每个人都熟悉的方法轻松保护您的服务免遭窥视。 但是,当您需要更高级别的安全性或HTTPS不可用时,您需要替代方法。 例如,您可能需要跟踪每个客户对API的使用,或者需要确切地知道谁在进行所有这些调用。 您可以将HTTPS与客户端身份验证一起使用,但这将需要设置完整的PKI基础结构以及一种安全的方式来标识您的客户并交换私钥。 与基于SOAP的WS-Security服务相比,我们没有可用于REST的标准。

解决此问题的一种常见方法(Microsoft,Amazon,Google和Yahoo采用这种方法)是通过基于客户端和服务之间的共享机密对消息进行签名。 请注意,使用这种方法,我们仅对数据进行签名,而不对数据进行加密。 在这种情况下,我们所讨论的签名通常称为基于哈希的消息认证代码(简称HMAC)。 使用HMAC,我们基于已交换的密钥为请求创建消息认证代码(MAC)。

在本文中,我将向您展示如何为基于Play 2.0的REST服务实现此算法。 如果您使用其他技术,则步骤将几乎相同。

HMAC方案

对于客户端,我将仅使用基于HTTPClient的简单应用程序。 要实现这一点,我们必须采取以下步骤:

  1. 首先,我们需要与外部客户端交换共享机密。 通常,这是由API提供程序使用电子邮件发送给客户端的,或者提供程序具有您可以在其中查找共享密钥的网站。 请注意,此机密仅在您和服务之间共享,每个客户端将具有不同的共享机密。 这不是像公用密钥那样共享的东西,
  2. 为了确保客户端和服务在相同的内容上计算签名,我们需要对要签名的请求进行规范化。 如果我们不这样做,则服务器可能会以与客户端不同的方式解释空格,并得出签名无效的结论。
  3. 基于此规范化消息,客户端使用共享机密创建HMAC值。
  4. 现在,客户端已准备好将请求发送到服务。 他将HMAC值添加到标头中,还将一些内容标识为用户。 例如,用户名或其他公共值。
  5. 当服务接收到请求时,它将从标头中提取用户名和HMAC值。
  6. 根据用户名,服务知道应该使用哪个共享密钥对消息进行签名。 例如,该服务将从某处的数据存储中检索此信息。
  7. 现在,服务以与客户端相同的方式对请求进行规范化,并为其自身计算HMAC值。
  8. 如果来自客户端的HMAC与从服务器计算出的HMAC相匹配,则您将知道消息的完整性得到保证,并且客户端就是他所说的身份。 如果提供了错误的用户名,或者使用了错误的机密来计算标题,则HMAC值将不匹配。

要实现HMAC,我们需要做什么? 在以下部分中,我们将研究以下主题。

  • 确定用于输入的字段。
  • 创建可以计算此HMAC的客户端代码并添加相应的标头
  • 创建基于Play 2.0的拦截器来检查HMAC标头

确定输入字段

我们要做的第一件事是确定HMAC计算的输入。 下表描述了我们将包括的元素:

领域 描述
HTTP方法 使用REST,我们执行的HTTP方法定义了服务器端的行为。 对特定URL的删除与对该URL的GET处理不同。
内容MD5 这个HTTP标头是标准的HTTP标头。 这是请求正文的MD5哈希。 如果我们将此标头包含在HMAC代码生成中,则会得到一个HMAC值,该值会随着请求正文的更改而更改。
Content-Type标头 Content-Type头是进行REST调用时的重要头。 服务器可以根据媒体类型对请求做出不同的响应,因此应将其包含在HMAC中。
日期标题 我们还包括创建请求以计算HMAC的日期。 在服务器端,我们可以确保日期在传输中没有更改。 除此之外,我们可以在服务器上添加消息过期功能。
路径 由于URI标识REST中的资源,因此调用的URL的路径部分也用于HMAC计算。

我们将包括的几乎是来自请求的以下信息:

PUT /example/resource/1
Content-Md5: uf+Fg2jkrCZgzDcznsdwLg==
Content-Type: text/plain; charset=UTF-8
Date: Tue, 26 Apr 2011 19:59:03 CEST

可用于创建HMAC签名的客户端代码

在下面,您可以看到客户端代码,我们将用它们调用受HMAC保护的服务。 这只是一个基于HTTPClient的快速客户端,我们可以使用它来测试我们的服务。

public class HMACClient {
 
 private final static String DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss z";
 private final static String HMAC_SHA1_ALGORITHM = "HmacSHA1";
 
 private final static String SECRET = "secretsecret";
 private final static String USERNAME = "jos";
 
 private static final Logger LOG = LoggerFactory.getLogger(HMACClient.class);
 
 public static void main(String[] args) throws HttpException, IOException, NoSuchAlgorithmException {
  HMACClient client = new HMACClient();
  client.makeHTTPCallUsingHMAC(USERNAME);
 }
 
 public void makeHTTPCallUsingHMAC(String username) throws HttpException, IOException, NoSuchAlgorithmException {
  String contentToEncode = "{\"comment\" : {\"message\":\"blaat\" , \"from\":\"blaat\" , \"commentFor\":123}}";
  String contentType = "application/vnd.geo.comment+json";
  //String contentType = "text/plain";
  String currentDate = new SimpleDateFormat(DATE_FORMAT).format(new Date());
 
  HttpPost post = new HttpPost("http://localhost:9000/resources/rest/geo/comment");
  StringEntity data = new StringEntity(contentToEncode,contentType,"UTF-8");
  post.setEntity(data);
 
  String verb = post.getMethod();
  String contentMd5 = calculateMD5(contentToEncode);
  String toSign = verb + "\n" + contentMd5 + "\n"
    + data.getContentType().getValue() + "\n" + currentDate + "\n"
    + post.getURI().getPath();
 
  String hmac = calculateHMAC(SECRET, toSign);
 
  post.addHeader("hmac", username + ":" + hmac);
  post.addHeader("Date", currentDate);
  post.addHeader("Content-Md5", contentMd5);
 
  HttpClient client = new DefaultHttpClient();
  HttpResponse response = client.execute(post);
 
  System.out.println("client response:" + response.getStatusLine().getStatusCode());
 }
 
 private String calculateHMAC(String secret, String data) {
  try {
   SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_SHA1_ALGORITHM);
   Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
   mac.init(signingKey);
   byte[] rawHmac = mac.doFinal(data.getBytes());
   String result = new String(Base64.encodeBase64(rawHmac));
   return result;
  } catch (GeneralSecurityException e) {
   LOG.warn("Unexpected error while creating hash: " + e.getMessage(), e);
   throw new IllegalArgumentException();
  }
 }
 
 private String calculateMD5(String contentToEncode) throws NoSuchAlgorithmException {
  MessageDigest digest = MessageDigest.getInstance("MD5");
  digest.update(contentToEncode.getBytes());
  String result = new String(Base64.encodeBase64(digest.digest()));
  return result;
 }
}

然后使用HMAC算法基于共享机密创建签名。

private String calculateHMAC(String secret, String data) {
  try {
   SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_SHA1_ALGORITHM);
   Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
   mac.init(signingKey);
   byte[] rawHmac = mac.doFinal(data.getBytes());
   String result = new String(Base64.encodeBase64(rawHmac));
   return result;
  } catch (GeneralSecurityException e) {
   LOG.warn("Unexpected error while creating hash: " + e.getMessage(), e);
   throw new IllegalArgumentException();
  }
 }

计算完HMAC值后,我们需要将其发送到服务器。 为此,我们提供了一个自定义标头:

post.addHeader("hmac", username + ":" + hmac);

如您所见,我们还添加了用户名。 服务器需要使用它来确定在服务器端使用哪个密钥来计算HMAC值。 现在,当我们运行此代码时,将执行一个简单的POST操作,将以下请求发送到服务器:

POST /resources/rest/geo/comment HTTP/1.1[\r][\n]
hmac: jos:+9tn0CLfxXFbzPmbYwq/KYuUSUI=[\r][\n]
Date: Mon, 26 Mar 2012 21:34:33 CEST[\r][\n]
Content-Md5: r52FDQv6V2GHN4neZBvXLQ==[\r][\n]
Content-Length: 69[\r][\n]
Content-Type: application/vnd.geo.comment+json; charset=UTF-8[\r][\n]
Host: localhost:9000[\r][\n]
Connection: Keep-Alive[\r][\n]
User-Agent: Apache-HttpClient/4.1.3 (java 1.5)[\r][\n]
[\r][\n]
{"comment" : {"message":"blaat" , "from":"blaat" , "commentFor":123}}

在Scala中实现/播放

到目前为止,我们已经看到客户需要做什么才能为我们提供正确的标题。 服务提供商通常会提供多种语言的特定库,用于处理消息签名的详细信息。 但是,正如您所看到的,手工操作并不困难。 现在,让我们看一下服务器端,在这里我们将scala与Play 2.0框架一起使用,以检查提供的标头是否包含正确的信息。 有关设置正确的Scala环境以测试此代码的更多信息,请参阅我以前在scala上的文章( http://www.smartjava.org/content/play-20-akka-rest-json-and-dependencies )。

首先要做的是设置正确的路由以支持此POST操作。 我们在conf / routes文件中执行此操作

POST /resources/rest/geo/comment   controllers.Application.addComment

这是基本的Play功能。 对/ resource / rest / geo / comment URL的所有POST调用都将传递到指定的控制器。 让我们看一下该操作的样子:

def addComment() = Authenticated {
    (user, request) => {
     // convert the supplied json to a comment object
     val comment = Json.parse(request.body.asInstanceOf[String]).as[Comment]
 
     // pass the comment object to a service for processing
     commentService.storeComment(comment)
     println(Json.toJson(comment))
        Status(201)
      }
  }

现在,它变得更加复杂了。 如您在上面的清单中所见,我们定义了一个addComment操作。 但是,与其像这样直接定义一个Action:

def processGetAllRequest() = Action {
    val result = service.processGetAllRequest;
    Ok(result).as("application/json");
  }

我们可以这样定义它:

def addComment() = Authenticated {
    (user, request) => {

我们在这里要做的是创建一个复合动作http://www.playframework.org/documentation/2.0/ScalaActionsComposition )。 因为Scala是一种功能语言,所以我们可以轻松地做到这一点。 您在此处看到的“已认证”引用只是对简单函数的简单引用,该函数以另一个函数作为参数。 在“已验证”功能中,我们将检查HMAC签名。 您可以将其读为使用批注,但现在无需任何特殊构造。 因此,我们的HMAC检查是什么样的。

import play.api.mvc.Action
import play.api.Logger
import play.api.mvc.RequestHeader
import play.api.mvc.Request
import play.api.mvc.AnyContent
import play.api.mvc.Result
import controllers.Application._
import java.security.MessageDigest
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import org.apache.commons.codec.binary.Base64
import play.api.mvc.RawBuffer
import play.api.mvc.Codec
 
/**
 * Obejct contains security actions that can be applied to a specific action called from
 * a controller.
 */
object SecurityActions {
 
  val HMAC_HEADER = "hmac"
  val CONTENT_TYPE_HEADER = "content-type"
  val DATE_HEADER = "Date"
 
  val MD5 = "MD5"
  val HMACSHA1 = "HmacSHA1"
 
  /**
   * Function authenticated is defined as a function that takes as parameter
   * a function. This function takes as argumens a user and a request. The authenticated
   * function itself, returns a result.
   *
   * This Authenticated function will extract information from the request and calculate
   * an HMAC value.
   *
   *
   */
  def Authenticated(f: (User, Request[Any]) => Result) = {
    // we parse this as tolerant text, since our content type
    // is application/vnd.geo.comment+json, which isn't picked
    // up by the default body parsers. Alternative would be
    // to parse the RawBuffer manually
    Action(parse.tolerantText) {
 
      request =>
        {
          // get the header we're working with
          val sendHmac = request.headers.get(HMAC_HEADER);
 
          // Check whether we've recevied an hmac header
          sendHmac match {
 
            // if we've got a value that looks like our header 
            case Some(x) if x.contains(":") && x.split(":").length == 2 => {
 
              // first part is username, second part is hash
              val headerParts = x.split(":");
              val userInfo = User.find(headerParts(0))
 
              // Retrieve all the headers we're going to use, we parse the complete 
              // content-type header, since our client also does this
              val input = List(
                request.method,
                calculateMD5(request.body),
                request.headers.get(CONTENT_TYPE_HEADER),
                request.headers.get(DATE_HEADER),
                request.path)
 
              // create the string that we'll have to sign
              val toSign = input.map(
                a => {
                  a match {
                    case None => ""
                    case a: Option[Any] => a.asInstanceOf[Option[Any]].get
                    case _ => a
                  }
                }).mkString("\n")
 
              // use the input to calculate the hmac
              val calculatedHMAC = calculateHMAC(userInfo.secret, toSign)
 
              // if the supplied value and the received values are equal
              // return the response from the delegate action, else return
              // unauthorized
              if (calculatedHMAC == headerParts(1)) {
                f(userinfo, request)
              } else {
                 Unauthorized
              }
            }
 
            // All the other possibilities return to 401 
            case _ => Unauthorized
          }
        }
    }
  }
 
  /**
   * Calculate the MD5 hash for the specified content
   */
  private def calculateMD5(content: String): String = {
    val digest = MessageDigest.getInstance(MD5)
    digest.update(content.getBytes())
    new String(Base64.encodeBase64(digest.digest()))
  }
 
  /**
   * Calculate the HMAC for the specified data and the supplied secret
   */
  private def calculateHMAC(secret: String, toEncode: String): String = {
    val signingKey = new SecretKeySpec(secret.getBytes(), HMACSHA1)
    val mac = Mac.getInstance(HMACSHA1)
    mac.init(signingKey)
    val rawHmac = mac.doFinal(toEncode.getBytes())
    new String(Base64.encodeBase64(rawHmac))
  }
}

那是很多代码,但是其中大多数将很容易理解。 “ calculateHMAC”和“ calculateMD5”方法只是围绕Java功能的基本scala包装器。 该类内的文档应该足以了解正在发生的事情。 但是,我确实想在这段代码中突出几个有趣的概念。 首先是方法签名:

def Authenticated(f: (User, Request[Any]) => Result) = {

这意味着Authenticated方法本身将另一个方法(或函数,如果要调用该方法)作为参数。 如果回头看我们的路线目标,您会发现我们只是这样做:

def addComment() = Authenticated {
    (user, request) => ...

现在,当调用此“已认证”方法时会发生什么? 我们要做的第一件事是检查HMAC标头是否存在并且格式正确:

val sendHmac = request.headers.get(HMAC_HEADER);
  sendHmac match {
 
            // if we've got a value that looks like our header 
            case Some(x) if x.contains(":") && x.split(":").length == 2 => {
            ...
            }
 
            // All the other possibilities return to 401 
            case _ => Unauthorized

我们通过对HMAC标头使用匹配来实现。 如果它包含正确格式的值,我们将以与客户相同的方式处理标头并计算HMAC值。 如果不是,则返回401。如果HMAC值正确,则使用以下代码将其委托给提供的函数:

if (calculatedHMAC == headerParts(1)) {
                f(userInfo, request)
              } else {
                 Unauthorized
              }

就是这样。 使用此代码,您可以轻松地使用HMAC来检查邮件在传输过程中是否已更改,以及您的客户是否真正为您所知。 如您所见,非常简单。 只是Play 2.0中有关JSON使用的一小部分便条。 如果您查看操作代码,可以看到我使用标准的JSON功能:

def addComment() = Authenticated {
    (user, request) => {
     // convert the supplied json to a comment object
     val comment = Json.parse(request.body.asInstanceOf[String]).as[Comment]
 
     // pass the comment object to a service for processing
     commentService.storeComment(comment)
     println(Json.toJson(comment))
        Status(201)
      }
  }

首先,我们使用'json.parse'将接收到的JSON解析为'comment'类,然后存储注释,并将命令对象转换回字符串值。 不是最有用的代码,但它很好地演示了Play 2.0提供的一些JSON功能。 为了从JSON转换为对象并再次返回,使用了一种称为“隐式转换”的方法。 我不会在细节上过多介绍,但是可以在这里找到很好的解释: http : //www.codecommit.com/blog/ruby/implicit-conversions-more-powerful-t… 。 这里发生的是JSON.parse和Json.toJson方法在Comment类上寻找特定的方法。 如果找不到它,它将在其范围内查找特定的操作。 要查看此方法如何用于JSON解析,让我们看一下Comment类及其配套对象:

import play.api.libs.json.Format
import play.api.libs.json.JsValue
import play.api.libs.json.JsObject
import play.api.libs.json.JsString
import play.api.libs.json.JsNumber
import play.api.libs.json.JsArray
 
object Comment {
 
  implicit object CommentFormat extends Format[Comment] {
 
    def reads(json: JsValue): Comment = {
      val root = (json \ "comment")
 
      Comment(
        (root \ "message").as[String],
        (root \ "from").as[String],
        (root \ "commentFor").as[Long])
 
    }
 
    def writes(comment: Comment): JsValue = {
      JsObject(List("comment" ->
        JsObject(Seq(
          "message" -> JsString(comment.message),
          "from" -> JsString(comment.message),
          "commentFor" -> JsNumber(comment.commentFor)))))
    }
  }
 
}
 
case class Comment(message: String, from: String, commentFor: Long) {}

您在此处看到的是,在伴随对象中,我们创建了一个新的“格式”对象。 现在,当使用“ Comment”类时,JSON操作将使用此对象中的“读取”和“写入”操作在JSON之间进行转换。 非常强大的功能,尽管有些神奇;-)有关在此示例中使用的Scala / Play环境的更多信息,请参见我以前的文章:
http://www.smartjava.org/content/play-20-akka-rest-json-and-dependencies
http://www.smartjava.org/content/using-querulous-scala-postgresql

参考:来自Smart Java博客的JCG合作伙伴 Jos Dirksen 使用HMAC(Play 2.0)保护REST服务


翻译自: https://www.javacodegeeks.com/2012/04/dzoneprotect-rest-service-using-hmac.html

hmac hmac.new

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值