我们有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

使用HMAC(Play 2.0)保护REST服务相关推荐

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

    hmac hmac.new 我们有HTTPS,还需要什么? 当您谈论基于REST的API的安全性时,人们通常会指向HTTPS. 借助HTTPS,您可以使用每个人都熟悉的方法轻松保护您的服务免遭窥视. ...

  2. 使用HTTPS和OAuth 2.0保护服务到服务的Spring微服务

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 如果您使 ...

  3. 使用Spring Security和OAuth 2.0保护Spring微服务架构

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 每个开发 ...

  4. 《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统 文章目录 <深入理解 Spring Cl ...

  5. 使用Spring Security Oauth2 和 JWT保护微服务--Uaa授权服务器的编写

    学习自深入理解微服务 采用Spring Security OAuth2 和 JWT的方式,Uaa服务只需要验证一次,返回JWT.返回的JWT包含了用户的所有信息,包括权限信息 从三个方面讲解: JWT ...

  6. WSE3.0构建Web服务安全(3):WSE3.0策略配置、证书、签名、与实例开发

    继WSE3.0构建Web服务安全(1):WSE3.0安全机制与实例开发和WSE3.0构建Web服务安全(2):非对称加密.公钥.密钥.证书.签名的区别和联系以及X.509 证书的获得和管理之后,今天我 ...

  7. 基于 Spring Security OAuth2和 JWT 构建保护微服务系统

    我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的.尤其是前后端完全分离之后,我们的用户信息不一 ...

  8. 《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十七章 使用 Spring Cloud OAuth2 保护微服务系统 文章目录 <深入理解 Spring Cloud 与微服务构 ...

  9. 安全狗等级保护建设服务内容有哪些?

    针对国家出台的等级保护2.0,安全根据此法规进行了全面的梳理和研究,同时根据自身完善的产品体系和专业的安全服务优势,能够达到帮助需求用户对完成等级保护建设服务内容,那么这些服务内容有哪些呢,接下来一起 ...

最新文章

  1. android高德地图自定义带数字marker图标,自定义图标-点标记-示例中心-JS API 示例 | 高德地图API...
  2. 在java面向对象编程中_谈一谈你对封装_继承_多态概念的理解_Java新职篇:面向对象编程的3个原则是什么?...
  3. idea 快捷键获取类的全限定类名(包名+类名)
  4. Spring框架相关问题
  5. 基于python的图片修复程序-可用于水印去除
  6. Understand Google file system paper
  7. 添加删除桌面用户ubuntu10.10
  8. 联想集团委任杨澜为独立董事:任期三年年薪235万元
  9. 做问答系统是对题目修改的bug
  10. SQL基础面试题(五)
  11. linux 加速播放软件,VirtualBox 6.1首次推出Linux 5.4支持,加速的视频播放及更多功能...
  12. Scala安装和开发环境配置教程
  13. idea报错500——问题分析解决1
  14. iphone如何显示实时网速和内存空间
  15. java简单实现布谷鸟过滤器的
  16. B站这个视频我是跪着看完的
  17. 苹果审核被拒1.1和5.0
  18. xen服务器池虚拟机自启,给在XenCenter管理的XenServer服务器上创建的虚拟机共享(上传)宿主机的镜像文件...
  19. 用户协议html代码,微信小程序同意用户协议确认投稿页面设计制作开发教程
  20. Win10更新之后彻底卸载Windows Defender和图标的方法

热门文章

  1. thinking-in-java(11) 持有对象
  2. 关于 tomcat启动后无法访问的问题(localhost:8080 (or your port specified))
  3. mybatis_user_guide(7) SQL语句构建器类
  4. 动态资源Servlet接口
  5. junit 测试执行顺序_JUnit 5中的测试执行顺序
  6. 无服务器–仅仅是构建现代应用程序的一种方法?
  7. stringbuffer_StringBuffer的存在的含义
  8. activiti 多租户_Activiti中具有独立数据库架构的多租户
  9. 如何在AWS EC2实例上部署Spring Boot应用程序
  10. 使用Spring WebFlux从Corda节点流式传输数据