API网关怎么设计?你知道吗?

1. 什么是API网关?

API网关是一个轻量的java http 接口组件,可无缝将普通的Service方法转换成http接口。并从以下几个方卖弄来达到提高开发效率与接口质量的目的。

(1) 去掉mvc控制器,将http请求无缝接入JAVA服务接口

(2) 统一出入参格式

(3) 统一异常规范

(4) 自动检测服务接口规范

2.API网关与普通Http接口实现流程对比

API网关怎么设计?你知道吗?_第1张图片

API网关怎么设计?你知道吗?_第2张图片

3.网关实现流程与技术

API网关怎么设计?你知道吗?_第3张图片

名称 类型 描述
method string 方法名称
paramter json 业务参数
timestamp long 请求时间戳

实现技术

  • java servlet
  • spring ioc
  • JSON 转换工具的使用

4.接口安全的业务需求

(1) 接口安全级别分组

  • 白名单组
  • 黑名单组
  • 黑白名单组

(2) 基于Token安全机制认证需求

  • 登录鉴权
  • 防止业务参数串改
  • 保护有用户敏感信息
  • 防签名伪造

(3) Token认证整体架构

**整体架构分为Token生成与认证两部分 **

API网关怎么设计?你知道吗?_第4张图片

  • Token生成指在登录成功之后生成Token和秘钥,并其与用户信息、客户端信息一起存储到Token表,同时返回Token与Secret至客户端。
  • Token认证指客户端请求黑名单接口时,认证中心基于Token生成签名

Token表结构说明

名称 类别 说明 约束
id number id主键 主键,自增长
memberId number 会员ID
accessToken varchar(50) Token 索引
secret varchar(50) 密钥
createdTime datetime 创建时间
expiresTime datetime 有效期至
clientIp varchar(50) 客户端IP
clientType varchar(50) 客户端类别
eCode varchar(50) 设备标识
uCode varchar(50) 设备用户标识

签名规则

  • 已指定顺序拼接字符串 secret+method+param+token+timestamp+secret
  • 使用MD5进行加密,在转化成大写

签名的目的

  1. 防串改
  2. 防伪造
  3. 防重复使用签名

服务端签名验证的具体流程
API网关怎么设计?你知道吗?_第5张图片

(4) 基于API网关实现安全机制

签名认证与API网关的整体认证流程

API网关怎么设计?你知道吗?_第6张图片

两个流程

token生成:登录成功后,生成token与secret保存到数据库

Token认证相关解决方案:

  • 接口如果表示黑白名单
  • 签名具体验证流程
  • 用户ID等信息如果传递给业务实现接口

5. 代码实现

core包下类

public class ApiStore {
     
    private ApplicationContext applicationContext;
    // API 接口住的地方 
    private HashMap<String, ApiRunnable> apiMap = new HashMap<String, ApiRunnable>();
    
    
// spring ioc 
    public ApiStore(ApplicationContext applicationContext) {
     
        Assert.notNull(applicationContext);
        this.applicationContext = applicationContext;
    }
    /**
     * 
     */
    public void loadApiFromSpringBeans() {
     
    	// ioc 所有BEan
    	// spring ioc 扫描
        String[] names = applicationContext.getBeanDefinitionNames();
        Class<?> type;
        //反谢
        for (String name : names) {
     
            type = applicationContext.getType(name);
            for (Method m : type.getDeclaredMethods()) {
     
            	// 通过反谢拿到APIMapping注解
                APIMapping apiMapping = m.getAnnotation(APIMapping.class);
                if (apiMapping != null) {
     
                    addApiItem(apiMapping, name, m);
                }
            }
        }
    }

    public ApiRunnable findApiRunnable(String apiName) {
     
        return apiMap.get(apiName);
    }

    /**
     * 
     * 添加api 
* * @param apiMapping * api配置 * @param beanName * beanq在spring context中的名称 * @param method */
private void addApiItem(APIMapping apiMapping, String beanName, Method method) { ApiRunnable apiRun = new ApiRunnable(); apiRun.apiName = apiMapping.value(); apiRun.targetMethod = method; apiRun.targetName = beanName; apiRun.apiMapping=apiMapping; apiMap.put(apiMapping.value(), apiRun); } public ApiRunnable findApiRunnable(String apiName, String version) { return (ApiRunnable) apiMap.get(apiName + "_" + version); } public List<ApiRunnable> findApiRunnables(String apiName) { if (apiName == null) { throw new IllegalArgumentException("api name must not null!"); } List<ApiRunnable> list = new ArrayList<ApiRunnable>(20); for (ApiRunnable api : apiMap.values()) { if (api.apiName.equals(apiName)) { list.add(api); } } return list; } public List<ApiRunnable> getAll() { List<ApiRunnable> list = new ArrayList<ApiRunnable>(20); list.addAll(apiMap.values()); Collections.sort(list, new Comparator<ApiRunnable>() { public int compare(ApiRunnable o1, ApiRunnable o2) { return o1.getApiName().compareTo(o2.getApiName()); } }); return list; } public boolean containsApi(String apiName, String version) { return apiMap.containsKey(apiName + "_" + version); } public ApplicationContext getApplicationContext() { return applicationContext; } // 用于执行对应的API方法, public class ApiRunnable { String apiName; //bit.api.user.getUser String targetName; //ioc bean 名称 Object target; // UserServiceImpl 实例 Method targetMethod; // 目标方法 getUser APIMapping apiMapping; public Object run(Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (target == null) { // spring ioc 容器里面去服务Bean 比如GoodsServiceImpl target = applicationContext.getBean(targetName); } return targetMethod.invoke(target, args); } public Class<?>[] getParamTypes() { return targetMethod.getParameterTypes(); } public String getApiName() { return apiName; } public String getTargetName() { return targetName; } public Object getTarget() { return target; } public Method getTargetMethod() { return targetMethod; } public APIMapping getApiMapping() { return apiMapping; } } }
public class ApiRequest {
     

    private String memberId;
    private String accessToken;
    private String sign;
    private String uCode;
    private String eCode;
    private String timestamp;
    private String clientIp;
    private boolean isLogin;
    private String params;

    private String methodName;


    public String getMemberId() {
     
        return memberId;
    }

    public void setMemberId(String memberId) {
     
        this.memberId = memberId;
    }

    public String getAccessToken() {
     
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
     
        this.accessToken = accessToken;
    }

    public String getSign() {
     
        return sign;
    }

    public void setSign(String sign) {
     
        this.sign = sign;
    }

    public String getuCode() {
     
        return uCode;
    }

    public void setuCode(String uCode) {
     
        this.uCode = uCode;
    }

    public String geteCode() {
     
        return eCode;
    }

    public void seteCode(String eCode) {
     
        this.eCode = eCode;
    }

    public String getTimestamp() {
     
        return timestamp;
    }

    public void setTimestamp(String timestamp) {
     
        this.timestamp = timestamp;
    }

    public String getClientIp() {
     
        return clientIp;
    }

    public void setClientIp(String clientIp) {
     
        this.clientIp = clientIp;
    }

    public boolean isLogin() {
     
        return isLogin;
    }

    public void setLogin(boolean login) {
     
        isLogin = login;
    }

    public String getParams() {
     
        return params;
    }

    public void setParams(String params) {
     
        this.params = params;
    }

    public String getMethodName() {
     
        return methodName;
    }

    public void setMethodName(String methodName) {
     
        this.methodName = methodName;
    }
}


public class Token  implements java.io.Serializable{
     

    private long id;
    private String memberId;
    private String accessToken;
    private String secret;
    private Date createdTime;
    private Date expiresTime;
    private String clientIp;
    private String clientType;
    private String eCode;
    private String uCode;


    public long getId() {
     
        return id;
    }

    public void setId(long id) {
     
        this.id = id;
    }

    public String getMemberId() {
     
        return memberId;
    }

    public void setMemberId(String memberId) {
     
        this.memberId = memberId;
    }

    public String getAccessToken() {
     
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
     
        this.accessToken = accessToken;
    }

    public String getSecret() {
     
        return secret;
    }

    public void setSecret(String secret) {
     
        this.secret = secret;
    }

    public Date getCreatedTime() {
     
        return createdTime;
    }

    public void setCreatedTime(Date createdTime) {
     
        this.createdTime = createdTime;
    }

    public Date getExpiresTime() {
     
        return expiresTime;
    }

    public void setExpiresTime(Date expiresTime) {
     
        this.expiresTime = expiresTime;
    }

    public String getClientIp() {
     
        return clientIp;
    }

    public void setClientIp(String clientIp) {
     
        this.clientIp = clientIp;
    }

    public String getClientType() {
     
        return clientType;
    }

    public void setClientType(String clientType) {
     
        this.clientType = clientType;
    }

    public String geteCode() {
     
        return eCode;
    }

    public void seteCode(String eCode) {
     
        this.eCode = eCode;
    }

    public String getuCode() {
     
        return uCode;
    }

    public void setuCode(String uCode) {
     
        this.uCode = uCode;
    }
}


public interface TokenService {
     

    public Token createToken();

    public Token getToken(String token);
}


public class ApiGatewayHand implements InitializingBean, ApplicationContextAware {
     
    private static final Logger logger = LoggerFactory.getLogger(ApiGatewayHand.class);

    private static final String METHOD = "method";
    private static final String PARAMS = "params";

    ApiStore apiSorte;
    final ParameterNameDiscoverer parameterUtil;
    private TokenService tokenService;

    public ApiGatewayHand() {
     
        parameterUtil = new LocalVariableTableParameterNameDiscoverer();
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
     
        apiSorte = new ApiStore(context);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
     
        apiSorte.loadApiFromSpringBeans();
    }

    public void handle(HttpServletRequest request, HttpServletResponse response) {
     
        // 系统参数验证
        String params = request.getParameter(PARAMS);
        String method = request.getParameter(METHOD);
        Object result;
        ApiRunnable apiRun = null;
        ApiRequest apiRequest = null;
        try {
     
            apiRun = sysParamsValdate(request);
            // 构建apiRequest
            apiRequest = buildApiRequest(request);
            // 签名验证
            if (apiRequest.getAccessToken() != null)
                signCheck(apiRequest);

            if (apiRun.getApiMapping().useLogin()) {
     
                if (apiRequest.isLogin()) {
     
                     throw new ApiException("009","调用失败:用户未登陆");
                }
            }


            logger.info("请求接口={" + method + "} 参数=" + params + "");
            Object[] args = buildParams(apiRun, params, request, response,apiRequest);
            result = apiRun.run(args);
        } catch (ApiException e) {
     
            response.setStatus(500);// 封装异常并返回
            logger.error("调用接口={" + method + "}异常  参数=" + params + "", e);
            result = handleError(e);
        } catch (InvocationTargetException e) {
     
            response.setStatus(500);// 封装业务异常并返回
            logger.error("调用接口={" + method + "}异常  参数=" + params + "", e.getTargetException());
            result = handleError(e.getTargetException());
        } catch (Exception e) {
     
            response.setStatus(500);// 封装业务异常并返回
            logger.error("其他异常", e);
            result = handleError(e);
        }

        // 统一返回结果
        returnResult(result, response);
    }

    private ApiRequest buildApiRequest(HttpServletRequest request) {
     
        ApiRequest apiRequest = new ApiRequest();
        apiRequest.setAccessToken(request.getParameter("token"));
        apiRequest.setSign(request.getParameter("sign"));
        apiRequest.setTimestamp(request.getParameter("timestamp"));
        apiRequest.seteCode(request.getParameter("eCode"));
        apiRequest.setuCode(request.getParameter("uCode"));
        apiRequest.setParams(request.getParameter("params"));
        return apiRequest;
    }

    private ApiRequest signCheck(ApiRequest request) throws ApiException {
     
        Token token = tokenService.getToken(request.getAccessToken());
        if (token == null) {
     
            throw new ApiException("验证失败:指定'Token'不存在");
        }
        if (token.getExpiresTime().before(new Date())) {
     
            throw new ApiException("验证失败:指定'Token'已失效");
        }
        // 生成签名
        String methodName = request.getMethodName();
        String accessToken = token.getAccessToken();
        String secret = token.getSecret();
        String params = request.getParams();
        String timestamp = request.getTimestamp();
        String sign = Md5Util.MD5(secret + methodName + params + token + timestamp + secret);

        if (!sign.toUpperCase().equals(request.getSign())) {
     
            throw new ApiException("验证失败:签名非法");
        }

        // 时间验证
        if (Math.abs(Long.valueOf(timestamp) - System.currentTimeMillis()) > 10 * 60 * 1000) {
     
            throw new ApiException("验证失败:签名失效");
        }

        request.setLogin(true);
        request.setMemberId(token.getMemberId());
        return request;
    }


    private Object handleError(Throwable throwable) {
     
        String code = "";
        String message = "";

        if (throwable instanceof ApiException) {
     
            code = "0001";
            message = throwable.getMessage();
        } // 扩展异常规范
        else {
     
            code = "0002";
            message = throwable.getMessage();
        }

        Map<String, Object> result = new HashMap<>();
        result.put("error", code);
        result.put("msg", message);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream stream = new PrintStream(out);
        throwable.printStackTrace(stream);
//        result.put("stack", out.toString());
        return result;
    }

    private ApiRunnable sysParamsValdate(HttpServletRequest request) throws ApiException {
     
        String apiName = request.getParameter(METHOD);
        String json = request.getParameter(PARAMS);

        ApiRunnable api;
        if (apiName == null || apiName.trim().equals("")) {
     
            throw new ApiException("调用失败:参数'method'为空");
        } else if (json == null) {
     
            throw new ApiException("调用失败:参数'params'为空");
        } else if ((api = apiSorte.findApiRunnable(apiName)) == null) {
     
            throw new ApiException("调用失败:指定API不存在,API:" + apiName);
        }
        // 多一个签名参数
        // 
        return api;
    }

    /***
     * 验证业务参数,和构建业务参数对象
     * @param run
     * @param paramJson
     * @param request
     * @param response
     * @param apiRequest
     * @return
     * @throws ApiException
     */
    private Object[] buildParams(ApiRunnable run, String paramJson, HttpServletRequest request, HttpServletResponse response, ApiRequest apiRequest)
            throws ApiException {
     
        Map<String, Object> map = null;
        try {
     
            map = UtilJson.toMap(paramJson);
        } catch (IllegalArgumentException e) {
     
            throw new ApiException("调用失败:json字符串格式异常,请检查params参数 ");
        }
        if (map == null) {
     
            map = new HashMap<>();
        }

        Method method = run.getTargetMethod();// javassist
        List<String> paramNames = Arrays.asList(parameterUtil.getParameterNames(method));
        // goods ,id
        Class<?>[] paramTypes = method.getParameterTypes(); //反射

        for (Map.Entry<String, Object> m : map.entrySet()) {
     
            if (!paramNames.contains(m.getKey())) {
     
                throw new ApiException("调用失败:接口不存在‘" + m.getKey() + "’参数");
            }
        }
        Object[] args = new Object[paramTypes.length];
        for (int i = 0; i < paramTypes.length; i++) {
     
            if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
     
                args[i] = request;
            } else if (paramTypes[i].isAssignableFrom(ApiRequest.class)){
     
                args[i] = apiRequest;
            }else if (map.containsKey(paramNames.get(i))) {
     
                try {
     
                    args[i] = convertJsonToBean(map.get(paramNames.get(i)), paramTypes[i]);
                } catch (Exception e) {
     
                    throw new ApiException("调用失败:指定参数格式错误或值错误‘" + paramNames.get(i) + "’"
                            + e.getMessage());
                }
            } else {
     
                args[i] = null;
            }
        }
        return args;
    }

    // 将MAP转换成具体的目标方方法参数对象
    private <T> Object convertJsonToBean(Object val, Class<T> targetClass) throws Exception {
     
        Object result = null;
        if (val == null) {
     
            return null;
        } else if (Integer.class.equals(targetClass)) {
     
            result = Integer.parseInt(val.toString());
        } else if (Long.class.equals(targetClass)) {
     
            result = Long.parseLong(val.toString());
        } else if (Date.class.equals(targetClass)) {
     
            if (val.toString().matches("[0-9]+")) {
     
                result = new Date(Long.parseLong(val.toString()));
            } else {
     
                throw new IllegalArgumentException("日期必须是长整型的时间戳");
            }
        } else if (String.class.equals(targetClass)) {
     
            if (val instanceof String) {
     
                result = val;
            } else {
     
                throw new IllegalArgumentException("转换目标类型为字符串");
            }
        } else {
     
            result = UtilJson.convertValue(val, targetClass);
        }
        return result;
    }

    private void returnResult(Object result, HttpServletResponse response) {
     
        try {
     
            UtilJson.JSON_MAPPER.configure(
                    SerializationFeature.WRITE_NULL_MAP_VALUES, true);
            String json = UtilJson.writeValueAsString(result);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html/json;charset=utf-8");
            response.setHeader("Cache-Control", "no-cache");
            response.setHeader("Pragma", "no-cache");
            response.setDateHeader("Expires", 0);
            if (json != null)
                response.getWriter().write(json);
        } catch (IOException e) {
     
            logger.error("服务中心响应异常", e);
            throw new RuntimeException(e);
        }
    }

    public static void main(String args[]) {
     
        //{"goods":{"goodsName":"daa","goodsId":"1111"},"id":19}
        String mapString = "{\"goods\":{\"goodsName\":\"daa\",\"goodsId\":\"1111\"},\"id\":19}";
        Map<String, Object> map = UtilJson.toMap(mapString);
        System.out.print(map);
        UtilJson.convertValue(map.get("goods"), Goods.class);
        String str = "{\"goodsName\":\"daa\",\"goodsId\":\"1111\"}";
        Goods goods = UtilJson.convertValue(str, Goods.class);
        System.out.print(goods);
    }
}


public class ApiGatewayServlet extends HttpServlet {
     
    private static final long serialVersionUID = 1L;
    ApplicationContext context;
    private ApiGatewayHand apiHand;

    @Override
    public void init() throws ServletException {
     
        super.init();
        context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        apiHand = context.getBean(ApiGatewayHand.class);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     
        apiHand.handle(request, response);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
     
        apiHand.handle(req, resp);
    }
}


service包下的类

@Service
//goodsServiceImpl 
public class GoodsServiceImpl {
     
	//无缝集成 
	@APIMapping(value = "bit.api.goods.add",useLogin = true)
	public Goods addGoods(Goods goods, Integer id, ApiRequest apiRequest){
     
		return goods;
	}

	@APIMapping("bit.api.goods.get")
	public Goods getGodds(Integer id){
     
		return new Goods("vvv","1111");
	}
	public static class Goods implements Serializable{
     
		private String goodsName;
		private String goodsId;
		public Goods(){
     

		}
		public Goods(String goodsName, String goodsId) {
     
			this.goodsName = goodsName;
			this.goodsId = goodsId;
		}

		public String getGoodsName() {
     
			return goodsName;
		}
		public void setGoodsName(String goodsName) {
     
			this.goodsName = goodsName;
		}
		public String getGoodsId() {
     
			return goodsId;
		}
		public void setGoodsId(String goodsId) {
     
			this.goodsId = goodsId;
		}
	}
}


common包下的类

public class ApiException extends Exception{
     
	private String code;

	public ApiException(String message) {
     
		super(message);
	}

	public ApiException( String code,String message) {
     
		super(message);
		this.code = code;
	}

	public String getCode() {
     
		return code;
	}

	public void setCode(String code) {
     
		this.code = code;
	}
}



    /**
     * 对字符串md5加密(大写+数字)
     *
     * @param s 传入要加密的字符串
     * @return MD5加密后的字符串
     */

    public static String MD5(String s) {
     
        char hexDigits[] = {
     &#39;0&#39;, &#39;1&#39;, &#39;2&#39;, &#39;3&#39;, &#39;4&#39;, &#39;5&#39;, &#39;6&#39;, &#39;7&#39;, &#39;8&#39;, &#39;9&#39;, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;D&#39;, &#39;E&#39;, &#39;F&#39;};

        try {
     
            byte[] btInput = s.getBytes();
            // 获得MD5摘要算法的 MessageDigest 对象
            MessageDigest mdInst = MessageDigest.getInstance(&quot;MD5&quot;);
            // 使用指定的字节更新摘要
            mdInst.update(btInput);
            // 获得密文
            byte[] md = mdInst.digest();
            // 把密文转换成十六进制的字符串形式
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (int i = 0; i &lt; j; i++) {
     
                byte byte0 = md[i];
                str[k++] = hexDigits[byte0 &gt;&gt;&gt; 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (Exception e) {
     
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
     
        String md5 = MD5(&quot;password&quot;);
    }
}


import java.io.IOException;
import java.util.Map;

import org.springframework.util.StringUtils;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public class UtilJson {
     
	public static final ObjectMapper JSON_MAPPER = newObjectMapper(), JSON_MAPPER_WEB = newObjectMapper();

	private static ObjectMapper newObjectMapper() {
     
		ObjectMapper result = new ObjectMapper();
		result.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
		result.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
		result.setSerializationInclusion(Include.NON_NULL);
		result.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);	//不输出value=null的属性
		result.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        result.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);

		return result;
	}

	public static ObjectMapper getObjectMapper() {
     
		return JSON_MAPPER;
	}

	public static String writeValueAsString(Object value) {
     
		try {
     
			return value == null ? null : JSON_MAPPER.writeValueAsString(value);
		} catch (IOException e) {
     
			throw new IllegalArgumentException(e); // TIP: 原则上,不对异常包装,这里为什么要包装?因为正常情况不会发生IOException
		}
	}

	@SuppressWarnings(&quot;unchecked&quot;)
	public static Map<String, Object> toMap(Object value) throws IllegalArgumentException {
     
		return convertValue(value, Map.class);
	}
	
	
	public static <T> T convertValue(Object value, Class<T> clazz) throws IllegalArgumentException {
     
		if (StringUtils.isEmpty(value)) return null;
		try {
     
			if (value instanceof String) 
				value = JSON_MAPPER.readTree((String) value);
			return JSON_MAPPER.convertValue(value, clazz);
		} catch (IOException e) {
     
			throw new IllegalArgumentException(e);
		}
	}

	
	
}


6.开发API接口安全弱点

  • 数据窃取:用户的密码等信息被不轨之人窃取,登录账号发布敏感信息,盗刷等
  • 数据篡改:提交的数据被抓包后进行篡改再提交
  • 数据泄露:爬虫将业务数据甚至核心数据抓取,直接或间接造成损失

7.开放API接口解决方案

(1) RSA/DES加密

对称加密: DES AES

非对称加密: RSA

API网关怎么设计?你知道吗?_第7张图片
(2) MD5混淆算法

  • 信息摘要算法
  • 不可逆
  • 可以加盐,增加破解复杂性
  • MD5流程

API网关怎么设计?你知道吗?_第8张图片

(3) TOKEN令牌

  • 有令牌者过,无则拦截
  • Token交互流程

API网关怎么设计?你知道吗?_第9张图片

史上最全的并发编程脑图:https://www.processon.com/view/5d43e6cee4b0e47199351b7f

你可能感兴趣的:(各个场景解决方案,API网关)