我在尝试获取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
您不能将OpenID Connect身份验证作为gRPC的一部分,用户必须首先在您的网站上进行身份验证,然后您才应该收到访问令牌。
然后您可以将带有gRPC的访问令牌发送到API。如果然后返回401 http状态,则需要刷新(获取一个新的)访问令牌。
为了使您的生活更轻松,减少复杂性和理智,我建议您将IdentityServer放在它自己的服务中,独立于客户机/API。否则,很难对系统进行推理,也很难进行调试。
当调用rest模板获取以下异常时,有人能告诉如何解析html内容吗? restClientException:提取类型[class java.lang.Object]和内容类型[text/html;charset=UTF-8]的响应时出错;嵌套异常为org.springframework.http.converter.httpmessagenotreadableException:JSON分析错误
我试图使用独立的应用程序使用WCF web服务。我可以使用Internet Explorer查看此服务,也可以在Visual studio服务引用中查看。 这就是我得到的错误 如何更改它以使用正确的内容类型? 这是我的配置文件 这是堆栈
我在使用Spring5 reactive WebClient时遇到了一个问题,当时我请求一个endpoint,该endpoint返回一个格式正确的json响应,内容类型为“text/plain;charset=utf-8”。例外情况是 以下是我提出请求的方式: 编辑:头是“正确”设置的(Accept,Content-Type),我尝试了不同的Content-Type(json,json+UTF8,
问题内容: 使用firebug时,我在asp.net mvc 4项目中收到此有线错误“ NetworkError:415无法处理… xt / xml; charset = utf-8’– ”。 代码: 服务代码: 该接口是: 和网络配置,我在WCF中使用了无文件: 问题答案: 您需要使用,而不是代码中的常规。这将为端点配置适当的绑定()和行为()以遵守该属性。
当我请求一个endpoint返回内容类型为“text/plain;charset=UTF-8”的正确格式的json响应时,我遇到了Spring5 reactive WebClient的问题。例外情况是 编辑2:下一段引用摘自Spring文档(https://docs.Spring.io/Spring/docs/current/spring-framework-reference/web-react
我正在开发一个使用C#[MVC4.5]从Magento网站[V1.5.1]下载订单列表的项目,在开始之前,我从www服务器上构建了一个dev服务器复制完整的文件,并且我的程序在dev服务器上运行得很好。之后,我将webservice引用[http://dev.example.com/index.php/api/v2_soap/?wsdl]更改为[http://www.example.com/ind