微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

GRPC-web RPCException 错误的 gRPC 响应无效的内容类型值:text/html;字符集=utf-8

如何解决GRPC-web RPCException 错误的 gRPC 响应无效的内容类型值:text/html;字符集=utf-8

我在尝试将 gRPC API(使用 C#)获取到 blazor 客户端时出错,起初它运行良好,但在添加 IdentityServer4 并使用 CORS 用于 gRPC-Web 后,类似于 docs。这是与错误相关的代码
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();
                }
            }
        }
    }
}

后端/服务/UserService.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());
            
        }
    } 
}

FrontEnd/Program.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();

        }
    }
}

FrontEnd/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,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,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,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",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

解决方法

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

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

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

我的建议是,您在三种不同的服务中拥有此架构:

enter image description here

gRPC 只是一种传输,类似于 HTTP,并且在 API 中,您有这个基本架构(幻灯片取自我的一个培训课程):

enter image description here

JwtBearer 将检查访问令牌以验证您的身份,然后授权模块接管并检查您是否被允许进入。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。