• .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一些预设组件接入第三方登录的方法。

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

  • 相关阅读:
    算法 排序
    Windows系统安装Git
    oracle 查询语句
    .NET CORE AddRazorRuntimeCompilation
    清除html頁面文本框緩存
    ORACLE 生成UUID
    Unable to resolve service for type`***` while attempting to activatre `***`
    xml文件导入Oracle数据库
    jquery 日历控件
    判断并获取一对多表格数据
  • 原文地址:https://www.cnblogs.com/mosakashaka/p/12608156.html
Copyright © 2020-2023  润新知