本项目包含三部分:统一登录站点,受保护的webapi接口站点,以及blazor前端项目。
这个Demo是我的学习项目,是借鉴了(copy)了众多前辈代码完成的,如有不足之处还请多多指教。代码地址: https://dev.azure.com/1903268310/_git/blazor-sso-demo
[HttpPost]
public async Task Login(string userName, string password, string returnUrl = null)
{
ViewData["returnUrl"] = returnUrl;
UserInfo user = _userService.GetUser(userName, password);
if (user != null)
{
HttpContext.Response.Cookies.Append("token",TokenService.GenerateToken(user));
await HttpContext.SignInAsync(new IdentityServerUser(user.UserName), new AuthenticationProperties
{
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(3)
});
if (returnUrl != null)
{
return Redirect(returnUrl);
}
return View();
}
else
{
return View();
}
}
关于该对象的具体介绍可以去官网查看。该对象是blazor内置对象,用于为页面提供用户身份认证信息。
其中最重要的是 Task GetAuthenticationStateAsync()方法。 该方法返回一个AuthenticationState对象,它包含用户的认证信息,你会发现该对象的构造函数接受一个ClaimsPrincipal对象作为参数,做过ASP.NET Core身份认证的同学想必对该对象并不陌生。
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/?view=aspnetcore-3.1
Hello, @context.User.Identity.Name
|
Authentication Failure!
AuthorizeView 组件根据用户是否有权查看来选择性地显示 UI。AuthorizeView 组件支持基于角色或基于策略的授权 :
You can only see this if you're an admin or superuser.
通常,我们需要公开身份验证状态作为级联参数。级联参数是blazor中将父组件内的数据流转到子组件的一种方式。
MainLayout.razor:
@functions
{
[CascadingParameter]
Task AuthenticationState { get; set; }
}
并且修改App.razor中代码如下:
Sorry, there's nothing at this address.
之后,在Program.Main中添加选项和授权服务:
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
然后,AuthorizeView会通过依赖注入的AuthenticationStateProvider对象的GetAuthenticationStateAsync()方法 获取用户当前状态,然后通过Authorized和NotAuthorized标签选择显示的内容。该组件还包含了一个 AuthenticationState 类型的 context 变量,可以使用该变量来访问有关已登录用户的信息。
用于获取身份认证状态的GetAuthenticationStateAsync()方法是怎样实现的呢,我们只需要创建出包含已登录用户信息的ClaimsPrincipal对象即可。我们可以设计一个返回当前登录用户信息的后台接口,也可从jwtToken中获取我们需要的内容。本项目采用的是后者,jwtToken的解析可以参考项目源码。
CustomAuthenticationStateProvider.cs
public override async Task GetAuthenticationStateAsync()
{
var authState = new AuthenticationState(await GetUser(useCache: true));
//通知AuthorizeView页面身份认证信息发生改变
NotifyAuthenticationStateChanged(Task.FromResult(authState));
return authState;
}
private async ValueTask GetUser(bool useCache = false)
{
var now = DateTimeOffset.Now;
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
{
return _cachedUser;
}
_cachedUser = await FetchUser();
_userLastCheck = now;
return _cachedUser;
}
private async Task FetchUser()
{
var token = await _httpClient.GetStringAsync("/auth/token");
if (string.IsNullOrWhiteSpace(token))
{
return new ClaimsPrincipal(new ClaimsIdentity());
}
return new ClaimsPrincipal(new ClaimsIdentity(TokenService.ParseClaims(token), "jwt"));
}
ServiceCollection对象包含一个AddHttpClient()扩展方法,当你使用它的时候,就意味着你可以在程序中按照依赖注入惯例使用IHttpClientFactory,你可以为不同的httpClient对象指定名字(key),以及BaseAddress。
Program.cs
builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddHttpClient("api", client => { client.BaseAddress = new Uri("http://localhost:5001"); })
.AddHttpMessageHandler()
.AddHttpMessageHandler();
builder.Services.AddHttpClient("auth", client =>
{
client.BaseAddress = new Uri("http://localhost:5000");
})
.AddHttpMessageHandler();
更重要的是,你可以为每一个httpClient指定DelegatingHandler,DelegatingHandler的作用类似于asp.net请求中的中间件。它可以修改请求头以及请求内容,处理响应等。
public class RequestMessageHandler : DelegatingHandler
{
private readonly CustomAuthenticationStateProvider _authenticationStateProvider;
public RequestMessageHandler(CustomAuthenticationStateProvider authenticationStateProvider)
{
_authenticationStateProvider = authenticationStateProvider;
}
protected override async Task SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
HttpResponseMessage responseMessage;
if (!authState.User.Identity.IsAuthenticated)
{
responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
else
{
responseMessage = await base.SendAsync(request, cancellationToken);
}
if (responseMessage.StatusCode == HttpStatusCode.Unauthorized)
{
_authenticationStateProvider.SignIn();
}
return responseMessage;
}
}
public class IdentityHandler : DelegatingHandler
{
protected override async Task SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
//携带身份标识-Cookie
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
var responseMessage = await base.SendAsync(request, cancellationToken);
return responseMessage;
}
}
以上就是示例的blazor部分,统一登录站点使用的是IdentityServer4来实现,用于分发token,token的校验由接口站点完成。两者实现都较为基础,此处不再赘述。
e = await base.SendAsync(request, cancellationToken);
return responseMessage;
}
}
以上就是示例的blazor部分,统一登录站点使用的是IdentityServer4来实现,用于分发token,token的校验由接口站点完成。两者实现都较为基础,此处不再赘述。