如何验证从一个应用程序在 .NetCore API 中的不同应用程序上发出的 AntiForgeryToken? [英] How to validate AntiForgeryToken issued from one Application on different Application in .NetCore API?

查看:17
本文介绍了如何验证从一个应用程序在 .NetCore API 中的不同应用程序上发出的 AntiForgeryToken?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有两个应用程序,ASP.NET Core Web APP (app 1) 和 .NET CORE API (app 2).我在 (app 1) 中发出一个 AntiForgeryToken 并以隐藏字段的形式返回它(实际上 .netcore 使它自动).现在,当用户提交表单时,数据转到 (app 2) 并使用 CORS,我能够将 (app 1) 发布的 AntiForgeryToken cookie 发送到 (app 2) 以便我可以验证它并在 (app 2) 我添加了 validateAntiFOorgeryToken 属性以便我可以验证它.以下是我在 chrome 开发工具响应选项卡上收到的错误.

我尝试了所有方法并搜索了所有 SO 但此特定错误无法找到答案.

System.InvalidOperationException:没有注册Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter"类型的服务.在 Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)在 Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)在 Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute.CreateInstance(IServiceProvider serviceProvider)在 Microsoft.AspNetCore.Mvc.Filters.DefaultFilterProvider.ProvideFilter(FilterProviderContext context, FilterItem filterItem)在 Microsoft.AspNetCore.Mvc.Filters.DefaultFilterProvider.OnProvidersExecuting(FilterProviderContext 上下文)在 Microsoft.AspNetCore.Mvc.Filters.FilterFactory.CreateUncachedFiltersCore(IFilterProvider[] filterProviders, ActionContext actionContext, List`1 filterItems)在 Microsoft.AspNetCore.Mvc.Filters.FilterFactory.GetAllFilters(IFilterProvider[] filterProviders, ActionContext actionContext)在 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvokerCache.GetCachedResult(ControllerContext controllerContext)在 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvokerProvider.OnProvidersExecuting(ActionInvokerProviderContext 上下文)在 Microsoft.AspNetCore.Mvc.Infrastructure.ActionInvokerFactory.CreateInvoker(ActionContext actionContext)在 Microsoft.AspNetCore.Mvc.Routing.ActionEndpointFactory.<>c__DisplayClass7_0.<CreateRequestDelegate>b__0(HttpContext context)在 Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)--- 从上一个抛出异常的位置开始的堆栈跟踪结束 ---在 Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext 上下文)在 Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext 上下文)标题========接受:application/json, text/javascript, */*;q=0.01接受编码:gzip、deflate、br接受语言:en-GB,en-US;q=0.9,en;q=0.8缓存控制:无缓存连接:关闭内容长度:98内容类型:应用程序/json饼干:.AspNetCore.Antiforgery.P09gDl3q4JU = CfDJ8MTlw3i2dFxEnHbgYLq-NTBvWTMlXSM5JV9sH03i3b4ulUq0JlSns86jxwas797wsxz9mOS2JDlK6nhntJGc80bpNNwUyUnOQou-iTEwsykSBE7-yfc05pjknlLMNciWmrLzxHaJ-kpG8Tjnqo7jxbc主机:本地主机:44375编译指示:无缓存推荐人:https://localhost:44389/account/signup用户代理:Mozilla/5.0(Windows NT 10.0;Win64;x64)AppleWebKit/537.36(KHTML,如 Gecko)Chrome/79.0.3945.88 Safari/537.36来源:https://localhost:44389x-xsrf-token:CfDJ8MTlw3i2dFxEnHbgYLq-NTB8DUVF73lPTj3HAOL2bhD-sIDv2N4Fs0JowHt6W-WB5oQltt8ELtCH03XYq2QAxz96SYSeDR-pZ6XMXyQNhpYpZ6XMXyQNhpYpYfWpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYsgsrf-x-xsrf-token:x-xsrf-tokensec-fetch-site: 同一站点秒取模式:cors

这是应用程序 1 的代码:

(查看)

<div class="form-inputs"><form id="signup-form" method="post"><!-- 输入和提交元素-->@Html.AntiForgeryToken()<input type="email" name="email" data-send="true" placeholder="Email Address"/><span class="error"></span><input type="password" name="password" data-send="true" placeholder="Password" data-validate="true" data-val-min="8" data-val-max="20"data-val-length-error="密码必须在 8 到 20 个字符之间" data-val-empty-error="需要密码"/><span class="error"></span><input type="password" name="confirmpassword" data-send="true" data-validate="true" data-val-compare="password" data-val-empty-error="需要确认密码"data-val-compare-error="密码不匹配" placeholder="确认密码"/><span class="error"></span><input type="text" name="username" placeholder="Username" data-send="true" data-validate="true" data-val-min="3" data-val-max="10"data-val-length-error="用户名必须在 3 到 10 个字符之间" data-val-empty-error="需要用户名"/><span class="error"></span><div class="button-holder"><button id="signup" class="action"><span id="loader"><i class="fas fa-spinner"></i></span><span id="button-text">注册</span></button>

<div class="terms-holder"><p>创建帐户即表示您同意我们的<span><a href="#">使用条款</a></span>和<span><a href="#">隐私政策</a></span></p>

</表单>

这里是应用程序 2 的代码:

(startup.cs)

使用 System.Text;使用 Authentication.Models;使用 Authentication.Services;使用 Microsoft.AspNetCore.Authentication.JwtBearer;使用 Microsoft.AspNetCore.Builder;使用 Microsoft.AspNetCore.Hosting;使用 Microsoft.AspNetCore.Http;使用 Microsoft.Extensions.Configuration;使用 Microsoft.Extensions.DependencyInjection;使用 Microsoft.Extensions.Hosting;使用 Microsoft.IdentityModel.Tokens;命名空间 API{公开课启动{公共启动(IConfiguration配置){配置=配置;}公共 IConfiguration 配置 { 获取;}只读字符串 OnlyOrigin = "OnlyOrigin";//这个方法被运行时调用.使用此方法向容器添加服务.public void ConfigureServices(IServiceCollection 服务){services.AddCors(options =>{options.AddDefaultPolicy(建造者 =>builder.WithOrigins("https://localhost:44389").AllowAnyMethod().AllowCredentials().WithHeaders("content-type","X-XSRF-TOKEN"));});services.AddAntiforgery(options =>{//使用 CookieBuilder 属性设置 Cookie 属性†.options.HeaderName = "X-XSRF-TOKEN";options.SuppressXFrameOptionsHeader = false;});//配置强类型设置对象var appSettingsSection = Configuration.GetSection("AppSettings");services.Configure(appSettingsSection);//配置jwt认证var appSettings = appSettingsSection.Get();var key = Encoding.ASCII.GetBytes(appSettings.Secret);services.AddAuthentication(x =>{x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(x =>{x.RequireHttpsMetadata = false;x.SaveToken = true;x.TokenValidationParameters = 新的 TokenValidationParameters{ValidateIssuerSigningKey = true,IssuerSigningKey = new SymmetricSecurityKey(key),ValidateIssuer = 假,验证受众 = 假};});services.AddControllers();services.AddScoped<IAccount, AccountManager>();services.AddSingleton();}//这个方法被运行时调用.使用此方法配置 HTTP 请求管道.公共无效配置(IApplicationBuilder 应用程序,IWebHostEnvironment 环境){如果 (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseCors();app.UseHttpsRedirection();app.UseRouting();app.UseAuthorization();app.UseEndpoints(endpoints =>{端点.MapControllers();});}}}

(控制器):

使用 API.Models;使用 Authentication.Models;使用 Authentication.Services;使用 Microsoft.AspNetCore.Cors;使用 Microsoft.AspNetCore.Http;使用 Microsoft.AspNetCore.Mvc;命名空间 API.Controllers{[路由(api/身份验证")][接口控制器]公共类 AuthenticationController : ControllerBase{私有只读 IAccount 帐户服务;私有只读 HttpContext 上下文;公共 AuthenticationController(IAccount accountService, IHttpContextAccessor contextAccessor){this.accountService = accountService;this.Context = contextAccessor.HttpContext;}[HttpPost][消耗(应用程序/json")][产生(应用程序/json")][路线(帐户/注册")]//[EnableCors("Only Origin")][验证AntiForgeryToken]public string Signup([FromBody] Account account){如果(!ModelState.IsValid){返回无效";}返回 accountService.CreateAccount(account);}[HttpPost][消耗(应用程序/json")][产生(应用程序/json")][路由(帐户/登录")][验证AntiForgeryToken]公共 IActionResult 登录(LoginViewModel loginViewModel){如果 (!ModelState.IsValid){返回错误请求(新结果{类型 = "错误",返回 = "无效"});}结果 result = accountService.Login(loginViewModel.Email, loginViewModel.Password);返回确定(结果);}}}

(JQuery) :

this.send = function () {警报(CSRF);$.ajax({url: "https://localhost:44375/api/authentication/account/signup",标题:{'内容类型':'应用程序/json','X-XSRF-令牌':csrf},xhr 字段:{withCredentials: 真},方法:'POST',数据类型:'json',数据:getDataToSend(),成功:功能(数据){console.log('成功:' + 数据);}});}

(请求头):

:authority: 本地主机:44375:方法: POST:path:/api/authentication/account/signup:方案:https接受:应用程序/json、文本/javascript、*/*;q=0.01接受编码:gzip、deflate、br接受语言:en-GB,en-US;q=0.9,en;q=0.8缓存控制:无缓存内容长度:98内容类型:应用程序/json饼干:.AspNetCore.Antiforgery.P09gDl3q4JU = CfDJ8MTlw3i2dFxEnHbgYLq-NTBvWTMlXSM5JV9sH03i3b4ulUq0JlSns86jxwas797wsxz9mOS2JDlK6nhntJGc80bpNNwUyUnOQou-iTEwsykSBE7-yfc05pjknlLMNciWmrLzxHaJ-kpG8Tjnqo7jxbc来源:https://localhost:44389pragma: 无缓存推荐人:https://localhost:44389/account/signup秒取模式:corssec-fetch-site: 同一站点用户代理:Mozilla/5.0(Windows NT 10.0;Win64;x64)AppleWebKit/537.36(KHTML,如 Gecko)Chrome/79.0.3945.88 Safari/537.36x-xsrf-token:CfDJ8MTlw3i2dFxEnHbgYLq-NTB8DUVF73lPTj3HAOL2bhD-sIDv2N4Fs0JowHt6W-WB5oQltt8ELtCH03XYq2QAxz96SYSeDR-pZ6XMXyQnhpYpZ6XMXYQNhpYpYfWpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYpYsG-srf-token:x-xsrf-token:CfDJ8MTlw3i2dFxEnHbgYLq-NTB8DUVF73lPTj3HAOL2bhD-sIDv2N4Fs0JowHt6W-WB5oQltt8

解决方案

如果您不希望发生该异常,则必须使用

services.AddControllersWithViews()

代替

services.AddControllers()

I have two Apps, ASP.NET Core Web APP ( app 1 ) and .NET CORE API ( app 2 ). I issue an AntiForgeryToken in the ( app 1 ) and return it in form as hidden field ( actually .netcore makes it automatically ). now when the user submit the form the data goes to ( app 2 ) and using CORS I was able to send the cookie of AntiForgeryToken issued by ( app 1 ) to ( app 2 ) so i can validate it and on ( app 2 ) I added the validateAntiFOrgeryToken Attribute so that I can validate it. and below is the error I receive on chrome dev tools response tab.

I tried everything and searched all SO but this specific error cant find an answer to it.

System.InvalidOperationException: No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter' has been registered.
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute.CreateInstance(IServiceProvider serviceProvider)
   at Microsoft.AspNetCore.Mvc.Filters.DefaultFilterProvider.ProvideFilter(FilterProviderContext context, FilterItem filterItem)
   at Microsoft.AspNetCore.Mvc.Filters.DefaultFilterProvider.OnProvidersExecuting(FilterProviderContext context)
   at Microsoft.AspNetCore.Mvc.Filters.FilterFactory.CreateUncachedFiltersCore(IFilterProvider[] filterProviders, ActionContext actionContext, List`1 filterItems)
   at Microsoft.AspNetCore.Mvc.Filters.FilterFactory.GetAllFilters(IFilterProvider[] filterProviders, ActionContext actionContext)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvokerCache.GetCachedResult(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvokerProvider.OnProvidersExecuting(ActionInvokerProviderContext context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionInvokerFactory.CreateInvoker(ActionContext actionContext)
   at Microsoft.AspNetCore.Mvc.Routing.ActionEndpointFactory.<>c__DisplayClass7_0.<CreateRequestDelegate>b__0(HttpContext context)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: application/json, text/javascript, */*; q=0.01
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cache-Control: no-cache
Connection: close
Content-Length: 98
Content-Type: application/json
Cookie: .AspNetCore.Antiforgery.P09gDl3q4JU=CfDJ8MTlw3i2dFxEnHbgYLq-NTBvWTMlXSM5JV9sH03i3b4ulUq0JlSns86jxwas797wsxz9mOS2JDlK6nhntJGc80bpNNwUyUnOQou-iTEwsykSBE7-yfc05pjknlLMNciWmrLzxHaJ-kpG8Tjnqo7jxbc
Host: localhost:44375
Pragma: no-cache
Referer: https://localhost:44389/account/signup
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
origin: https://localhost:44389
x-xsrf-token: CfDJ8MTlw3i2dFxEnHbgYLq-NTB8DUVF73lPTj3HAOL2bhD-sIDv2N4Fs0JowHt6W-WB5oQltt8ELtCH03XYq2QAxz96SYseDR-pZ6XMXAXRE8mpGUatGKtWp_yNohpbYt2ZQD25NzYqYdw-fCi3hsdQf1g
sec-fetch-site: same-site
sec-fetch-mode: cors

here is the code for app 1:

( View )

<div class="form-container">

    <div class="form-inputs">
        <form id="signup-form" method="post">
            <!-- Input and Submit elements -->
            @Html.AntiForgeryToken()
            <input type="email" name="email" data-send="true" placeholder="Email Address" />
            <span class="error"></span>
            <input type="password" name="password" data-send="true" placeholder="Password" data-validate="true" data-val-min="8" data-val-max="20" data-val-length-error="Password must be between 8 and 20 characters" data-val-empty-error="Password is required" />
            <span class="error"></span>
            <input type="password" name="confirmpassword" data-send="true" data-validate="true" data-val-compare="password" data-val-empty-error="Confirm Password is required" data-val-compare-error="Password doesn't match" placeholder="Confirm Password" />
            <span class="error"></span>
            <input type="text" name="username" placeholder="Username" data-send="true" data-validate="true" data-val-min="3" data-val-max="10" data-val-length-error="Username must be between 3 and 10 characters" data-val-empty-error="Username is required" />
            <span class="error"></span>
            <div class="button-holder">
                <button id="signup" class="action"><span id="loader"><i class="fas fa-spinner"></i></span><span id="button-text">signup</span></button>
            </div>
            <div class="terms-holder">
                <p>By Creating an account you agree to our <span><a href="#">Terms of Use</a></span> and <span><a href="#">Privacy Policy</a></span></p>
            </div>
        </form>



    </div>
</div>

and here the code for app 2 :

(startup.cs)

using System.Text;
using Authentication.Models;
using Authentication.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }
        readonly string OnlyOrigin = "OnlyOrigin";

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {


            services.AddCors(options => 
            {
                options.AddDefaultPolicy(
                builder => builder.WithOrigins("https://localhost:44389")
                    .AllowAnyMethod()
                    .AllowCredentials()
                    .WithHeaders("content-type","X-XSRF-TOKEN")
                    );

            });
            services.AddAntiforgery(options =>
            {
                // Set Cookie properties using CookieBuilder properties†.
                options.HeaderName = "X-XSRF-TOKEN";
                options.SuppressXFrameOptionsHeader = false;
            });
            // configure strongly typed settings objects
            var appSettingsSection = Configuration.GetSection("AppSettings");
            services.Configure<AppSettings>(appSettingsSection);

            // configure jwt authentication
            var appSettings = appSettingsSection.Get<AppSettings>();
            var key = Encoding.ASCII.GetBytes(appSettings.Secret);
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
            });
            services.AddControllers();
            services.AddScoped<IAccount, AccountManager>();
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors();
            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

( controller ) :

using API.Models;
using Authentication.Models;
using Authentication.Services;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
    [Route("api/authentication")]
    [ApiController]
    public class AuthenticationController : ControllerBase
    {
        private readonly IAccount accountService;
        private readonly HttpContext Context;

        public AuthenticationController(IAccount accountService, IHttpContextAccessor contextAccessor)
        {
            this.accountService = accountService;
            this.Context = contextAccessor.HttpContext;
        }

        [HttpPost]
        [Consumes("application/json")]
        [Produces("application/json")]
        [Route("account/signup")]
        //[EnableCors("Only Origin")]
        [ValidateAntiForgeryToken]
        public string Signup([FromBody] Account account)

        {
            if(!ModelState.IsValid)
            {
                return "not valid";
            }
            return accountService.CreateAccount(account);

        }

        [HttpPost]
        [Consumes("application/json")]
        [Produces("application/json")]
        [Route("account/signin")]
        [ValidateAntiForgeryToken]
        public IActionResult Signin(LoginViewModel loginViewModel)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(new Result
                {
                    Type = "Error",
                    Return = "not valid"
                });
            }
            Result result = accountService.Login(loginViewModel.Email, loginViewModel.Password);
            return Ok(result);
        }



    }
}

( JQuery ) :

this.send = function () {
        alert(csrf);
        $.ajax({
            url: "https://localhost:44375/api/authentication/account/signup",
            headers: {
                'Content-Type': 'application/json',
                'X-XSRF-TOKEN': csrf
            },
            xhrFields: {
                withCredentials: true
            },
            method: 'POST',
            dataType: 'json',
            data: getDataToSend(),
            success: function (data) {
                console.log('succes: ' + data);
            }
        });
    }

( Request Headers ) :

:authority: localhost:44375
:method: POST
:path: /api/authentication/account/signup
:scheme: https
accept: application/json, text/javascript, */*; q=0.01
accept-encoding: gzip, deflate, br
accept-language: en-GB,en-US;q=0.9,en;q=0.8
cache-control: no-cache
content-length: 98
content-type: application/json
cookie: .AspNetCore.Antiforgery.P09gDl3q4JU=CfDJ8MTlw3i2dFxEnHbgYLq-NTBvWTMlXSM5JV9sH03i3b4ulUq0JlSns86jxwas797wsxz9mOS2JDlK6nhntJGc80bpNNwUyUnOQou-iTEwsykSBE7-yfc05pjknlLMNciWmrLzxHaJ-kpG8Tjnqo7jxbc
origin: https://localhost:44389
pragma: no-cache
referer: https://localhost:44389/account/signup
sec-fetch-mode: cors
sec-fetch-site: same-site
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
x-xsrf-token: CfDJ8MTlw3i2dFxEnHbgYLq-NTB8DUVF73lPTj3HAOL2bhD-sIDv2N4Fs0JowHt6W-WB5oQltt8ELtCH03XYq2QAxz96SYseDR-pZ6XMXAXRE8mpGUatGKtWp_yNohpbYt2ZQD25NzYqYdw-fCi3hsdQf1g

解决方案

If you don't want that exception to occur you have to use

services.AddControllersWithViews()

instead of

services.AddControllers()

这篇关于如何验证从一个应用程序在 .NetCore API 中的不同应用程序上发出的 AntiForgeryToken?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
C#最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆