互联网上的大平台都会对外提供api,但这些api不能不通过任何验证就能直接访问,这样风险会非常高,也是不合理的,比如微信公众号,七牛云,阿里巴巴相关应用的接入等等,我们接触最多的客户端的实现,平台端很少有人知道是怎么做到的,下面我们一起学习了解一下。
AppUtils.java
package com.xxx.common.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.UUID;
/**
*
@Title: AppUtils
@Description: 随机产生唯一的app_id和app_secret
@date 2024/01/16 17:07
*/
public class AppUtils {
//生成 app_secret 密钥
private final static String SERVER_NAME = "XXXSYWXT09";
private final static String[] chars = new String[]{"a", "b", "c", "d", "e", "f",
"g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
"t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z"};
private final static String[] upperChars = new String[]{"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z"};
public static final String ALLCHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String UPPERCHAR = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* <p>
* 获取appId
* </P>
* @date 2024/01/16 17:10
*/
public static String getAppId() {
return SERVER_NAME + getUpperRandomString(6);
}
/**
* <p>
* 生成appSecret
* </P>
* @date 2024/01/16 17:30
*/
public static String getAppSecret(String appId) {
try {
return getRandomString(16);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
/**
* 返回一个定长的随机字符串(只包含大写字母、数字)
*
* @param length
* 随机字符串长度
* @return 随机字符串
*/
public static String getUpperRandomString(int length) {
StringBuffer sb = new StringBuffer();
SecureRandom random = new SecureRandom();
for (int i = 0; i < length; i++) {
sb.append(UPPERCHAR.charAt(random.nextInt(UPPERCHAR.length())));
}
return sb.toString();
}
/**
* 返回一个定长的随机字符串(只包含大小写字母、数字)
*
* @param length 随机字符串长度
* @return 随机字符串
*/
public static String getRandomString(int length) {
StringBuffer sb = new StringBuffer();
SecureRandom random = new SecureRandom();
for (int i = 0; i < length; i++) {
sb.append(ALLCHAR.charAt(random.nextInt(ALLCHAR.length())));
}
return sb.toString();
}
}
/**
* 服务启动时加载appId和appSecret到缓存
*/
@Override
@PostConstruct
public void initThirdAppSecret()
{
App app = new App();
app.setStatus(1);
List<App> appList = thirdAppMapper.selectThirdApp(app);
for (App app : appList)
{
redisUtil.set(app.getAppId(), app.getAppSecret());
}
log.info("加载appId和appSecret到缓存成功");
}
app.java
package com.xxx.domain;
import lombok.Data;
/**
* @Description app实体
* @Author liqinglong
* @DateTime 2024-01-16 17:19
* @Version 1.0
*/
@Data
public class App {
private String appId;
private String appSecret;
private Integer status;
}
注意在实际应用中,每添加或修改appId时都保存到缓存,保存缓存里的appId和appSecret是最新的。
package com.xxx.api.config.openapi.config.sign;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
1. @author liqinglong
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Signature {
}
SignatureAspect.java
package com.xxx.api;
import com.alibaba.fastjson.JSONObject;
import com.xx.api.config.openapi.config.BodyReaderHttpServletRequestWrapper;
import com.xxx.api.config.openapi.config.HttpHelper;
import com.xxx.common.exception.CustomException;
import com.xxx.common.redis.RedisUtil;
import com.xxx.common.utils.SignUtil;
import com.xxx.common.utils.StringUtils;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.*;
/**
1. @author pdai
*/
@Aspect
@Component
public class SignatureAspect {
@Autowired
private RedisUtil redisUtil;
/**
* SIGN_HEADER.
*/
private static final String SIGN_HEADER = "X-SIGN";
/**
* pointcut.
*/
@Pointcut("execution(@com.xxx.api.config.openapi.config.sign.Signature * *(..))")
private void verifySignPointCut() {
// nothing
}
/**
* verify sign.
*/
@Before("verifySignPointCut()")
public void verify() {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
// 判断请求方式
String method = request.getMethod();
if ("POST".equals(method)) {
// 获取请求Body参数
try{
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
String body = HttpHelper.getBodyString(requestWrapper);
String bodyString = URLDecoder.decode(body, "utf-8");
if (StringUtils.isEmpty(bodyString)) {
throw new CustomException("请求参数不能为空");
}
// 解析参数转JSON格式
JSONObject jsonObject = JSONObject.parseObject(bodyString);
// 验签
validation(jsonObject);
} catch (Exception e){
if(e instanceof CustomException){
throw new CustomException(e.getMessage(),((CustomException) e).getCode());
}
throw new CustomException("验证签名失败,请稍后重试");
}
}
if ("GET".equals(method)) {
// 获取请求参数
Map allRequestParam = getAllRequestParam(request);
Set<Map.Entry<String, String>> entries = allRequestParam.entrySet();
// 参数转JSON格式
JSONObject jsonObject = new JSONObject();
entries.forEach(key -> {
jsonObject.put(key.getKey(), key.getValue());
});
// 验签
validation(jsonObject);
}
}
/**
* 验签
*
* @param body 请求参数
* @return
* @throws IOException
*/
private boolean validation(JSONObject body){
//请求签名
String sign = body.getString("sign");
body.remove("sign");
String appId = body.getString("appId");
if (StringUtils.isEmpty(appId)) {
throw new CustomException("应用id不能为空");
}
String appSecret = (String)redisUtil.get(appId);
if (StringUtils.isEmpty(appSecret)) {
throw new CustomException("应用id不存在或应用处于禁用状态");
}
//根据APPID查询密钥进行重签
String sign1 = SignUtil.getSign(body,appSecret);
// 校验签名
if (!StringUtils.equals(sign1, sign)) {
throw new CustomException("签名错误");
}
return true;
}
/**
* 获取客户端GET请求中所有的请求参数
*
* @param request
* @return
*/
private Map getAllRequestParam(final HttpServletRequest request) {
Map res = new HashMap();
Enumeration temp = request.getParameterNames();
if (null != temp) {
while (temp.hasMoreElements()) {
String en = (String) temp.nextElement();
String value = request.getParameter(en);
res.put(en, value);
//如果字段的值为空,判断若值为空,则删除这个字段>
if (null == res.get(en) || "".equals(res.get(en))) {
res.remove(en);
}
}
}
return res;
}
}
/**
* @Description 验证查询用户是不
* @param jsonObject
* @return com.hw.common.web.AjaxResult
*/
@Signature
@PostMapping(value = "/checkIsBlank")
public AjaxResult checkIsBlank(@RequestBody JSONObject jsonObject) {
return AjaxResult.success(airCustBlanklistService.checkIsBlank(jsonObject));
}
通讯协议:HTTP
请求方式:POST
请求报文格式:JSON
请求头部:“Content-Type” 必须设置为application/json;charset=UTF-8
向平台申请appId和appSecret。
appId:应用唯一标识,每个客户端唯一
appSecret:应用密钥,用于签名
成功申请后,得到appId和appSecret如下:
appId: 3BTN5hc5
appSecret: 63f1824265b58f88352e22c0d5e7967ed98ce726
2.2 签名方式
平台给客户端提供appId和appSecret,客户端需保证appSecret保密性。
平台同时提供签名工具类:SignUtil.java,供客户端生成签名。
SignUtil.java 代码:
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
@Slf4j
public class SignUtil {
/**
* 请求参数签名
*
* @param params 请求参数
* @return 签名
*/
public static String getSign(Map<String, Object> params, String key) {
if (params == null) {
params = new HashMap<>();
}
String str = jointParams(sortMapByKey(params), key);
return SecureUtil.md5(str).toUpperCase();
}
/**
* Map按key ASCII 进行排序
*
* @param map 待排序map
* @return 排序完成的map
*/
private static Map<String, Object> sortMapByKey(Map<String, Object> map) {
if (map.isEmpty()) {
return map;
}
Map<String, Object> sortMap = new TreeMap<>(Comparator.naturalOrder());
sortMap.putAll(map);
return sortMap;
}
/**
* 把map 拼接成key=value& 格式,并且处理了相关参数
*
* @param params
* @return
*/
private static String jointParams(Map<String, Object> params, String key) {
StringBuilder contrastSign = new StringBuilder();
for (Map.Entry<String, Object> entry : params.entrySet()) {
String entryKey = entry.getKey();
Object entryValue = entry.getValue();
if (entryKey != null && entryValue != null) {
if (entryValue instanceof Map || entryValue instanceof List) {
char[] chs = JSONObject.toJSONString(entryValue, SerializerFeature.WriteMapNullValue).toCharArray();
Arrays.sort(chs);
entryValue = new String(chs);
}
if (entryValue instanceof String) {
if (StringUtils.isEmpty(entryValue.toString())){
continue;
}
}
contrastSign
.append(entryKey)
.append("=")
.append(entryValue)
.append("&");
}
}
contrastSign.append("key=").append(key);
return contrastSign.toString();
}
}
2.2.1 SignUtil.java 的依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.13</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
2.2.3 示例
调用平台的接口:验证是否被列为黑名单接口 /checkIsBlank
封装请求参数
//1.设置请求参数
Map<String, Object> params = new HashMap<>();
params.put("customerName","贵州太极云科技有限公司");
params.put("idNumber","xxx");
params.put("contactPhone","138xxxx9024");
//2.获取AppId和AppSecret,根据实际情况配置,这里的数据为测试数据
Map<String, Object> publicParams = new HashMap<>();
publicParams.put("AppId","AX5aA3U8");
publicParams.put("AppSecret","16a83104651b7463bec74064a6e46b5e07b9a45e");
JSONObject accountParams = new JSONObject(publicParams);
//3.签名并封装公共请求参数,此方法为固定调用即可
params = AirportDataUtil.getSignParams(params,accountParams);
//4.发送post请求调用接口,请根据实际情况配置,这里的数据为测试数据
String url = "http://localhost:5008/blacklist/checkIsBlank";
HttpResult httpResult = HttpUtils.doJsonPost(url,params);
if (httpResult.getCode() == 200){
//6.获取响应数据,根据实际情况处理
JSONObject body = JSON.parseObject(httpResult.getData());
if (body.containsKey("code") && body.getInteger("code") >= 400){
//7.根据实际情况处理异常
throw new CustomException(body.getString("msg"));
}
System.out.println("获取响应数据:"+body);
}
签名并封装请求参数类
AirportDataUtil.java
public class AirportDataUtil {
public static Map<String,Object> getSignParams(Map<String,Object> params, JSONObject accountParams){
String appId = accountParams.get("AppId") == null?null:accountParams.getString("AppId");
String appSecret = accountParams.get("AppSecret") == null?null:accountParams.getString("AppSecret");
if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(appSecret)){
throw new CustomException("渠道接口密钥不存在");
}
params.put("appId",appId);
String sign = SignUtil.getSign(params,appSecret);
params.put("sign",sign);
return params;
}
}
工具类HttpUtils.java
import com.alibaba.fastjson.JSONObject;
import com.shop.cereshop.commons.domain.express.HttpResult;
import org.apache.http.client.methods.CloseableHttpResponse;
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.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
public class HttpUtils {
private final static String CHARSET_DEFAULT = "UTF-8";
/**
* post请求 编码格式默认application/json
*
* @param url 请求url
* @return
*/
public static HttpResult doJsonPost(String url, Object obj) {
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
CloseableHttpResponse resp = null;
HttpResult result = new HttpResult();
try {
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
httpPost.setHeader("Accept", "application/json");
httpPost.setEntity(new StringEntity(JSONObject.toJSONString(obj), CHARSET_DEFAULT));
resp = httpClient.execute(httpPost);
String body = EntityUtils.toString(resp.getEntity(), CHARSET_DEFAULT);
int statusCode = resp.getStatusLine().getStatusCode();
result.setStatus(statusCode);
result.setBody(body);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != resp) {
try {
resp.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
}
返回结果实体类HttpResult.java
import lombok.Data;
/**
* http请求接口返回结果实体
*/
@Data
public class HttpResult {
private int code;
private Object data;
private String msg;
public HttpResult() {
}
public HttpResult(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
}
应答Json
{
"msg": "操作成功",
"code": 200,
"data": true
}