zoukankan      html  css  js  c++  java
  • 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个注释才是好的。因为别人光看代码行就懂了!

  • 相关阅读:
    PHPExcel常用方法总结
    ThinkPHP多语言在分组下的用法!
    derby 的字符集
    最小化msn,skype到右下角(Win 7)
    用javascript控制 youtube动画,直接打开html会失败
    maven 的本地仓库
    excel读取csv的字符集问题
    在dos控制台显示utf8字符
    关于在php和mysql中日期型date,datetime,timestamp的使用
    用 java jar执行时的classpath的问题
  • 原文地址:https://www.cnblogs.com/woailibian/p/6579850.html
Copyright © 2011-2022 走看看