【漏洞】

跨站点脚本(XSS)攻击

【原因】

跨站点脚本(也称为xss)是一个漏洞,攻击者可以发送恶意代码(通常在(Javascript的形式)给另一个用户。因为浏览器无法知道脚本是否值得信任,所以它将在用户上下文中执行脚本,从而允许攻击者访问任何cookie。

【解决】增加敏感脚本过滤器(转一篇整理较好的实现方案: https://blog.csdn.net/yucaifu1989/article/details/61616870

使用filter过滤xss攻击,filter实现脚注入攻击过滤源码 。  先说一下实现思路:

1. 使用正则表达式的方式实现脚本过滤,这个方法准确率较高,但是可能根据不能的要求会变动;

2. 为了保证配置灵活(包括正则表达式灵活),使用xml配置文件的方式记录配置信息,配置信息包含是否开启校验、是否记录日志、是否中断请求、是否替换脚本字符等;

3. 为保证xml与正则表达式的特殊字符不冲突,使用<![CDATA[]]>标签存放正则表达式,但是在类中需要特殊处理;

4. 通过继承HttpRequestWrapper的方式实现request中header和parameter信息过滤;

5. xml解析使用dom4j,稍后会对这个工具的使用写一篇文章,暂时辛苦大家去网站查找资料

6. 使用XSSSecurityManager类实现配置信息加载和处理,XSSSecurityConfig记录匹配信息,XSSSecurityCon标识程序所需常量;

 

一共改了 7 个文件,如下:

1、XSSHttpRequestWrapper

package com.sg.security;

import java.io.IOException;

import java.util.Enumeration;

import java.util.Map;

import java.util.Set;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletRequestWrapper;

import javax.servlet.http.HttpServletResponse;

/**

* @author winnie

* @date

* @describe request信息封装类,用于判断、处理request请求中特殊字符

*/

public class XSSHttpRequestWrapper extends HttpServletRequestWrapper {

/**

* 封装http请求

* @param request

*/

public XSSHttpRequestWrapper(HttpServletRequest request) {

super(request);

}

@Override

public String getHeader(String name) {

String value = super.getHeader(name);

// 若开启特殊字符替换,对特殊字符进行替换

if(XSSSecurityConfig.REPLACE){

XSSSecurityManager.securityReplace(name);

}

return value;

}

@Override

public String getParameter(String name) {

String value = super.getParameter(name);

// 若开启特殊字符替换,对特殊字符进行替换

if(XSSSecurityConfig.REPLACE){

XSSSecurityManager.securityReplace(name);

}

return value;

}

/**

* 没有违规的数据,就返回false;

*

* @return

*/

@SuppressWarnings("unchecked")

private boolean checkHeader(){

Enumeration<String> headerParams = this.getHeaderNames();

while(headerParams.hasMoreElements()){

String headerName = headerParams.nextElement();

String headerValue = this.getHeader(headerName);

if(XSSSecurityManager.matches(headerValue)){

return true;

}

}

return false;

}

/**

* 没有违规的数据,就返回false;

*

* @return

*/

@SuppressWarnings("unchecked")

private boolean checkParameter(){

Map<String,Object> submitParams = this.getParameterMap();

Set<String> submitNames = submitParams.keySet();

for(String submitName : submitNames){

Object submitValues = submitParams.get(submitName);

if(submitValues instanceof String){

if(XSSSecurityManager.matches((String)submitValues)){

return true;

}

}else if(submitValues instanceof String[]){

for(String submitValue : (String[])submitValues){

if(XSSSecurityManager.matches((String)submitValue)){

return true;

}

}

}

}

return false;

}

/**

* 没有违规的数据,就返回false;

* 若存在违规数据,根据配置信息判断是否跳转到错误页面

* @param response

* @return

* @throws IOException

* @throws ServletException

*/

public boolean validateParameter(HttpServletResponse response) throws ServletException, IOException{

// 开始header校验,对header信息进行校验

if(XSSSecurityConfig.IS_CHECK_HEADER){

if(this.checkHeader()){

return true;

}

}

// 开始parameter校验,对parameter信息进行校验

if(XSSSecurityConfig.IS_CHECK_PARAMETER){

if(this.checkParameter()){

return true;

}

}

return false;

}

}

2、XSSSecurityFilter

package com.sg.security;

import java.io.IOException;

import javax.servlet.Filter;

import javax.servlet.FilterChain;

import javax.servlet.FilterConfig;

import javax.servlet.ServletException;

import javax.servlet.ServletRequest;

import javax.servlet.ServletResponse;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

/**

* @author winnie

* @date

* @describe 安全信息审核类

*/

public class XSSSecurityFilter implements Filter{

private static Logger logger = Logger.getLogger(XSSSecurityFilter.class);

/**

* 销毁操作

*/

public void destroy() {

logger.info("XSSSecurityFilter destroy() begin");

XSSSecurityManager.destroy();

logger.info("XSSSecurityFilter destroy() end");

}

/**

* 安全审核

* 读取配置信息

*/

public void doFilter(ServletRequest request, ServletResponse response,

FilterChain chain) throws IOException, ServletException {

// 判断是否使用HTTP

checkRequestResponse(request, response);

// 转型

HttpServletRequest httpRequest = (HttpServletRequest) request;

HttpServletResponse httpResponse = (HttpServletResponse) response;

// http信息封装类

XSSHttpRequestWrapper xssRequest = new XSSHttpRequestWrapper(httpRequest);

// 对request信息进行封装并进行校验工作,若校验失败(含非法字符),根据配置信息进行日志记录和请求中断处理

if(xssRequest.validateParameter(httpResponse)){

if(XSSSecurityConfig.IS_LOG){

// 记录攻击访问日志

// 可使用数据库、日志、文件等方式

}

if(XSSSecurityConfig.IS_CHAIN){

httpRequest.getRequestDispatcher(XSSSecurityCon.FILTER_ERROR_PAGE).forward( httpRequest, httpResponse);

return;

}

}

chain.doFilter(xssRequest, response);

}

/**

* 初始化操作

*/

public void init(FilterConfig filterConfig) throws ServletException {

XSSSecurityManager.init(filterConfig);

}

/**

* 判断Request ,Response 类型

* @param request

*            ServletRequest

* @param response

*            ServletResponse

* @throws ServletException

*/

private void checkRequestResponse(ServletRequest request,

ServletResponse response) throws ServletException {

if (!(request instanceof HttpServletRequest)) {

throw new ServletException("Can only process HttpServletRequest");

}

if (!(response instanceof HttpServletResponse)) {

throw new ServletException("Can only process HttpServletResponse");

}

}

}

3、XSSSecurityManager

package com.sg.security;

import java.util.Iterator;

import java.util.regex.Pattern;

import javax.servlet.FilterConfig;

import org.apache.log4j.Logger;

import org.dom4j.DocumentException;

import org.dom4j.Element;

import org.dom4j.io.SAXReader;

/**

* @author winnie

* @date

* @describe 安全过滤配置管理类,由XSSSecurityManger修改

*/

public class XSSSecurityManager {

private static Logger logger = Logger.getLogger(XSSSecurityManager.class);

/**

* REGEX:校验正则表达式

*/

public static String REGEX;

/**

* 特殊字符匹配

*/

private static Pattern XSS_PATTERN ;

private XSSSecurityManager(){

//不可被实例化

}

public static void init(FilterConfig config){

logger.info("XSSSecurityManager init(FilterConfig config) begin");

//初始化过滤配置文件

String xssPath = config.getServletContext().getRealPath("/")

+ config.getInitParameter("securityconfig");

// 初始化安全过滤配置

try {

if(initConfig(xssPath)){

// 生成匹配器

XSS_PATTERN = Pattern.compile(REGEX);

}

} catch (DocumentException e) {

logger.error("安全过滤配置文件xss_security_config.xml加载异常",e);

}

logger.info("XSSSecurityManager init(FilterConfig config) end");

}

/**

* 读取安全审核配置文件xss_security_config.xml

* 设置XSSSecurityConfig配置信息

* @param path 配置文件地址 eg C:/apache-tomcat-6.0.33/webapps/security_filter/WebRoot/config/xss/xss_security_config.xml

* @return

* @throws DocumentException

*/

@SuppressWarnings("unchecked")

public static boolean initConfig(String path) throws DocumentException {

logger.info("XSSSecurityManager.initConfig(String path) begin");

Element superElement = new SAXReader().read(path).getRootElement();

XSSSecurityConfig.IS_CHECK_HEADER = new Boolean(getEleValue(superElement,XSSSecurityCon.IS_CHECK_HEADER));

XSSSecurityConfig.IS_CHECK_PARAMETER = new Boolean(getEleValue(superElement,XSSSecurityCon.IS_CHECK_PARAMETER));

XSSSecurityConfig.IS_LOG = new Boolean(getEleValue(superElement,XSSSecurityCon.IS_LOG));

XSSSecurityConfig.IS_CHAIN = new Boolean(getEleValue(superElement,XSSSecurityCon.IS_CHAIN));

XSSSecurityConfig.REPLACE = new Boolean(getEleValue(superElement,XSSSecurityCon.REPLACE));

Element regexEle = superElement.element(XSSSecurityCon.REGEX_LIST);

if(regexEle != null){

Iterator<Element> regexIt = regexEle.elementIterator();

StringBuffer tempStr = new StringBuffer("^");

//xml的cdata标签传输数据时,会默认在\前加\,需要将\\替换为\

while(regexIt.hasNext()){

Element regex = (Element)regexIt.next();

String tmp = regex.getText();

tmp = tmp.replaceAll("\\\\\\\\", "\\\\");

tempStr.append(tmp);

tempStr.append("|");

}

if(tempStr.charAt(tempStr.length()-1)=='|'){

REGEX= tempStr.substring(0, tempStr.length()-1)+"$";

logger.info("安全匹配规则"+REGEX);

}else{

logger.error("安全过滤配置文件加载失败:正则表达式异常 "+tempStr.toString());

return false;

}

}else{

logger.error("安全过滤配置文件中没有 "+XSSSecurityCon.REGEX_LIST+" 属性");

return false;

}

logger.info("XSSSecurityManager.initConfig(String path) end");

return true;

}

/**

* 从目标element中获取指定标签信息,若找不到该标签,记录错误日志

* @param element 目标节点

* @param tagName 制定标签

* @return

*/

private static String getEleValue(Element element, String tagName){

if (isNullStr(element.elementText(tagName))){

logger.error("安全过滤配置文件中没有 "+XSSSecurityCon.REGEX_LIST+" 属性");

}

return element.elementText(tagName);

}

/**

* 对非法字符进行替换

* @param text

* @return

*/

public static String securityReplace(String text){

if(isNullStr(text)){

return text;

}else{

return text.replaceAll(REGEX, XSSSecurityCon.REPLACEMENT);

}

}

/**

* 匹配字符是否含特殊字符

* @param text

* @return

*/

public static boolean matches(String text){

if(text==null){

return false;

}

return XSS_PATTERN.matcher(text).matches();

}

/**

* 释放关键信息

*/

public static void destroy(){

logger.info("XSSSecurityManager.destroy() begin");

XSS_PATTERN = null;

REGEX = null;

logger.info("XSSSecurityManager.destroy() end");

}

/**

* 判断是否为空串,建议放到某个工具类中

* @param value

* @return

*/

public static boolean isNullStr(String value){

return value == null || value.trim().equals("");

}

}

4、 XSSSecurityConfig

package com.sg.security;

/**

* @author winnie

* 安全过滤配置信息类

*/

public class XSSSecurityConfig {

/**

* CHECK_HEADER:是否开启header校验

*/

public static boolean IS_CHECK_HEADER;

/**

* CHECK_PARAMETER:是否开启parameter校验

*/

public static boolean IS_CHECK_PARAMETER;

/**

* IS_LOG:是否记录日志

*/

public static boolean IS_LOG;

/**

* IS_LOG:是否中断操作

*/

public static boolean IS_CHAIN;

/**

* REPLACE:是否开启替换

*/

public static boolean REPLACE;

}

5、XSSSecurityCon

package com.sg.security;

/**

* @author winnie

* @date

* @describe

*/

public class XSSSecurityCon {

/**

* 配置文件标签 isCheckHeader

*/

public static String IS_CHECK_HEADER = "isCheckHeader";

/**

* 配置文件标签 isCheckParameter

*/

public static String IS_CHECK_PARAMETER = "isCheckParameter";

/**

* 配置文件标签 isLog

*/

public static String IS_LOG = "isLog";

/**

* 配置文件标签 isChain

*/

public static String IS_CHAIN = "isChain";

/**

* 配置文件标签 replace

*/

public static String REPLACE = "replace";

/**

* 配置文件标签 regexList

*/

public static String REGEX_LIST = "regexList";

/**

* 替换非法字符的字符串

*/

public static String REPLACEMENT = "";

/**

* FILTER_ERROR_PAGE:过滤后错误页面

*/

public static String FILTER_ERROR_PAGE = "/common/filtererror.jsp";

}

6、xss_security_config.xml

<?xml version="1.0" encoding="UTF-8"?>

<XSSConfig>

<!-- 是否进行header校验 -->

<isCheckHeader>false</isCheckHeader>

<!-- 是否进行parameter校验 -->

<isCheckParameter>true</isCheckParameter>

<!-- 是否记录日志 -->

<isLog>true</isLog>

<!-- 是否中断请求 -->

<isChain>false</isChain>

<!-- 是否开启特殊字符替换 -->

<replace>true</replace>

<!-- 是否开启特殊url校验 -->

<isCheckUrl>true</isCheckUrl>

<regexList>

<!-- 匹配含有字符: alert( ) -->

<regex><![CDATA[.*[A|a][L|l][E|e][R|r][T|t]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: window.location = -->

<regex><![CDATA[.*[W|w][I|i][N|n][D|d][O|o][W|w]\\.[L|l][O|o][C|c][A|a][T|t][I|i][O|o][N|n]\\s*=.*]]></regex>

<!-- 匹配含有字符:style = x:ex pression ( ) -->

<regex><![CDATA[.*[S|s][T|t][Y|y][L|l][E|e]\\s*=.*[X|x]:[E|e][X|x].*[P|p][R|r][E|e][S|s]{1,2}[I|i][O|o][N|n]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: document.cookie -->

<regex><![CDATA[.*[D|d][O|o][C|c][U|u][M|m][E|e][N|n][T|t]\\.[C|c][O|o]{2}[K|k][I|i][E|e].*]]></regex>

<!-- 匹配含有字符: eval( ) -->

<regex><![CDATA[.*[E|e][V|v][A|a][L|l]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: unescape() -->

<regex><![CDATA[.*[U|u][N|n][E|e][S|s][C|c][A|a][P|p][E|e]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: execscript( ) -->

<regex><![CDATA[.*[E|e][X|x][E|e][C|c][S|s][C|c][R|r][I|i][P|p][T|t]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: msgbox( ) -->

<regex><![CDATA[.*[M|m][S|s][G|g][B|b][O|o][X|x]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: confirm( ) -->

<regex><![CDATA[.*[C|c][O|o][N|n][F|f][I|i][R|r][M|m]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: prompt( ) -->

<regex><![CDATA[.*[P|p][R|r][O|o][M|m][P|p][T|t]\\s*\\(.*\\).*]]></regex>

<!-- 匹配含有字符: <script> </script> -->

<regex><![CDATA[.*<[S|s][C|c][R|r][I|i][P|p][T|t]>.*</[S|s][C|c][R|r][I|i][P|p][T|t]>.*]]></regex>

<!-- 匹配含有字符: 含有一个符号: "  -->

<regex><![CDATA[[.&[^\"]]*\"[.&[^\"]]*]]></regex>

<!-- 匹配含有字符: 含有一个符号: '  -->

<regex><![CDATA[[.&[^']]*'[.&[^']]*]]></regex>

<!-- 匹配含有字符: 含有回车换行 和 <script> </script> -->

<regex><![CDATA[[[.&[^a]]|[|a|\n|\r\n|\r|\u0085|\u2028|\u2029]]*<[S|s][C|c][R|r][I|i][P|p][T|t]>.*</[S|s][C|c][R|r][I|i][P|p][T|t]>[[.&[^a]]|[|a|\n|\r\n|\r|\u0085|\u2028|\u2029]]*]]></regex>

</regexList>

</XSSConfig>

7、web.xml配置

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5"

xmlns="http://java.sun.com/xml/ns/javaee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

<welcome-file-list>

<welcome-file>index.jsp</welcome-file>

</welcome-file-list>

<!-- 信息安全审核 -->

<filter>

<filter-name>XSSFiler</filter-name>

<filter-class>

com.sg.security.XSSSecurityFilter

</filter-class>

<init-param>

<param-name>securityconfig</param-name>

<param-value>

/WebRoot/config/xss/xss_security_config.xml

</param-value>

</init-param>

</filter>

<!-- 拦截请求类型 -->

<filter-mapping>

<filter-name>XSSFiler</filter-name>

<url-pattern>*.jsp</url-pattern>

</filter-mapping>

<filter-mapping>

<filter-name>XSSFiler</filter-name>

<url-pattern>*.do</url-pattern>

</filter-mapping>

</web-app>

Xml代码  
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app version="2.5"
  3. xmlns="http://java.sun.com/xml/ns/javaee"
  4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  6. http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  7. <welcome-file-list>
  8. <welcome-file>index.jsp</welcome-file>
  9. </welcome-file-list>
  10. <!-- 信息安全审核 -->
  11. <filter>
  12. <filter-name>XSSFiler</filter-name>
  13. <filter-class>
  14. com.sg.security.XSSSecurityFilter
  15. </filter-class>
  16. <init-param>
  17. <param-name>securityconfig</param-name>
  18. <param-value>
  19. /WebRoot/config/xss/xss_security_config.xml
  20. </param-value>
  21. </init-param>
  22. </filter>
  23. <!-- 拦截请求类型 -->
  24. <filter-mapping>
  25. <filter-name>XSSFiler</filter-name>
  26. <url-pattern>*.jsp</url-pattern>
  27. </filter-mapping>
  28. <filter-mapping>
  29. <filter-name>XSSFiler</filter-name>
  30. <url-pattern>*.do</url-pattern>
  31. </filter-mapping>
  32. </web-app>

【漏洞三】跨站点脚本(XSS)攻击的更多相关文章

  1. 跨站点脚本编制-XSS 描述及解决方法

    跨站点脚本编制可能是一个危险的安全性问题,在设计安全的基于 Web 的应用程序时应该考虑这一点.本文中,描述了这种问题的本质.它是如何起作用的,并概述了一些推荐的修正策略. 当今的大多数网站都对 We ...

  2. 跨站点脚本编制实例(AppScan扫描结果)

    最近工作要求解决下web的项目的漏洞问题,扫描漏洞是用的AppScan工具,其中有很多是关于跨站点脚本编制问题的.下面就把这块东西分享出来. 原创文章,转载请注明 ------------------ ...

  3. ASP.NET Core中的OWASP Top 10 十大风险-跨站点脚本攻击 (XSS)

    不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: https://dotnetcoretutorials.com/201 ...

  4. 跨站点脚本攻击XSS

    来源:http://www.freebuf.com/articles/web/15188.html 跨站点脚本攻击是一种Web应用程序的攻击,攻击者尝试注入恶意脚本代码到受信任的网站上执行恶意操作.在 ...

  5. IBM Rational AppScan:跨站点脚本攻击深入解析

    IBM Rational AppScan:跨站点脚本攻击深入解析    了解黑客如何启动跨站点脚本攻击(cross-site scripting,XSS),该攻击危害(及不危害)什么,如何检测它们,以 ...

  6. 跨站点脚本编制 - SpringBoot配置XSS过滤器(基于mica-xss)

    1. 简介   XSS,即跨站脚本编制,英文为Cross Site Scripting.为了和CSS区分,命名为XSS.   XSS是最普遍的Web应用安全漏洞.这类漏洞能够使得攻击者嵌入恶意脚本代码 ...

  7. 网站跨站点脚本,Sql注入等攻击的处理

    从360安全论坛里找到的一段代码,经过整理封装,直接在站点Global.asax文件或写一个HttpModule来拦截恶意请求即可: http://bbs.webscan.360.cn/forum.p ...

  8. [原]网站跨站点脚本,Sql注入等攻击的处理

    从360安全论坛里找到的一段代码,经过整理封装,直接在站点Global.asax文件或写一个HttpModule来拦截恶意请求即可: http://bbs.webscan.360.cn/forum.p ...

  9. 跨站点脚本编制 - SpringBoot配置XSS过滤器(基于Jsoup)

    1. 跨站点脚本编制   风险:可能会窃取或操纵客户会话和 cookie,它们可能用于模仿合法用户,从而使黑客能够以该用户身份查看或变更用户记录以及执行事务.   原因:未对用户输入正确执行危险字符清 ...

随机推荐

  1. Java中的集合(七)双列集合顶层接口------Map接口架构

    Java中的集合(七)双列集合顶层接口------Map接口 一.Map接口的简介 通过List接口,我们知道List接口下的集合是单列集合,数据存储是单列的结构.Map接口下是一个键值对(key-v ...

  2. DNS域传输漏洞利用总结

    操作基本的步骤是: 1) 输入nslookup命令进入交互式shell 2) Server 命令参数设定查询将要使用的DNS服务器 3) Ls命令列出某个域中的所有域名 4) Exit命令退出程序 攻 ...

  3. Rocket - diplomacy - misaligned

    https://mp.weixin.qq.com/s/poCJBcx45clXHm6Uuv8M6w 介绍AddressSet.misaligned的实现.之前介绍的比较概括,也有偏差.这里根据实际执行 ...

  4. 【HIVE & Spark】将hive引擎换成Spark,运行速度快!怎么换?请看本文

    本教程仅仅是使用spark,能在hive用就行. 1.下载Spark; 2.WinSCP上传spark压缩包到虚拟机: 3.tar -zxvf spark-2.3.3-bin-without-hado ...

  5. Java实现 LeetCode 687 最长同值路径(递归)

    687. 最长同值路径 给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值. 这条路径可以经过也可以不经过根节点. 注意:两个节点之间的路径长度由它们之间的边数表示. 示例 1: 输入: ...

  6. Java实现 蓝桥杯VIP 基础练习 Huffuman树

    基础练习 Huffuman树 问题描述 Huffman树在编码中有着广泛的应用.在这里,我们只关心Huffman树的构造过程. 给出一列数{pi}={p0, p1, -, pn-1},用这列数构造Hu ...

  7. Java实现蓝桥杯模拟递增的数

    问题描述 一个正整数如果任何一个数位不大于右边相邻的数位,则称为一个数位递增的数,例如1135是一个数位递增的数,而1024不是一个数位递增的数. 给定正整数 n,请问在整数 1 至 n 中有多少个数 ...

  8. Java实现 LeetCode 230 2的幂

    231. 2的幂 给定一个整数,编写一个函数来判断它是否是 2 的幂次方. 示例 1: 输入: 1 输出: true 解释: 20 = 1 示例 2: 输入: 16 输出: true 解释: 24 = ...

  9. Linux rsyslogd日志服务

    日志基本格式 基本日志格式包含四列: 事件发生的时间 发生事件的服务器的主机名 产生事件的服务名或程序名 事件的具体信息 /etc/rsyslog.conf配置文件 mail.*            ...

  10. 为.netcore助力--WebApiClient正式发布core版本

    1 前言 WebApiClient已成熟稳定,发布了WebApiClient.JIT和WebApiClient.AOT两个nuget包,累计近10w次下载.我对它的高可扩展性设计相当满意和自豪,但We ...