网站开发注意移动慧生活app下载
一、何为签名
我们知道无论是restful api还是传统接口、亦或是其他形式接口的调用,接口签名都是非常重要的安全机制,它可以确保请求的发起者是经过认证和授权的客户端,同时也可以防止接口被攻击,请求参数被篡改等等。
用大白话来解释就是,如果你写的接口没有签名验证,那么不管是谁都可以调用,只要路径、参数是对的就可以调用,这样就会非常不安全,因此我们会对接口添加签名验证。
首先,我们要了解原理,举个例子来讲,假设有一场线上的会议,而会议室呢并不是谁都可以进去的,光有会议室号是不行的还需要密码,如果更严格一些,可能还需要填写公共的信息内容等,那这些密码信息等,都是实现约定好的,提供给相关人就可以。
同理,签名也是双方约定好的,客户端(调用接口的用户)需要带着事先约定好的签名去调用,然后服务端(被调用)需要对这个签名进行验证,如果是他约定好的,那么就放行,让其直接调用即可。
二、签名设计
那么签名该如何设计呢?那就要说道签名的规则了。
签名一般会包括以下几部分:
- appid 和 app\secret
- timestamp 时间戳
- version 版本号
- signature 所有数据的签名信息。
接下来依次介绍他们的作用:
1) appid & appsecret
appid :应用的标识
appsecret:私匙(相当于密码)
通常我们会线下分配appid和appsecret,也就是提前声明好,针对不同的调用方分配不同的appid和appsecret。一般来说appid 和appsecret是一一对应的,用于对接口数据进行加密、解密。
2) timestamp(时间戳)用时间戳可以防止暴力请求
sign机制可以防止参数被篡改,但无法防ddos攻击(第三方使用正确的参数,不停的请求服务器,使之无法正常提供服务)。因此,还需要引入时间戳机制。
而服务端则需要根据当前时间和sign值的时间戳进行比较,差值超过一段时间则不予通过客户端的请求,直接给客户端响应某些错误提示等。
若要求不高,则客户端和服务端可以仅仅使用精确到秒或分钟的时间戳,据此形成sign值来校验有效性。这样可以使一秒或一分钟内的请求是有效的。
若要求较高,则还需要约定一个解密算法,使服务端可以从sign值中解析出发起请求的时间戳。
3) 版本号version
防止重复提交,至少为10位。针对查询接口,版本号只用于日志落地,便于后期日志核查。 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
4)signature
signature 的规则可以随便定义,按照自己的想法即可,我们以 md5(sort(body)+appSecret)举例,即 先对请求体的字段按照字母a-z顺序进行排序,然后在其尾部拼接appSecret,再经过md5计算出签名,然后用户在请求中添加签名即可。
正常会将appid、timestamp、nonce、signature这四个字段放入请求头中,但我们这里暂时定义规则为只要将body中的key按照大小顺序排好序以后,在拼接上appsecret即可。
三、代码演示
1、传统的http请求
package com.demo.test;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.jfinal.aop.Clear;
import com.jfinal.core.Controller;
import com.jfinal.kit.Prop;
import com.jfinal.kit.PropKit;
import oracle.jdbc.proxy.annotation.Post;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;import javax.servlet.ServletInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;@Clear
public class DemoController extends Controller {private static final long EXPIRE_TIME = 10 * 60 * 1000; // 10分钟的毫秒数public void save() throws IOException {// 检查请求方法是否为 POSTif (!"POST".equalsIgnoreCase(getRequest().getMethod())) {System.out.println("Request method"+ getRequest().getMethod());renderText("Only POST requests are allowed for this method.");return;}// 获取请求头信息String headerValue = getHeader("sign");System.out.println("Header Value: " + headerValue);// 获取请求参数String paramValue = getPara("paramName");System.out.println("Request Parameter Value: " + paramValue);// 获取请求 URLString requestUrl = getRequest().getRequestURL().toString();System.out.println("Request URL: " + requestUrl);// 获取请求体信息String requestBody = getRawRequestBody(getRequest().getInputStream());System.out.println("Request Body: " + requestBody);JSONObject pass = validateSign(headerValue,requestBody);if(pass.getString("success").equals(false)){logger.warn("签名认证失败,请求接口:{},请求IP:{},请求参数:{}"+requestUrl+ JSON.toJSONString(paramValue));
// String url =request.getRequestURI()+"?"+ getIpAddress(request)+ JSON.toJSONString(request.getParameterMap());
// CacheKit.put("eam", "url", url);renderJson(pass);}else{//成功逻辑renderJson(pass);}
// renderText("POST request processed");}private JSONObject validateSign(String requestSign,String requestBody) throws IOException {JSONObject reponseJson = new JSONObject();String msg;boolean flag;try {if (StringUtils.isEmpty(requestSign)) {logger.info("签名为空, signature=" + requestSign);msg = "签名为空";reponseJson.put("msg", msg);reponseJson.put("flag", false);return reponseJson;}//时间戳JSONObject jsonObject = JSONObject.parseObject(requestBody);Long now = System.currentTimeMillis();Long requestTimestamp = Long.parseLong( jsonObject.getString("timestamp"));if ((now - requestTimestamp) > EXPIRE_TIME) {logger.info("请求时间超过规定范围时间10分钟, signature=" + requestSign);msg = "请求时间超过规定范围时间10分钟";reponseJson.put("msg", msg);reponseJson.put("flag", false);return reponseJson;}//排序 requestBodyString body = sortJsonString(requestBody);//获取密钥,这里密钥为了方便,写在了配置文件中,可随意定义一些复杂内容如jgjgro4h3h5b5b5b9nnkx0fkeProp p = PropKit.use("config.properties");String secret = p.get("emsSecret");String ls = body + secret;String sign = DigestUtils.md5Hex(ls.getBytes());//混合密钥md5,加密后的sign,用它来和请求传过来的sign对比,如果一致则通过if(StringUtils.equals(sign, requestSign)){logger.info("请求时间超过规定范围时间10分钟, signature=" + requestSign);msg = "请求成功!";reponseJson.put("msg", msg);reponseJson.put("flag", true);return reponseJson;}else {msg = "请求失败!sign不匹配";reponseJson.put("msg", msg);reponseJson.put("flag", false);return reponseJson;}}catch (Exception e){logger.error("Error in execute EmaController. The wrong message is:"+ e.getMessage());}msg = "请求失败";reponseJson.put("msg", msg);reponseJson.put("flag", false);return reponseJson;}private static String sortJsonString(String requestBody) throws JsonProcessingException {// 将 JSON 字符串转换为 JsonNodeObjectMapper objectMapper = new ObjectMapper();JsonNode jsonNode = objectMapper.readTree(requestBody);// 使用 TreeMap 对属性进行排序TreeMap<String, JsonNode> sortedProperties = new TreeMap<>();Iterator<Map.Entry<String, JsonNode>> fields = jsonNode.fields();while (fields.hasNext()) {Map.Entry<String, JsonNode> entry = fields.next();sortedProperties.put(entry.getKey(), entry.getValue());}// 创建新的 JsonNode,按照字母a-z的顺序排列JsonNode sortedJsonNode = objectMapper.valueToTree(sortedProperties);// 将排序后的 JsonNode 转换回 JSON 字符串String sortedJsonString = objectMapper.writeValueAsString(sortedJsonNode);// 输出排序后的 JSON 字符串return sortedJsonString;}// 读取请求体的方法private String getRawRequestBody(ServletInputStream inputStream) {StringBuilder stringBuilder = new StringBuilder();BufferedReader bufferedReader = null;try {bufferedReader = new BufferedReader(new InputStreamReader(inputStream));char[] charBuffer = new char[128];int bytesRead;while ((bytesRead = bufferedReader.read(charBuffer)) != -1) {stringBuilder.append(charBuffer, 0, bytesRead);}} catch (IOException e) {e.printStackTrace();} finally {if (bufferedReader != null) {try {bufferedReader.close();} catch (IOException e) {e.printStackTrace();}}}return stringBuilder.toString();}
}
2、Restful风格
这里由于无法直接获取请求体,因此封装了个方法getRawRequestBody(), 如果是采用restful风格开发,可以直接通过@ReqequstBody Map<String String>params获取请求体,如:
@RestController
public class UserController {@PostMapping("/test")public void createUser(@RequestBody Map<String String> body) {// 在这里处理System.out.pring(body);}
}
获取请求参数如:
@RequestMapping(value = "/test", method = RequestMethod.POST)
@ResponseBody
public StringTestUrl(@RequestParam("username")String username, @RequestParam("pwd")String pwd) {String txt = username + pwd;return txt;
}
暂时先写到这里。。。未完