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