当前位置: 首页 > 知识库问答 >
问题:

GRPC-web RPCException错误gRPC响应。无效的内容类型值:text/html;Charset=UTF-8

李意致
2023-03-14

我在尝试获取gRPC API(使用C#)到blazor客户端时遇到了一个错误,一开始它工作得很好,但是在添加IdentityServer4并使用CORS for gRPC-Web之后,就像在文档中一样。下面是与错误相关的代码。
backend/startup.cs

namespace BackEnd
{
    public class Startup
    {
        public IWebHostEnvironment Environment { get; }
        public IConfiguration Configuration { get; }
        private string _clientId = null;
        private string _clientSecret = null;

        public Startup(IWebHostEnvironment environment, IConfiguration configuration)
        {
            Environment = environment;
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            // Initialize certificate
            var cert = new X509Certificate2(Path.Combine(".", "IdsvCertificate.pfx"), "YouShallNotPass123");

            var migrationAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            // The connection strings is in user secret
            string connectionString = Configuration["ConnectionStrings:DefaultConnection"];

            _clientId = Configuration["OAuth:ClientId"];
            _clientSecret = Configuration["OAuth:ClientSecret"];

            services.AddControllersWithViews();

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(connectionString));

            services.AddIdentity<ApplicationUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddRoles<IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddClaimsPrincipalFactory<ClaimsFactory>()
                .AddDefaultTokenProviders();


            var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;

                // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                options.EmitStaticAudienceClaim = true;
                options.UserInteraction = new UserInteractionOptions() 
                { 
                    LoginUrl = "/Account/Login", 
                    LogoutUrl = "/Account/Logout" 
                };
            })
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddInMemoryApiResources(Config.ApiResources)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryClients(Config.Clients)
                .AddProfileService<ProfileService>()
                .AddAspNetIdentity<ApplicationUser>()
                .AddConfigurationStore(options => 
                {
                    options.ConfigureDbContext = b => b.UseNpgsql(connectionString, 
                        sql => sql.MigrationsAssembly(migrationAssembly));
                })
                .AddOperationalStore(options => 
                {
                    options.ConfigureDbContext = b => b.UseNpgsql(connectionString, 
                        sql => sql.MigrationsAssembly(migrationAssembly));
                });

            // Add signed certificate to identity server
            builder.AddSigningCredential(cert);
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            // Enable CORS for gRPC
            services.AddCors(o => o.AddPolicy("AllowAll", builder =>
            {
                builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
            }));

            // Add profile service
            services.AddScoped<IProfileService, ProfileService>();

            services.AddAuthentication()
                .AddGoogle("Google", options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

                    options.ClientId = _clientId;
                    options.ClientSecret = _clientSecret;
                    options.SaveTokens = true;
                    options.ClaimActions.MapJsonKey("role", "role");
                });

                services.AddAuthorization();

                services.AddGrpc(options => 
                {
                    options.EnableDetailedErrors = true;
                });
        }

        public void Configure(IApplicationBuilder app)
        {
            InitializeDatabase(app);

            if (Environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();

            app.UseRouting();
            app.UseIdentityServer();
            app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
            app.UseAuthentication();
            app.UseCors("AllowAll");
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<UserService>().RequireCors("AllowAll");
                endpoints.MapDefaultControllerRoute().RequireAuthorization();
            });
        }
        
        // Based on IdentityServer4 document
        private void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
                context.Database.Migrate();
                if (!context.Clients.Any())
                {
                    foreach (var client in Config.Clients)
                    {
                        context.Clients.Add(client.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.IdentityResources.Any())
                {
                    foreach (var resource in Config.IdentityResources)
                    {
                        context.IdentityResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.ApiScopes.Any())
                {
                    foreach (var resource in Config.ApiScopes)
                    {
                        context.ApiScopes.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }
            }
        }
    }
}

后端/服务/用户服务.cs

namespace BackEnd
{
    [Authorize(Roles="User")]
    public class UserService : User.UserBase
    
    {
        private readonly ILogger<UserService> _logger;
        private readonly ApplicationDbContext _dataContext;
        public UserService(ILogger<UserService> logger, ApplicationDbContext dataContext)
        {
            _logger = logger;
            _dataContext = dataContext;
        }

        public override async Task<Empty> GetUser(UserInfo request, ServerCallContext context)
        {
            var response = new Empty();
            var userList = new UserResponse();

            if (_dataContext.UserDb.Any(x => x.Sub == request.Sub))
            {
                var newUser = new UserInfo(){ Id = userList.UserList.Count, Sub = request.Sub, Email = request.Email };

                _dataContext.UserDb.Add(newUser);
                userList.UserList.Add(newUser);

                await _dataContext.SaveChangesAsync();
            }
            else
            {
                var user = _dataContext.UserDb.Single(u => u.Sub == request.Sub);
                userList.UserList.Add(user);
            }
            
            return await Task.FromResult(response);
        }

        public override async Task<ToDoItemList> GetToDoList(UuidParameter request, ServerCallContext context)
        {
            var todoList = new ToDoItemList();
            var userInfo = new UserInfo();

            var getTodo = (from data in _dataContext.ToDoDb
                           where data.Uuid == userInfo.Sub
                           select data).ToList();

            todoList.ToDoList.Add(getTodo);

            return await Task.FromResult(todoList);
        }

        public override async Task<Empty> AddToDo(ToDoStructure request, ServerCallContext context)
        {
            var todoList = new ToDoItemList();
            var userInfo = new UserInfo();
            var newTodo = new ToDoStructure()
            {
                Id = todoList.ToDoList.Count,
                Uuid = request.Uuid,
                Description = request.Description,
                IsCompleted = false
            };

            todoList.ToDoList.Add(newTodo);
            await _dataContext.ToDoDb.AddAsync(newTodo);
            await _dataContext.SaveChangesAsync();

            return await Task.FromResult(new Empty());
        }

        public override async Task<Empty> PutToDo(ToDoStructure request, ServerCallContext context)
        {
            var response = new Empty();
            _dataContext.ToDoDb.Update(request);
            await _dataContext.SaveChangesAsync();

            return await Task.FromResult(response);
        }

        public override async Task<Empty> DeleteToDo(DeleteToDoParameter request, ServerCallContext context)
        {
            var item = (from data in _dataContext.ToDoDb
                        where data.Id == request.Id
                        select data).First();
                        
            _dataContext.ToDoDb.Remove(item);
            var result = await _dataContext.SaveChangesAsync();

            return await Task.FromResult(new Empty());
            
        }
    } 
}

前端/程序.cs

namespace FrontEnd
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient()
                { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            // Connect server to client
            builder.Services.AddScoped(services => 
            {
                var baseAddressMessageHandler = services.GetRequiredService<AuthorizationMessageHandler>()
                    .ConfigureHandler(
                        authorizedUrls: new[] { "https://localhost:5001" },
                        scopes: new[] { "todoApi" }
                    );
                baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
                var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
                var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
                    { 
                        HttpHandler = httpHandler
                    });

                return new User.UserClient(channel);
            });
            
            // Add Open-ID Connect authentication
            builder.Services.AddOidcAuthentication(options =>
            {
                builder.Configuration.Bind("Authentication:Google", options.ProviderOptions);
                options.ProviderOptions.DefaultScopes.Add("role");
                options.UserOptions.RoleClaim = "role";  // Important to get role claim
            }).AddAccountClaimsPrincipalFactory<CustomUserFactory>();

            builder.Services.AddOptions();
            
            builder.Services.AddAuthorizationCore();

            await builder.Build().RunAsync();

        }
    }
}

前端/Pages/Todolist.razor.cs

namespace FrontEnd.Pages
{
    public partial class TodoList
    {
        [Inject]
        private User.UserClient UserClient { get; set; }
        [Inject]
        private IJSRuntime JSRuntime { get; set; }
        [CascadingParameter] 
        public Task<AuthenticationState> authenticationStateTask { get; set; }
        public string Description { get; set; }
        public string ToDoDescription { get; set; }
        public RepeatedField<ToDoStructure> ServerToDoResponse { get; set; } = new RepeatedField<ToDoStructure>();

        protected override async Task OnInitializedAsync()
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            Console.WriteLine($"IsAuthenticated: {user.Identity.IsAuthenticated} |  IsUser: {user.IsInRole("User")}");

            if (user.Identity.IsAuthenticated && user.IsInRole("User"))
            {
                await GetUser(); // Error when trying to call this function
            }
        }

        // Fetch usser from server
        public async Task GetUser()
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            var userRole = user.IsInRole("User");
            var userUuid = user.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
            var subjectId = user.Claims.FirstOrDefault(c => c.Type == "sub").Value;
            var userEmail = user.Claims.FirstOrDefault(c => c.Type == "email").Value;
            var request = new UserInfo(){ Sub = subjectId, Email = userEmail };

            await UserClient.GetUserAsync(request);
            await InvokeAsync(StateHasChanged);
            await GetToDoList();
        }

        // Fetch to-do list from server
        private async Task GetToDoList()
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            var userUuid = user.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
            var request = new UuidParameter(){ Uuid = userUuid };
            var response = await UserClient.GetToDoListAsync(request);
            ServerToDoResponse = response.ToDoList;
        }

        // Add to-do list to the server
        public async Task AddToDo(KeyboardEventArgs e)
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            var userUuid = user.Claims.FirstOrDefault(c => c.Type == "Sub").Value;

            if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(Description) || 
                e.Key == "NumpadEnter" && !string.IsNullOrWhiteSpace(Description))
            {
                var request = new ToDoStructure()
                { 
                    Uuid = userUuid, 
                    Description = this.Description, 
                };
                await UserClient.AddToDoAsync(request);
                await InvokeAsync(StateHasChanged);
                await GetToDoList();
            } 
        }

        // Update the checkbox state of the to-do list
        public async Task PutToDoIsCompleted(int id, string description, bool isCompleted, MouseEventArgs e)
        {
            if (isCompleted == false && e.Button== 0)
            {
                isCompleted = true;
            } 
            else if (isCompleted == true && e.Button == 0)
            {
                isCompleted = false;
            }

            var request = new ToDoStructure()
            { 
                Id = id, 
                Description = description, 
                IsCompleted = isCompleted 
            };

            await UserClient.PutToDoAsync(request);
            await GetToDoList();
        }

        // Edit mode function
        private async Task EditToDo(int todoId, string description, bool isCompleted)
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            var userUuid = user.Claims.FirstOrDefault(c => c.Type == "Sub").Value;
            // Get the index of the to-do list
            int grpcIndex = ServerToDoResponse.IndexOf(new ToDoStructure() 
            { 
                Id = todoId, 
                Uuid = userUuid,
                Description = description, 
                IsCompleted = isCompleted
            });

            ToDoDescription = ServerToDoResponse[grpcIndex].Description;

            // Make text area appear and focus on text area and edit icon dissapear based on the to-do list index
            await JSRuntime.InvokeVoidAsync("editMode", "edit-icon", "todo-description", "edit-todo", grpcIndex);
            await JSRuntime.InvokeVoidAsync("focusTextArea", todoId.ToString(), ToDoDescription);
        }

        // Update the to-do description
        public async Task PutToDoDescription(int id, string htmlId, string oldDescription, string newDescription, bool isCompleted)
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            var userUuid = user.Claims.FirstOrDefault(c => c.Type == "Sub").Value;
            var request = new ToDoStructure()
            { 
                Id = id, 
                Uuid = userUuid,
                Description = newDescription, 
            };

            int grpcIndex = ServerToDoResponse.IndexOf(new ToDoStructure() 
            { 
                Id = id, 
                Description = oldDescription, 
                IsCompleted = isCompleted
            });

            // Text area auto resize function
            await JSRuntime.InvokeVoidAsync("theRealAutoResize", htmlId);
            // Make text area display to none and edit icon appear base on the to-do list index
            await JSRuntime.InvokeVoidAsync("initialMode", "edit-icon", "todo-description", "edit-todo", grpcIndex);
            await UserClient.PutToDoAsync(request);
            await GetToDoList();
        }

        // Delete to-do
        public async Task DeleteToDo(int id)
        {
            var request = new DeleteToDoParameter(){ Id = id };
            
            await UserClient.DeleteToDoAsync(request);
            await GetToDoList();
        }
    }
}

这是控制台的输出

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Status(StatusCode="Cancelled", Detail="Bad gRPC response. Invalid content-type value: text/html; charset=utf-8")
Grpc.Core.RpcException: Status(StatusCode="Cancelled", Detail="Bad gRPC response. Invalid content-type value: text/html; charset=utf-8")
   at FrontEnd.Pages.TodoList.GetUser() in C:\Users\bryan\source\repos\Productivity_App\frontend\Pages\TodoList.razor.cs:line 50
   at FrontEnd.Pages.TodoList.OnInitializedAsync() in C:\Users\bryan\source\repos\Productivity_App\frontend\Pages\TodoList.razor.cs:line 35
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)

这是尝试使用IdentityServer4进行身份验证时终端中的输出(虽然身份验证和授权工作正常)

[21:11:15 Debug] Grpc.AspNetCore.Web.Internal.GrpcWebMiddleware
Detected gRPC-Web request from content-type 'application/grpc-web'.

[21:11:15 Information] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler
AuthenticationScheme: Identity.Application was challenged.

[21:11:15 Debug] IdentityServer4.Hosting.CorsPolicyProvider
CORS request made for path: /Account/Login from origin: https://localhost:5001 but was ignored because path was not for an allowed IdentityServer CORS endpoint

共有1个答案

郭永怡
2023-03-14

您不能将OpenID Connect身份验证作为gRPC的一部分,用户必须首先在您的网站上进行身份验证,然后您才应该收到访问令牌。

然后您可以将带有gRPC的访问令牌发送到API。如果然后返回401 http状态,则需要刷新(获取一个新的)访问令牌。

为了使您的生活更轻松,减少复杂性和理智,我建议您将IdentityServer放在它自己的服务中,独立于客户机/API。否则,很难对系统进行推理,也很难进行调试。

 类似资料: