在Asp.Net Core 中使用外部登陆(google、微博…)

原文出自Rui Figueiredo的博文《External Login Providers in ASP.NET Core》 (本文很长)

摘要:本文主要介绍了使用外部登陆提供程序登陆的流程,以及身份认证的流程。

为了能够使用google、facebook、twitter、微博等外部登陆提供程序,从而避免创建本地账户以及电子邮件验证等繁琐步骤,我们一般会引用到外部登陆服务,将验证用户身份的任务委托给他们。外部验证最为流行的协议就是OAuth2和OpenId Connect。

在Asp.Net中使用外部登陆提供商的文档非常少,更糟糕的是当地使用“File -> New Project”创建项目所生成的模板代码也很复杂,并不容易看得懂然后照着做。而且如果你不了解身份认证中间件在Asp.Net中是如何工作的,那么基本上是不可能弄懂那些模板代码的。

为了真正了解如何在Asp.Net中使用外部登陆,那么必须先理解中间件管道以及特定的身份认证中间件是如何工作的,以及一点OAuth协议。

本博客文章解释了所有这些部分是如何组合在一起的,并提供了有关如何利用身份验证中间件和外部登录提供程序本身和结合ASP.NET Core Identity的示例。

中间件管道

当一个请求进入Asp.Net Core程序,请求会通过由中间件组成的中间件管道。管道中的每个中间件都“有机会(译者注:如果一个中间件短路了那么后续的中间件就没机会了)”检查、处理请求,传递到下一个中间件,然后在后面的中间件都执行之后再做些额外的操作。

管道在Startup类中的Config方法中定义,下面是一个添加到管道中的中间件的例子:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.Use(async (HttpContext context, Func next) =>
    {
        // 在执行下一个中间件之前做些事
        await next.Invoke(); // 下一个中间件做的事
        // 在执行下一个中间件之后做些事    
    });
}

需要注意的一件重要的事情是所有的中间件都可以访问HttpContext的实例。
通过这个httpContext实例,他们可以向其它的中间件“发送”信息。例如,如果管道末端的中间件通过执行类似HttpContext.Items[“LoginProvider”] =“Google”的方式来更改HttpContext,则所有位于其之前的中间件都将能够访问该值。

另一个重要的事情是,任何中间件都可以停止管道(短路),即它可以选择不调用下一个中间件。这对外部登录提供程序(external login provider)尤其重要。

例如,如果你用Google作为你的外部登录提供程序,则用户将在成功验证后重定向到http://YourAppDomain.com/signin-google。如果你已经尝试了(使用默认的Visual Studio模板生成的代码)使用外部登录提供程序(本例子使用的是Google),那么你可能已经注意到没有Controller 或者Action,或者看起来没有其他任何响应上述URL的内容。

发生了什么呢?其实 GoogleAuthentication 中间件查找该URL,并且当它发现它时 GoogleAuthentication 中间件将“接管”请求,然后也不会调用管道中的任何其他中间件,即MVC中间件。

作为这种行为的结果,中间件运行的顺序非常重要。

想象一下,你的程序支持多个外部登录提供程序(例如Facebook和Google)的情况。当他们运行时,需要有一个中间件,即 CookieAuthentication 中间件,它能够将他们放入HttpContext中的信息转换成代表登录用户的cookie(本文后面给出了示例)。

The Authentication Middleware

使中间件成为认证中间件的原因是它继承了一个名为AuthenticationMiddleware的类,这个类只是创建一个AuthenticationHandler。大部分身份认证功能都在AuthenticationHandler里面。

尽管我们不打算描述如何创建自己的身份验证中间件,我们将描述身份验证中间件如何进行交互,以及当你有多个认证中间件在管道中时,他们如何相互交互。

在添加AuthenticationMiddleware时,你最少要指定三个值

  • AuthenticationScheme
  • AutomaticAuthenticate 标志
  • AutomaticChallenge 标志

你可以将 AuthenticationScheme 视为身份验证中间件的名称。 在以前的ASP.NET版本中,这被称为authentication type。

AutomaticAuthenticate 标志指定管道中的中间件应该在它拿到请求时就立即“认证”用户。例如,如果使用 AutomaticAuthenticate = true 将cookie 中间件添加到管道,则会在请求中查找 authentication cookie,并使用它创建 ClaimsPrincipal 并将其添加到 HttpContext 。顺便说一句,这就是让用户“登录”的原因。

如果你要使用 AutomaticAuthenticate = false 设置 cookie 中间件,并且在该cookie中间件的请求中有一个 authentication cookie,则用户不会自动“登录”。

在以前的ASP.NET版本中,具有 AutomaticAuthenticate = true 的认证中间件被称为active认证中间件,而 AutomaticAuthenticate = false 被称为passive认证中间件。

The Challenge

你可以“Challenge”一个身份验证中间件。这是一个在ASP.NET Core之前不存在的新术语。我不知道把它称为Challenge的原因,所以我不会试图描述为什么这样叫。相反,我会给你一些中间件被“Challenged”时会发生什么事情的例子。

译者注: challenge 有 挑战的意思,也有 质疑,质询,对…质询的意思,记住它的其他意思,会对你理解下文有帮助

例如,Cookie中间件在“Challenged”时会将用户重定向到登录页面。Google身份验证中间件返回302响应,将用户重定向到Google的OAuth登录页面。通常challenge 认证中间件,你需要给它命名(通过它的AuthenticationScheme属性)。例如,要challenge 一个带有 AuthenticationScheme =“Google” 身份验证中间件,你可以在controller action 中执行此操作:

public IActionResult DoAChallenge()
{
    return Challenge("Google");
}

但是,你可以发出一个“naked”的challenge(即不命名任何认证中间件,例如返回Challenge),然后具有AutomaticChallenge = true的认证中间件将是被选中的认证中间件。

与认证中间件进行交互

Challenge只是可以在认证中间件上“执行(performed)”的操作之一。The others are AuthenticateSignIn and SignOut.

例如,如果你向身份验证中间件“发起(issue)” 身份验证(Authenticate )操作(假设此示例在controller action中):

var claimsPrincipal = await context.Authentication.AuthenticateAsync("ApplicationCookie");

译者注:context.Authentication.AuthenticateAsync在2.0中已经过时,只需将其修改为context.AuthenticateAsync即可,不过返回值类型已经由 ClaimsPrincipal 变为 AuthenticateResult ,不过AuthenticateResult中含有 ClaimsPrincipal, 参考信息

这将导致中间件尝试认证并返回一个ClaimsPrincipal。例如,cookie中间件会在请求中查找cookie,并使用cookie中包含的信息构建 ClaimsPrincipal 和 ClaimsIdentity 。

一般来讲,如果给认证中间件配置了AutomaticAuthenticate = false ,那么你需要手动发起认证。

也可以发起(issue)SignIn:

await context.Authentication.SignInAsync("ApplicationCookie", claimsPrincipal);

译者注:这个也过时了,参考上一个

如果“ApplicationCookie”是一个cookie中间件,它将修改响应,以便在客户端创建一个cookie。该cookie将包含重新创建作为参数传递的 ClaimsPrincipal 所需的所有信息。

最后,SignOut,例如,cookie中间件将删除标识用户的cookie。下面这段代码展示了如何在名为“ApplicationCookie”的身份验证中间件上调用注销(sign out)的示例:

await context.Authentication.SignOutAsync("ApplicationCookie"/*这里是中间件的AuthenticationScheme*/);

译者注:这个也过时了,参考上一个

中间件交互示例

如果没有示例,那么很难想象这些东西是如何组合在一起的,接下来将展示一个使用cookie身份验证中间件的简单示例。

以下是Cookie身份验证和MVC中间件的设置:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{           
    app.UseCookieAuthentication(new CookieAuthenticationOptions{
        AuthenticationScheme = "MyCookie",
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,            
        LoginPath = new PathString("/account/login")                   
    });

    app.UseMvcWithDefaultRoute();
}

当一个请求到达配置了这个管道的ASP.NET Core应用程序时,会发生什么情况呢?cookie身份验证中间件将检查请求并查找cookie。这是因为认证中间件配置了AutomaticAuthenticate = true。如果cookie位于请求中,则将其解密并转换为ClaimsPrincipal并在将其设置到HttpContext.User上。之后,cookie中间件将调用管道中的下一个中间件,本例中是MVC。如果cookie不在请求中,cookie中间件将直接调用MVC中间件。

如果用户执行了带有[Authorize]属性注释的controller action 请求,且用户未登录(即未设置HttpContext.User),例如:

[Authorize]
public IActionResult ActionThatRequiresAnAuthenticatedUser()
{
    //...
}

一个 challenge 会被发起(issue),并且含有 AutomaticChallenge = true的认证中间件会处理它。cookie中间件通过将用户重定向到LoginPath(将状态码设为302,和Location 头设为/account/login)来响应challenge。

或者,如果你的身份验证中间件未设置为AutomaticChallenge = true,并且你想“challenge”它,则可以指定AuthenticationScheme

[Authorize(ActiveAuthenticationSchemes="MyCookie")]
public IActionResult ActionThatRequiresAnAuthenticatedUser()
{
    //...
}

译者注:ActiveAuthenticationSchemes已经过时,使用AuthenticationSchemes替换

为了涵盖所有可能的方式来发出challenge,你也可以使用控制器中的Challenge方法:

public IActionResult TriggerChallenge()
{        
    return Challenge("MyCookie");
}

用这种方法手动发起challenge时需要注意一件重要事。如果你对身份验证中间件(例如“MyCookie”)发出了一个challenge,然后身份验证中间件“将用户登入”(在这种情况下,请求中有一个对应这个中间件的cookie),那么中间件会将challenge作为响应未经授权的访问,并将用户重定向到/Account/ccessDenied。你可以通过在CookieAuthenticationOptions中设置AccessDeniedPath来更改该路径。

这背后的原因是,如果用户已经登录,并且向签入该用户的中间件发出challenge,则这意味着用户没有足够的权限(例如,不具有所需的角色)。

以前版本的ASP.NET中的行为是将用户重定向回登录页面。但是,如果使用外部登录提供程序,则会造成问题。

外部登录提供程序会“记住”你已经登录。这就是为什么如果你已经登录到Facebook,并且你使用了一个允许你登录Facebook的网络应用,你将被重定向到Facebook,然后立即返回到网络应用(假设你已经授权在Facebook的网络应用程序)。如果你没有足够的权限,可能会导致重定向循环。因此,在这些情况下,为了避免导致重定向循环,ASP.NET Core中的身份验证中间件会将用户重定向到拒绝访问页面。

使用外部登陆提供器中间件

依赖外部登录提供程序时,最简单的设置是配置一个cookie身份验证中间件,负责对用户进行登陆。然后再配置一个我们要使用的特定外部登录提供程序的中间件。

如果我们想要使用Google登陆,我们可以像这样配置我们的管道:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions{
        AuthenticationScheme = "MainCookie",
        AutomaticAuthenticate = true,
        AutomaticChallenge = false                
    });

    app.UseGoogleAuthentication(new GoogleOptions{
        AuthenticationScheme = "Google",                        
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "YOUR_CLIENT_SECRET",
        CallbackPath = new PathString("/signin-google"),
        SignInScheme = "MainCookie"
    });

    app.UseMvcWithDefaultRoute();
}

译者注:UseXyzAuthentication系列扩展方法已经过时,取而代之的是在ConfigService中的AddXyz()系列
例如:

 public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseIdentity();
    app.UseCookieAuthentication(new CookieAuthenticationOptions
       { LoginPath = new PathString("/login") });
    app.UseFacebookAuthentication(new FacebookOptions
       { AppId = Configuration["facebook:appid"],  AppSecret = Configuration["facebook:appsecret"] });
} 

替换为

public void ConfigureServices(IServiceCollection services) {
    services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores();
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(o => o.LoginPath = new PathString("/login"))
                .AddFacebook(o =>
                {
                    o.AppId = Configuration["facebook:appid"];
                    o.AppSecret = Configuration["facebook:appsecret"];
                });
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseAuthentication();
}

每当有这个配置的请求进来,它将“通过”cookie中间件,cookie 中间件将检查它寻找一个属于他的cookie。cookie的名字决定了cookie是否属于特定的中间件。默认的是将AuthenticationScheme加上.​​AspNetCore.。所以对于MainCookie 这个cookie的名字就是.AspNetCore.MainCookie。

如果请求中没有cookie,cookie身份验证中间件只是调用管道中的下一个中间件。在这个例子中是Google身份验证中间件。我们在这个例子中将Google身份验证中间件命名为“Google”。当我们使用外部登录提供者时,提供者必须知道我们的Web应用程序。总会有一个步骤,外部登陆提供者让你注册你的应用程序,你会得到一个ID和一个Secret (我们稍后将会详细说明为什么需要这些东西)。在示例是ClientId和ClientSecret属性。

接下来我们定义了一个CallbackPath。当用户使用外部登录提供程序成功登录时,外部登录提供程序会发出重定向,以便将用户重定向回 发起登录进程的Web应用程序。CallbackPath 必须与外部登录提供程序将用户重定向到的位置 相匹配(稍后你会明白)。

最后,SignInScheme指定在认证成功后,Google认证中间件将使用哪一个AuthenticationScheme发起SignIn。

外部登录提供商中间件将“干预”请求的唯一情况是中间件被“challenged”或请求与CallbackPath匹配。

我们先来看看这个challenge。想象一下你有一个像这样的controller action:

public IActionResult SignInWithGoogle()
{
    var authenticationProperties = new AuthenticationProperties{
        RedirectUri = Url.Action("Index", "Home")
    };

    return Challenge(authenticationProperties, "Google");
}

当你发起challenge时,你可以指定AuthenticationProperties的一个实例。AuthenticationProperties类允许你指定用户在成功验证的情况下应该重定向到的其他选项。当发出这个challenge时,Google Authentication 中间件会将响应状态代码更改为302然后重定向到Google的OAuth2登录URL。它看起来像这样:

https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http%3A%2F%www.yourdomain.com%2Fsignin-google&scope=openid%20profile%20email&state=....

然后用户登录/授权Web应用程序,然后Google将其重定向回Web应用程序。例如,如果你在Google注册你的网络应用程序时将重定向URI定义为http://www.yourdomain.com/signin-goole,那么在用户成功通过Google身份验证之后,他将被重定向到。http://www.yourdomain.com/signin-goole

当请求到来时,如果配置正确,它将匹配 CallbackPath(/signin-google),然后Google Authentication 中间件将接管该请求。

这个请求看起来可能是这样:

http://www.yourdomain.com/signin-google?state=…&code=4/j5FtSwx5qyQwwl8XQgi4L6LPZcxxeqgMl0Lr7bG8SKA&authuser=0&session_state=…&prompt=none

查询字符串中的code值将用于向Google发出请求并获取有关用户的信息(这是OAuth2协议的一部分,将在下一部分中进行更详细的说明)。请注意,这是由Web应用程序向Google发送的请求。这对用户是透明的。通过对该请求(使用代码的那个)的响应,GoogleAuthentication中间件创建一个ClaimsPrincipal并调用配置中间件时提供的SignInScheme“登录”。最后,响应被更改为302重定向到challenge中的AuthenticationProperties中指定的重定向URL(在本例中是Home控制器中的Index aciton)。

使用额外的Cookie中间件来启用中间认证步骤

如果你曾尝试将默认Visual Studio模板与外部登录提供程序一起使用,那么你可能已经注意到,如果使用外部登录提供程序进行身份验证,则会将你带到要求你创建本地用户帐户的页面。
用户在登录之前必须经过这个中间步骤。

这是通过使用两个cookie身份验证中间件来实现的。

一个主动查找请求中的cookie,并登录用户(AutomaticAuthenticate = true)。这个通常被称为ApplicationCookie,或者在我们的例子中叫做MainCookie。而另一个是被动的(AutomaticAuthenticate = false,即它不会自动设置HttpContext.User与各个Cookie中的ClaimsIdentity用户)。这个通常被称为ExternalCookie,因为它是外部登录提供者发起“登录”的地方。

外部登录提供程序的SignInScheme设置为external cookie中间件(使用AutomaticAuthenticate = false配置的中间件),并设置RedirectUri到指定的controller action,由这个action“手动”调用该SignInScheme中的“Authentication”来发起challenge。

下面是示例:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions{
        AuthenticationScheme = "MainCookie",
        AutomaticAuthenticate = true,
        AutomaticChallenge = false
    });

    app.UseCookieAuthentication(new CookieAuthenticationOptions{
        AuthenticationScheme = "ExternalCookie",
        AutomaticAuthenticate = false,
        AutomaticChallenge = false                             
    });            

    app.UseGoogleAuthentication(new GoogleOptions{
        AuthenticationScheme = "Google",
        SignInScheme = "ExternalCookie",
        CallbackPath = new PathString("/signin-google"),
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "YOUR_CLIENT_SECRET"
    });

    app.UseMvcWithDefaultRoute();
}

译者注:上述方法已经过时,参考1 参考2

主要变化在于AutomaticAuthenticateAutomaticChallenge被替代,因为这辆属性的意图其实只能用在一个中间件上,即只能让一个认证中间件,自动触发Authenticate 或者Challenge,所以他们移除了由 AddAuthentication(option) 指定,你可以先看这篇博客,因为不影响流程理解。

这和以前的情况唯一的区别是,现在有一个额外的身份验证中间件(ExternalCookie),外部登录提供程序中的SignInScheme也被设置到了这个中间件。

当我们在这种情况下进行挑战时,我们必须将用户重定向到一个controller action,该action在ExternalCookie中“手动”触发Authenticate。代码看起来如下:

public IActionResult Google()
{
    var authenticationProperties = new AuthenticationProperties
    {
        RedirectUri = Url.Action("HandleExternalLogin", "Account")
    };

    return Challenge(authenticationProperties, "Google");
}

Account controller中的 HandleExternalLogin 方法 :

public async Task HandleExternalLogin()
{
    var claimsPrincipal = await HttpContext.Authentication.AuthenticateAsync("ExternalCookie");

    //do something the the claimsPrincipal, possibly create a new one with additional information
    //create a local user, etc

    await HttpContext.Authentication.SignInAsync("MainCookie", claimsPrincipal);
    await HttpContext.Authentication.SignOutAsync("ExternalCookie");
    return Redirect("~/");
}

译者注:这里的代码到了2.0时略有变化,参见之前的内容

我们在这个控制器动作中所做的是在ExternalCookie中间件中“手动”触发一个Authenticate动作。这将返回从请求中的 cookie 重建的ClaimsPrincipal。由于我们已经设置了SignInScheme = ExternalCookie,所以在验证成功之后,该cookie由 Google Authentication 中间件设置。GoogleAuthentication中间件在内部将执行类似以下的操作:

HttpContext.Authentication.SignInAsync("ExternalCookie", claimsPrincipalWithInformationFromGoogle);

这就是为什么ExternalCookie中间件创建cookie的原因。

接下来我们可以使用ClaimsPrincipal中包含的信息做一些额外的操作,例如检查用户(通过ClaimsPrincipal.Claims中包含的电子邮件)是否已经有本地帐户,如果没有将用户重定向到提供创建本地帐户选项的页面(这是默认的Visual Studio模板所做的)。

在这个例子中,我们简单地向MainCookie中间件发出SignIn操作,这将导致该Cookie中间件更改发送给用户的响应,以便创建encoded 的ClaimsPrincipal的cookie(即,响应将具有编码ClaimsPrincipal的名为.AspNetCore.MainCookie的cookie)。

请记住,这个中间件是一个具有AutomaticAuthenticate = true的中间件,这意味着在每个请求中它将检查它寻找一个cookie(名为.AspNetCore.MainCookie),如果它存在,它将被解码成ClaimsPrincipal并设置在HttpContext.User上,然后使用户登录。最后,我们只需发起一个SignOut到ExternalCookie中间件。这会导致中间件删除相应的cookie。

我们从用户的视角来回顾一下:

  1. 用户请求了一个action ,这个action向Google认证中间件发起challenge,例如, /Account/SignInWithGoogle。challenge action定义了RedirectUrl,例如/Account/HandleExternalLogin
  2. 响应将用户浏览器重定向到Google的OAuth登录页面
  3. 成功验证和授权Web应用程序后,Google会将用户重定向回Web应用程序。例如/signin-google?code=…
  4. Google身份验证中间件将接管请求(CallBackPath匹配/signin-google),并将使用一次性使用的code来获取有关用户的信息。最后,它将发起SignIn到ExternalCookie,并发起重定向到第1步中定义的RedirectUrl。
  5. 在RedirectUrl的controller action中,手动运行了ExternalCookie的Authenticaticate。这返回了一个包含谷歌的用户信息的ClaimsPrincipal,最后,向MainCookie发起一个SignIn并将ClaimsPrincipal传递给它(如果需要的话,创建一个含有额外信息的新的ClaimsPrincipal)。向​​ExternalCookie 发起SignOut,以便其Cookie被删除。

OAuth2简述

在上面的例子中,我们使用了一个client Id,一个client secret,一个 callback URL,我们简单地提到Google的回应包含了一个“code”,但是我们并没有用到所有这些信息。

这些都是OAuth2协议的术语,具体来说就是“授权码工作流程”(你可以在这里找到更全面的OAuth2说明)。

使用OAuth的第一步是注册客户端。在本文的例子中,客户端是你的Web应用程序,你必须注册,以便外部登录提供程序具有关于它的信息。这些信息是必需的,以便在向用户提交授权表单时,提供商以显示应用程序的名称,以及在用户接受或拒绝应用程序的“要求”后知道将用户重定向到哪里。

在OAuth中,这些“requirements”被称为“scopes”。 Google的两个scopes“item”的示例是“profile”和“email”。
当你的应用程序将用户重定向到Google并包含这些范围时,系统会询问用户是否可以访问profile和email信息。

总之,当你向外部登录提供者注册你的应用程序时,你必须为你的应用程序提供(至少)一个名字,并且提供一个回调url(e.g. www.mydomain.com/signin-google)。

然后你将得到一个客户端ID和一个客户端密钥。客户端ID和client密码是你的Web应用程序开始使用外部登录提供程序所需的全部东西。以下是用户浏览器,Web应用程序和外部登录提供程序之间的交互图。这里的术语我用的很随意,实际的术语应该是授权服务器,而实际上包含用户帐户的服务器就是资源服务器。他们可能是一样的。如果你需要对这些术语进行更加严格的描述,你应该阅读关于OAuth的 digitial ocean article about OAuth
图表:

这是授权码授权。还有其他的工作流程,但是对于一个Web应用程序,这是你要使用的。这里需要注意的重要的事情是,code只能被使用一次,client secret永远不会发送到用户的浏览器。这样就很难让人冒充你的Web应用程序。如果有人想冒充你的应用程序,那么他们要拿到你的client secret ,为此,他们要能进入你的服务器才行。

ASP.NET Identity 是怎么做的?

当你使用Visual Studio创建一个新项目并选择带有成员资格和授权的Web应用程序,并为外部登录提供程序添加一个身份验证中间件时,你将得到类似于以下的启动配置:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{

    //...

    app.UseIdentity();

    app.UseGoogleAuthentication(new GoogleOptions
    {
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "CLIENT_SECRET"
    });

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

如果你看看UseIdentity扩展方法的源代码,你会发现类似这样的东西:

app.UseCookieAuthentication(identityOptions.Cookies.ExternalCookie);
app.UseCookieAuthentication(identityOptions.Cookies.TwoFactorRememberMeCookie);
app.UseCookieAuthentication(identityOptions.Cookies.TwoFactorUserIdCookie);
app.UseCookieAuthentication(identityOptions.Cookies.ApplicationCookie);

译者注:在2.0中,由于Use系列方法被Add系列方法取代,所以这些代码会发生变化。

这与我们之前描述的很相似。不同的是,有两个新的外部认证中间件(TwoFactorRememberMeCookie和TwoFactorUserIdCookie 它们不在本文的讨论范围之内)以及“主要”认证中间件(具有AutomaticAuthenticate = true的中间件)和我们使用的存储外部登录提供程序认证结果(ExternalCookie)被交换(然而他们呢的执行顺序不会受到影响)。

另外,GoogleAuthentication中间件配置了所有的默认选项。CallbackPath的默认值是 new PathString(“/ signin-google”),还做了一些事情来指定你使用的特定的外部登陆提供器中间件。

手动发起外部登陆提供器中间件的challenge被放在了 AccountController 的ExternalLogin 方法中。

public IActionResult ExternalLogin(string provider, string returnUrl = null)
{        
    var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { ReturnUrl = returnUrl });
    var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
    return Challenge(properties, provider);
}

如果你要查看SignInManager中ConfigureExternalAuthenticationProperties的源代码,你会发现它只是像我们前面的示例中那样创建一个AuthenticationProperties实例:

public virtual AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null)
{
  AuthenticationProperties authenticationProperties = new AuthenticationProperties()
  {
    RedirectUri = redirectUrl
  };
  authenticationProperties.Items["LoginProvider"] = provider;
  return authenticationProperties;
}

稍后使用带有“LoginProvider”的“item”。我会在适当的时候突出显示它。

从AccountController的ExternalLogin action中可以看出,RedirectUri在AccountController上也被设置为ExternalLoginCallback action。让我们看看这个action(我删除了不相关的部分):

public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
    var info = await _signInManager.GetExternalLoginInfoAsync();

    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
    if (result.Succeeded)
    {
        return RedirectToLocal(returnUrl);
    }
    else
    {
        // If the user does not have an account, then ask the user to create an account.
        ViewData["ReturnUrl"] = returnUrl;
        ViewData["LoginProvider"] = info.LoginProvider;
        var email = info.Principal.FindFirstValue(ClaimTypes.Email);
        return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
    }
}

第一行,var info = await _signInManager.GetExternalLoginInfoAsync();在external cookie中间件中触发一个Authentication 。但是返回的不是ClaimsPrincipal的实例,它将返回包含以下属性的ExternalLoginInfo类的实例:

  • Principal (ClaimsPrincipal)
  • LoginProvider
    — 这是从AuthenticationProperties的Items中读取的。在描述challenge的时候,我曾经提到带有“LoginProvider”键的item将会在以后被使用。这是使用它的地方。
  • ProviderKey
    — 这是ClaimsPrincipal中的声明http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier的值,你可以将其视为来自外部登录提供程序的UserId

下一行var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
这将检查AspNetUserLogins表中是否有记录。此表将外部登录提供程序和“provider key”(这是外部登录提供程序的用户标识)链接到AspNetUsers表中的用户(该表的主键是LoginProvider和ProviderKey的组合键) 。

下面是该表中记录的示例:

因此,如果你使用Google登录,并且你的Google“用户ID”为123123123123123123,并且你之前已将你的本地用户(稍后会详细介绍)与此外部登录关联,则ExternalLoginSignInAsync将向 主 Cookie中间件发出signIn并向外部cookie中间件发出SignOut。

当用户第一次访问时,AspNetUserLogins表中将不会有任何本地用户或记录,并且方法将简单地返回SignInResult.Failed。然后将用户重定向到ExternalLoginConfirmation页面:

在这个页面中,用户会被要求确认他想用来创建本地帐户的电子邮件(即AspNetUsers表中的记录)。

当你单击注册按钮时,你将被带到AccountController中的ExternalLoginConfirmation action,这是它的简化版本:

public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
{
    var info = await _signInManager.GetExternalLoginInfoAsync();

    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };

    await _userManager.CreateAsync(user);

    await _userManager.AddLoginAsync(user, info);

    await _signInManager.SignInAsync(user, isPersistent: false);

    return RedirectToLocal(returnUrl);
}

第一行:var info = await _signInManager.GetExternalLoginInfoAsync;

该行将获取存储在external Cookie中的信息并返回ExternalLoginInfo的实例。这与ExternalLoginCallback中完成的事完全相同。

第二行:var user = new ApplicationUser {UserName = model.Email,Email = model.Email};该行使用在用户单击Register的页面中输入的电子邮件创建ASP.NET Identity用户的新实例。

第三行在AspNetUsers表中创建一个新用户: await _userManager.CreateAsync(user);

第四行: await _userManager.AddLoginAsync(user,info);

该行将新创建的用户与我们刚才使用的外部登录提供程序相关联。这意味着在AspNetUserLogins中创建一条新记录。

此表中的记录有四列,LoginProvider(info.LoginProvider,例如“Google”),ProviderKey(info.ProviderKey,例如123123123123,你可以认为它是刚刚登录的用户的Google用户标识),ProviderDisplayName (至少在2017/04/29的ASP.NET Identity的这个版本中是这样的),最后是UserId,它是第三行中新创建的用户的用户标识。

最后 await _signInManager.SignInAsync(user, isPersistent: false);

译者注:最终的SignInAsync源码是:

public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null)
    {
        var userPrincipal = await CreateUserPrincipalAsync(user);
        // Review: should we guard against CreateUserPrincipal returning null?
        if (authenticationMethod != null)
        {
            userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod));
        }
        await Context.SignInAsync(IdentityConstants.ApplicationScheme,
            userPrincipal,
            authenticationProperties ?? new AuthenticationProperties());
    }

参见

为用户创建一个ClaimsPrincipal并向application Cookie发出一个SignIn。这个application Cookie是AutomaticAuthenticate = true的cookie,这意味着在下一个请求中,该中间件将设置HttpContext.User与cookie中编码的用户,有使用户“登录”。请注意,外部cookie从未在此流程中被删除。这不是一个大问题,因为当用户最终退出时,SignInManager.SignOutAsync被调用,并且在内部向所有认证中间件发起SignOut。

总结全文就是:如何在Asp.NetCore中使用外部登陆提供程序,包含只使用authentication中间件和与Identity共同使用。

使用ASP.NET Core Identity和外部登录提供程序还有一些事情。你可以将其中多个外部登陆提供程序关联到本地用户帐户。而且你可以将他们全部移除,如果你确定不会“shoot yourself on the foot”,例如移除所有用户登录的方式,不过这可能成为另一篇博文的话题。

ASP.NET Identity登入技術剖析

ASP.NET Identity是微軟所貢獻的開源專案,用來提供ASP.NET的驗證、授權等等機制。本篇文章介紹ASP.NET Identity在執行登入功能時,與瀏覽器、還有第三方驗證服務之間的運作流程。主要為自己留個紀錄,也希望能幫助到有需要的開發人員。(本篇內容大幅度簡化了ASP.NET Identity的運作細節,用以傳達登入功能的運作概念。實際ASP.NET Identity在運作的時候,比本篇說明的複雜很多。)

前言01

Unauthorized(未登入)

未登入01

  1. 當使用者使用瀏覽器,第一次進入ASP.NET站台。
  2. 因為還沒有完成登入的動作,所以被ASP.NET判斷為「未登入」。
  3. 這時使用者要求使用的資源,如果是被打上[Authorize]標籤的Controller或是Action。[Authorize]標籤會判別使用者未登入,就回傳HTTP 401狀態碼。
  4. ApplicationCookieMiddleware是一個Identity掛載到ASP.NET的Middleware,這個Middleware會去攔截HTTP 401狀態碼。

    未登入02

  5. ApplicationCookieMiddleware攔截到HTTP 401狀態碼之後,會更改回傳的內容。改為回傳HTTP 302狀態碼以及一個Login頁面的URL。

    未登入03

  6. 瀏覽器接收到HTTP 302狀態碼,會自動跳轉頁面到回傳內容所夾帶的Login頁面URL。
  7. ASP.NET站台會回傳Login頁面給瀏覽器,要求使用者進行登入作業。

Authentication(驗證)

驗證01

  1. 使用者在Login頁面,選擇使用Facebook驗證後,Login頁面會連結到ExternalLogin這個Action。
  2. ExternalLogin在收到使用者選擇使用Facebook驗證後,會回傳一個ChallengeResult, 來引發Challenge。因為使用者是選擇使用Facebook驗證,所以這個Challenge動作會交由FacebookAuthenticationMiddleware來處理。
  3. 接著FacebookAuthenticationMiddleware會發起一個OAuth的流程,來在Facebook站台、使用者瀏覽器之間交換資訊,用以認證一個使用者。(參考資料:OAuth 2.0 筆記 – Yu-Cheng Chuang)
  4. 完成OAuth流程之後,FacebookAuthenticationMiddleware就可以依照取得的使用者資訊,來建立一個FBUser。
  5. FBUser會被拿來做為SignIn動作的參數。這個SignIn動作,會被導到Identity掛載的ExternalCookieMiddleware去執行。

    驗證02

  6. 在ExternalCookieMiddleware裡,會將FBUser編碼為Cookie內容,並且附加到回傳內容裡。
  7. 完成SignIn動作後,FacebookAuthenticationMiddleware會更改回傳的內容。改為回傳HTTP 302狀態碼、編碼為Cookie內容的FBUser、以及一個ExternalLoginCallback URL。

Authorization(授權)

授權01

  1. 瀏覽器接收到HTTP 302狀態碼,會自動跳轉頁面到回傳內容所夾帶的ExternalLoginCallback URL,並且也同時回傳編碼為Cookie內容的FBUser。
  2. ASP.NET會從Cookie內容裡解碼出FBUser,並且依照編碼FBUser為Cookie時的定義,將登入狀態定義為「未登入」。

    授權02

  3. 接著這個FBUser,會被提交給ASP.NET Identity,用以從Identity裡取得系統使用的APPUser。這個APPUser除了使用者相關資料外,也包含了授權給該使用者的Role資料。
  4. APPUser會被拿來做為SignIn動作的參數。這個SignIn動作,會被導到Identity掛載的ApplicationCookieMiddleware去執行。
  5. 在ApplicationCookieMiddleware裡,會將APPUser編碼為Cookie內容,並且附加到回傳內容裡。
  6. 完成SignIn動作後,ASP.NET Identity會更改回傳的內容。改為回傳HTTP 302狀態碼、以及編碼為Cookie內容的APPUser。

Authorized(已登入)

已登入01

  1. 完成上述流程之後。使用者每次使用瀏覽器進入ASP.NET站台時,都會夾帶編碼為Cookie內容的APPUser。
  2. ASP.NET會從Cookie內容裡解碼出APPUser,並且依照編碼APPUser為Cookie時的定義,將登入狀態定義為「已登入」。

    已登入02

  3. 使用者要求使用的資源,如果是被打上[Authorize]標籤的Controller或是Action。[Authorize]標籤會判別使用者已登入,允許並執行功能內容。
  4. ASP.NET站台執行執行功能內容後,會回傳功能頁面給瀏覽器。至此也就完成了,整個ASP.NET Identity登入的流程。

.Net缓存管理框架CacheManager

Cache缓存在计算机领域是一个被普遍使用的概念。硬件中CPU有一级缓存,二级缓存, 浏览器中有缓存,软件开发中也有分布式缓存memcache, redis。缓存无处不在的原因是它能够极大地提高硬件和软件的运行速度。在项目开发中,性能慢的地方常常是IO操作频繁的地方,读取数据库是我们常见的消耗性能的地方。这个时候,如果将使用频繁的数据缓存到能够高速读取的介质中,下次访问时,不用再去请求数据库,直接从缓存中获取所需的数据,就能够大大提高性能。这篇文章主要讨论的是在.Net开发中,如何使用CacheManager框架方便的管理项目中的缓存。

一,CacheManager介绍以及优点

CacheManager是开源的.Net缓存管理框架。它不是具体的缓存实现,而是在缓存之上,方便开发人员配置和管理各种不同的缓存,为上层应用程序提供统一的缓存接口的中间层。

下面是CacheManager的一些优点:

  • 让开发人员的生活更容易处理和配资缓存,即使是非常复杂的缓存方案。
  • CacheManager能够管理多种缓存,包含 内存, appfabric, redis, couchbase, windows azure cache, memorycache等。
  • 提供了额外的功能,如缓存同步、并发更新、事件、性能计数器等…

二,CacheManager开始之旅

CacheManager上手还是非常简单的。下面使用内存缓存结合CacheManager的一个实例,能够帮助我们快速的熟悉CacheManager如何使用。

首先在Visual Studio中创建一个Console Application.

使用Nuget为项目添加CacheManager包引用。CacheManager包含了很多的Package. 其中CacheManager.Core是必须的,其它的针对不同缓存平台上有不同的对应Package.

这个Demo中,我们使用内存作为缓存,所以只是需要CacheManager.Core和CacheManager.SystemRuntimeCaching

接着在Main函数中配置好我们的缓存:

复制代码
 1 using System;
 2 using CacheManager.Core;
 3 namespace ConsoleApplication
 4 {
 5     class Program
 6     {
 7         static void Main(string[] args)
 8         {
 9             var cache = CacheFactory.Build("getStartedCache", settings =>
10             {
11                 settings.WithSystemRuntimeCacheHandle("handleName");
12             });
13         }
14     }
15 }
复制代码

上面代码中使用CacheFactory创建了一个名称为getStartedCache的缓存实例,这个缓存实例使用的是SystemRunTime Cache, 内存缓存。一个缓存实例是可以配置多个Handle的,我们可以使用内存来作为存储介质,也可以使用Redis分布式缓存作为存储介质,并且可以同时在一个缓存实例中使用,后面我们再介绍多级缓存的配置和使用。

接下来,我们添加一些测试缓存的代码

复制代码
 1 static void Main(string[] args)
 2 {
 3 
 4     var cache = CacheFactory.Build("getStartedCache", settings =>
 5     {
 6         settings.WithSystemRuntimeCacheHandle("handleName");
 7     });
 8 
 9     cache.Add("keyA", "valueA");
10     cache.Put("keyB", 23);
11     cache.Update("keyB", v => 42);
12     Console.WriteLine("KeyA is "   cache.Get("keyA"));      // should be valueA
13     Console.WriteLine("KeyB is "   cache.Get("keyB"));      // should be 42
14     cache.Remove("keyA");
15     Console.WriteLine("KeyA removed? "   (cache.Get("keyA") == null).ToString());
16     Console.WriteLine("We are done...");
17     Console.ReadKey();
18 }
复制代码

三,CacheManager多级缓存配置

实际开发中,我们常常会需要使用多级缓存。

一种常见的情况是,你有一个分布式式缓存服务器,例如redis,独立的缓存服务器能够让我们的多个系统应用程序都能够共享这些缓存的数据,因为这些缓存项的创建是昂贵的。

和访问数据库相比,分布式缓存速度较快,但是和内存相比,还是不够快。因为分布式缓存使用还需要序列化和网络传输的时间消耗。

这个时候里,做个分级缓存是个好的解决方案,将内存缓存结合分布式缓存使用,使用频率高的数据直接从内存中读取,这将大大提高应用程序的整体性能。

使用内存缓存的读取速度能够达到分布式缓存的100倍,甚至更高。

使用CacheManager, 配置多级缓存是一件非常容易的事情

复制代码
 1 var cache = CacheFactory.Build("myCache", settings =>
 2 {
 3     settings
 4         .WithSystemRuntimeCacheHandle("inProcessCache")//内存缓存Handle
 5         .And
 6         .WithRedisConfiguration("redis", config =>//Redis缓存配置
 7         {
 8             config.WithAllowAdmin()
 9                 .WithDatabase(0)
10                 .WithEndpoint("localhost", 6379);
11         })
12         .WithMaxRetries(1000)//尝试次数
13         .WithRetryTimeout(100)//尝试超时时间
14         .WithRedisBackPlate("redis")//redis使用Back Plate
15         .WithRedisCacheHandle("redis", true);//redis缓存handle
16 });
复制代码

上面代码中,内存缓存和Redis缓存配置部分很容易看明白。但是BackPlate是什么作用? 接下来,我们看看CacheManager中的BackPlate挡板机制。

四, BackPlate解决分布式缓存中的同步问题

对于大型的软件系统,常常都是分为很多独立的子项目,各个子项目为了节约成本或者是方便数据共享,常常会共用同一个分布缓存服务器。这样在使用多级缓存的时候,就有可能出现数据不一致的情况。

假设在系统A中的更新了缓存中的一个数据项,这个时候CacheManager会在A设置的所有的缓存handle中更新改数据,这里也包括了分布式缓存上的数据。但是在系统B中的内存缓存中,还是会存在着旧的未更新的数据。当系统B从缓存中取这条记录的时候,就会出现内存缓存和分布式缓存中的数据不一致的情况。

为了防止这一点,缓存管理器有一个功能叫做cachebackplate将尝试同步多个系统中的缓存。

上面设置的多级缓存中,我们就将redis作为BackPlate的源. 也就是说所有的数据都需要以redis中缓存的数据为蓝本。

在设置redis作为BackPlate之后,同样发生上面的数据不一致的情况的时候,只要redis中的数据被修改了,就会触发CacheManager更新所有系统中的内存缓存中的数据,和redis中的数据保持一致。

同步的工作是如何完成的?

每次一条缓存记录被删除或更新的时候,Cache Manager会发送一个消息,让BackPlate存储这次的数据变化信息。所有其它的系统将异步接收这些消息,并将相应地作出更新和删除操作,保证数据的一致性。

五,ExpirationMode和CacheUpdateMode

涉及到缓存,就必然有缓存过期的问题。CacheManager中提供了一些简单的缓存过期方式设置。

复制代码
1 public enum ExpirationMode
2 {
3     None = 0,
4     Sliding = 1,
5     Absolute = 2,
6 }
复制代码

同时CacheManager还为多级缓存之间设置不同的数据更新策略

复制代码
1 public enum CacheUpdateMode
2 {
3     None = 0,
4     Full = 1,
5     Up = 2,
6 }
复制代码

使用Sliding和Up, 我们我可以为多级缓存设置不同的缓存过期时间,这样使用频率高的数据就能够保存在访问速度更快的内存中,访问频率次高的放到分布式缓存中。当CacheManager在内存中找不到缓存数据的时候,就会尝试在分布式缓存中找。找到后,根据Up设置,会再将该缓存数据保存到内存缓存中。

具体的配置方式如下:

复制代码
 1 var cache = CacheFactory.Build("myCache", settings =>
 2 {
 3     settings.WithUpdateMode(CacheUpdateMode.Up)
 4         .WithSystemRuntimeCacheHandle("inProcessCache")//内存缓存Handle
 5         .WithExpiration(ExpirationMode.Sliding, TimeSpan.FromSeconds(60)))
 6         .And
 7         .WithRedisConfiguration("redis", config =>//Redis缓存配置
 8         {
 9             config.WithAllowAdmin()
10                 .WithDatabase(0)
11                 .WithEndpoint("localhost", 6379);
12         }).
13         .WithExpiration(ExpirationMode.Sliding, TimeSpan. FromHours  (24)))
14         .WithMaxRetries(1000)//尝试次数
15         .WithRetryTimeout(100)//尝试超时时间
16         .WithRedisBackPlate("redis")//redis使用Back Plate
17         .WithRedisCacheHandle("redis", true);//redis缓存handle
18 
19 });
复制代码

六,缓存使用分析

在缓存使用中,对于缓存hit和miss数据态比较关系,这些数据能够帮助我们分析和调整缓存的设置,帮助缓存使用地更加合理。

1 var cache = CacheFactory.Build("cacheName", settings => settings
2     .WithSystemRuntimeCacheHandle("handleName")
3         .EnableStatistics()
4         .EnablePerformanceCounters());

在配置好缓存的Statistic功能后,我们就能够跟踪到缓存的使用情况了, 下面就是分别打印各个缓存handle中的分析数据。

复制代码
 1 foreach (var handle in cache.CacheHandles)
 2 {
 3     var stats = handle.Stats;
 4     Console.WriteLine(string.Format(
 5             "Items: {0}, Hits: {1}, Miss: {2}, Remove: {3}, ClearRegion: {4}, Clear: {5}, Adds: {6}, Puts: {7}, Gets: {8}",
 6                 stats.GetStatistic(CacheStatsCounterType.Items),
 7                 stats.GetStatistic(CacheStatsCounterType.Hits),
 8                 stats.GetStatistic(CacheStatsCounterType.Misses),
 9                 stats.GetStatistic(CacheStatsCounterType.RemoveCalls),
10                 stats.GetStatistic(CacheStatsCounterType.ClearRegionCalls),
11                 stats.GetStatistic(CacheStatsCounterType.ClearCalls),
12                 stats.GetStatistic(CacheStatsCounterType.AddCalls),
13                 stats.GetStatistic(CacheStatsCounterType.PutCalls),
14                 stats.GetStatistic(CacheStatsCounterType.GetCalls)
15             ));
16 }
复制代码

七,结尾

缓存是个好东西,用好了能够极大的提高性能。缓存的使用本身是个很大的话题,这边文章只是从缓存管理这个角度介绍了CachManager的使用。

下面是CacheManager相关的资料和链接:

官方主页

http://cachemanager.net/

源代码

https://github.com/MichaCo/CacheManager

官方MVC项目的Sample

https://github.com/MichaCo/CacheManager/tree/master/samples/CacheManager.Samples.Mvc

最近在思考不同情况下缓存使用的区别问题。对于互联网项目来说,数据的一致性要求常常不太高,缓存管理中,关注点可能在缓存的命中率上。对于应用系统,访问请求不大,但是对于数据的一致性要求较高,缓存中的数据更新策略可能更加重要。

怎样才是好的适合应用系统的缓存设计呢? 如果大家有兴趣,欢迎探讨指教

正则表达式整理

 普通字符

符号 说明
. 除“\n”之外的任何单个字符。要匹配“\n”在内的任何字符,请使用像“(.|\n)”的模式。在中括号表达式时 [.] 只会匹配 .字符,等价于 .
\d 匹配一个数字字符。等价于[0-9]。
\w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”
\s 匹配任意的空白符,包括空格、制表符、换页符等等。等价于 [\f\n\r\t\v]。
\f 匹配一个换页符。等价于\x0c和\cL。
\n 匹配一个换行符。等价于\x0a和\cJ。
\r 匹配一个回车符。等价于\x0d和\cM。
\t 匹配一个制表符。等价于\x09和\cI。
\v 匹配一个垂直制表符。等价于\x0b和\cK。

定位符

一个网站如果要求你填写的QQ号必须为5位到12位数字时,可以使用:^\d{5,12}$。因为使用了^和$,所以输入的整个字符串都要用来和\d{5,12}来匹配,也就是说整个输入必须是5到12个数字.

符号 说明
^ 匹配字符串的开始
$ 匹配字符串的结束
\b 匹配单词的开始或结束 例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。

字符集合

要想查找数字,字母或数字,空白是很简单的,因为已经有了对应这些字符集合的元字符,但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办?

符号 说明
x y
[xyz] 字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。
[a-z] 字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。

反义字符

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义

符号 说明
\D 匹配一个非数字字符。等价于[^0-9]。
\W 匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。
\S 匹配任何非空白字符。等价于[^ \f\n\r\t\v]。
\B 匹配不是单词开头或结束的位置。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
[^xyz] 负值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“p”。

限定符

符号 说明
* 匹配前面的子表达式零次或多次。*等价于{0,}。
+ 匹配前面的子表达式一次或多次。+等价于{1,}。
? 匹配前面的子表达式零次或一次。?等价于{0,1}。
{n} 匹配确定的n次。
{n,} 至少匹配n次。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n,m} 最少匹配n次且最多匹配m次。“o{0,1}”等价于“o?”。

贪婪与懒惰

非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。

? 紧跟在任何一个其他限制符 *,+,?,{n},{n,},{n,m} 后面时,匹配模式是非贪婪的。

例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”

符号 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

分组

符号 说明
\num 匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,“(.)\1”匹配两个连续的相同字符。
$num 替换匹配的引用
(pattern) 匹配exp,并捕获文本到自动命名的组里。在JScript 中则使用 $0…$9 属性
(?exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?’name’exp)
(?:pattern) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
(?=pattern) 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95
(?<=pattern) 反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“(?<=95
(?!pattern) 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95
(?<!pattern) 反向否定预查,与正向否定预查类拟,只是方向相反。例如“(?<!95

 

正向预查

现在,我们假设需要仅匹配 Windows,不匹配后面的版本号,并且要求 Windows 后面的版本号只能是 数字类型,换言之,XP 和 Vista 不能被匹配,
在正则表达式中,可以使用 正向预查 来解决这个问题。本例中,写法是:“Windows(?= [\d.]+\b)”。
它的语法是在 子模式内部 前面加“?=”,表示的意思是:首先,要匹配的文本必须满足此子模式前面的表达式(本例,“Windows ”);其次,此子模式不参与匹配。

Text:
Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.
Windows 95 and Windows 98 are the successor.
Then Windows 2000 and Windows Xp appeared.
Windows Vista is the Latest version of the family.

RegEx:
Windows( ?=[\d.]+\b)

Result:(带下划线的为成功匹配的)
Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.
Windows 95 and Windows 98 are the successor.
Then Windows 2000 and Windows Xp appeared.
Windows Vista is the Latest version of the family.

可以将 正向预查 理解成为自定义的边界(\b),这个边界位于表达式末。

引用

aspnetcore 的依赖注入

https://dotnet.myget.org/gallery/dotnet-core

工厂方法

services.AddTransient<Func<string,string,BaseController>>((provider) =>
{
return (string server, string name) =>
{
name = “Gateway.Controllers.” + name;
var type = Assembly.GetEntryAssembly().GetType(name);
BaseController instance = null;
if (type != null)
{
instance = ActivatorUtilities.CreateInstance(provider, type, null) as BaseController;
}
return instance;
};
});

三种生命周期管理模式

只有在充分了解ServiceScope的创建过程以及它与ServiceProvider之间的关系之后,我们才会对ServiceProvider支持的三种生命周期管理模式(Singleton、Scope和Transient)具有深刻的认识。就服务实例的提供方式来说,它们之间具有如下的差异:

  • Singleton:ServiceProvider创建的服务实例保存在作为根节点的ServiceProvider上,所有具有同一根节点的所有ServiceProvider提供的服务实例均是同一个对象。
  • Scoped:ServiceProvider创建的服务实例由自己保存,所以同一个ServiceProvider对象提供的服务实例均是同一个对象。
  • Transient:针对每一次服务提供请求,ServiceProvider总是创建一个新的服务实例。

为了让读者朋友们对ServiceProvider支持的这三种不同的生命周期管理模式具有更加深刻的理解,我们照例来做一个简单的实例演示。我们在一个控制台应用中定义了如下三个服务接口(IFoo、IBar和IBaz)以及分别实现它们的三个服务类(Foo、Bar和Baz)。

现在我们在作为程序入口的Main方法中创建了一个ServiceCollection对象,并采用不同的生命周期管理模式完成了针对三个服务接口的注册(IFoo/Foo、IBar/Bar和IBaz/Baz分别Transient、Scoped和Singleton)。我们接下来针对这个ServiceCollection对象创建了一个ServiceProvider(root),并采用创建ServiceScope的方式创建了它的两个“子ServiceProvider”(child1和child2)。

为了验证ServiceProvider针对Transient模式是否总是创建新的服务实例,我们利用同一个ServiceProvider(root)获取针对服务接口IFoo的实例并进行比较。为了验证ServiceProvider针对Scope模式是否仅仅在当前ServiceScope下具有“单例”的特性,我们先后比较了同一个ServiceProvider(child1)和不同ServiceProvider(child1和child2)两次针对服务接口IBar获取的实例。为了验证具有“同根”的所有ServiceProvider针对Singleton模式总是返回同一个服务实例,我们比较了两个不同child1和child2两次针对服务接口IBaz获取的服务实例。如下所示的输出结构印证了我们上面的论述。

Centos安装Syncthing同步工具

Syncthing是一个开源的同步工具,支持多版本控制,同时支持Windows、Mac OS X、Linux等客户端,和Resilio有点类似,但是又略有不同,这篇文章介绍一下Centos安装Syncthing工具的方法。

syncthing

一、下载与安装

Syncthing工具配置非常的简单,小z博客以CentOS X64为例,如果您需要其它版本的客户端请访问:syncthing官网下载。言归正传,下面就开始分别执行命令:


### 下载客户端
wget http://soft.hixz.org/linux/syncthing-linux-amd64-v0.14.11.tar.gz
### 解压
tar -zxvf syncthing-linux-amd64-v0.14.11.tar.gz
### 进入目录
cd syncthing-linux-amd64-v0.14.11
### 复制到环境变量
cp syncthing /usr/local/bin/

接着我们需要先运行一次让Syncthing自动生成初始配置文件,上面已经加入环境变量,直接输入syncthing即可运行,会看到下面的运行结果。


[root@xiaoz ~]# syncthing
[monitor] 20:37:05 INFO: Starting syncthing
[start] 20:37:05 INFO: Generating ECDSA key and certificate for syncthing...
[7NYBG] 20:37:05 INFO: syncthing v0.14.11 "Dysprosium Dragonfly" (go1.7.3 linux-amd64) jenkins@build.syncthing.net 2016-11-15 06:23:48 UTC
[7NYBG] 20:37:05 INFO: My ID: 7NYBGD4-AL5FI6M-6P5ULKJ-QSPFASO-T57T4QW-WETWQXT-CAGTJ2I-3PFQGQP
[7NYBG] 20:37:06 INFO: Single thread hash performance is 154 MB/s using minio/sha256-simd (95 MB/s using crypto/sha256).
[7NYBG] 20:37:06 INFO: Default folder created and/or linked to new config
[7NYBG] 20:37:06 INFO: Defaults saved. Edit /root/.config/syncthing/config.xml to taste or use the GUI
[7NYBG] 20:37:06 INFO: Ready to synchronize sxdwy-d7npj (readwrite)
[7NYBG] 20:37:06 INFO: Using discovery server https://discovery-v4-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC
[7NYBG] 20:37:06 INFO: Using discovery server https://discovery-v4-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ
[7NYBG] 20:37:06 INFO: Using discovery server https://discovery-v4-4.syncthing.net/v2/?id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW
[7NYBG] 20:37:06 INFO: Using discovery server https://discovery-v6-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC
[7NYBG] 20:37:06 INFO: Using discovery server https://discovery-v6-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ
[7NYBG] 20:37:06 INFO: Using discovery server https://discovery-v6-4.syncthing.net/v2/?id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW
[7NYBG] 20:37:06 INFO: TCP listener ([::]:22000) starting
[7NYBG] 20:37:06 INFO: Completed initial scan (rw) of folder sxdwy-d7npj
[7NYBG] 20:37:06 INFO: Loading HTTPS certificate: open /root/.config/syncthing/https-cert.pem: no such file or directory
[7NYBG] 20:37:06 INFO: Creating new HTTPS certificate
[7NYBG] 20:37:07 INFO: GUI and API listening on 127.0.0.1:8384
[7NYBG] 20:37:07 INFO: Access the GUI via the following URL: http://127.0.0.1:8384/
[7NYBG] 20:37:07 INFO: Device 7NYBGD4-AL5FI6M-6P5ULKJ-QSPFASO-T57T4QW-WETWQXT-CAGTJ2I-3PFQGQP is "xiaoz" at [dynamic]
[7NYBG] 20:37:11 INFO: Automatic upgrade (current "v0.14.11" < latest "v0.14.12")
[7NYBG] 20:37:17 INFO: Detected 0 NAT devices

二、修改配置文件

上一个步骤输入syncthing已经成功运行,并生成了对应的配置文件,输入Ctrl C退出客户端。我们需要修改下默认的配置文件:vi ~/.config/syncthing/config.xml大概在22行左右的配置,将127.0.0.1修改为0.0.0.0,如下截图。

2016-11-22_204409

三、放行端口

syncthing默认监听8384端口,我们需要在iptables放行这个端口,依次输入下面的命令。


### 放行8384端口
/sbin/iptables -I INPUT -p tcp --dport 8384 -j ACCEPT
/etc/init.d/iptables save
service iptables restart 

四、测试访问

再次输入syncthing命令启动Syncthing客户端,然后在浏览器输入:http://您的服务器IP:8384进行访问。

runsyncthing

Syncthing默认支持中文语言,首次登录会让您设置用户名和密码,到这里基本上就完成了,如何添加其它设备和同步文件夹可以自行研究下。

五、其它说明

如果希望Syncthing在后台运行可以使用nohup命令来实现:nohup syncthing &

六、总结

Syncthing可以在不同设备之间实现同步,前提是已经安装Syncthing客户端,另外还支持历史版本的功能,如果有条件您完整可以利用Syncthing打造自己私有的同步工具。原创文章,转载请注明。

此文参考了:Syncthing: 一个在计算机之间同步文件/文件夹的私密安全同步工具
Syncthing官网:https://syncthing.net/

mac 装双系统..

1.U盘格式化为ms-dos(fat)  主引导记录..注意U盘名字不能是中文..否则格式化失败

2.无法创建可引导的USB驱动器—-要先在磁盘工具里面移除 windows 的ISO!

3.按options选项..进入efi硬盘

4.不能装docker  说虚拟化不能开启…任务管理器.cpu可以看到

https://apple.stackexchange.com/questions/120361/how-to-turn-on-hardware-virtualization-on-late-2013-macbook-pro-for-windows-8-1?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

It sounds like you\’re running into the same issue I did, where after booting into Windows the VT-x shows as \’Disabled\’ in Task Manager.

Not sure how or why, but after going into

  • OS X
  • System Preferences
  • Target Disk
  • Select the BOOTCAMP disk as the startup disk

Everything was well after that and I could happily use Hyper-V, even from a cold boot.

If I cold booted using the Options-key, and then selecting Windows, VT-x was disabled in Task Manager.

Go figure. Could some Mac genius out there explain this one?

This thread explains that you have to boot using the CSM-BIOS layer. discussions.apple.com/thread/6720461?tstart=0 ; In addition it also provides a command line to permanently fix this problem. First use diskutil list to work out Windows partition, then sudo /usr/sbin/bless --device /dev/disk0s4 --setBoot --legacy --legacydrivehint /dev/disk0 – Chui Tey Dec 23 \’16 at 23:10

接着上面的说,这个在mac本上就没有BIOS主板系统,但是昂贵的Mac肯定也是有虚拟化服务的~只不过Mac本不是手动启动,而是每次启动完OSX系统自动启动~ 但是如果 第一次启动的是 bootcamp的Windows 系统 那么 这个 虚拟化是启动不了的。。。这时候有一个解决办法就是先启动OSX系统,再更具目标磁盘重启到bootcamp的Windows系统~ 参考下图~

通过这个启动盘重启的Windows虚拟化是 打开的状态~ 如下图

虚拟化状态打开后就可以下载安装 HoloLens 的模拟器了 ~ 而且在开发调试中 也一定要把虚拟化打开

typescript

Typescript 2.0之后,tsd和typings都可以去掉了。要获得lodash的类型定义文件只需要

npm install @types/lodash

这样一来,typescript的工作流就和普通的Node.js项目没什么区别了。
更重要的是Typescript 2.1之后,async/await可以直接编译到ES5。babel什么的,再见吧

https://github.com/typings/typings

你真的了解volatile关键字吗?

一、Java内存模型

想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。

举个简单的例子:在java中,执行下面这个语句:

1
i  = 10 ;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

比如同时有2个线程执行这段代码,假如初始时i的值为10,那么我们希望两个线程执行完之后i的值变为12。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的工作内存当中,然后线程1进行加1操作,然后把i的最新值11写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存。

最终结果i的值是11,而不是12。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

那么如何确保共享变量在多线程访问时能够正确输出结果呢?

在解决这个问题之前,我们要先了解并发编程的三大概念:原子性,有序性,可见性。

二、原子性

1.定义

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.实例

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

同样地反映到并发编程中会出现什么结果呢?

举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

1
i = 9;

假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

3.Java中的原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

请分析以下哪些操作是原子性操作:

1
2
3
4
x = 10;         //语句1
y = x;         //语句2
x ;           //语句3
x = x 1;     //语句4

咋一看,可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x 和 x = x 1包括3个操作:读取x的值,进行加1操作,写入新的值。

所以上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

关于synchronized和Lock的使用,参考:关于synchronized和ReentrantLock之多线程同步详解

三、可见性

1.定义

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

2.实例

举个简单的例子,看下面这段代码:

1
2
3
4
5
6
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;

由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到工作内存中,然后赋值为10,那么在线程1的工作内存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到线程2的工作内存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.Java中的可见性

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

四、有序性

1.定义

有序性:即程序执行的顺序按照代码的先后顺序执行。

2.实例

举个简单的例子,看下面这段代码:

1
2
3
4
5
6
int i = 0;             
boolean flag = false;
i = 1;                //语句1 
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

1
2
3
4
int a = 10;    //语句1
int r = 2;    //语句2
a = a 3;    //语句3
r = a*a;     //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

1
2
3
4
5
6
7
8
9
10
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 //线程2:
while(!inited ){
   sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

3.Java中的有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

①程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

②锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作

③volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

④传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

⑤线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

⑦线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

⑧对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

下面我们来解释一下前4条规则:

对于程序次序规则来说,就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,但是虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

五、深入理解volatile关键字

1.volatile保证可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

先看一段代码,假如线程1先执行,线程2后执行:

1
2
3
4
5
6
7
8
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰之后就变得不一样了:

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

2.volatile不能确保原子性

下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
    public volatile int inc = 0;
    public void increase() {
        inc ;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i ){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j )
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1//保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过AtomicInteger。

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

3.volatile保证有序性

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

1
2
3
4
5
6
7
8
//x、y为非volatile变量
//flag为volatile变量
x = 2;        //语句1
y = 0;        //语句2
flag = true//语句3
x = 4;         //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

那么我们回到前面举的一个例子:

1
2
3
4
5
6
7
8
9
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

六、volatile的实现原理

1.可见性

处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

2.有序性

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

七、volatile的应用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

下面列举几个Java中使用volatile的几个场景。

①.状态标记量

1
2
3
4
5
6
7
8
9
volatile boolean flag = false;
 //线程1
while(!flag){
    doSomething();
}
  //线程2
public void setFlag() {
    flag = true;
}

根据状态标记,终止线程。

②.单例模式中的double check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
为什么要使用volatile 修饰instance?

主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

参考文章

Java并发编程:volatile关键字解析
【死磕Java并发】—–深入分析volatile的实现原理
Java并发机制的底层实现原理
Volatile的实现原理