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 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操作。 但是,与其像这样直接定义一个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 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

hmac hmac.new

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

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

相关文章

java怎么打印writer,Java PrintWriter print()方法

Java PrintWriter print()方法java.io.PrintWriter.print(String s) 方法打印的字符串。如果参数为null,则输出字符串“null”。否则,将根据平台的默认字符编码将字符串的字符转换为字节,并以完全write(int)方法的方式写入这些字节。1 语法pu…

论赵泽彬多终端应⽤下的架构设计

导读: 1.应⽤特点及关键问题 2.解决思路及实现⽅案 3.实践效果及未来⽅向

导出EXCEL遇到问题

EXCEL设置的格式要与写入信息的格式要匹配,比如写入信息是字符串类型,而EXCEL单元格是DATE类型则会出错。 转载于:https://www.cnblogs.com/tianSky/p/5218120.html

python watchdog占用,python基于watchdog库全自动化监控目录文件

楔子有些时候我们需要对一个目录进行监控,检测其内部是否有文件的新增、删除、以及每个文件的内容是否发生变化,这个时候如果是你的话,你会选择怎么做呢?显然也是一个比较麻烦的工作,倒不是说难,主要是比较…

罐中研讨会:设置JBoss BRMS全日研讨会

是否在寻找一种简单的方法来宣传,展示或演示使用JBoss业务规则管理系统(BRMS)产品的入门难度? 别无所求,因为我们召集了这个研讨会,因此您可以围绕JBoss BRMS构建一个晚上,半天或全天的学习课程…

论AI在叮咚智能音箱中的演进和应用

导读:顶级科技公司都在抢夺智能语音交互的入口权。比如小米,阿里,亚马逊,搜狗等

Robotium双client測试框架

互联网的本质就是信息交换。移动互联网更是如此, 所以很多移动互联网的服务类应用中有着身份地位不同的两种用户(比如:交易中的买家和卖家, 教学中的老师和学生, 打车中的车主和乘客)。近期的工作是给公司的某个产品建立android客户端測试框架,使用的自己…

php 点击文本框弹出时间,点击Input框弹出日期选项

点击text文本框弹出日期选择器body{margin:0;padding:0;font:14px Verdana, Arial, sans-serif;line-height:200%;}#content{padding-left:50px;padding-right:50px;}#content h2{font-size:18px;color:#1E9300;padding-top:8px;margin-bottom:8px;}#content h3{margin-top:8px…

自适应设计与响应式设计

目前非常流行自适应设计与响应式设计,而且经常让人混淆,自适应设计不应与自适应布局混为一谈,它们是完全不一样的概念。在这先说明下这两者的异同:自从移动终端飞速发展以来,各种各样的机型突飞猛进,很多网…

SVN还原与SVN更新区别

最近工作有点忙,让领导帮我改了一下前端代码,后面又有新的需求,然后就让我SVN还原一下再更新,我就直接更新啦,顺便总结一下这两者的区别,怕影响项目进度,然后采用把项目删除,再从SVN…

php mysql怎么实现,使用php与mysql怎么实现一个无限级分类

使用php与mysql怎么实现一个无限级分类发布时间:2020-12-25 15:25:14来源:亿速云阅读:84作者:Leah本篇文章为大家展示了使用php与mysql怎么实现一个无限级分类,内容简明扼要并且容易理解,绝对能使你眼前一亮…

jsf tree组件_JSF和“立即”属性–命令组件

jsf tree组件JSF中的即时属性通常被误解。 如果您不相信我&#xff0c;请查看Stack Overflow 。 造成混淆的部分原因可能是由于输入&#xff08;即<h&#xff1a;inputText />&#xff09;和命令&#xff08;即<h&#xff1a;commandButton />&#xff09;组件都立…

笔记本电脑电池显示4%可用(已接通电源),经过清灰又莫名奇妙的可以续航啦,很奇怪!

公司的办公电脑&#xff0c;估计是转手租用好几年了&#xff0c;今天打开一看电池一直充电充不进去。刚开始一直充电显示笔记本电脑电池显示7%可用&#xff0c;等会断电打开笔记本电脑电池显示4%可用。我已经感觉电池坏了&#xff0c;然后我感觉电脑好久没有清灰了&#xff0c;…

call()和apply()方法(切换上下文)

call方法: 语法&#xff1a;call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 定义&#xff1a;调用一个对象的一个方法&#xff0c;以另一个对象替换当前对象。 apply方法&#xff1a; 语法&#xff1a;apply([thisObj[,argArray]]) 定义&#xff1a;应用某一对象的一个方法&…

python爬取ajax动态内容肯德基门店,Python爬虫如何爬取KFC地址

随着人们生活水平的提高&#xff0c;肯德基这类的快餐消费的十分火爆&#xff0c;成为小朋友和年轻人的最爱&#xff0c;我们到一个地方想要找到肯德基店面&#xff0c;会根据导航获取肯德基地址&#xff0c;那你知道我们也可以使用python爬虫获取KFC地址吗&#xff1f;我们可以…

使用Java 8 CompletableFuture和Rx-Java Observable

我想使用Java 8 CompletableFuture和Rx-Java Observable探索一个简单的分散聚集场景。 场景很简单–产生大约10个任务&#xff0c;每个任务返回一个字符串&#xff0c;最终将结果收集到一个列表中。 顺序的 其顺序版本如下&#xff1a; public void testSequentialScatterGa…

百度网盘限速解决方案

解决各种网盘工具的助手,解决百度网盘限速的解决办法。 KinhDown 官网:https://kinhdown.kinh.cc/ KinhDown 支持百度网盘 Windows、安卓,可以免登录解除部分百度网盘资源限速问题。 下载 雷鸟下载 雷鸟下载是一款基于多资源超线程技术的下载软件,雷鸟下载针对各类网盘做…

hdu 2028

PS:以前对long long型的数据就一直不怎么明白...弄了好久... long long a; scanf("%lld",&a); printf("%lld",a); 这样才行 代码:#include "stdio.h"void swap(long long *a,long long *b){ long long t; t*a; *a*b; *bt;}long long gcd(lon…

MyEclipes+JSP+SSH+MySQL实现一个文章发布系统

导读:随着计算机技术的迅速发展,网络正以一种前所未有的冲击力影响着人类的生产和生活。网络的快速发展,颠覆了传统的信息传播方式,冲破了传统的时间,空间的局限性,继而引发了人类阅读方式的变革。现如今,网络阅读已成为一种新的时尚,在这种趋势下,文章发布系统应运而…