# httpsign **Repository Path**: fastquery2016/httpsign ## Basic Information - **Project Name**: httpsign - **Description**: RESTful API 签名认证 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 172 - **Forks**: 62 - **Created**: 2018-04-23 - **Last Updated**: 2024-08-26 ## Categories & Tags **Categories**: restful **Tags**: None ## README ## 简介 欢迎使用 HTTP Sign. 本项目将解决HTTP通信中的如下问题: - 防止重放攻击 - 防止中途篡改数据 - 保证请求服务幂等 从而,尽可能地让HTTP通信接近安全. ## 使用 ```xml org.fastquery httpsign 1.0.5 ``` ### 准备一个JAX-RS Resource Classes ```java @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` ```java @org.fastquery.httpsign.Authorization public class AuthorizationContainerRequestFilter extends org.fastquery.httpsign.AuthAbstractContainerRequestFilter { @Override public String getAccessKeySecret(String accessKeyId) { // 根据 accessKeyId 找出 accessKeySecret } } ``` ### 编写作用于客户端的`ClientRequestFilter` ```java public class AuthorizationClientRequestFilter extends org.fastquery.httpsign.AuthAbstractClientRequestFilter { @Override public String getAccessKeySecret(String accessKeyId) { // 根据 accessKeyId 找出 accessKeySecret } } ``` ### 在Jersey环境里使用 服务端: ```java @ApplicationPath("userResorce") public class Application extends ResourceConfig { public Application() throws IOException { register(HelloWorldResource.class); register(AuthorizationContainerRequestFilter.class); } } ``` JAX-RS客户端: ```java 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环境里使用 服务端: ```xml ``` 客户端: ```xml ``` ## HTTP Sign 的设计 ### 字面约定 |字面格式|含义| |:-----|:-----| |< >|变量| |[ ]|可选项| |{ }|必选项| |||互斥关系| |标点符号|本文一律采用英文标点符号| ### 请求参数名,命名规则 1. 首字母小写,如果参数名由多个单词组成,相连单词的首字母要大写(例: userInfo) 2. 英文缩写词一律小写(例:vcd) 3. 只能由 [A\~Z]、[a\~z]、[0\~9] 以及字符"-"、"_"、"." 组成参数名 4. 不能以数字开头 5. 不允许出现中文及拼音命名 ### 术语表 | 术语 | 全称 | 中文 | 说明 | |:-----|:-----|:-----|:-----| |`RS`|RESTful Web Services|WEB REST服务|REST 架构风格的Web服务| |`SecurityGroup`|Security Group|安全组|安全组制定安全策略| |`GMT`|Greenwich Mean Time|格林尼治标准时间|指位于英国伦敦郊区的皇家格林尼治天文台的标准时间| |`URIPath`|Uniform Resource Identifier Path|统一资源标识符的路径|用于标识某一互联网资源路径| |`RFC`|Request 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:///path/hi`| |公共header|每个接口都包含的通用请求头|详见 [公共参数](#公共请求头common-request-headers)| |公共参数|每个接口都包含的通用参数|详见 [公共参数](#公共请求头common-request-headers)| ## 公共参数 ### 公共请求头(Common Request Headers) |名称|是否必选|描述| |:-----|:-----:|:-----| |Authorization|是|用于验证请求合法性的认证信息| |Accept|是|默认:"application/json",表示发送端(客户端)希望从服务端接收到的数据类型| |Content-Length|是|[RFC2616](https://tools.ietf.org/html/rfc2616)中定义的HTTP请求内容长度(一般的http客户端工具都会自动带上这个请求头)| |Date|是|HTTP 1.1协议中规定的GMT时间,例如:Wed, 28 Mar 2018 09:09:19 GMT| |Host|是|访问Host值(一般的http客户端工具都会自动带上这个请求头)| ### 公共请求参数(Common Http Request Parameters) |名称|是否必选|类型|描述| |:-----|:-----:|:-----:|:-----| |`nonce`|是|`String`|随机数,长度范围\[8,36\]| |`accessKeyId`|是|`String`|accessKeyId(长度范围\[8,36\])和accessKeySecret(长度范围\[6,36\])从云端申请,accessKeyId 用来标识身份的,一个 accessKeyId 对应唯一的 accessKeySecret , 而 accessKeySecret 会用来生成签名 Signature| |`signatureMethod`|否|`String`|签名算法,目前支持HMACSHA256和HMACSHA1.默认采用:HMACSHA1验证签名| |`token`|否|`String`|临时证书所用的Token,需要结合临时密钥一起使用| 服务端将从 QueryString 获得这些参数. ## 签名机制 用户在HTTP请求中增加`Authorization`的Header来包含签名(Signature)信息,表明这个消息已被签名,认证是否通过,服务端说了算. Authorization的值如何得到,其计算规则如下: ```java 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代码作为示例: ```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: ```java 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时间: ```java // 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+ 的时间格式转换: ```java // 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://<默认80可以省略>/path/hi?action=myInfo`",那么URIPath为"/path/hi". 若有请求URL "`https://:8080/path/hi?action=myInfo`",那么URIPath为"/path/hi". 若有请求URL "`https://:8080/path/hi`",那么URIPath为"/path/hi". 若有请求URL "`https://:8080/`",那么URIPath为"/". 若有请求URL "`https://:8080?action=myInfo`",那么URIPath为"". 以Java代码为示例,获取URIPath: ```java 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进制| |:-----:|:-----:| |A|65| |N|78| |R|82| |S|83| |T|84| |i|105| |l|108| |o|111| 则,做字典升序排列后的顺序是:A N R S T i l o - 9.2. 对参数编码 对做字典升序排列之后的请求参数的值进行URL编码(参数名称严格按照上文提及到的命名规范,因此不用编码,因为它的组成字符都是[RFC3986](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)中明确说明的不用编码的字符).遵循[RFC3986](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)规定,编码规则如下: - 9.2.1. 参数值用UTF-8字符集; - 9.2.2. 对于字符 A\~Z、a\~z、0\~9 以及字符"-"、"_"、"."、"\~"不编码; - 9.2.3. 对其它字节做[RFC3986](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)中规定的百分号编码(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](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)规定中不对`~`进行编码. `URLEncoder.encode("*", "utf-8")` 输出的结果是 `*`, [RFC3986](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)规定,没有说不对`*`这个符号进行编码. `URLEncoder.encode(" ", "utf-8")` 输出的结果是 `+`, [RFC3986](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)规定,编码结果采用%XY格式(XY: 16进制字面). 目前发现这些差异性 因此,使用JAVA的URLEncoder进行URL编码,不能满足我们所约定的编码规范,需要对它的处理结果稍作该进. 将URLEncoder.encode处理的结果的`+` 替换成`%20`,`*` 替换为 `%2A` `%7E` 替换回`~`. ```java 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个参数: ```js { "nonce" : "1aabcde-5268-3326-c845-56kljgwexe", "action" : "myInfo", "offset" : 1, "secretKeyId" : "BKJGW40598092JXMWNRF", "limit" : 15 } ``` 步骤1: 对参数做字典升序排列 ```js { "action" : "myInfo", "limit" : 15, "nonce" : "1aabcde-5268-3326-c845-56kljgwexe", "offset" : 1, "secretKeyId" : "BKJGW40598092JXMWNRF" } ``` 步骤2: 遵循[RFC3986](https://tools.ietf.org/html/rfc3986?spm=a2c4g.11186623.2.6.qtLqZF)对请求参数的值进行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"| |typeId|7| |nonce|"e6e03b6f-7de2-4d02-8e04-3ccbad143389"| 设,请求Body为:"**蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也**". **解**: 此解,意在阐述计算Authorization的过程,为了便于读者阅读,故,代码紧凑一看到底. ```java // 密钥 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 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 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); // 构建 stringToSign StringBuilder 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(); // 计算出signature javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HMACSHA1"); mac.init(new javax.crypto.spec.SecretKeySpec(accessKeySecret.getBytes( java.nio.charset.Charset.forName("utf-8")), "HMACSHA1")); byte[] signData = mac.doFinal(stringToSign.getBytes(java.nio.charset.Charset.forName("utf-8"))); String signature = java.util.Base64.getEncoder().encodeToString(signData); // 得出authorization String authorization = "Basic " + signature; // 断言:authorization等于"Basic 3qo3tKAYM16Pr88Lpr5WPj2VJco=" org.junit.Assert.assertThat(authorization, org.hamcrest.Matchers.equalTo("Basic 3qo3tKAYM16Pr88Lpr5WPj2VJco=")); ``` 截至这里, 解毕. ## 返回结果 ### 正确返回结果 若 API 调用成功,错误码`code`为0,并且会返回结果数据. 示例如下: ```js { code:0, data:<结果数据> } ``` ### 错误返回结果 若 API 调用失败,错误码`code`不为 0,`message`字段会显示详细错误信息(成功返回没有该字段). 示例如下: ```js { "code": 40001, "message": "传递的请求头Authorization不符合规范." } ``` #### 标准公共错误码 根据[RFC 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)定义,将如下状态码定义(Status Code Definitions)作为公共错误码: |错误码|描述| |:-----:|:-----| |400|Bad Request| |401|Unauthorized| |402|Payment Required| |403|Forbidden| |404|Not Found| |405|Method Not Allowed| |406|Not Acceptable| |407|Proxy Authentication Required| |408|Request Timeout| |409|Conflict| |410|Gone| |411|Length Required| |412|Precondition Failed| |413|Request Entity Too Large| |414|Request-URI Too Long| |415|Unsupported Media Type| |416|Requested Range Not Satisfiable| |417|Expectation Failed| |428|Precondition Required| |429|Too Many Requests| |431|Request Header Fields Too Large| |500|Internal Server Error| |501|Not Implemented| |502|Bad Gateway| |503|Service Unavailable| |504|Gateway Timeout| |505|HTTP Version Not Supported| |511|Network Authentication Required| #### 自定义公共错误码 自定义错误码由5位数字组成(除0表示成功外),前3位数表示对应的HTTP状态码(HTTP Status Code).目前自定义的错误前缀如下: - 400XX 请求错误 - 403XX 被禁止 - 404XX 找不到 - 500XX 内部错误 - 503XX 服务不可用 |错误码|描述| |:-----:|:-----| |40000|没有传递请求头Authorization.| |40001|传递的请求头Authorization不符合规范.| |40002|传递的请求头Accept不符合要求,要么是"application/json" 要么是 "application/xml".| |40003|请求头Date必须传递,并且必须是HTTP 1.1协议中规定的GMT时间.| |40004|请求端的时间不能比服务器时间快10分钟或慢10分钟.| |40008|名称为nonce的请求参数没有传递.| |40009|nonce的长度不能超过36且不能小与8.| |40010|名称为accessKeyId的请求参数没有传递.| |40011|根据accessKeyId没有找到对应的accessKeySecret.| |40012|签名算法要么传递HMACSHA1或HMACSHA256,要不传递(默认:HMACSHA1).| |40013|传递的token错误.| |40014|token认证失败.| |40015|有请求body,而没有传递请求头Content-MD5.| |40016|计算请求body的MD5出错.| |40017|计算Authorization出错.| |40018|传过来的Authorization是错的.| |40300|在10分钟内不能传递相同的随机码.| |50300|服务不可用.| 版权归[习习风](https://gitee.com/xixifeng.com)所有,请认准开源地址: https://gitee.com/xixifeng.com/httpsign https://github.com/xixifeng/httpsign 以获得最近更新. ## 推荐开源项目 - [https://gitee.com/xixifeng.com/fastquery](https://gitee.com/xixifeng.com/fastquery) 基于ASM的DB操作框架 - [https://gitee.com/xixifeng.com/pjaxpage](https://gitee.com/xixifeng.com/pjaxpage) 支持PJAX的分页解决方案 - [https://gitee.com/xixifeng.com/httpsign](https://gitee.com/xixifeng.com/httpsign) RESTful API 签名认证 ## 反馈 https://gitee.com/xixifeng.com/httpsign/issues 秉承自由、开放、分享的精神,本项目每次升级之后,代码和文档手册都会在第一时间完全开源,以供大家查阅、批评、指正.笔者技术水平有限,bug或不周之处在所难免,所以,遇到有问题或更好的建议时,还请大家通过[issue](https://gitee.com/xixifeng.com/httpsign/issues)来向我们反馈. ## 捐助 Httpsign 采用 Apache 许可的开源项目, 使用完全自由, 免费. 如果 httpsign 对你有帮助, 可以用捐助来表示谢意.