如何过滤xss攻击
过滤xss攻击的方法:
1.XSS过滤器示例代码:
packagecom.devframe.filter;importjavax.servlet.*;
importjavax.servlet.http.HttpServletRequest;
importjava.io.IOException;publicclassXssFilterimplementsFilter{@Override
publicvoidinit(FilterConfigconfig){
}@Override
publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)
throwsIOException,ServletException{
XssHttpServletRequestWrapperxssRequest=newXssHttpServletRequestWrapper(
(HttpServletRequest)request);
chain.doFilter(xssRequest,response);
}@Override
publicvoiddestroy(){
}}
2.request进行XSS过滤,代码:
packagecom.devframe.filter;importorg.apache.commons.io.IOUtils;
importorg.springframework.http.HttpHeaders;
importorg.springframework.http.MediaType;
importorg.springframework.util.StringUtils;importjavax.servlet.ReadListener;
importjavax.servlet.ServletInputStream;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletRequestWrapper;
importjava.io.ByteArrayInputStream;
importjava.io.IOException;
importjava.util.LinkedHashMap;
importjava.util.Map;publicclassXssHttpServletRequestWrapperextendsHttpServletRequestWrapper{
/**
*没被包装过的HttpServletRequest(特殊场景,需要自己过滤)
*/
privateHttpServletRequestorgRequest;
/**
*html过滤
*/
privatefinalstaticHTMLFilterHTML_FILTER=newHTMLFilter();
/**
*Constructsarequestobjectwrappingthegivenrequest.
*
*@paramrequestrequest
*@throwsIllegalArgumentExceptioniftherequestisnull
*/
XssHttpServletRequestWrapper(HttpServletRequestrequest)throwsIllegalArgumentException{
super(request);
orgRequest=request;
}@Override
publicServletInputStreamgetInputStream()throwsIOException{
//非json类型,直接返回
if(!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
returnsuper.getInputStream();
}//为空,直接返回
Stringjson=IOUtils.toString(super.getInputStream(),"utf⑻");
if(!StringUtils.hasText(json)){
returnsuper.getInputStream();
}//xss过滤
json=xssEncode(json);
finalByteArrayInputStreambis=newByteArrayInputStream(json.getBytes("utf⑻"));
returnnewServletInputStream(){
@Override
publicbooleanisFinished(){
returntrue;
}@Override
publicbooleanisReady(){
returntrue;
}@Override
publicvoidsetReadListener(ReadListenerreadListener){
}@Override
publicintread(){
returnbis.read();
}
};
}/**
*过滤参数
*@paramname参数name,也要过滤
*@returnStringvalue
*/
@Override
publicStringgetParameter(Stringname){
Stringvalue=super.getParameter(xssEncode(name));
if(StringUtils.hasText(value)){
value=xssEncode(value);
}
returnvalue;
}/**
*过滤参数,值为数组
*@paramname参数名
*@returnString[]
*/
@Override
publicString[]getParameterValues(Stringname){
String[]parameters=super.getParameterValues(name);
if(parameters==null||parameters.length==0){
returnnull;
}for(inti=0;i<parameters.length;i++){
parameters[i]=xssEncode(parameters[i]);
}
returnparameters;
}/**
*过滤参数,返回键值对情势的参数类型
*@returnMap
*/
@Override
publicMap<String,String[]>getParameterMap(){
Map<String,String[]>map=newLinkedHashMap<>();
Map<String,String[]>parameters=super.getParameterMap();
for(Stringkey:parameters.keySet()){
String[]values=parameters.get(key);
for(inti=0;i<values.length;i++){
values[i]=xssEncode(values[i]);
}
map.put(key,values);
}
returnmap;
}/**
*获得request的头属性,并且进行xss过滤,返回它的值
*@paramname属性名
*@returnString值
*/
@Override
publicStringgetHeader(Stringname){
Stringvalue=super.getHeader(xssEncode(name));
if(StringUtils.hasText(value)){
value=xssEncode(value);
}
returnvalue;
}privateStringxssEncode(Stringinput){
returnHTML_FILTER.filter(input);
}/**
*获得最原始的request
*@returnHttpServletRequest原始的request
*/
publicHttpServletRequestgetOrgRequest(){
returnorgRequest;
}/**
*获得最原始的request,明确不进行xss过滤的
*@paramrequestrequest
*@returnHttpServletRequest原始request
*/
publicstaticHttpServletRequestgetOrgRequest(HttpServletRequestrequest){
if(requestinstanceofXssHttpServletRequestWrapper){
return((XssHttpServletRequestWrapper)request).getOrgRequest();
}
returnrequest;
}
}
3.XSS过滤工具类,示例代码:
packagecom.devframe.filter;importjava.util.*;
importjava.util.concurrent.ConcurrentHashMap;
importjava.util.concurrent.ConcurrentMap;
importjava.util.logging.Logger;
importjava.util.regex.Matcher;
importjava.util.regex.Pattern;/**
*
*HTMLfilteringutilityforprotectingagainstXSS(CrossSiteScripting).
*
*ThiscodeislicensedLGPLv3
*
*ThiscodeisaJavaportoftheoriginalworkinPHPbyCalHendersen.
*http://code.iamcal.com/php/lib_filter/
*
*Thetrickiestpartofthetranslationwashandlingthedifferencesinregexhandling
*betweenPHPandJava.Theseresourceswerehelpfulintheprocess:
*
*http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html
*http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php
*http://www.regular-expressions.info/modifiers.html
*
*Anoteonnamingconventions:instancevariablesareprefixedwitha"v";global
*constantsareinallcaps.
*
*Sampleuse:
*Stringinput=...
*Stringclean=newHTMLFilter().filter(input);
*
*Theclassisnotthreadsafe.Createanewinstanceifindoubt.
*
*Ifyoufindbugsorhavesuggestionsonimprovement(especiallyregarding
*performance),pleasecontactus.Thelatestversionofthis
*source,andourcontactdetails,canbefoundathttp://xss-html-filter.sf.net
*
*@authorJosephO'Connell
*@authorCalHendersen
*@authorMichaelSembWever
*/
publicfinalclassHTMLFilter{/**regexflagunionrepresenting/simodifiersinphp**/
privatestaticfinalintREGEX_FLAGS_SI=Pattern.CASE_INSENSITIVE|Pattern.DOTALL;
privatestaticfinalPatternP_COMMENTS=Pattern.compile("<!--(.*?)-->",Pattern.DOTALL);
privatestaticfinalPatternP_COMMENT=Pattern.compile("^!--(.*)--$",REGEX_FLAGS_SI);
privatestaticfinalPatternP_TAGS=Pattern.compile("<(.*?)>",Pattern.DOTALL);
privatestaticfinalPatternP_END_TAG=Pattern.compile("^/([a-z0⑼]+)",REGEX_FLAGS_SI);
privatestaticfinalPatternP_START_TAG=Pattern.compile("^([a-z0⑼]+)(.*?)(/?)$",REGEX_FLAGS_SI);
privatestaticfinalPatternP_QUOTED_ATTRIBUTES=Pattern.compile("([a-z0⑼]+)=([\"'])(.*?)\\2",REGEX_FLAGS_SI);
privatestaticfinalPatternP_UNQUOTED_ATTRIBUTES=Pattern.compile("([a-z0⑼]+)(=)([^\"\\s']+)",REGEX_FLAGS_SI);
privatestaticfinalPatternP_PROTOCOL=Pattern.compile("^([^:]+):",REGEX_FLAGS_SI);
privatestaticfinalPatternP_ENTITY=Pattern.compile("&#(\\d+);?");
privatestaticfinalPatternP_ENTITY_UNICODE=Pattern.compile("&#x([0⑼a-f]+);?");
privatestaticfinalPatternP_ENCODE=Pattern.compile("%([0⑼a-f]{2});?");
privatestaticfinalPatternP_VALID_ENTITIES=Pattern.compile("&([^&;]*)(?=(;|&|$))");
privatestaticfinalPatternP_VALID_QUOTES=Pattern.compile("(>|^)([^<]+?)(<|$)",Pattern.DOTALL);
privatestaticfinalPatternP_END_ARROW=Pattern.compile("^>");
privatestaticfinalPatternP_BODY_TO_END=Pattern.compile("<([^>]*?)(?=<|$)");
privatestaticfinalPatternP_XML_CONTENT=Pattern.compile("(^|>)([^<]*?)(?=>)");
privatestaticfinalPatternP_STRAY_LEFT_ARROW=Pattern.compile("<([^>]*?)(?=<|$)");
privatestaticfinalPatternP_STRAY_RIGHT_ARROW=Pattern.compile("(^|>)([^<]*?)(?=>)");
privatestaticfinalPatternP_AMP=Pattern.compile("&");
privatestaticfinalPatternP_QUOTE=Pattern.compile("<");
privatestaticfinalPatternP_LEFT_ARROW=Pattern.compile("<");
privatestaticfinalPatternP_RIGHT_ARROW=Pattern.compile(">");
privatestaticfinalPatternP_BOTH_ARROWS=Pattern.compile("<>");//@xxxcouldgrowlarge...maybeusesesat'sReferenceMap
privatestaticfinalConcurrentMap<String,Pattern>P_REMOVE_PAIR_BLANKS=newConcurrentHashMap<String,Pattern>();
privatestaticfinalConcurrentMap<String,Pattern>P_REMOVE_SELF_BLANKS=newConcurrentHashMap<String,Pattern>();/**setofallowedhtmlelements,alongwithallowedattributesforeachelement**/
privatefinalMap<String,List<String>>vAllowed;
/**countsofopentagsforeach(allowable)htmlelement**/
privatefinalMap<String,Integer>vTagCounts=newHashMap<String,Integer>();/**htmlelementswhichmustalwaysbeself-closing(e.g."<img/>")**/
privatefinalString[]vSelfClosingTags;
/**htmlelementswhichmustalwayshaveseparateopeningandclosingtags(e.g."<b></b>")**/
privatefinalString[]vNeedClosingTags;
/**setofdisallowedhtmlelements**/
privatefinalString[]vDisallowed;
/**attributeswhichshouldbecheckedforvalidprotocols**/
privatefinalString[]vProtocolAtts;
/**allowedprotocols**/
privatefinalString[]vAllowedProtocols;
/**tagswhichshouldberemovediftheycontainnocontent(e.g."<b></b>"or"<b/>")**/
privatefinalString[]vRemoveBlanks;
/**entitiesallowedwithinhtmlmarkup**/
privatefinalString[]vAllowedEntities;
/**flagdeterminingwhethercommentsareallowedininputString.*/
privatefinalbooleanstripComment;
privatefinalbooleanencodeQuotes;
privatebooleanvDebug=false;
/**
*flagdeterminingwhethertotrytomaketagswhenpresentedwith"unbalanced"
*anglebrackets(e.g."<btext</b>"becomes"<b>text</b>").Ifsettofalse,
*unbalancedanglebracketswillbehtmlescaped.
*/
privatefinalbooleanalwaysMakeTags;/**Defaultconstructor.
*
*/
publicHTMLFilter(){
vAllowed=newHashMap<>();finalArrayList<String>a_atts=newArrayList<String>();
a_atts.add("href");
a_atts.add("target");
vAllowed.put("a",a_atts);finalArrayList<String>img_atts=newArrayList<String>();
img_atts.add("src");
img_atts.add("width");
img_atts.add("height");
img_atts.add("alt");
vAllowed.put("img",img_atts);finalArrayList<String>no_atts=newArrayList<String>();
vAllowed.put("b",no_atts);
vAllowed.put("strong",no_atts);
vAllowed.put("i",no_atts);
vAllowed.put("em",no_atts);vSelfClosingTags=newString[]{"img"};
vNeedClosingTags=newString[]{"a","b","strong","i","em"};
vDisallowed=newString[]{};
vAllowedProtocols=newString[]{"http","mailto","https"};//noftp.
vProtocolAtts=newString[]{"src","href"};
vRemoveBlanks=newString[]{"a","b","strong","i","em"};
vAllowedEntities=newString[]{"amp","gt","lt","quot"};
stripComment=true;
encodeQuotes=true;
alwaysMakeTags=true;
}/**Setdebugflagtotrue.Otherwiseusedefaultsettings.Seethedefaultconstructor.
*
*@paramdebugturndebugonwithatrueargument
*/
publicHTMLFilter(finalbooleandebug){
this();
vDebug=debug;}/**Map-parameterconfigurableconstructor.
*
*@paramconfmapcontainingconfiguration.keysmatchfieldnames.
*/
publicHTMLFilter(finalMap<String,Object>conf){assertconf.containsKey("vAllowed"):"configurationrequiresvAllowed";
assertconf.containsKey("vSelfClosingTags"):"configurationrequiresvSelfClosingTags";
assertconf.containsKey("vNeedClosingTags"):"configurationrequiresvNeedClosingTags";
assertconf.containsKey("vDisallowed"):"configurationrequiresvDisallowed";
assertconf.containsKey("vAllowedProtocols"):"configurationrequiresvAllowedProtocols";
assertconf.containsKey("vProtocolAtts"):"configurationrequiresvProtocolAtts";
assertconf.containsKey("vRemoveBlanks"):"configurationrequiresvRemoveBlanks";
assertconf.containsKey("vAllowedEntities"):"configurationrequiresvAllowedEntities";vAllowed=Collections.unmodifiableMap((HashMap<String,List<String>>)conf.get("vAllowed"));
vSelfClosingTags=(String[])conf.get("vSelfClosingTags");
vNeedClosingTags=(String[])conf.get("vNeedClosingTags");
vDisallowed=(String[])conf.get("vDisallowed");
vAllowedProtocols=(String[])conf.get("vAllowedProtocols");
vProtocolAtts=(String[])conf.get("vProtocolAtts");
vRemoveBlanks=(String[])conf.get("vRemoveBlanks");
vAllowedEntities=(String[])conf.get("vAllowedEntities");
stripComment=conf.containsKey("stripComment")?(Boolean)conf.get("stripComment"):true;
encodeQuotes=conf.containsKey("encodeQuotes")?(Boolean)conf.get("encodeQuotes"):true;
alwaysMakeTags=conf.containsKey("alwaysMakeTags")?(Boolean)conf.get("alwaysMakeTags"):true;
}privatevoidreset(){
vTagCounts.clear();
}privatevoiddebug(finalStringmsg){
if(vDebug){
Logger.getAnonymousLogger().info(msg);
}
}//---------------------------------------------------------------
//myversionsofsomePHPlibraryfunctions
publicstaticStringchr(finalintdecimal){
returnString.valueOf((char)decimal);
}publicstaticStringhtmlSpecialChars(finalStrings){
Stringresult=s;
result=regexReplace(P_AMP,"&",result);
result=regexReplace(P_QUOTE,""",result);
result=regexReplace(P_LEFT_ARROW,"<",result);
result=regexReplace(P_RIGHT_ARROW,">",result);
returnresult;
}//---------------------------------------------------------------
/**
*givenausersubmittedinputString,filteroutanyinvalidorrestricted
*html.
*
*@paraminputtext(i.e.submittedbyauser)thanmaycontainhtml
*@return"clean"versionofinput,withonlyvalid,whitelistedhtmlelementsallowed
*/
publicStringfilter(finalStringinput){
reset();
Strings=input;debug("************************************************");
debug("INPUT:"+input);s=escapeComments(s);
debug("escapeComments:"+s);s=balanceHTML(s);
debug("balanceHTML:"+s);s=checkTags(s);
debug("checkTags:"+s);s=processRemoveBlanks(s);
debug("processRemoveBlanks:"+s);s=validateEntities(s);
debug("validateEntites:"+s);debug("************************************************\n\n");
returns;
}publicbooleanisAlwaysMakeTags(){
returnalwaysMakeTags;
}publicbooleanisStripComments(){
returnstripComment;
}privateStringescapeComments(finalStrings){
finalMatcherm=P_COMMENTS.matcher(s);
finalStringBufferbuf=newStringBuffer();
if(m.find()){
finalStringmatch=m.group(1);//(.*?)
m.appendReplacement(buf,Matcher.quoteReplacement("<!--"+htmlSpecialChars(match)+"-->"));
}
m.appendTail(buf);returnbuf.toString();
}privateStringbalanceHTML(Strings){
if(alwaysMakeTags){
//
//tryandformhtml
//
s=regexReplace(P_END_ARROW,"",s);
s=regexReplace(P_BODY_TO_END,"<$1>",s);
s=regexReplace(P_XML_CONTENT,"$1<$2",s);}else{
//
//escapestraybrackets
//
s=regexReplace(P_STRAY_LEFT_ARROW,"<$1",s);
s=regexReplace(P_STRAY_RIGHT_ARROW,"$1$2><",s);//
//thelastregexpcauses'<>'entitiestoappear
//(weneedtodoalookaheadassertionsothatthelastbracketcan
//beusedinthenextpassoftheregexp)
//
s=regexReplace(P_BOTH_ARROWS,"",s);
}returns;
}privateStringcheckTags(Strings){
Matcherm=P_TAGS.matcher(s);finalStringBufferbuf=newStringBuffer();
while(m.find()){
StringreplaceStr=m.group(1);
replaceStr=processTag(replaceStr);
m.appendReplacement(buf,Matcher.quoteReplacement(replaceStr));
}
m.appendTail(buf);s=buf.toString();//thesegettalliedinprocessTag
//(remembertoresetbeforesubsequentcallstofiltermethod)
for(Stringkey:vTagCounts.keySet()){
for(intii=0;ii<vTagCounts.get(key);ii++){
s+="</"+key+">";
}
}returns;
}privateStringprocessRemoveBlanks(finalStrings){
Stringresult=s;
for(Stringtag:vRemoveBlanks){
if(!P_REMOVE_PAIR_BLANKS.containsKey(tag)){
P_REMOVE_PAIR_BLANKS.putIfAbsent(tag,Pattern.compile("<"+tag+"(\\s[^>]*)?></"+tag+">"));
}
result=regexReplace(P_REMOVE_PAIR_BLANKS.get(tag),"",result);
if(!P_REMOVE_SELF_BLANKS.containsKey(tag)){
P_REMOVE_SELF_BLANKS.putIfAbsent(tag,Pattern.compile("<"+tag+"(\\s[^>]*)?/>"));
}
result=regexReplace(P_REMOVE_SELF_BLANKS.get(tag),"",result);
}returnresult;
}privatestaticStringregexReplace(finalPatternregex_pattern,finalStringreplacement,finalStrings){
Matcherm=regex_pattern.matcher(s);
returnm.replaceAll(replacement);
}privateStringprocessTag(finalStrings){
//endingtags
Matcherm=P_END_TAG.matcher(s);
if(m.find()){
finalStringname=m.group(1).toLowerCase();
if(allowed(name)){
if(!inArray(name,vSelfClosingTags)){
if(vTagCounts.containsKey(name)){
vTagCounts.put(name,vTagCounts.get(name)-1);
return"</"+name+">";
}
}
}
}//startingtags
m=P_START_TAG.matcher(s);
if(m.find()){
finalStringname=m.group(1).toLowerCase();
finalStringbody=m.group(2);
Stringending=m.group(3);//debug("inastartingtag,name='"+name+"';body='"+body+"';ending='"+ending+"'");
if(allowed(name)){
Stringparams="";finalMatcherm2=P_QUOTED_ATTRIBUTES.matcher(body);
finalMatcherm3=P_UNQUOTED_ATTRIBUTES.matcher(body);
finalList<String>paramNames=newArrayList<String>();
finalList<String>paramValues=newArrayList<String>();
while(m2.find()){
paramNames.add(m2.group(1));//([a-z0⑼]+)
paramValues.add(m2.group(3));//(.*?)
}
while(m3.find()){
paramNames.add(m3.group(1));//([a-z0⑼]+)
paramValues.add(m3.group(3));//([^\"\\s']+)
}StringparamName,paramValue;
for(intii=0;ii<paramNames.size();ii++){
paramName=paramNames.get(ii).toLowerCase();
paramValue=paramValues.get(ii);//debug("paramName='"+paramName+"'");
//debug("paramValue='"+paramValue+"'");
//debug("allowed?"+vAllowed.get(name).contains(paramName));if(allowedAttribute(name,paramName)){
if(inArray(paramName,vProtocolAtts)){
paramValue=processParamProtocol(paramValue);
}
params+=""+paramName+"=\""+paramValue+"\"";
}
}if(inArray(name,vSelfClosingTags)){
ending="/";
}if(inArray(name,vNeedClosingTags)){
ending="";
}if(ending==null||ending.length()<1){
if(vTagCounts.containsKey(name)){
vTagCounts.put(name,vTagCounts.get(name)+1);
}else{
vTagCounts.put(name,1);
}
}else{
ending="/";
}
return"<"+name+params+ending+">";
}else{
return"";
}
}//comments
m=P_COMMENT.matcher(s);
if(!stripComment&&m.find()){
return"<"+m.group()+">";
}return"";
}privateStringprocessParamProtocol(Strings){
s=decodeEntities(s);
finalMatcherm=P_PROTOCOL.matcher(s);
if(m.find()){
finalStringprotocol=m.group(1);
if(!inArray(protocol,vAllowedProtocols)){
//badprotocol,turnintolocalanchorlinkinstead
s="#"+s.substring(protocol.length()+1,s.length());
if(s.startsWith("#//")){
s="#"+s.substring(3,s.length());
}
}
}returns;
}privateStringdecodeEntities(Strings){
StringBufferbuf=newStringBuffer();Matcherm=P_ENTITY.matcher(s);
while(m.find()){
finalStringmatch=m.group(1);
finalintdecimal=Integer.decode(match).intValue();
m.appendReplacement(buf,Matcher.quoteReplacement(chr(decimal)));
}
m.appendTail(buf);
s=buf.toString();buf=newStringBuffer();
m=P_ENTITY_UNICODE.matcher(s);
while(m.find()){
finalStringmatch=m.group(1);
finalintdecimal=Integer.valueOf(match,16).intValue();
m.appendReplacement(buf,Matcher.quoteReplacement(chr(decimal)));
}
m.appendTail(buf);
s=buf.toString();buf=newStringBuffer();
m=P_ENCODE.matcher(s);
while(m.find()){
finalStringmatch=m.group(1);
finalintdecimal=Integer.valueOf(match,16).intValue();
m.appendReplacement(buf,Matcher.quoteReplacement(chr(decimal)));
}
m.appendTail(buf);
s=buf.toString();s=validateEntities(s);
returns;
}privateStringvalidateEntities(finalStrings){
StringBufferbuf=newStringBuffer();//validateentitiesthroughoutthestring
Matcherm=P_VALID_ENTITIES.matcher(s);
while(m.find()){
finalStringone=m.group(1);//([^&;]*)
finalStringtwo=m.group(2);//(?=(;|&|$))
m.appendReplacement(buf,Matcher.quoteReplacement(checkEntity(one,two)));
}
m.appendTail(buf);returnencodeQuotes(buf.toString());
}privateStringencodeQuotes(finalStrings){
if(encodeQuotes){
StringBufferbuf=newStringBuffer();
Matcherm=P_VALID_QUOTES.matcher(s);
while(m.find()){
finalStringone=m.group(1);//(>|^)
finalStringtwo=m.group(2);//([^<]+?)
finalStringthree=m.group(3);//(<|$)
m.appendReplacement(buf,Matcher.quoteReplacement(one+regexReplace(P_QUOTE,""",two)+three));
}
m.appendTail(buf);
returnbuf.toString();
}else{
returns;
}
}privateStringcheckEntity(finalStringpreamble,finalStringterm){return";".equals(term)&&isValidEntity(preamble)
?'&'+preamble
:"&"+preamble;
}privatebooleanisValidEntity(finalStringentity){
returninArray(entity,vAllowedEntities);
}privatestaticbooleaninArray(finalStrings,finalString[]array){
for(Stringitem:array){
if(item!=null&&item.equals(s)){
returntrue;
}
}
returnfalse;
}privatebooleanallowed(finalStringname){
return(vAllowed.isEmpty()||vAllowed.containsKey(name))&&!inArray(name,vDisallowed);
}privatebooleanallowedAttribute(finalStringname,finalStringparamName){
returnallowed(name)&&(vAllowed.isEmpty()||vAllowed.get(name).contains(paramName));
}
}
在web.xml配置filter的使用示例:
<filter>
<filter-name>xssFilter</filter-name>
<filter-class>com.devframe.filter.XssFilter</filter-class>
</filter><filter-mapping>
<filter-name>xssFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
本文来源:https://www.yuntue.com/post/61917.html | 云服务器网,转载请注明出处!