前言
设计一个安全的对外开放接口
接口安全问题
1、请求身份是否合法?
2、请求参数是否被篡改?
3、请求是否唯一?
AccessKey&SecretKey (开放平台)
请求身份
为开发者分配appid(开发者标识,确保唯一)和secret(用于接口加密,确保不易被穷举,生成算法不易被猜测)。
防止篡改
参数签名
1、生成请求时候的时间戳
数据是很容易被抓包的,但是经过如上的加密,加签处理,就算拿到数据也不能看到真实的数据;但是有不法者不关心真实的数据,而是直接拿到抓取的数据包进行恶意请求;这时候可以使用时间戳机制,在每次请求中加入当前的时间,服务器端会拿到当前时间和消息中的时间相减,看看是否在一个固定的时间范围内比如5分钟内;这样恶意请求的数据包是无法更改里面时间的,所以5分钟后就视为非法请求了;
2、拼接参数,带上时间戳
按照请求参数名的字母升序排列非空请求参数,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA;
例如:
appid=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8&home=world&name=hello&nonce=d567bcd5-6a04-4e3c-aa56-4774822a390e&secret=AsQa6fqKXkwafq4fr6laZXz7DC0PEBHcVk0×tamp=1655630951933&work=java
3、对stringA进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值。
3、发送请求时参数中需要携带sign,不需要携带secret;
例如
http://127.0.0.1:8080/hello?&work=java&appid=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8&name=hello&sign=bf5753e61597122754e920ee86223c80&nonce=1437d89f-0809-496f-81df-506111bb58af×tamp=1655631068339&home=world
案例
一、客户端 :
public static void main(String[] args) throws Exception{
Map<String, String> map = new HashMap<>();
map.put("appid", "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8");
map.put("secret", "AsQa6fqKXkwafq4fr6laZXz7DC0PEBHcVk0");
map.put("timestamp", System.currentTimeMillis() + "");
map.put("name", "hello");
map.put("home", "world");
map.put("work", "java");
map.put("nonce", UUID.randomUUID().toString());
// 按map的key字母排序
Map<String, String> sortMap = MapSort.sortMapByKey(map);
StringBuilder sb = new StringBuilder();
for (String key : sortMap.keySet()) {
sb.append(key).append("=").append(sortMap.get(key)).append("&");
}
// 计算签名
System.out.println("======================: " + sb.toString());
String sign = MD5Util.string2MD5(sb.toString());
map.put("sign", sign);
// 移除密钥
map.remove("secret");
// 生成URL
StringBuilder URL = new StringBuilder("http://127.0.0.1:8080/hello?");
for (String key : map.keySet()) {
URL.append("&");
URL.append(key).append("=").append(map.get(key));
}
String url = URL.toString();
System.out.println(url);
//Thread.sleep(15000);
HttpClientUtils.doGet(url);
}
二、服务端:
package com.test.controller;
import com.test.bean.Result;
import com.test.httpclient.HttpClientUtils;
import com.test.utils.MD5Util;
import com.test.utils.MapSort;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* @Auther: sai
* @Date: 2022/6/15 0015 21:21
* @ClassName: HelloController
* @Version: 1.0
* @Description:
*/
@Controller
public class HelloController {
private static final Map<String, String> map = new HashMap<String, String>() {
};
static {
map.put("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8", "AsQa6fqKXkwafq4fr6laZXz7DC0PEBHcVk0");
}
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public Result hello(HttpServletRequest request) {
HashMap<String, String> mapParam = new HashMap<>();
mapParam.put("appid", request.getParameter("appid"));
mapParam.put("timestamp", request.getParameter("timestamp"));
mapParam.put("name", request.getParameter("name"));
mapParam.put("home", request.getParameter("home"));
mapParam.put("work", request.getParameter("work"));
mapParam.put("nonce", request.getParameter("nonce"));
mapParam.put("sign", request.getParameter("sign"));
// 校验sign是否存在
if (StringUtils.isEmpty(request.getParameter("sign"))) {
return new Result("1", "sign参数不正确", "failed");
}
//查询数据库appi对应的secret
String secret = map.get(request.getParameter("appid"));
if (StringUtils.isEmpty(secret)) {
return new Result("0", "appid不存在", "OK...");
}
// 校验时间
long newTimeMillis = System.currentTimeMillis();
if (newTimeMillis - Long.parseLong(request.getParameter("timestamp")) > 15000) {
return new Result("1", "超时", "failed");
}
//计算sign
mapParam.remove("sign");
for (String key : mapParam.keySet()) {
System.out.println(key + " ================== " + mapParam.get(key));
}
Map<String, String> sortMap = MapSort.sortMapByKey(mapParam);
sortMap.put("secret", secret);
StringBuilder sb = new StringBuilder();
for (String key : sortMap.keySet()) {
sb.append(key).append("=").append(sortMap.get(key)).append("&");
}
// 计算签名
String newSign = MD5Util.string2MD5(sb.toString());
if (!newSign.equals(request.getParameter("sign"))) {
return new Result("1", "签名不正确", "failed");
}
return new Result("0", "hello", "OK...");
}
}
MD5工具类
package com.test.utils;
import java.security.MessageDigest;
/**
* @Auther: sai
* @Date: 2022/6/15 0015 22:35
* @ClassName: MD5Util
* @Version: 1.0
* @Description: 用MD5加密解密
*/
public class MD5Util {
/***
* MD5加码 生成32位md5码
*/
public static String string2MD5(String inStr){
MessageDigest md5 = null;
try{
md5 = MessageDigest.getInstance("MD5");
}catch (Exception e){
System.out.println(e.toString());
e.printStackTrace();
return "";
}
char[] charArray = inStr.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++){
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16)
hexValue.append("0");
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
/**
* 加密解密算法 执行一次加密,两次解密
*/
public static String convertMD5(String inStr){
char[] a = inStr.toCharArray();
for (int i = 0; i < a.length; i++){
a[i] = (char) (a[i] ^ 't');
}
String s = new String(a);
return s;
}
// 测试主函数
public static void main(String args[]) {
String s = new String("tangfuqiang");
System.out.println("原始:" + s);
System.out.println("MD5后:" + string2MD5(s)); // 20b75697d8bf931a6730662ae117c3bf
System.out.println("加密的:" + convertMD5(s));
System.out.println("解密的:" + convertMD5(convertMD5(s)));
}
}
HttpClientUtils工具类
package com.test.httpclient;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.*;
import java.util.logging.Logger;
/**
* @Auther: sai
* @Date: 2022/6/14 0014 22:24
* @ClassName: HttpClientUtils
* @Version: 1.0
* @Description:
*/
public class HttpClientUtils {
Logger log = Logger.getLogger("com.test.httpclient.HttpClientUtils");
public static void doGet(String url) {
try {
CloseableHttpClient client = HttpClients.createDefault();
// 发送get请求
HttpGet request = new HttpGet(url);
// 发送请求
HttpResponse httpResponse = client.execute(request);
// 验证请求是否成功
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 得到请求响应信息
String str = EntityUtils.toString(httpResponse.getEntity(), "utf-8");
// 返回json
System.out.println(str);
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>oop_Interface</artifactId>
<version>1.0-SNAPSHOT</version>
<name>oop_Interface</name>
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>