基于HTTP协议带用户认证的GIT开发环境设置

it 的访问可以采用 HTTP 或 SSH 协议安全的访问,通常我们使用 gitlib 进行 Web 管理,但是在 Linux 命令行开发环境下,基本都是使用 SSH 协议,只需要在 gitlib 里面配置好相应的 SSH Key 就可以。

由于现在开发环境的特殊情况,我们需要在 Linux 命令行开发环境下,不能使用 SSH 方式,而只能使用 HTTP 协议进行安全访问,并且需要对开发者进行认证,并且开发者在本地开发环境中的用户名和密码需要加密存储。

接下来我就简单介绍我们的开发团队是如何在 Linux 命令行环境下进行 GIT 开发环境配置。

1. 创建 用户名/密码 文件(明文密码)
在自己的 $HOME 目录下,编辑 .netrc 文件,内容如下:

1
2
machine git.xxxxx.net
    login xxx@xxx.com password xxxxxx

2. 创建 GnuPG 密钥
在自己的$HOME 目录下,执行命令:

1
2
gpg --gen-key
注:默认回车即可,RSA密钥选择1024,2048太慢,但安全性好

可以使用以下命令查看已生成的密钥:

1
gpg --list-key

3. 加密 用户名/密码 文件
在自己的 $HOME 目录下,执行命令:

1
2
gpg -o ~/.netrc.gpg -er yourname ~/.netrc
注:执行完成后,可以删除明文密码文件 .netrc

4. 设置用户的 Git 配置
在自己的 $HOME 目录下,执行命令:

1
2
3
4
5
#此方法会缓存用户名/密码,不需要每次都输入
git config --global credential.helper 'store'
#此方法需要每次都输入用户名/密码
#git config --global credential.helper 'netrc -f ~/.netrc.gpg -d'

此时可以编辑 .gitconfig 文件,填写更多信息:

1
2
3
4
5
6
7
8
[user]
    name = XXX
    email = xxx@xxx.com
[core]
    excludesfile = /home/xxx/.gitignoreglobal
[credential]
    helper = store
    #helper = netrc -f ~/.netrc.gpg -d

5. 开始 GIT 环境

注:需要使用新版本Git(我使用的是2.2.2),同时将 git-credential-netrc 脚本拷贝到Git安装目录(libexec/git-core)

终于解决“Git Windows客户端保存用户名与密码”的问题

传说中的Git果然名不虚传:速度快,想分就分,想合就合(分支管理方便)…

但有一个地方不爽,很不爽:每次Pull或Push时都要输入用户名与密码,无法保存用户名与密码。

程序员的天性之一 —— 讨厌重复(恨),程序员的天性之二 —— 解决问题(爱),爱与恨的结合就能碰撞出火花 —— 集中精力寻找解决方案(静)。

准备工作:

1. 远离微博(普天之下,最容易让人分心的东西非微博莫属)。

2. 确定路线,这个问题的路线很明显,要分别从服务器端与客户端下手。

开始干活:

1. 先从服务器端下手,下载Bonobo Git Server的源代码看一下服务器端是如何验证的。

web.config中就有答案,原来用的是http basic authentication。

<location path=”Git.aspx”>
<system.web>
<authorization>
<allow users=”?” />
</authorization>
</system.web>
</location>

有了这个答案就可以和服务器端说88,并投入客户端的怀抱…

2. 客户端用的是msysgit TortoiseGit,TortoiseGit只是壳,msysgit才是真相。

pull与push操作实际上都是调用msysgit中的git pull与git push命令,但git命令并没有传递用户名与密码的参数。

怎么办?… 问Google呗,对Google说话要精炼,不能哆嗦,我是这样说的:“git username password”;还是Google给力,第1页最后1条就给出了线索 —— Setup a remote git repository using http with push support and digest auth,见下图:

虽然Google出来的文章是针对Linux的,但不要轻易认为问题是Windows下的,对Linux下的解决方法看都不看一眼。直接找到答案很难,更多的时候我们是在找线索,并在发现的蛛丝马迹中不断地思考可能的解决方法。

这里的“.netrc”就是线索,利用这个线索继续对Google说:“git netrc windows”…

第1页第5个,”Stack Overflow”的大名映入眼帘:

Git – How to use netrc file on windows – Stack Overflow

Google再怎么 1也比不上品牌的力量,看到Stack Overflow,就看到了希望,就有点击的冲动。

点开一看,立即有“百度”(这里是“众里寻她千百度”的缩写)的感觉:

这就是正确答案,我们已经验证过了,下面详细描述一下解决方法:

1. 在Windows中添加一个HOME环境变量,值为%USERPROFILE%,如下图:

2. 在“开始》运行”中打开%Home%,新建一个名为“_netrc”的文件。

3. 用记事本打开_netrc文件,输入Git服务器名、用户名、密码,并保存。示例如下:

machine git.cnblogs.com
login cnblogs_user
password cnblogs_pwd

问题解决,Git更给力了!

微信授权的一些问题

1.网页授权域名

http://ngrok.ciqiuwl.cn

2 curl: (60) SSL certificate problem: unable to get local issuer certificate

这是 SSL 证书问题所致,在使用 SDK 调用微信支付等相关的操作时可能会遇到报 “SSL certificate problem: unable to get local issuer certificate” 的错误。

微信公众平台提供的文档中建议对部分较敏感的操作接口使用 https 协议进行访问,例如微信支付和红包等接口中涉及到操作商户资金的一些操作。 wechat SDK 遵循了官方建议,所以在调用这些接口时,除了按照官方文档设置操作证书文件外,还需要保证服务器正确安装了 CA 证书。

  1. 下载 CA 证书你可以从 http://curl.haxx.se/ca/cacert.pem 下载 或者 使用微信官方提供的证书中的 CA 证书 rootca.pem 也是同样的效果。
  2. 在 php.ini 中配置 CA 证书只需要将上面下载好的 CA 证书放置到您的服务器上某个位置,然后修改 php.ini 的 curl.cainfo 为该路径(绝对路径!),重启 php-fpm 服务即可。
    curl.cainfo = /path/to/downloaded/cacert.pem

    注意证书文件路径为绝对路径!以自己实际情况为准。

    其它修改 HTTP 类源文件的方式是不允许的。

在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 的模拟器了 ~ 而且在开发调试中 也一定要把虚拟化打开