平台接入实现方案

发布时间:2024年01月16日

引言

互联网上的大平台都会对外提供api,但这些api不能不通过任何验证就能直接访问,这样风险会非常高,也是不合理的,比如微信公众号,七牛云,阿里巴巴相关应用的接入等等,我们接触最多的客户端的实现,平台端很少有人知道是怎么做到的,下面我们一起学习了解一下。

平台端

  1. 定义生成appId和appSecret的工具类

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();
    }
}
  1. 服务启动时加载appId和appSecret到缓存
	/**
	 * 服务启动时加载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是最新的。

  1. 定义签名注解
    Signature.java
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 {
}
  1. 签名注解的实现

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;
    }
}
  1. 在接口方法上加注解@Signature实现接入验证
/**
     * @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));
    }

客户端

  1. 数据传输说明

通讯协议:HTTP
请求方式:POST
请求报文格式:JSON
请求头部:“Content-Type” 必须设置为application/json;charset=UTF-8

  1. 接口接入
    2.1 申请appId和appSecret

向平台申请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
}
文章来源:https://blog.csdn.net/liqinglonguo/article/details/135629554
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。