2017-09-04 64 views
5

Tôi đang cố di chuyển dự án này https://github.com/asadsahi/AspNetCoreSpa từ .net lõi 1.1 đến 2.0 nhưng gặp sự cố sau khi đăng nhập thành công. Sau khi đăng nhập, GET GET của tôi gọi e. g. đến https://localhost:44331/api/profile/test kết thúc bằng chuyển hướng (302) và tôi không biết tại sao. Tôi đã nhận được một mã thông báo mang và nó có vẻ tốt.Cuộc gọi API ASP.NET Core 2 được chuyển hướng (302)

định dạng tiêu đề

Yêu cầu: ủy quyền: Bearer [thẻ]

[Route("api/[controller]")] 
public class ProfileController : BaseController 
{ 
    private readonly UserManager<ApplicationUser> _userManager; 
    private readonly ILogger _logger; 

    public ProfileController(ILoggerFactory loggerFactory, UserManager<ApplicationUser> userManager) 
    { 
     _logger = loggerFactory.CreateLogger<ProfileController>(); 
     _userManager = userManager; 
    } 

    [HttpGet("test")] 
    public async Task<IActionResult> Test() 
    { 
     return Json(ModelState.GetModelErrors()); 
    } 
} 

[Authorize] 
[ServiceFilter(typeof(ApiExceptionFilter))] 
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] 
public class BaseController : Controller 
{ 
    public BaseController() 
    { 
    } 
} 

Startup.cs:

public void ConfigureServices(IServiceCollection services) 
    { 
     if (_hostingEnv.IsDevelopment()) 
     { 
      services.AddSslCertificate(_hostingEnv); 
     } 
     else 
     { 
      services.Configure<MvcOptions>(o => o.Filters.Add(new RequireHttpsAttribute())); 
     } 
     services.AddOptions(); 
     services.AddCors(); 
     services.AddLogging(); 
     services.AddResponseCompression(options => 
     { 
      options.MimeTypes = Helpers.DefaultMimeTypes; 
     }); 

     services.AddAuthentication(sharedOptions => 
     { 
      sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 
      sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 
      sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 


     }).AddJwtBearer(cfg => 
     { 
      cfg.SaveToken = true; 
      cfg.TokenValidationParameters = new TokenValidationParameters 
      { 
       ValidIssuer = Configuration["Authentication:BearerTokens:Issuer"], 
       ValidAudience = Configuration["Authentication:BearerTokens:Audience"], 
       IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Authentication:BearerTokens:Key"])), 
       ValidateIssuerSigningKey = false, 
       ValidateLifetime = true, 
       ClockSkew = TimeSpan.Zero 
      }; 
      cfg.Events = new JwtBearerEvents 
      { 

       OnAuthenticationFailed = context => 
       { 
        var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents)); 
        logger.LogError("Authentication failed.", context.Exception); 
        return Task.CompletedTask; 
       }, 

       OnMessageReceived = context => 
       { 
        return Task.CompletedTask; 
       }, 
       OnChallenge = context => 
       { 
        var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents)); 
        logger.LogError("OnChallenge error", context.Error, context.ErrorDescription); 
        return Task.CompletedTask; 
       } 
      }; 
     }); 

     services.AddDbContext<ApplicationDbContext>(options => 
     { 
      string useSqLite = Startup.Configuration["Data:useSqLite"]; 
      if (useSqLite.ToLower() == "true") 
      { 
       options.UseSqlite(Startup.Configuration["Data:SqlLiteConnectionString"]); 
      } 
      else 
      { 
       options.UseSqlServer(Startup.Configuration["Data:SqlServerConnectionString"]); 
      } 
      options.UseOpenIddict(); 
     }); 


     services.AddIdentity<ApplicationUser, ApplicationRole>() 
      .AddEntityFrameworkStores<ApplicationDbContext>() 
      .AddDefaultTokenProviders(); 

     //services.ConfigureApplicationCookie(options => 
     //{ 

     // options.LoginPath = "/login"; 
     // options.Events.OnRedirectToLogin = context => 
     // { 
     //  if (context.Request.Path.StartsWithSegments("/api") && 
     //   context.Response.StatusCode == (int)HttpStatusCode.OK) 
     //  { 
     //   context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; 
     //  } 
     //  else 
     //  { 
     //   context.Response.Redirect(context.RedirectUri); 
     //  } 
     //  return Task.FromResult(0); 
     // }; 
     //}); 


     services.AddOAuthProviders(); 

     services.AddCustomOpenIddict(); 

     services.AddMemoryCache(); 

     services.RegisterCustomServices(); 

     services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); 

     services.AddCustomizedMvc(); 

     // Node services are to execute any arbitrary nodejs code from .net 
     services.AddNodeServices(); 

     services.AddSwaggerGen(c => 
     { 
      c.SwaggerDoc("v1", new Info { Title = "AspNetCoreSpa", Version = "v1" }); 
     }); 
    } 

public void Configure(IApplicationBuilder app) 
    { 
     app.AddDevMiddlewares(); 

     if (_hostingEnv.IsProduction()) 
     { 
      app.UseResponseCompression(); 
     } 

     app.SetupMigrations(); 

     app.UseXsrf(); 

     app.UseStaticFiles(); 

     app.UseAuthentication(); 

     app.UseMvc(routes => 
     { 
      // http://stackoverflow.com/questions/25982095/using-googleoauth2authenticationoptions-got-a-redirect-uri-mismatch-error 
      routes.MapRoute(name: "signin-google", template: "signin-google", defaults: new { controller = "Account", action = "ExternalLoginCallback" }); 

      routes.MapSpaFallbackRoute(
       name: "spa-fallback", 
       defaults: new { controller = "Home", action = "Index" }); 
     }); 
    } 

My IServiceCollection-Extensions:

public static IServiceCollection AddCustomizedMvc(this IServiceCollection services) 
    { 
     services.AddMvc(options => 
     { 
      options.Filters.Add(typeof(ModelValidationFilter)); 
     }) 
     .AddJsonOptions(options => 
     { 
      options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; 
     }); 

     return services; 
    } 

    public static IServiceCollection AddOAuthProviders(this IServiceCollection services) 
    { 
     services.AddAuthentication() 
      .AddFacebook(o => 
      { 
       o.AppId = Startup.Configuration["Authentication:Facebook:AppId"]; 
       o.AppSecret = Startup.Configuration["Authentication:Facebook:AppSecret"]; 
      }); 

     services.AddAuthentication() 
      .AddGoogle(o => 
      { 
       o.ClientId = Startup.Configuration["Authentication:Google:ClientId"]; 
       o.ClientSecret = Startup.Configuration["Authentication:Google:ClientSecret"]; 
      }); 
     services.AddAuthentication() 
      .AddTwitter(o => 
      { 
       o.ConsumerKey = Startup.Configuration["Authentication:Twitter:ConsumerKey"]; 
       o.ConsumerSecret = Startup.Configuration["Authentication:Twitter:ConsumerSecret"]; 
      }); 

     services.AddAuthentication() 
      .AddMicrosoftAccount(o => 
      { 
       o.ClientId= Startup.Configuration["Authentication:Microsoft:ClientId"]; 
       o.ClientSecret = Startup.Configuration["Authentication:Microsoft:ClientSecret"]; 
      }); 

     return services; 
    } 

    public static IServiceCollection AddCustomOpenIddict(this IServiceCollection services) 
    { 

     // Configure Identity to use the same JWT claims as OpenIddict instead 
     // of the legacy WS-Federation claims it uses by default (ClaimTypes), 
     // which saves you from doing the mapping in your authorization controller. 
     services.Configure<IdentityOptions>(options => 
     { 
      options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name; 
      options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject; 
      options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role; 

     }); 

     // Register the OpenIddict services. 
     services.AddOpenIddict() 
      // Register the Entity Framework stores. 
      .AddEntityFrameworkCoreStores<ApplicationDbContext>() 

      // Register the ASP.NET Core MVC binder used by OpenIddict. 
      // Note: if you don't call this method, you won't be able to 
      // bind OpenIdConnectRequest or OpenIdConnectResponse parameters. 
      .AddMvcBinders() 

      // Enable the token endpoint. 
      .EnableTokenEndpoint("/connect/token") 

      // Enable the password and the refresh token flows. 
      .AllowPasswordFlow() 
      .AllowRefreshTokenFlow() 

      // During development, you can disable the HTTPS requirement. 
      .DisableHttpsRequirement() 

      // Register a new ephemeral key, that is discarded when the application 
      // shuts down. Tokens signed using this key are automatically invalidated. 
      // This method should only be used during development. 
      .AddEphemeralSigningKey(); 

     // On production, using a X.509 certificate stored in the machine store is recommended. 
     // You can generate a self-signed certificate using Pluralsight's self-cert utility: 
     // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip 
     // 
     // services.AddOpenIddict() 
     //  .AddSigningCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75"); 
     // 
     // Alternatively, you can also store the certificate as an embedded .pfx resource 
     // directly in this assembly or in a file published alongside this project: 
     // 
     // services.AddOpenIddict() 
     //  .AddSigningCertificate(
     //   assembly: typeof(Startup).GetTypeInfo().Assembly, 
     //   resource: "AuthorizationServer.Certificate.pfx", 
     //   password: "OpenIddict"); 

     return services; 
    } 
    public static IServiceCollection AddCustomDbContext(this IServiceCollection services) 
    { 
     // Add framework services. 

     return services; 
    } 
    public static IServiceCollection RegisterCustomServices(this IServiceCollection services) 
    { 
     // New instance every time, only configuration class needs so its ok 
     services.Configure<SmsSettings>(options => Startup.Configuration.GetSection("SmsSettingsTwillio").Bind(options)); 
     services.AddTransient<UserResolverService>(); 
     services.AddTransient<IEmailSender, EmailSender>(); 
     services.AddTransient<ISmsSender, SmsSender>(); 
     services.AddScoped<ApiExceptionFilter>(); 
     return services; 
    } 

Đây gói của tôi:

<ItemGroup> 
<PackageReference Include="AspNet.Security.OAuth.Introspection" Version="2.0.0-*" /> 
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="2.0.0-*" /> 
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.AzureAppServicesIntegration" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Antiforgery" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Authentication.Twitter" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.0.0" /> 
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" /> 
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0" /> 
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" /> 
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.0.0" /> 
<PackageReference Include="Microsoft.AspNetCore.AngularServices" Version="1.1.0-beta-000002" /> 
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="1.0.0-beta3-final" /> 
<PackageReference Include="AspNet.Security.OAuth.LinkedIn" Version="1.0.0-beta3-final" /> 
<PackageReference Include="OpenIddict" Version="2.0.0-*" /> 
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="2.0.0-*" /> 
<PackageReference Include="OpenIddict.Mvc" Version="2.0.0-*" /> 
<PackageReference Include="SendGrid" Version="9.9.0" /> 
<PackageReference Include="MailKit" Version="1.18.0" /> 
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /> 
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="1.0.0" /> 
<PackageReference Include="Twilio" Version="5.6.3" /> 
<PackageReference Include="Stripe.net" Version="10.4.0" /> 
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> 
<PackageReference Include="Webpack" Version="4.0.0" /> 
<PackageReference Include="Serilog" Version="2.5.0" /> 
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" /> 
<PackageReference Include="Serilog.Sinks.Seq" Version="3.3.3" /> 
<PackageReference Include="Bogus" Version="17.0.1" /> 
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0"> 
    <PrivateAssets>All</PrivateAssets> 
</PackageReference> 
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0"> 
    <PrivateAssets>All</PrivateAssets> 
</PackageReference> 

    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0" PrivateAssets="All" /> 
    </ItemGroup> 

    <ItemGroup> 
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" /> 
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> 
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" /> 
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" /> 
    </ItemGroup> 

Dưới đây là các bản ghi của tôi:

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] 
    Request starting HTTP/1.1 GET http://localhost:44331/api/profile/test 
application/json; charset=UTF-8 
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] 
    Authorization failed for user: (null). 
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] 
    Authorization failed for user: (null). 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] 
    Authorization failed for the request at filter 
'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'. 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] 
    Authorization failed for the request at filter 
'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'. 
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1] 
    Executing ChallengeResult with authentication schemes(). 
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1] 
    Executing ChallengeResult with authentication schemes(). 
info: 
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12] 
    AuthenticationScheme: Identity.Application was challenged. 
info: 
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12] 
    AuthenticationScheme: Identity.Application was challenged. 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] 
    Executed action 
AspNetCoreSpa.Server.Controllers.api.ProfileController.Test (AspNetCoreSpa) 
in 43.3105ms 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] 
    Executed action 
AspNetCoreSpa.Server.Controllers.api.ProfileController.Test (AspNetCoreSpa) 
in 43.3105ms 
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] 
    Request finished in 67.4133ms 302 
infoinfo: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] 
    Request finished in 67.4133ms 302 
: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] 
    Request starting HTTP/1.1 GET http://localhost:44331/Account/Login? 
ReturnUrl=%2Fapi%2Fprofile%2Ftest application/json; charset=UTF-8 
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] 
    Request starting HTTP/1.1 GET http://localhost:44331/Account/Login? 
ReturnUrl=%2Fapi%2Fprofile%2Ftest application/json; charset=UTF-8 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] 
    Executing action method 
AspNetCoreSpa.Server.Controllers.HomeController.Index (AspNetCoreSpa) with 
arguments ((null)) - ModelState is Valid 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] 
    Executing action method 
AspNetCoreSpa.Server.Controllers.HomeController.Index (AspNetCoreSpa) with 
arguments ((null)) - ModelState is Valid 
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1] 
    Executing ViewResult, running view at path /Views/Home/Index.cshtml. 
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1] 
    Executing ViewResult, running view at path /Views/Home/Index.cshtml. 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] 
    Executed action AspNetCoreSpa.Server.Controllers.HomeController.Index 
(AspNetCoreSpa) in 13.2746ms 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] 
    Executed action AspNetCoreSpa.Server.Controllers.HomeController.Index 
(AspNetCoreSpa) in 13.2746ms 
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] 
    Request finished in 79.2352ms 200 text/html; charset=utf-8 
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] 
    Request finished in 79.2352ms 200 text/html; charset=utf-8 

Tôi tự hỏi về dòng sau:

Authorization sai đối với user: (null)

Đã tìm thấy ASP.NET Core - Authorization failed for user: (null) này nhưng có không có câu trả lời nào được nêu ra và tôi nghĩ rằng nó là một .net lõi 1 vấn đề.

Cảm ơn sự giúp đỡ của bạn!

Trân trọng Simon

+0

Tôi gặp sự cố chính xác tương tự. 302 đang cố đưa tôi đến ~/Tài khoản/Đăng nhập. Sau đó, tôi nhận được 404 vì tuyến đường đó không tồn tại. – Bloodhound

+0

Tôi cũng phải đối mặt với cùng một vấn đề sau khi nâng cấp lên lõi asp.net 2 – cuppy

+0

bạn có giải quyết được vấn đề này không? – Hristo

Trả lời

1

tôi gặp phải vấn đề tương tự và để giải quyết vấn đề tôi phải bao gồm các chương trình xác thực trong Authorize thuộc tính trên bộ điều khiển.

Trong trường hợp của bạn:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 
[ServiceFilter(typeof(ApiExceptionFilter))] 
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] 
public class BaseController : Controller 
{ 
    public BaseController() 
    { 
    } 
} 
0

Đối với tôi, nó có liên quan đến Cross-Origin Resource Sharing (CORS). Tôi có một API trong một dịch vụ ứng dụng trong Azure với CORS được kích hoạt. Tôi có 302 biến mất khi tôi thêm người gọi API vào danh sách các nguồn gốc được cho phép.

Tôi phải làm điều này mặc dù tôi đã thêm các nguồn gốc này vào mã khởi động lõi aspnet của mình.

0

Khi bạn gọi AddIdentity, nó thêm xác thực Cookie, ghi đè xác thực người mang JWT dự định của bạn. Một cách để giải quyết vấn đề này là chuyển cuộc gọi AddIdentity trước khi thiết lập xác thực JWT. Sau đây là đoạn code mà làm việc cho tôi:

// setup identity 
services.AddIdentity<ApplicationUser, ApplicationRole>() 
    .AddEntityFrameworkStores<MyMoneyDbContext>() 
    .AddDefaultTokenProviders(); 

// setup Jwt authentication 
services.AddAuthentication(options => 
{ 
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 
}) 
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => 
{ 
    jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters 
    { 
     ValidateIssuerSigningKey = true, 
     ... 

Một lựa chọn khác là sử dụng AddIdentityCore nhưng tôi không bao giờ cố gắng đó.

Các vấn đề liên quan