利用 IIS 中的浏览器缓存(谷歌页面速度问题) [英] Leverage browser caching in IIS (google pagespeed issue)

查看:27
本文介绍了利用 IIS 中的浏览器缓存(谷歌页面速度问题)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有几个关于利用浏览器缓存的问题,但我没有找到任何关于如何在 ASP.NET 应用程序中执行此操作的有用信息.Google 的 Pagespeed 告诉我们这是性能最大的问题.到目前为止,我在我的 web.config 中做到了这一点:

There are several questions about leveraging browser caching but I didn't find anything useful for how to do this in an ASP.NET application. Google's Pagespeed tells this is performance biggest problem. So far I did this in my web.config:

<system.webServer>
  <staticContent>
    <!--<clientCache cacheControlMode="UseExpires"
            httpExpires="Fri, 24 Jan 2014 03:14:07 GMT" /> -->
    <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.24:00:00" />
  </staticContent>
</system.webServer>

注释代码有效.我可以将过期标头设置为将来的某个特定时间,但我无法设置 cacheControlMaxAge 来设置从现在开始缓存静态内容的天数.这是行不通的.我的问题是:

Commented code works. I can set expire header to be some particular time in future but I was not able to set cacheControlMaxAge to set how many days from now static content would be cached. It does not work. My questions is:

我该怎么做?我知道可以只为特定文件夹设置缓存,这将是一个很好的解决方案,但它也不起作用.应用程序托管在 Windows Server 2012 上,在 IIS8 上,应用程序池设置为经典.

How can I do that? I know it is possible to set caching only for specific folder which would be good solution, but it isn't working also. Application is hosted on Windows Server 2012,on IIS8, application pool is set to classic.

在网络配置中设置此代码后,页面速度为 72(之前为 71).50 个文件未缓存.(现在 49 岁)我想知道为什么,我刚刚意识到实际上缓存了一个文件(svg 文件).不幸的是 png 和 jpg 文件不是.这是我的 web.config

After I set this code in web config I got pagespeed of 72 (before was 71). 50 files were not cached. (Now 49) I was wondering why and I just realized that one file was actually cached (svg file). Unfortunately png and jpg file were not. This is my web.config

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

<configuration>
  <configSections>
    <section name="exceptionManagement" type="Microsoft.ApplicationBlocks.ExceptionManagement.ExceptionManagerSectionHandler,Microsoft.ApplicationBlocks.ExceptionManagement" />
    <section name="jsonSerialization"     type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions,   Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E34" requirePermission="false" allowDefinition="Everywhere" />
    <sectionGroup name="elmah">
      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"    />
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"    />
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    </sectionGroup>
  </configSections>

  <exceptionManagement mode="off">
    <publisher mode="off" assembly="Exception"  type="blabla.ExceptionHandler.ExceptionDBPublisher"  connString="server=188......;database=blabla;uid=blabla;pwd=blabla; " />
  </exceptionManagement>
  <location path="." inheritInChildApplications="false">
    <system.web>
      <httpHandlers>
        <add verb="GET,HEAD" path="ScriptResource.axd"  type="System.Web.Handlers.ScriptResourceHandler,System.Web.Extensions, Version=1.0.61025.0,  Culture=neutral, PublicKeyToken=31bf3856ad364e34" validate="false" />
        <add verb="GET" path="Image.ashx" type="blabla.WebComponents.ImageHandler, blabla/>"
        <add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory" />
        <add verb="*" path="*.jpg" type="System.Web.StaticFileHandler" />
        <add verb="GET" path="*.js" type="System.Web.StaticFileHandler" />
        <add verb="*" path="*.gif" type="System.Web.StaticFileHandler" />
        <add verb="GET" path="*.css" type="System.Web.StaticFileHandler" />
      </httpHandlers>
      <compilation defaultLanguage="c#" targetFramework="4.5.1" />
      <trace enabled="false" requestLimit="100" pageOutput="true" traceMode="SortByTime" localOnly="true"/>
      <authentication mode="Forms">
        <forms loginUrl="~/user/login.aspx">
          <credentials passwordFormat="Clear">
            <user name="blabla" password="blabla" />
          </credentials>
        </forms>
      </authentication>
      <authorization>
        <allow users="*" />
      </authorization>
      <sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="20" />
      <globalization requestEncoding="utf-8" responseEncoding="utf-8" culture="en-GB" uiCulture="en-GB" />
      <xhtmlConformance mode="Transitional" />
      <pages controlRenderingCompatibilityVersion="4.5" clientIDMode="AutoID">
        <namespaces>

        </namespaces>
        <controls>
          <add assembly="Microsoft.AspNet.Web.Optimization.WebForms" namespace="Microsoft.AspNet.Web.Optimization.WebForms" tagPrefix="webopt" />
        </controls>
      </pages>
      <webServices>
        <protocols>
          <add name="HttpGet" />
          <add name="HttpPost" />
        </protocols>
      </webServices>
    </system.web>
  </location>
  <appSettings>

  </appSettings>
  <connectionStrings>

  </connectionStrings>
  <system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization maxJsonLength="200000" />
      </webServices>
    </scripting>
  </system.web.extensions>
  <startup>
    <supportedRuntime version="v2.0.50727" />
    <supportedRuntime version="v1.1.4122" />
    <supportedRuntime version="v1.0.3705" />
  </startup>
  <system.webServer>


    <rewrite>
      <providers>
        <provider name="ReplacingProvider" type="ReplacingProvider, ReplacingProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5ab632b1f332b247">
          <settings>
            <add key="OldChar" value="_" />
            <add key="NewChar" value="-" />
          </settings>
        </provider>
        <provider name="FileMap" type="DbProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0525b0627da60a5e">
          <settings>
            <add key="ConnectionString" value="server=;database=blabla;uid=blabla;pwd=blabla;App=blabla"/>
            <add key="StoredProcedure" value="Search.GetRewriteUrl"/>
            <add key="CacheMinutesInterval" value="0"/>
          </settings>
        </provider>
      </providers>
      <rewriteMaps configSource="rewritemaps.config" />
      <rules configSource="rewriterules.config" />
    </rewrite>
    <modules>
      <remove name="ScriptModule" />
      <add name="ScriptModule" preCondition="managedHandler" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3456AD264E35" />
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
    <handlers>
      <add name="Web-JPG" path="*.jpg" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="C:WindowsMicrosoft.NETFramework64v4.0.30319aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
      <add name="Web-CSS" path="*.css" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="C:WindowsMicrosoft.NETFramework64v4.0.30319aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
      <add name="Web-GIF" path="*.gif" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="C:WindowsMicrosoft.NETFramework64v4.0.30319aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
      <add name="Web-JS" path="*.js" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="C:WindowsMicrosoft.NETFramework64v4.0.30319aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" />
    </handlers>
    <validation validateIntegratedModeConfiguration="false" />
    <httpErrors errorMode="DetailedLocalOnly" existingResponse="Auto">
      <remove statusCode="404" subStatusCode="-1"/>
      <remove statusCode="500" subStatusCode="-1"/>
      <error statusCode="404" path="error404.htm" responseMode="File"/>
      <error statusCode="500" path="error.htm" responseMode="File"/>
    </httpErrors>
  </system.webServer>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="soapBinding_AdriagateService" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="2147483647" maxBufferSize="2147483647" maxReceivedMessageSize="2147483647" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true" messageEncoding="Text">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" />
          <security mode="None" />
        </binding>
      </basicHttpBinding>
      <netTcpBinding>
        <binding name="NetTcpBinding_ITravellerService" closeTimeout="00:10:00" openTimeout="00:10:00" sendTimeout="00:10:00" maxReceivedMessageSize="2147483647" maxBufferPoolSize="2147483647">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" />
          <security mode="None" />
        </binding>
      </netTcpBinding>
    </bindings>
    <client>
      <endpoint address="blabla" bindingConfiguration="soapBinding_blabla" contract="" Address="blabla" name="blabla" />
        <endpoint address="blabla" binding="basicHttpBinding" bindingConfiguration="soapBinding_IImagesService"
          contract="ImagesService.IImagesService" name="soapBinding_IImagesService"/>
        <identity>
          <servicePrincipalName value="blabla"/>
        </identity>
      </endpoint>
    </client>
  </system.serviceModel>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.5.2.14234" newVersion="1.5.2.14234" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-4.5.0.0" newVersion="4.5.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.web>
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" />
    </httpModules>
  </system.web>
  <elmah>
    <security allowRemoteAccess="false" />
  </elmah>
  <location path="elmah.axd" inheritInChildApplications="false">
    <system.web>
      <httpHandlers>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
      </httpHandlers>

    </system.web>
    <system.webServer>
      <handlers>
        <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
      </handlers>
    </system.webServer>
  </location>
</configuration>

如果我设置了确切的到期日期,则缓存有效,但不适用于 jpg、gif ....仅适用于 png

If I set exact expiry date, caching is working, but not for jpg,gif....only for png

如果我在这里设置 cacheControlCustom="public" :

If I set cacheControlCustom="public" like here:

<clientCache cacheControlCustom="public" 
cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" /> 

缓存正在工作,但仍然不适用于 jpeg 和 gif;它只适用于 svgs 和 pngs.

caching is working but still not for jpegs and gifs; it only works for svgs and pngs.

推荐答案

大多数浏览器缓存问题都可以通过查看响应头来解决(可以在 Google chrome 开发者工具中完成).

Most of the browser caching issues can be resolved by viewing the response headers (can be done in Google chrome developer tools).

现在您的 web.config 文件的 clientCache 部分应该将您的输出缓存设置为最大年龄,如下图所示已经设置了 max-age86400,即 1 天(以秒为单位).

Now the clientCache section of your web.config file should set your output caching to a maximum age as you see in the image below has set the max-age to 86400 which is 1 day in seconds.

这是此设置的 web.config 片段.

Here is the web.config snippet for this setup.

<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />

现在太好了,响应标头在 Cache-Control 标头上设置了一个 max-age 属性.所以浏览器应该缓存内容.嗯,这基本上是正确的,但有些浏览器需要设置另一个标志.特别是为缓存控制标头设置的 public 标志.这可以通过使用 web.config 中的 cacheControlCustom 属性轻松添加.这是一个例子.

Now thats great, the response header has a max-age property set on the Cache-Control header. So the browser should be caching the content. Well this is mostly true but some browsers require another flag to be set. Specifically the public flag set for the cache control header. This can be easily added by using the cacheControlCustom attribute in the web.config. Here is an example.

<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />

现在,当我们重试页面并检查标题时.

Now when we retry the page and inspect the headers.

现在,从上图可以看出,我们现在拥有值 public, max-age=86400.所以我们的浏览器拥有缓存资源所需的一切.现在检查谷歌浏览器的标题和网络选项卡会对我们有所帮助.

Now as you can see from the image above we now have the value public, max-age=86400. So our browser has all it needs to cache the resources. Now examining the headers and the network tab of google chrome will help us.

这是对文件的第一个请求..注意文件没有被缓存......

Here is the first request to the file.. Note the file is not cached...

现在让我们回到这个页面(注意:不要刷新页面,我们稍后会讨论).您将看到现在从缓存返回的响应(如圆圈所示).

Now lets navigate back to this page (NOTE: do not refresh the page, we will talk about that in a second). You will see the response in now returning from cache (as circled).

现在如果我使用 F5 或使用浏览器刷新功能刷新页面会发生什么.等等.. (from cache) 去哪儿了.

Now what happens if I refresh the page using either F5 or using the browser refresh feature. Wait.. where did the (from cache) go.

在 Google Chrome(不确定其他浏览器)中使用刷新按钮将重新下载静态资源,而不管缓存标头如何(请在此处插入说明).这意味着资源已被重新检索,并且发送了最大年龄标头.

Well in Google Chrome (not sure about other browsers) using the refresh button will re-download the static resources regardless of the cache header (insert clarification here please). That means that the resources has been re-retrieved and the max age header sent over.

现在在完成上述所有解释之后,请务必测试如何监控缓存标头.

Now after all the explanation above, be sure to test how you are monitoring the cache headers.

更新

根据您的评论,您说您有一个名为 Image.ashx 的通用处理程序 (IHttpHandler),内容类型为 image/jpg.现在您可能期望默认行为是缓存此处理程序.但是,IIS 将扩展名 .ashx(正确地)视为动态脚本,并且在没有在代码本身中显式设置缓存标头的情况下不受缓存的影响.

Based on your comments you stated you have a generic handler (IHttpHandler) named Image.ashx with the content type of image/jpg. Now you may expect the default behaviour would be to cache this handler. However IIS sees the extension .ashx (correctly) as a dynamic script and is not subject to caching without explicitly setting the cache headers in the code itself.

现在这是您需要小心的地方,因为通常 IHttpHandlers 实际上不应该被缓存,因为它们通常提供动态内容.现在,如果该内容不太可能更改,您可以直接在代码中设置缓存标头.这是使用 Response 上下文在 IHttpHandlers 中设置缓存头的示例.

Now this is where you need to be careful, as typically IHttpHandlers should infact not be cached as they usually deliver dynamic content. Now if that content is unlikely to change you could set your cache headers directly in the code. Here is an example of setting cache headers in IHttpHandlers using the Response context.

context.Response.ContentType = "image/jpg";

context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetSlidingExpiration(true);

context.Response.TransmitFile(context.Server.MapPath("~/out.jpg"));

现在查看代码,我们在 Cache 属性上设置一些属性.为了获得所需的响应,我设置了属性.

Now looking at the code we are setting a few properties on the Cache property. To get the desired response I have set the properties.

  • context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1)); 告诉输出缓存设置 max-age= 部分的 >Cache-Control 标头在未来的 1 天(86400 秒).
  • context.Response.Cache.SetCacheability(HttpCacheability.Public); 告诉输出缓存将 Cache-Control 标头设置为 public.这非常重要,因为它告诉浏览器缓存到对象.
  • context.Response.Cache.SetSlidingExpiration(true); 告诉输出缓存确保它正在设置 Cache- 的 max-age= 部分正确控制 标题.如果不设置滑动过期时间,IIS 输出缓存将忽略最大年龄标头.把这些放在一起给了我这个结果.
  • context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1)); tells the out put cache to set the max-age= part of the Cache-Control header to be 1 day in the future (86400 seconds).
  • context.Response.Cache.SetCacheability(HttpCacheability.Public); tells the out put cache to set the Cache-Control header to public. This is quite important as it tells the browser to cache to object.
  • context.Response.Cache.SetSlidingExpiration(true); tells the output cache to ensure it is setting the max-age= part of the Cache-Control header properly. Without setting the sliding expiration, the IIS out put caching will ignore the max age header. Putting this together gives me this result.

正如我上面所说的,您可能不想缓存 .ashx 文件,因为它们通常提供动态内容.但是,如果该动态内容在给定时间内不太可能发生变化,您可以使用上述方法来交付您的 .ashx 文件.

As I stated above you may not want to cache the .ashx files as they typically deliver dynamic content. However if that dynamic content is not likely to change in a given period you can use the methods above to deliver your .ashx file.

现在结合上面列出的过程,您还可以设置 ETag(请参阅wiki) 缓存标头的组件,以便浏览器可以验证由自定义字符串传递的内容.维基指出:

Now in conjunction with the process listed above you could also set the ETag (see wiki) component of the cache headers so the browser can verify the content being delivered by a custom string. The wiki states:

ETag 是一个不透明的标识符,由网络服务器分配给特定的在 URL 中找到的资源的版本.如果资源内容在那URL 不断变化,分配了一个新的和不同的 ETag.

An ETag is an opaque identifier assigned by a web server to a specific version of a resource found at a URL. If the resource content at that URL ever changes, a new and different ETag is assigned.

所以这实际上是浏览器的某种唯一标识,用于识别响应中传递的内容.通过提供此标头,浏览器在下一次重新加载时将通过 If-None-Match 标头与来自上次响应的 ETag 一起发送.我们可以修改我们的处理程序以检测 If-None-Match 标头并将其与我们自己生成的 Etag 进行比较.现在没有精确的科学来生成 ETags,但一个好的经验法则是提供一个标识符,它很可能只定义一个实体.在这种情况下,我喜欢使用连接在一起的两个字符串,例如.

So this is really some sort of unique identification for the browser to identify the content being delivered in the response. By providing this header the browser on the next reload will send over a If-None-Match header with the ETag from the last response. We can modify our handler to detect the If-None-Match header and compare it to our own generated Etag. Now there is no exact science to generating ETags but a good rule of thumb is to deliver an identifier that will most likely define only one entity. In this case I like to use two strings concatenated together such as.

System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/saveNew.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();

在上面的代码片段中,我们正在从我们的文件系统加载一个文件(您可以从任何地方获取它).然后我使用 GetHashCode() 方法(在所有对象上)来获取对象的整数哈希码.在示例中,我连接了文件名的哈希值,然后是上次写入日期.最后写入日期的原因是如果文件被更改,哈希码也会更改,从而使指纹不同.

In the snippet above we are loading a file from our file system (you could get this from anywhere). I am then using the GetHashCode() method (on all objects) to get the integer hash code of the object. In the example I concat the hash of the file name, then the last write date. The reason for the last write date is in case the file is changed the hash code is changed as well, thus making the finger prints different.

这将生成一个类似于 306894467-210133036 的哈希码.

This will generate a hash code similar to 306894467-210133036.

那么我们如何在我们的处理程序中使用它.下面是处理程序的新修改版本.

So how do we use this in our handler. Below is the newly modified version of the handler.

System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/out.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();
var browserETag = context.Request.Headers["If-None-Match"];

context.Response.ClearHeaders();
if(browserETag == eTag)
{
    context.Response.Status = "304 Not Modified";
    context.Response.End();
    return;
}
context.Response.ContentType = "image/jpg";
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetSlidingExpiration(true);
context.Response.Cache.SetETag(eTag);
context.Response.TransmitFile(file.FullName);

如您所见,我已经更改了很多处理程序,但是您会注意到我们生成了 Etag 哈希,检查传入的 If-None-Match标题.如果 etag 哈希值和标头相等,那么我们通过返回状态代码 304 Not Modified 告诉浏览器内容没有改变.

As you can see, I have changed quite alot of the handler however you will notice that we generate the Etag hash, check for an incoming If-None-Match header. If the etag hash and the header are equal then we tell the browser that the content hasnt changed by returning the status code 304 Not Modified.

接下来是相同的处理程序,除了我们通过调用添加 ETag 标头:

Next was the same handler except we add the ETag header by calling:

context.Response.Cache.SetETag(eTag);

当我们在浏览器中运行它时,我们得到了.

When we run this up in the browser we get.

您将从图像中看到(因为我确实更改了文件名),我们现在拥有缓存系统的所有组件.ETag 作为标头传递,浏览器正在发送请求标头 If-None-Match,因此我们的处理程序可以相应地响应缓存文件更改.

You will see from the image (as i did change the file name) that we now have all the components of our cache system in place. The ETag is being delivered as a header, and the browser is sending the request header If-None-Match so our handler can respond accordingly to cache file changed.

这篇关于利用 IIS 中的浏览器缓存(谷歌页面速度问题)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆