.net core Identity学习(三) 第三方认证接入

简介

.net core在nuget中提供了微软、google、Facebook和twitter的Identity接入包,这里主要以MS作为例子。

微软官方文档可以参见这个链接,但是.net core的文档个人认为作为教学并不是特别好,利用了很多VS中的功能隐藏掉了很多细节,当当当点几下,就可以认证了,对于需要自定义一些流程的情况,可能会不方便,而且也不太利于理解这个过程。

除了官方文档,主要参考了这篇文章,对Identity External认证进行了一些剖析,可能不是针对现在的2.1 SDK,但还是能学到不少。(吐槽一句.net core更新太快了,很多教学文章写的内容在新的sdk中已经有了变化,不知道微软这样怎么抓住使用者。。)

接入流程

分几步来说明

  1. 接入准备:在接入网站中,对app进行注册,获得接入需要的client id,client secret等信息
  2. 组件注册:在.net core中注册对应的认证组件
  3. 用户第三方认证入口:用户从应用跳转到第三方网站进行授权
  4. 认证信息处理登录:第三方网站返回授权信息后,应用进一步处理,登入用户

还是以微软的接入认证为例

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 =&gt; 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登录接入

与微软登录类似,有了之前的代码,仅需要修改很少的一部分:

  1. 在google的开发者页面注册应用,获取app id和私钥,并配置在系统中
  2. 添加google认证的Nuget包Microsoft.AspNetCore.Authentication.Google
  3. 注册组件,连接在微软登录之后。
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一些预设组件接入第三方登录的方法。

不过国内常用的恐怕是微博、微信这些吧,这个需要再看看~

原文地址:https://www.cnblogs.com/mosakashaka/p/12608156.html