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個注釋才是好的。因為別人光看代碼行就懂了!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM