GRPC-web RPCException 错误的 gRPC 响应。内容类型值无效:text/html;字符集=utf-8
GRPC-web RPCException Bad gRPC response. Invalid content-type value: text/html; charset=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();
}
}
}
}
}
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();
}
}
}
这是控制台的输出
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 放在它自己的服务中,独立于 client/api。否则很难对系统进行推理,也很难调试。
我的建议是您在三种不同的服务中拥有此架构:
gRPC 只是一种传输方式,与 HTTP 类似,在 API 中,你有这个基本架构(幻灯片取自我的一个培训 类):
JwtBearer 将检查访问令牌以验证您的身份,然后授权模块接管并检查您是否被允许进入。
我在尝试将 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();
}
}
}
}
}
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();
}
}
}
这是控制台的输出
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 放在它自己的服务中,独立于 client/api。否则很难对系统进行推理,也很难调试。
我的建议是您在三种不同的服务中拥有此架构:
gRPC 只是一种传输方式,与 HTTP 类似,在 API 中,你有这个基本架构(幻灯片取自我的一个培训 类):
JwtBearer 将检查访问令牌以验证您的身份,然后授权模块接管并检查您是否被允许进入。