.net core Identity学习(三) 第三方认证接入
简介
.net core在nuget中提供了微软、google、Facebook和twitter的Identity接入包,这里主要以MS作为例子。
微软官方文档可以参见这个链接,但是.net core的文档个人认为作为教学并不是特别好,利用了很多VS中的功能隐藏掉了很多细节,当当当点几下,就可以认证了,对于需要自定义一些流程的情况,可能会不方便,而且也不太利于理解这个过程。
除了官方文档,主要参考了这篇文章,对Identity External认证进行了一些剖析,可能不是针对现在的2.1 SDK,但还是能学到不少。(吐槽一句.net core更新太快了,很多教学文章写的内容在新的sdk中已经有了变化,不知道微软这样怎么抓住使用者。。)
接入流程
分几步来说明
- 接入准备:在接入网站中,对app进行注册,获得接入需要的client id,client secret等信息
- 组件注册:在.net core中注册对应的认证组件
- 用户第三方认证入口:用户从应用跳转到第三方网站进行授权
- 认证信息处理登录:第三方网站返回授权信息后,应用进一步处理,登入用户
还是以微软的接入认证为例
1. 接入准备
首先需要在微软开发者网站(地址)注册应用,主要有如下几点
- 应用程序Id:client_id,配置时使用
- 应用程序机密:client_secret,MS只有一次查看密钥的机会,要注意记下来,也是配置时使用
- 平台:需要添加一个平台,其中重定向URL需要配置,指向应用中接收认证返回的地址。对于我测试的情况,指定了本机VS运行的端口。而signin-microsoft是微软认证中间件默认接收认证信息的地址,一般不修改默认配置就指定这个路径
2.组件注册
注册时获取到的client_id和client_secret需要配置到程序中,官方建议测试阶段用SecretManager,生产阶段用Azure的服务,不过这里我就是简单的存在了appsettings里。
在Nuget中查找“Microsoft.AspNetCore.Authentication.MicrosoftAccount”这个包(这里吐槽一下这个包要输入比较全的名字才好查找。。)安装。
然后在StartUp中配置Service和Pipeline;
ConfigureServices中: services.AddAuthentication().AddMicrosoftAccount(op => { op.ClientId = _config["Authentication:Microsoft:ApplicationId"]; op.ClientSecret = _config["Authentication:Microsoft:Password"]; });
Configure中:
app.UseAuthentication();
Identity的注册在第一篇中记录了,这里略过。
服务注册这里通过AddMicrosoftAccount方法注册MS的认证组件,对应到OAuth里的重定向用户,和处理第三方重定向返回的授权码(code)。
这里的_config是注入的Configuration服务,测试应用我是直接写在配置文件里的(实际上即使是生产环境,感觉普通来说写在这里也没有很大问题。。)。
注意到最开始有一个AddAuthentication(),官方文档是说通过这个方法可以重写认证配置。这个方法返回AuthenticationBuilder类型对象,通过这个对象,才能在后面串联各种第三方Provider认证方法。
3.用户第三方认证入口
如果通过VS自带的模版来启用认证,认证页面和控制方法都会在引用的类库中,如果需要修改,得用基价工具提取出来,而且是Razor页面类型的代码。
这里我是参考搭建的代码和那篇文章修改添加的相关功能。
登录页面
@using Microsoft.AspNetCore.Identity
@inject SignInManager _signInManager
@{
var providers = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
}
@if (providers.Count != 0)
{
<form asp-action="ExternalLoginChallenge" asp-route-returnUrl="@(Context.Request.Query["ReturnUrl"])" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in providers)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="使用@(provider.DisplayName)账户登录">@provider.DisplayName</button>
}
</p>
</div>
</form>
}
从SignInManager中找到注册的认证组件(目前只有MS的),生成一个按钮,通过按钮触发认证。
而按钮实现认证的功能在Controller中:
[HttpPost]
public IActionResult ExternalLoginChallenge(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider.
var redirectUrl = Url.Action("ExternalLogin", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
redirectUrl:注意这个参数并不是第三方网站回传授权码的地址,而是发生在OAuth认证第五步(还记得吗^^?)之后,应用已经获得了所需要的Access Token,再重定向到哪个页面。
returnUrl:这是.net种常见的一个参数,区别前两个,这个是一个自定义参数,当最终认证成功(用户完成登录)后,应该将用户定向到哪个页面。
而第三方网站回传授权码的地址,实在我们调用AddMicrosoftAccount()时指定的,如果没有指定(这里我就没有指定),就会是默认的signin-microsoft,这也和前面看到的注册应用时配置的一直。注意这个值必须一致,否则我在测试时发现会提示返回地址无效。
这里的properties属性,主要操作也是配置了一下这个redirectUrl。
最后通过ChallengeResult()方法,像provider发出Challenge,而我们注册的MS中间件收到Challenge方法后就会重定向用户到第三方网站进行认证了
4.认证信息处理
假设用户同意授权,按照OAuth流程会通过重定向回传授权码,应用程序响应授权码并获取AccessToken完成认证,这一系列操作注册的中间价会自动完成,我们是无需参与的。
中间件会通过这个Token获取到用户所需的信息,并将其登录(SignIn)在External认证组件中。
此时用户并未真正完成登录,此时的SignIn是以另外的Key将用户信息存储在Cache中的。
接着中间价会将用户重定向到上一步中我们指定的redirectUrl,此时我们的代码开始接手:
[HttpGet] public async Task<IActionResult> ExternalLogin(string returnUrl = null, string remoteError = null) { var m = new LoginModel(); m.ReturnUrl = returnUrl ?? Url.Content("~/"); if (remoteError != null) { m.ErrorMessage = $"Error from external provider: {remoteError}"; ModelState.AddModelError(string.Empty, m.ErrorMessage); return View("Login", m); } var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { m.ErrorMessage = "Error loading external login information."; ModelState.AddModelError(string.Empty, m.ErrorMessage); return View("Login", m); }
// Sign in the user with this external login provider if the user already has a login. var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); if (result.Succeeded) { return LocalRedirect(returnUrl); } if (result.IsLockedOut) { return RedirectToPage("./Lockout"); } else { // If the user does not have an account, then ask the user to create an account. m.Provider = info.ProviderDisplayName; if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) { m.Email = info.Principal.FindFirstValue(ClaimTypes.Email); } return View(m); }
}
几个关键的操作:
_signInManager.GetExternalLoginInfoAsync():取出第三方登录的信息。
_signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey,...):利用第三方信息登录,在这里会检查对应该对应第三方(Provider)登录信息中,是否存在该用户(ProviderKey)的注册情况,如果存在,那么直接将用户引导到登录前想访问的页面(或者主页),第一次显然该用户并未在系统里注册。
对于用户还未注册的情况,会走到最后的return View(m);
我在View中的代码,基本也参考了Identity预设的实现:
<div class="row">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label for="Email" class="control-label">邮箱</label>
<input type="email" name="Email" value="@(Model.Email)" class="form-control" />
</div>
<button type="submit" class="btn btn-default">注册</button>
</form>
</div>
</div>
提示用户用该邮箱地址创建应用内的账户,点击注册后进入到如下流程:
[HttpPost] [ActionName("ExternalLogin")] public async Task<IActionResult> ExternalLoginPost(string Email, string returnUrl = null) { var m = new LoginModel(); m.ReturnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { m.ErrorMessage = "Error loading external login information during confirmation."; ModelState.AddModelError(string.Empty, m.ErrorMessage); return View("Login", m); } m.Provider = info.ProviderDisplayName;
if (ModelState.IsValid) { //It's possible that the user already registered var user = await _userManager.FindByEmailAsync(Email); var result = IdentityResult.Success; if (user != null) { if (Email.Equals(info.Principal.FindFirstValue(ClaimTypes.Email))) { //same user, link them directly if (!user.EmailConfirmed) { user.EmailConfirmed = true; result = await _userManager.UpdateAsync(user); } } else { ModelState.AddModelError(string.Empty, "该账号已经注册"); return View(m); } } else { user = new FaUser { UserName = Email, Email = Email }; result = await _userManager.CreateAsync(user); } if (result.Succeeded) { result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); return Redirect(m.ReturnUrl); } } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } return View(m);
}
还是挑重点来说,这里的流程我相比Identity本来的代码有所更改
通过_userManager.FindByEmailAsync(Email);查找该用户是否已经注册,如果未注册,则通过先创建该用户;如果已注册,判断第三方提供的邮箱和用户提供的邮箱是否相同,相同的情况下才允许注册。
邮箱验证过后通过_userManager.AddLoginAsync(user, info);关联用户和第三方登录信息,此时第三方信息真正存入了系统中,后续再像之前进行ExternalLoginSignInAsync的操作,也会直接成功了。
_signInManager.SignInAsync(user, isPersistent: false);:和普通的登录没有区别,这里用户真正登录了。
至此登录环节完成。
Google登录接入
与微软登录类似,有了之前的代码,仅需要修改很少的一部分:
- 在google的开发者页面注册应用,获取app id和私钥,并配置在系统中
- 添加google认证的Nuget包Microsoft.AspNetCore.Authentication.Google
- 注册组件,连接在微软登录之后。
services.AddAuthentication().AddMicrosoftAccount(op =>
{
op.ClientId = _config["Authentication:Microsoft:ApplicationId"];
op.ClientSecret = _config["Authentication:Microsoft:Password"];
})
.AddGoogle(op =>
{
op.ClientId = _config["Authentication:Google:ClientId"];
op.ClientSecret = _config["Authentication:Google:ClientSecret"];
});
之后的流程,和微软是公用的。
总结
以上就是通过Identity一些预设组件接入第三方登录的方法。
不过国内常用的恐怕是微博、微信这些吧,这个需要再看看~