• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

httpsign: RESTful API 签名认证

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:httpsign


开源软件地址:https://gitee.com/xixifeng.com/httpsign


开源软件介绍:

简介

欢迎使用 HTTP Sign.
本项目将解决HTTP通信中的如下问题:

  • 防止重放攻击
  • 防止中途篡改数据
  • 保证请求服务幂等

从而,尽可能地让HTTP通信接近安全.

使用

<dependency>    <groupId>org.fastquery</groupId>    <artifactId>httpsign</artifactId>    <version>1.0.5</version></dependency>

准备一个JAX-RS Resource Classes

@javax.ws.rs.Path("helloworld")public class HelloWorldResource {  @org.fastquery.httpsign.Authorization // 作用在方法上,那么该方法将进行签名认证  @javax.ws.rs.GET  @javax.ws.rs.Produces("text/plain")  public String getHello() {      return "hi";  }  }

编写作用于服务端的ContainerRequestFilter

@org.fastquery.httpsign.Authorizationpublic class AuthorizationContainerRequestFilter extends 		org.fastquery.httpsign.AuthAbstractContainerRequestFilter {	@Override	public String getAccessKeySecret(String accessKeyId) {		// 根据 accessKeyId 找出 accessKeySecret	}}

编写作用于客户端的ClientRequestFilter

public class AuthorizationClientRequestFilter extends 		org.fastquery.httpsign.AuthAbstractClientRequestFilter {	@Override	public String getAccessKeySecret(String accessKeyId) {		// 根据 accessKeyId 找出 accessKeySecret	}}

在Jersey环境里使用

服务端:

@ApplicationPath("userResorce")public class Application extends ResourceConfig {	public Application() throws IOException {		register(HelloWorldResource.class);		register(AuthorizationContainerRequestFilter.class);	}}

JAX-RS客户端:

javax.ws.rs.client.Client client = javax.ws.rs.client.ClientBuilder.newClient();client.register(AuthorizationClientRequestFilter.class);javax.ws.rs.client.WebTarget target = client.target("http://localhost:8080").path("userResorce/helloworld");// ... ...

在CXF+Spring环境里使用

服务端:

<jaxrs:server address="http://localhost...">	<jaxrs:serviceBeans>		<bean class="your package.HelloWorldResource" /> 	</jaxrs:serviceBeans>	<jaxrs:providers>		<bean class="org.fastquery.httpsign.sample.AuthorizationContainerRequestFilter" />	</jaxrs:providers></jaxrs:server>

客户端:

<jaxrs:client address="<your request address>" serviceClass="<your request service>">	<jaxrs:providers>		<bean class="org.fastquery.httpsign.sample.AuthorizationClientRequestFilter" />	</jaxrs:providers></jaxrs:client>

HTTP Sign 的设计

字面约定

字面格式含义
< >变量
[ ]可选项
{ }必选项
|互斥关系
标点符号本文一律采用英文标点符号

请求参数名,命名规则

  1. 首字母小写,如果参数名由多个单词组成,相连单词的首字母要大写(例: userInfo)
  2. 英文缩写词一律小写(例:vcd)
  3. 只能由 [A~Z]、[a~z]、[0~9] 以及字符"-"、"_"、"." 组成参数名
  4. 不能以数字开头
  5. 不允许出现中文及拼音命名

术语表

术语全称中文说明
RSRESTful Web ServicesWEB REST服务REST 架构风格的Web服务
SecurityGroupSecurity Group安全组安全组制定安全策略
GMTGreenwich Mean Time格林尼治标准时间指位于英国伦敦郊区的皇家格林尼治天文台的标准时间
URIPathUniform Resource Identifier Path统一资源标识符的路径用于标识某一互联网资源路径
RFCRequest For Comments一系列以编号排定的文件几乎所有的互联网标准都收录在RFC文件之中

相关名词解释

  1. 字典升序排列
    如同在字典中排列单词一样排序,按照字母表递增顺序排列,参与比较的两个单词,若它们的第一个字母相同,就比较第二个字母,依此类推.
    例如: "scheme , java , basic , sql , php" 做字典升序排列后的结果是 "basic , java , php , scheme , sql".

  2. 幂等性
    接口在设计上可以被完全相同的URL重复调用多次,而最终得到的结果是一致的.

使用限制

请求端的当前时间与服务器的当前时间之差的绝对值不能大于10分钟,否则拒绝处理. 也就是说,请求端的时间不能比服务器时间快10分钟或慢10分钟,否则,服务器不受理.

请求结构

  1. 服务地址
    接口按照功能划分成了不同的功能模块,每个模块使用不同的域名或上下文访问,具体域名或上下文请参考各个接口的文档.

  2. 通信协议
    所有接口均采用HTTPS通信.

  3. 请求方法
    支持 [GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS].

  4. 字符编码
    在无特别说明情况下,均使用UTF-8编码.

  5. API请求结构

    名称描述备注
    API入口API调用的RS服务的入口https://<domain>/path/hi
    公共header每个接口都包含的通用请求头详见 公共参数
    公共参数每个接口都包含的通用参数详见 公共参数

公共参数

公共请求头(Common Request Headers)

名称是否必选描述
Authorization用于验证请求合法性的认证信息
Accept默认:"application/json",表示发送端(客户端)希望从服务端接收到的数据类型
Content-LengthRFC2616中定义的HTTP请求内容长度(一般的http客户端工具都会自动带上这个请求头)
DateHTTP 1.1协议中规定的GMT时间,例如:Wed, 28 Mar 2018 09:09:19 GMT
Host访问Host值(一般的http客户端工具都会自动带上这个请求头)

公共请求参数(Common Http Request Parameters)

名称是否必选类型描述
nonceString随机数,长度范围[8,36]
accessKeyIdStringaccessKeyId(长度范围[8,36])和accessKeySecret(长度范围[6,36])从云端申请,accessKeyId 用来标识身份的,一个 accessKeyId 对应唯一的 accessKeySecret , 而 accessKeySecret 会用来生成签名 Signature
signatureMethodString签名算法,目前支持HMACSHA256和HMACSHA1.默认采用:HMACSHA1验证签名
tokenString临时证书所用的Token,需要结合临时密钥一起使用

服务端将从 QueryString 获得这些参数.

签名机制

用户在HTTP请求中增加Authorization的Header来包含签名(Signature)信息,表明这个消息已被签名,认证是否通过,服务端说了算.
Authorization的值如何得到,其计算规则如下:

Signature = base64(SignatureMethod(AccessKeySecret,            HttpMethod + "\n"            + Content-MD5 + "\n" //注意: 如果Content-MD5为""或null,后面就不能 + "\n" 了(去掉该行)            + Accept + "\n"             + Date + "\n"             + BuildCustomHeaders + "\n" //注意: 如果BuildCustomHeaders为""或null,后面就不能 + "\n" 了(去掉该行)            + URIPath + "\n"            + BuildRequestParameters))            Authorization = "Basic " + Signature  
  • 1.SignatureMethod
    可选算法,HMACSHA256和HMACSHA1.

  • 2.AccessKeySecret
    服务端颁发给用户的密钥,不能泄露,只允许用户知道.

  • 3.HttpMethod
    请求方法,可选值[GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS].

  • 4.Content-MD5
    表示请求主体(Request Body)数据的MD5值,对消息内容(不包括头部)计算MD5值获得128bit(比特位)数字,对该数字进行Base64编码而得到,如果没有Body该值为""(空字符串).
    注意: Content-MD5如果为""(空字符串),末尾的"\n"必须去掉.
    假设,body内容为"好好学习,天天向上",计算其Content-MD5,以Java代码作为示例:

     // 待计算的内容 String content = "好好学习,天天向上"; byte[] input = content.getBytes(java.nio.charset.Charset.forName("utf-8"));  // 1. 先计算出MD5加密的字节数组(16个字节) java.security.MessageDigest messageDigest = java.security.MessageDigest.getInstance("MD5"); messageDigest.update(input); byte[] md5Bytes =messageDigest.digest();  // 2. 再对这个字节数组进行Base64编码(而不是对长度为32的MD5字符串进行编码)。 // Java 8+ 中自带的Base64工具(java.util.Base64) String str = java.util.Base64.getEncoder().encodeToString(md5Bytes);  // 正确的值应该是 "BheE8OSZqgEXBcg6TjcrfQ=="  // 断言 assertThat(str, equalTo("BheE8OSZqgEXBcg6TjcrfQ=="));

    假设,给body的是一个文件,计算其Content-MD5:

     MessageDigest messageDigest = MessageDigest.getInstance("MD5"); try (InputStream data = new URL("https://gitee.com/uploads/36/788636_xixifeng.com.png").openStream()) { 	final byte[] buffer = new byte[1024]; 	int read = data.read(buffer, 0, 1024);  	while (read > -1) { 		messageDigest.update(buffer, 0, read); 		read = data.read(buffer, 0, 1024); 	} } catch (IOException e) { 	throw e; }  byte[] md5Bytes = messageDigest.digest(); String str = java.util.Base64.getEncoder().encodeToString(md5Bytes);  // 正确的值应该是 "5ErvegqUtShUeMfmowveow==" // 断言 assertThat(str, equalTo("5ErvegqUtShUeMfmowveow=="));
  • 5.Accept
    可选值: application/json 或 application/xml.

  • 6.Date
    表示此次请求的当前时间,必须为GMT时间,如"Wed, 28 Mar 2018 09:09:19 GMT".
    以Java代码作为示例,怎么获得GMT时间:

     // RFC 822 时间格式 String f = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; java.text.SimpleDateFormat rfc822DateFormat = new java.text.SimpleDateFormat(f, java.util.Locale.US); rfc822DateFormat.setTimeZone(new java.util.SimpleTimeZone(0, "GMT"));  // 将Date格式化成GMT时间格式的字符串 java.util.Date date = new java.util.Date(); String gmtStr = rfc822DateFormat.format(date);  // 将GMT时间格式的字符串解析成Date对象 java.util.Date d = rfc822DateFormat.parse(gmtStr);

    推荐使用 JAVA 8+ 的时间格式转换:

     // RFC 822 时间格式 String f = "EEE, dd MMM yyyy HH:mm:ss 'GMT'";  java.util.Locale l = java.util.Locale.US; java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern(f, l); java.time.LocalDateTime localDateTime = java.time.LocalDateTime.now(java.time.ZoneId.of("GMT")); // 将LocalDateTime格式化成GMT时间格式的字符串 String gmt = localDateTime.format(formatter);  // 将GMT时间格式的字符串解析成LocalDateTime对象 LocalDateTime ldt = LocalDateTime.parse(gmt,formatter);
  • 7.BuildCustomHeaders
    所有以X-Custom-做为前缀的HTTP Header被称为自定义请求头.
    BuildCustomHeaders构建规则如下:
    7.1 将所有以X-Custom-为前缀的HTTP请求头的名字转换成小写,例如将"X-Custom-Meta-Author: FastQuery"转换成"x-custom-meta-author: FastQuery".
    7.2 将上一步得到的所有HTTP请求头做字典升序排列.
    7.3 请求头名称与内容之间用":"号隔开,并且需要清空分割符":"左右的空白.例如需要将"x-custom-meta-author : FastQuery"处理成"x-custom-meta-author:FastQuery".
    7.4 每个完整的请求头(头名称:内容),它们之间用"\n"进行分隔,最后拼接成BuildCustomHeaders.
    7.5 BuildCustomHeaders 允许为""(空字符串).
    举例:若有一个请求头"X-CUSTOM-META-A:xx",那么,BuildCustomHeaders为"x-custom-meta-a:xx".
    若有2个请求头"X-CUSTOM-META-A:xx","X-CUSTOM-META-B:yy",那么,BuildCustomHeaders为"x-custom-meta-a:xx\nx-custom-meta-b:yy".

  • 8.URIPath
    URL端口与QueryString之间的地址,不含"?",在此称之为URIPath.举例:
    若有请求URL "https://<domain><默认80可以省略>/path/hi?action=myInfo",那么URIPath为"/path/hi".
    若有请求URL "https://<domain>:8080/path/hi?action=myInfo",那么URIPath为"/path/hi".
    若有请求URL "https://<domain>:8080/path/hi",那么URIPath为"/path/hi".
    若有请求URL "https://<domain>:8080/",那么URIPath为"/".
    若有请求URL "https://<domain>:8080?action=myInfo",那么URIPath为"".
    以Java代码为示例,获取URIPath:

     public class AuthorizationClientRequestFilter implements javax.ws.rs.client.ClientRequestFilter { 	@Override 	public void filter(javax.ws.rs.client.ClientRequestContext requestContext) { 		java.net.URI uri = requestContext.getUri(); 		String uriPath = uri.getPath(); 		LOG.debug("uriPath:{}",uriPath); 	} }
  • 9.BuildRequestParameters,构建规则如下:

    • 9.1. 对参数排序
      对所有请求参数按参数名做字典升序排列.
      实际上就是按照ASCII码从小至大排序,举例:

      字母ASCII码对应的10进制
      A65
      N78
      R82
      S83
      T84
      i105
      l108
      o111

    则,做字典升序排列后的顺序是:A N R S T i l o

    • 9.2. 对参数编码
      对做字典升序排列之后的请求参数的值进行URL编码(参数名称严格按照上文提及到的命名规范,因此不用编码,因为它的组成字符都是RFC3986中明确说明的不用编码的字符).遵循RFC3986规定,编码规则如下:

      • 9.2.1. 参数值用UTF-8字符集;

      • 9.2.2. 对于字符 A~Z、a~z、0~9 以及字符"-"、"_"、"."、"~"不编码;

      • 9.2.3. 对其它字节做RFC3986中规定的百分号编码(Percent-encoding),即一个"%"后面跟着两个表示该字节值的十六进制字母,字母一律采用大写形式.其格式:%XY,其中 XY 是字符对应 ASCII 码的 16 进制表示.
        比如:
        英文的空格" ",采用UTF-8字符集,对应的字节是:0X22, 因此其URL编码为%22;
        英文字符的"*",采用UTF-8字符集,对应的字节是:0X2A, 因此其URL编码为%2A.

      • 9.2.4. 对于扩展的 UTF-8 字符,编码成 %AB%CD 的格式;
        最初十进制[0,127],共128个代码是ASCII. 然而,大于127以上ASCII后面跟着第二个字节.这两个字节一起定义一个字符.
        举例:

        字符采用UTF-8字符集对应的字节
        α0XCEB1
        β0XCEB2
        γ0XCEB3

        那么,将可以算出URL

        字符URL代码
        α%CE%B1
        β%CE%B2
        γ%CE%B3
      • 9.2.5. 使用编码工具应该注意的事项
        该编码方式和一般采用的 application/x-www-form-urlencoded MIME 格式编码算法相似,但又有所不同.
        比如 Java 标准库中的 java.net.URLEncoder 实现了application/x-www-form-urlencoded MIME 格式编码, 就拿它来做比喻.
        URLEncoder.encode("~", "utf-8") 输出的结果是 %7E, RFC3986规定中不对~进行编码.
        URLEncoder.encode("*", "utf-8") 输出的结果是 *, RFC3986规定,没有说不对*这个符号进行编码.
        URLEncoder.encode(" ", "utf-8") 输出的结果是 +, RFC3986规定,编码结果采用%XY格式(XY: 16进制字面).
        目前发现这些差异性
        因此,使用JAVA的URLEncoder进行URL编码,不能满足我们所约定的编码规范,需要对它的处理结果稍作该进.
        将URLEncoder.encode处理的结果的+ 替换成%20,* 替换为 %2A %7E 替换回~.

         private static String specialUrlEncode(String value) throws UnsupportedEncodingException { 	return URLEncoder.encode(<待编码字符串>, "utf-8").replace("+", "%20").replace("*", "%2A") 	.replace("%7E", "~"); }
    • 9.3. 拼接参数
      按字典升序排列后,参数值经过上个步骤编好码后, 参数名和参数值用=连接,参数与参数之间用&连接. 截至这里,BuildRequestParameters构建完成.

    • 9.4 举例:
      假设有6个参数:

       {     "nonce" : "1aabcde-5268-3326-c845-56kljgwexe",     "action" : "myInfo",     "offset" : 1,     "secretKeyId" : "BKJGW40598092JXMWNRF",     "limit" : 15 }

      步骤1: 对参数做字典升序排列

       {     "action" : "myInfo",     "limit" : 15,     "nonce" : "1aabcde-5268-3326-c845-56kljgwexe",     "offset" : 1,     "secretKeyId" : "BKJGW40598092JXMWNRF" }

      步骤2: 遵循RFC3986对请求参数的值进行URL编码

      步骤3: 拼接参数
      action=myInfo&limit=15&nonce=1aabcde-5268-3326-c845-56kljgwexe&offset=1&secretKeyId=BKJGW40598092JXMWNRF 这就是BuildRequestParameters.

,根据如下假设,计算出Authorization.
设, AccessKeySecret 为: "KYA8A4-74E17B58B093";
设, 签名算法为:"HMACSHA1";
设, URIPath为:"/httpsign/userResorce/greet"
设,请求方法(Request Method)为: POST;
设,请求头为:

请求头名称
Authorization待计算
Accept"application/json"
Date"Wed, 11 Apr 2018 06:03:43 GMT"
X-Custom-Meta-Author"FastQuery.HttpSign"
X-Custom-Meta-Description"HTTP authentication techniques."
X-Custom-Meta-Range"52363"

设,请求参数(Request Parameters)为:

参数名称
accessKeyId"AP084671DF-5F8C-41D2"
typeId7
nonce"e6e03b6f-7de2-4d02-8e04-3ccbad143389"

设,请求Body为:"蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也".

:
此解,意在阐述计算Authorization的过程,为了便于读者阅读,故,代码紧凑一看到底.

// 密钥String accessKeySecret = "KYA8A4-74E17B58B093";String uriPath = "/httpsign/userResorce/greet";String httpMethod = "POST";String accept = "application/json";String date = "Wed, 11 Apr 2018 06:03:43 GMT";// 构建请求头java.util.TreeMap<String, String> headerTreeMap = new java.util.TreeMap<>();headerTreeMap.put("X-Custom-Content-Range", "52363");headerTreeMap.put("X-Custom-Meta-Author", "FastQuery.HttpSign");headerTreeMap.put("X-Custom-Meta-Description", "HTTP authentication techniques.");StringBuilder headersBuilder = new StringBuilder();headerTreeMap.forEach((k, v) -> headersBuilder.append(k.toLowerCase()).append(':').append(v).append('\n'));String headersStr = headersBuilder.toString();// 构建请求参数java.util.TreeMap<String, String> queryStringTreeMap = new java.util.TreeMap<>();queryStringTreeMap.put("accessKeyId", "AP084671DF-5F8C-41D2");queryStringTreeMap.put("typeId", "7");queryStringTreeMap.put("nonce", "e6e03b6f-7de2-4d02-8e04-3ccbad143389");StringBuilder requestParametersBuilder = new StringBuilder();queryStringTreeMap.forEach((k, v) -> {	try {		requestParametersBuilder.append('&').append(k).append('=')				.append(java.net.URLEncoder.encode(v, "utf-8").replace("+", "%20")				.replace("*", "%2A").replace("%7E", "~"));	} catch (java.io.UnsupportedEncodingException e) {		throw new RuntimeException("URL编码出错", e);	}});String requestParameters = requestParametersBuilder.substring(1);// 计算Content-MD5的值String requestBody = "蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也";byte[] input = requestBody.getBytes(java.nio.charset.Charset.forName("utf-8"));java.security.MessageDigest messageDigest = java.security.MessageDigest.getInstance("MD5");messageDigest.update(input);byte[] md5Bytes = messageDigest.digest();String contentMD5 = java.util.Base64.getEncoder().encodeToString(md5Bytes);// 构建 stringToSignStringBuilder sb = new StringBuilder();sb.append(httpMethod).append('\n');sb.append(contentMD5).append('\n');sb.append(accept).append('\n');sb.append(date).append('\n');sb.append(headersStr);sb.append(uriPath).append('\n');sb.append(requestParameters);String stringToSign = sb.toString();// 计算出signaturejavax.crypto.Mac mac = javax.crypto.Mac.getInstance("HMACSHA1") 

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
go-admin: go 后端api,包含gin+gorm+jwt+rbac。发布时间:2022-02-14
下一篇:
RestfulTool: 一套 Restful 服务开发辅助工具集发布时间:2022-02-14
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap