ASP.NET Core 身份认证 (Identity、Authentication)

Authentication和Authorization

每每说到身份验证、认证的时候,总不免说提及一下这2个词。他们的看起来非常的相似,但实际上他们是不一样的。

Authentication想要说明白的是 你是谁(你的身份是什么)

Authorization想要说明白的是 你能做什么(得到了什么权限)

但是这两个词通常是要同时存在的。要知道有什么权限前提是知道你是谁。

 

OAuth2认证

这是最近很流行的认证的标准。要完全理解他的话也要说上一大篇,在这里简单点说明:

第三方网站能够得到认证方提供的身份和授予的权限。就是上面提到的Authorization

 

说个例子

这里似乎说个栗子会比较好,例如搭乘飞机:

假设你购买了一张南方航空的机票。那么你去坐飞机的时候可能会出现以下场景:

1.到南方航空的柜台checkin。得到一张纸质的,上面有你身份证信息,航班信息。

2.到入站口被检票人员查票。检票员会查看你的机票是否正确,机票身份信息是否与你的身份证信心对应。

3.到VIP休息室等待登机。被服务人员告知你并没有权限进入VIP休息室,原因是购买的是普通票,非贵宾票。

4.登机,入座。空乘人员核对你的航班是否对应当前的航班。

好了,上面的几个场景跟认证是相当的相似。第一步checkin,对应的是认证系统,纸质票就是提供的票据。第二步就相当于你自己的网站,得到了南方航空的认证,只要知道是南方航空颁发的票据,你都认为是有效的。这里也有个特别的地方,就是机场不可能只认南方航空,可能东方航空,春秋航空都认,所以这个也是认证的特点,你的网站是可以同时实现多个具有相同规则的认证方提供的票据。第三步相当于是权限的验证,虽然客户手上是有票据,但由于票据上声明(Claim)的权限并不包含VIP休息室使用。第四步相当于允许的权限,有这个票据,可以指定做某些可做的事情。

 

为什么要用

现在的服务基本上都是集群的,进行的网络通讯也以无状态请求为主。而OAuth2就很好地能实现单点登录。

就是一个地方登录了,只要使用OAuth2的规范,其他所有使用的服务器都能验证这个授权的正确性。

 

怎么实现

说了这么多,其实更加多的人是想知道怎么实现吧~

这里会说一下最简单的实现方法。我使用的是asp.net core的实现,会和用asp.net实现的方法有一点区别。但是也有很多相似的地方,例如都是利用中间件来实现的,只需要修改很少一部分就能在asp.net上使用。

整个Demo会包含2个项目:

一个用于认证,颁发票据的服务。

一个是受认证方,用于根据票据提供服务的网站

 

第一步:生成一个空的asp.net core的项目。在已经具备.net core环境下;

为什么要一个空的项目呢?因为这个项目实在简单,不必要生成一个MVC,我们重点是实现认证。

 在指定的路径下,使用dotnet new web来创建,下面是创建之后的结构在VScode上查看的。可以看到是相当简单的。

可能有些人会有疑问,为什么项目文件是identity.csproj,不是json的后缀。其实是因为dotnet core 1.1已经升级了,为了使用MSBuild。

第二步,要把使用的包引用进来。打开项目文件identity.csproj。然后修改之后的文件如下:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />

    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.1"/>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.1.1"/>
    <PackageReference Include="Microsoft.NETCore.App" Version="1.1.1"/>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.1"/>
    <PackageReference Include="Microsoft.AspNetCore.Routing" Version="1.1.1"/>
    <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.1"/>
    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="1.1.1"/>
  </ItemGroup>

</Project>

最后的几个PackageReference就是引用的包了。这里由于没有使用IIS,所以只要引用Kestrel就可以部署了。加完引用包之后,是需要走一下dotnet restore。

完成之后我们继续第三步,重头戏来了,就是做一些认证的配置。代码如下:

 1 public class Startup
 2     {
 3         public Startup(IHostingEnvironment env)
 4         {
 5         }
 6 
 7         // This method gets called by the runtime. Use this method to add services to the container.
 8         // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
 9         public void ConfigureServices(IServiceCollection services)
10         {
11             services.AddTransient<IUserValidate,UserValidate>();
12         }
13 
14         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
15         public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
16         {
17             loggerFactory.AddConsole();
18 
19             if (env.IsDevelopment())
20             {
21                 app.UseDeveloperExceptionPage();
22             }
23 
24             string secretKey = "encrypt_the_validate_site_key";
25             var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
26             var options = new TokenGenerateOption
27             {
28                 Path = "/token",
29                 Audience = "http://validateSite.woailibian.com",
30                 Issuer = "http://thisSite.woailibian.com",
31                 SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
32                 Expiration = TimeSpan.FromMinutes(15),
33             };
34             var userValidate = app.ApplicationServices.GetService<IUserValidate>();
35             // var userValidate = new UserValidate();
36 
37             var tokenGenerator = new TokenGenerator(options, userValidate);
38             app.Map(options.Path, tokenGenerator.GenerateToken);
39 
40             app.Run(async (context) =>
41             {
42                 await context.Response.WriteAsync("This Service only use for authentication! ");
43             });
44         }
45 
46         class UserValidate : IUserValidate
47         {
48             public UserModel GetUserByContext(string userName, string password)
49             {
50                 UserModel rct = null;
51                 if (userName == "moto" && password == "P@sw0rd123")
52                 {
53                     rct = new UserModel { UserName = userName, UniqueId = "1234567890" };
54                 }
55 
56                 return rct;
57             }
58         }
59     }
Startup.cs

 

 

这里有必要解释一下,我们从24行开始。SecretKey这里是一条key,应该是认证方需要对使用方公开的信息。这里的字符要进行UTF转换。

Path指的是通过认证方的哪一个地址进行认证,其他地址则会忽略。

Audience是使用方的信息,认证只有有可能是重定向地址。Issuer是认证方自己的信息,可以理解为拍照、商标(例如上面例子说道的南方航空票据上南航的商标)。

Expiration是指token的有效时间

tokenGenerator是我们自己建的生成token的类。

app.Map是asp.net core中间件的特性,只根据指定的地址进行处理,并且具体的执行的方法是tokenGenerator的GenerateToken。

app.Run也是asp.net core的特性,这里意思是任何请求,只要上层没有做处理,都会走到这。

 

第四步,建立TokenGenerator。

 1 public class TokenGenerator
 2     {
 3         TokenGenerateOption _Option;
 4 
 5         public IUserValidate UserValidator { get; private set; }
 6         public TokenGenerator(TokenGenerateOption option, IUserValidate validator)
 7         {
 8             _Option = option;
 9             UserValidator = validator;
10         }
11 
12         async Task BadRequest(HttpContext context, string msg)
13         {
14             context.Response.StatusCode = 400;
15             await context.Response.WriteAsync(msg);
16         }
17 
18         internal void GenerateToken(IApplicationBuilder app)
19         {
20             app.Run(async context =>
21             {
22                 if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType)
23                 {
24                     await BadRequest(context,"format not corrent");
25                     return;
26                 }
27 
28                 var username = context.Request.Form["username"];
29                 var password = context.Request.Form["password"];
30 
31                 var userModel = UserValidator?.GetUserByContext(username, password);
32                 if (userModel == null)
33                 {
34                     await BadRequest(context, "Invalid username or password.");
35                     return;
36                 }
37 
38                 var now = DateTime.UtcNow;
39                 var claims = new Claim[]
40                 {
41                     new Claim(JwtRegisteredClaimNames.Sub, userModel.UniqueId),
42                     new Claim(JwtRegisteredClaimNames.UniqueName,userModel.UserName),
43                     new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
44                     new Claim(JwtRegisteredClaimNames.Iat, now.ToString(), ClaimValueTypes.Integer64)
45                 };
46 
47                 var jwt = new JwtSecurityToken(_Option.Issuer, _Option.Audience, claims, now, now.Add(_Option.Expiration), _Option.SigningCredentials);
48                 var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
49 
50                 var response = new
51                 {
52                     access_token = encodedJwt,
53                     expires_in = (int)_Option.Expiration.TotalSeconds,
54                 };
55 
56                 // Serialize and return the response
57                 context.Response.ContentType = "application/json";
58                 string responseStr = JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented });
59                 await context.Response.WriteAsync(responseStr);
60             });
61         }
62 
63     }
TokenGenerator

GenerateToken里面继续使用app.Run这个特性。

之后必须判断是否是POST的方式,并且请求的content-type是否是form(这里是OAuth2的标准)

之后通过UserValidator用于验证用户的账号密码是否正确。通过验证之后的结果,来生成指定的Claim。

通过JwtSecurityToken来生成整个token的实体,并且进行Encode成一个字符串。

最终通过json序列化自己定义的一个匿名类。

 

看了前面两段代码,是还漏了一个接口,两个实体的。下面是他们的代码

1     public interface IUserValidate
2     {
3         UserModel GetUserByContext(string userName,string password);
4     }
1     public class UserModel
2     {
3         public string UserName { get; set; }
4 
5         public string UniqueId { get; set; }
6     }
 1     public class TokenGenerateOption
 2     {
 3         public string Path { get; set; }
 4 
 5         public string Issuer { get; set; }
 6 
 7         public string Audience { get; set; }
 8 
 9         public TimeSpan Expiration { get; set; }10 
11         public SigningCredentials SigningCredentials { get; set; }
12     }

好了,identity(认证方)的项目基本上编码方面已经完成了。下面是现在的结构:

 

 

试一试

用dotnet run跑起来。之后用一些工具测试,例如Postman等等。下面是结果,可以看到有access_token和过期时间

 

去https://jwt.io验证,下面可以看到验证时解释了里面的内容。其实通过这个例子也想说明,其实认证方和接受方其实是没有通讯的,也能进行token的解密。

这可以解决多服务集群的单点登录问题。

 

写到这里本来是想做个使用方的demo,但是发现篇幅已经很长了。所以留到下一篇文章。

如果有经验的朋友看着这些代码,应该会觉得很丑陋。怎么这么粗糙地实现了,代码耦合度很高,并且无法做成一个类库,然后应用到不同的项目中。

其实这个是我专门做成这样的,为了用最简单粗暴的方法入门。OAuth2还有很多需要讲述的知识点,例如Refresh_token,配置文件公开等等。

这里主要是简述原理,做个演示。之后我会写一个事例,完全通过中间件实现的,这样包装之后就能多次应用了。

 

 

题外话:

最近在维护一个旧项目,里面的代码真的太恶心了。每个类都有5000行以上,10行重复的代码起码有20次。平均一个方法400行,里面通篇的region...

变量用拼音就算了,还用拼音缩写,还用中文....

里面能看到很多流行的模式,单例、工厂、仓储、Leader-Following等等。但是怎么说呢,觉得还是不要用好了~

单例里面的构造函数是public的,而且外面还真有地方实例化了。。。

工厂里面有很多的公共变量,而且这些变量在不同情境是要做不同的变化的。。。

仓储里面并没有具体的实现方法,是有几个Find,update,delete的方法。然后重点是。。。sql语句是在仓储之外写的,还有是linq也是。。。

Leader-Following这个写了根本没有,已经算最好的了。

话说虽然编码是为了给计算机识别,但是写代码的那个人也是说的人话,请在代码中写出人话!不然除了电脑,谁能看得懂!

说的人话,不是说要加多少注释,其实好的代码,一个屏幕页面,看到3、5个注释才是好的。因为别人光看代码行就懂了!

 

posted @ 2017-03-25 13:07  woailibian  阅读(24206)  评论(5编辑  收藏  举报