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

我们有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标头 进行REST调用时,Content-Type标头是重要的标头。 服务器可以根据媒体类型对请求做出不同的响应,因此应将其包含在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 objectval comment = Json.parse(request.body.asInstanceOf[String]).as[Comment]// pass the comment object to a service for processingcommentService.storeComment(comment)println(Json.toJson(comment))Status(201)}}

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

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 manuallyAction(parse.tolerantText) {request =>{// get the header we're working withval sendHmac = request.headers.get(HMAC_HEADER);// Check whether we've recevied an hmac headersendHmac 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 hashval 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 thisval 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 signval toSign = input.map(a => {a match {case None => ""case a: Option[Any] => a.asInstanceOf[Option[Any]].getcase _ => a}}).mkString("\n")// use the input to calculate the hmacval calculatedHMAC = calculateHMAC(userInfo.secret, toSign)// if the supplied value and the received values are equal// return the response from the delegate action, else return// unauthorizedif (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 objectval comment = Json.parse(request.body.asInstanceOf[String]).as[Comment]// pass the comment object to a service for processingcommentService.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.JsArrayobject 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/373004.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安装卡主_智能温室四周玻璃的安装学问还这么多

智能玻璃温室大棚是指顶部及四周以玻璃为覆盖材料的尖顶温室大棚,玻璃温室大棚这几年的流行是由于纹络型温室顶部阳光板问题的抗老化方面容易出现问题。因此很多客户为了种植获得更高的透光率,更长的使用年限,因而多选择全玻璃温室大棚。那么…

String类详解(1)

首先String是一个类。  1,实例化String类方法。 1)直接赋值:String name"haha"; 2)通过关键字:String namenew String("haha"); 2,String类的数据比较。 首先回顾一下,基础数据的比较…

第六章 计算机网络与i教案,大学计算机基础教案第6章计算机网络基础与应用.docx...

广东第二师范学院计算机科学系教案课程名称计算机基础I课程代码111012003课程类型公必√□ 专必□ 专选□ 公选□授课方式讲授□ 实践□案例讨论□ 上机√□考核方式考试□√ 考查□上机□√ 论文□教学总学时数16学分数1学时分配课堂讲授 2 学时;实践课 14 学时教材…

分享性能优化问题

谈谈性能优化问题 代码层面:避免使用css表达式,避免使用高级选择器,通配选择器。 缓存利用:缓存Ajax,使用CDN,使用外部js和css文件以便缓存,添加Expires头,服务端配置Etag&#xff0…

使用Scala,Play和Akka连接到RabbitMQ(AMQP)

在本文中,我们将研究如何从Scala连接到RabbitMQ,以便可以从应用程序中支持AMQP协议。 在此示例中,我将使用Play Framework 2.0作为容器(有关更多信息,请参阅我在该主题上的其他文章 )在其中运行应用程序&am…

阿尔法贝塔阀原理_图总结 - 阿尔法个贝塔 - 博客园

一.思维导图二.概念笔记图的存储结构1. 邻接矩阵定义:设图G有n (n大于等于1) 个顶点,则邻接矩阵是一个n阶方阵。当矩阵中的 [i,j] !0(下标从1开始) ,代表其对应的第i个顶点与第j个顶点是连接的特点无向图的邻接矩阵是对称矩阵,n个顶点的无向图…

WebApi Post 后台无法获取参数的解决方案

事件回放: 之前一段时间,公司里前端用的Angularjs 发送http请求也是用的ng的组件,后台是.Net的WebApi 前端 var data {PArgs: {PageIndex: 0,PageSize: 8,RowsCount: 0} };$http.post("/Api/Test/ABC", data).success(function (d…

南京大学计算机系周小莉,周会群

媒体报道:南京大学周会群:用计算机聪明地做实验Q《中国教育网络》A周会群Q:南京大学的高性能计算中心非常特殊,分布在物理,化学、天文、地球科学四个不同的学科中,为什么采取这种模式?A&#xf…

不要怂,就是GAN (生成式对抗网络) (五):无约束条件的 GAN 代码与网络的 Graph...

GAN 这个领域发展太快,日新月异,各种 GAN 层出不穷,前几天看到一篇关于 Wasserstein GAN 的文章,讲的很好,在此把它分享出来一起学习:https://zhuanlan.zhihu.com/p/25071913。相比 Wasserstein GAN &#…

用于MyBatis CRUD操作的Spring MVC 3控制器

到目前为止,我们已经为域类“ User ”创建了CRUD数据库服务,并且还将MyBatis配置与Spring Configuration文件集成在一起。 接下来,我们将使用Spring MVC创建一个网页,以使用MyBatis CRUD服务对数据库执行操作。 使用MyBatis 3创建…

2pin接口耳机_拆解报告:雷柏首款真无线耳机XS200

-----我爱音频网拆解报告第185篇-----雷柏是一家历史悠久的鼠标和键盘厂商,截至目前,雷柏(rapoo)总共出了四款耳机,此前曾推出过三款蓝牙耳机, 分别是S500 蓝牙立体声麦克风耳机,S200 蓝牙立体声麦克风耳机&#xff0c…

html表单中阴影,html5中input表单加边框,阴影效果.doc

文档介绍:CSS:input:focus{border-color:#99;}获取焦点时改变颜色focus能同时改变宽度长度背景色…….form,p(margin-bottom:30px;margin-left:20px;).shadow,.one,.two,.three,.four,.five,.six( height:50px; width:280px; border:C;).shadow( -moz-box-shadow:C;…

带有GSON和抽象类的JSON

经过多年使用org.json库在Java中支持JSON数据交换格式后,我已切换到Google Gson 。 org.json是一个较低级的库,因此您必须创建JSONObject,JSONArray,JSONString等…并执行其他低级工作。 Gson简化了这项工作。 它提供了简单的toJs…

深入理解javascript原型和闭包(3)——prototype原型

转载,原文地址http://www.cnblogs.com/wangfupeng1988/p/3978131.html 既typeof之后的另一位老朋友! prototype也是我们的老朋友,即使不了解的人,也应该都听过它的大名。如果它还是您的新朋友,我估计您也是javascript的…

python 温度 符号_Python通过小实例入门学习---1.0(温度转换)

1.安装Python 3 下载地址: Welcome to Python.org​www.python.org 2.“温度转换”实例:摄氏度--->华氏度 / 华氏度--->摄氏度 TempConvert.py TempStr = input("请输入带有符号的温度值:") if TempStr[-1] in ["f","F"]:C = (eval(Tem…

mysql 修改root密码

1.找到配置文件my.ini ,然后将其打开,可以选择用记事本打开 C:\Program Files (x86)\MySQL\MySQL Server 5.0 2.打开后,搜索mysqld关键字,找到后,在mysqld下面添加skip-grant-tables,保存退出。 PS&#x…

联想计算机CDROM启动,联想电脑光驱启动问题?

1、开机按del键或f2进入bios设置(不同主板按键不一样,一般是DEL,也可能是F2,可以参考下主板说明),将计算机的启动模式调成从光盘启动。也就是从cdrom启动,根据主板的不同,bios设置有所差异(一般是&#xff…

没有J2EE容器的JNDI和JPA

我们希望通过尽可能简单的设置来测试一些JPA代码。 计划仅使用Java和Maven,不使用应用程序服务器或其他J2EE容器。 我们的JPA配置需要两件事才能成功运行: 数据库来存储数据, JNDI访问数据库。 这篇文章分为两个部分。 第一部分显示了如何…

string 大小写转换

STL的algorithm库确实给我们提供了这样的便利&#xff0c;使用模板函数transform可以轻松解决这个问题&#xff0c;开发人员只需要提供一个函数对象&#xff0c;例如将char转成大写的toupper函数或者小写的函数tolower函数。 transform原型&#xff1a; 1 #include <string&…

linux服务器上svn的log_如何在 Centos 8 / RHEL 8 上安装和配置 VNC 服务器 | Linux 中国...

在 Centos 8 和 RHEL 8 系统中&#xff0c;默认未安装 VNC 服务器&#xff0c;它需要手动安装。在本文中&#xff0c;我们将通过简单的分步指南&#xff0c;介绍如何在 Centos 8 / RHEL 8 上安装 VNC 服务器。-- Pradeep KumarVNC(虚拟网络计算Virtual Network Computing)服务器…