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 }
这里有必要解释一下,我们从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 }
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个注释才是好的。因为别人光看代码行就懂了!