问题描述
尝试将gRPC API(使用C#)获取到blazor客户端时出现错误,起初它可以正常工作,但是在添加IdentityServer4并为gRPC-Web使用CORS之后,类似于
gRPC只是一种传输方式,类似于HTTP,并且在API中,您具有以下基本架构(幻灯片来自我的培训课程之一):
JwtBearer将检查访问令牌以验证您的身份,然后授权模块接管并检查是否允许您进入.
I got an error when trying to fetch gRPC API (using C#) to blazor client, at first it worked fine but after adding IdentityServer4 and use CORS for gRPC-Web similar like in the docs. Here's the code relevant to the error.
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();
}
}
}
}
}
BackEnd/Services/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, 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();
}
}
}
This is the output of the console
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)
This is the output in the terminal when trying to authenticate with IdentityServer4 (the authentication and authorization is working fine though)
[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
You can't do OpenID Connect authentication as part of gRPC, the user must have first authenticated on your web site and you should then have received the access token.
Then you can send the access token with gRPC to the API. If you then get a 401 http status back, then you need to refresh(get a new one) the access token.
To make your life easier and to reduce complexity and your sanity, I recommend that you put IdentityServer in its own service, standalone from the client/api. Otherwise its hard to reason about the system and it will be very hard to debug.
My recommendation is that you have this architecture, in three different services:
gRPC is just a transport, similar to HTTP and in the API, you have this basic architecture (slide taken from one of my training classes):
The JwtBearer will examine the access token to verify who you are and after that the authorization module takes over and check if you are allowed in.
这篇关于GRPC-web RPCException gRPC响应错误.无效的内容类型值:text/html;字符集= utf-8的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!