一、Form表單認證
之前的項目以MVC為主,采用的是from表單認證,Forms認證示意圖如下:

HTTP是一個無狀態的協議,WEB服務器在處理所有傳入HTTP請求時,根本就不知道某個請求是否是一個用戶的第一次請求與后續請求,或者是另一個用戶的請求。 WEB服務器每次在處理請求時,都會按照用戶所訪問的資源所對應的處理代碼,從頭到尾執行一遍,然后輸出響應內容,WEB服務器根本不會記住已處理了哪些用戶的請求,因此,我們通常說HTTP協議是無狀態的。
雖然HTTP協議與WEB服務器是無狀態,但我們的業務需求卻要求有狀態,典型的就是用戶登錄, 在這種業務需求中,要求WEB服務器端能區分某個請求是不是一個已登錄用戶發起的,或者當前請求是哪個用戶發出的。 在開發WEB應用程序時,我們通常會使用Cookie來保存一些簡單的數據供服務端維持必要的狀態。
登錄的操作通常會檢查用戶提供的用戶名和密碼,因此登錄狀態也必須具有足夠高的安全性。 在Forms身份認證中,由於登錄狀態是保存在Cookie中,而Cookie又會保存到客戶端,因此,為了保證登錄狀態不被惡意用戶偽造, ASP.NET采用了加密的方式保存登錄狀態。 為了實現安全性,ASP.NET采用【Forms身份驗證憑據】(即FormsAuthenticationTicket對象)來表示一個Forms登錄用戶, 加密與解密由FormsAuthentication的Encrypt與Decrypt的方法來實現。
1、用戶登錄的過程
-
檢查用戶提交的登錄名和密碼是否正確。
-
根據登錄名創建一個FormsAuthenticationTicket對象。
-
調用FormsAuthentication.Encrypt()加密。
-
根據加密結果創建登錄Cookie,並寫入Response。在登錄驗證結束后,一般會產生重定向操作, 那么后面的每次請求將帶上前面產生的加密Cookie,供服務器來驗證每次請求的登錄狀態。
var userid = Request["userid"]; var password = Request["password"]; if (userid == "123456" && password == "123456")//檢查用戶提交的登錄名和密碼是否正確 { var ticket = new FormsAuthenticationTicket( 1, userid, DateTime.Now, DateTime.Now.AddMinutes(5), true, "角色信息", "/" );//根據登錄名創建一個FormsAuthenticationTicket對象 var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));//調用FormsAuthentication.Encrypt()加密 cookie.HttpOnly = true; HttpContext.Response.Cookies.Add(cookie);//根據加密結果創建登錄Cookie,並寫入Response return Redirect("/");
}
2、每次請求時的過程
-
FormsAuthenticationModule嘗試讀取登錄Cookie。
-
從Cookie中解析出FormsAuthenticationTicket對象。過期的對象將被忽略。
-
根據FormsAuthenticationTicket對象構造FormsIdentity對象並設置HttpContext.User
-
UrlAuthorizationModule執行授權檢查。
using System.Web; using System.Web.Mvc; using System.Web.Security; namespace FormsAuth { public class MyAuthorizeAttribute:AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { var cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName]; var ticket = FormsAuthentication.Decrypt(cookie.Value); var roles = ticket.UserData; var inRoles = false; foreach (var role in roles.Split(',')) { if (Roles.Contains(role)) { inRoles = true; break; } } return inRoles; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { ActionResult result = new ContentResult { Content = "沒有頁面訪問權限!", ContentType = filterContext.HttpContext.Response.ContentType }; filterContext.Result = result ?? new HttpUnauthorizedResult(); } } }
在登錄與認證的實現中,FormsAuthenticationTicket和FormsAuthentication是二個核心的類型, 前者可以認為是一個數據結構,后者可認為是處理前者的工具類。UrlAuthorizationModule是一個授權檢查模塊,其實它與登錄認證的關系較為獨立, 因此,如果我們不使用這種基於用戶名與用戶組的授權檢查,也可以禁用這個模塊。由於Cookie本身有過期的特點,然而為了安全,FormsAuthenticationTicket也支持過期策略, 不過,ASP.NET的默認設置支持FormsAuthenticationTicket的可調過期行為,即:slidingExpiration=true 。 這二者任何一個過期時,都將導致登錄狀態無效。
Request.IsAuthenticated可以告訴我們當前請求是否已經過身份驗證, 我們來看一下這個屬性是如何實現的:
public bool IsAuthenticated { get { return (((this._context.User != null) && (this._context.User.Identity != null)) && this._context.User.Identity.IsAuthenticated); } }
DEMO下載:https://github.com/qiuxianhu/FormsAuth
二、HTTP基本認證(HTTP Basic Auth)
客戶端向服務端發送一個攜帶基於用戶名/密碼的認證憑證的請求。認證憑證的格式為“{UserName}:{Password}”,並采用Base64編碼,經過編碼的認證憑證被存放在請求報頭Authorization中,Authorization報頭值類似:Basic MTIzNDU2OjEyMzQ1Ng==。服務端接收到請求之后,從Authorization報頭中提取憑證並對其進行解碼,最后采用提取的用戶名和密碼實施認證。認證成功之后,該請求會得到正常的處理,並回復一個正常的響應。

新建一個Attribute:BasicAuthorizeAttribute用於實現basic認證
using System; using System.Net.Http; using System.Text; using System.Web.Http; using System.Web.Http.Controllers; namespace HttpAuth.App_Start { public class BasicAuthorizeAttribute : System.Web.Http.AuthorizeAttribute { protected override bool IsAuthorized(HttpActionContext actionContext) { //這個判斷是在進行跨域訪問時瀏覽器會發起一個Options請求去試探這個請求,但是他不會帶着data參數和一些header參數,所以認證肯定沒法通過導致無法繼續進行,所以給他直接認證通過。(對非跨域的則沒有影響) if (actionContext.Request.Method == HttpMethod.Options) { return true; } if (actionContext.Request.Headers.Authorization != null && actionContext.Request.Headers.Authorization.Parameter != null) { var authorizationParameter = Convert.FromBase64String(actionContext.Request.Headers.Authorization.Parameter); var basicArray = Encoding.Default.GetString(authorizationParameter).Split(':'); var userid = basicArray[0]; var password = basicArray[1]; if (userid == "123456" && password == "123456") { return true; } } return false; } /// <summary> /// 因為是繼承重寫的AuthorizeAttribute,在IsAuthorized 返回False的時候會執行這個方法 這里是返回一個401的錯誤信息 /// </summary> /// <param name="actionContext"></param> protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) { var responseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); //這句代碼指示瀏覽器 認證方式為Basic 然后瀏覽器自動彈出一個登陸窗口並以basic 的方式 加密后每次通過header 傳輸到服務器進行認證然后得到授權 responseMessage.Headers.Add("WWW-Authenticate", "Basic"); throw new HttpResponseException(responseMessage); } } }
用法也很簡單,在ApiController或方法上邊加屬性即可,如:
[BasicAuthorizeAttribute] // GET api/values [SwaggerOperation("GetAll")] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; }
效果如下:

DEMO下載: https://github.com/qiuxianhu/HttpAuthorization
三、OAuth 2.0
OAuth 2.0 是目前最流行的授權機制,用來授權第三方應用,獲取用戶數據。這個標准比較抽象,使用了很多術語,初學者不容易理解。其實說起來並不復雜,下面我就通過一個簡單的類比,幫助大家輕松理解,OAuth 2.0 到底是什么。
1、快遞員問題
我住在一個大型的居民小區。小區有門禁系統。進入的時候需要輸入密碼。我經常網購和外賣,每天都有快遞員來送貨。我必須找到一個辦法,讓快遞員通過門禁系統,進入小區。如果我把自己的密碼,告訴快遞員,他就擁有了與我同樣的權限,這樣好像不太合適。萬一我想取消他進入小區的權力,也很麻煩,我自己的密碼也得跟着改了,還得通知其他的快遞員。有沒有一種辦法,讓快遞員能夠自由進入小區,又不必知道小區居民的密碼,而且他的唯一權限就是送貨,其他需要密碼的場合,他都沒有權限?
2、授權機制的設計
於是,我設計了一套授權機制。
第一步,門禁系統的密碼輸入器下面,增加一個按鈕,叫做"獲取授權"。快遞員需要首先按這個按鈕,去申請授權。
第二步,他按下按鈕以后,屋主(也就是我)的手機就會跳出對話框:有人正在要求授權。系統還會顯示該快遞員的姓名、工號和所屬的快遞公司。
我確認請求屬實,就點擊按鈕,告訴門禁系統,我同意給予他進入小區的授權。
第三步,門禁系統得到我的確認以后,向快遞員顯示一個進入小區的令牌(access token)。令牌就是類似密碼的一串數字,只在短期內(比如七天)有效。
第四步,快遞員向門禁系統輸入令牌,進入小區。
有人可能會問,為什么不是遠程為快遞員開門,而要為他單獨生成一個令牌?這是因為快遞員可能每天都會來送貨,第二天他還可以復用這個令牌。另外,有的小區有多重門禁,快遞員可以使用同一個令牌通過它們。
3、互聯網場景
我們把上面的例子搬到互聯網,就是 OAuth 的設計了。首先,居民小區就是儲存用戶數據的網絡服務。比如,微信儲存了我的好友信息,獲取這些信息,就必須經過微信的"門禁系統"。其次,快遞員(或者說快遞公司)就是第三方應用,想要穿過門禁系統,進入小區。
最后,我就是用戶本人,同意授權第三方應用進入小區,獲取我的數據。簡單說,OAuth 就是一種授權機制。數據的所有者告訴系統,同意授權第三方應用進入系統,獲取這些數據。系統從而產生一個短期的進入令牌(token),用來代替密碼,供第三方應用使用。
4、令牌與密碼
令牌(token)與密碼(password)的作用是一樣的,都可以進入系統,但是有三點差異。
(1)令牌是短期的,到期會自動失效,用戶自己無法修改。密碼一般長期有效,用戶不修改,就不會發生變化。
(2)令牌可以被數據所有者撤銷,會立即失效。以上例而言,屋主可以隨時取消快遞員的令牌。密碼一般不允許被他人撤銷。
(3)令牌有權限范圍(scope),比如只能進小區的二號門。對於網絡服務來說,只讀令牌就比讀寫令牌更安全。密碼一般是完整權限。
上面這些設計,保證了令牌既可以讓第三方應用獲得權限,同時又隨時可控,不會危及系統安全。這就是 OAuth 2.0 的優點。
注意,只要知道了令牌,就能進入系統。系統一般不會再次確認身份,所以令牌必須保密,泄漏令牌與泄漏密碼的后果是一樣的。 這也是為什么令牌的有效期,一般都設置得很短的原因。OAuth 2.0 對於如何頒發令牌的細節,規定得非常詳細。具體來說,一共分成四種授權類型(authorization grant),即四種頒發令牌的方式,適用於不同的互聯網場景。
四、OWIN實現OAuth 2.0 之客戶端模式
1、原理
客戶端使用自己的名義,而不是用戶的名義,向“服務提供商” 進行認證。如何理解這句話?

如上圖,可以得出一個大概的結論
- 用戶(User)通過客戶端(Client)訪問受限資源(Resource)
- 因為資源受限,所以需要授權;而這個授權是Client與Authentication之間完成的,可以說跟User沒有什么關系
- 根據2得出,Resource與User沒有關聯關系,即User不是這個Resource的Owner(所有者)
2、過程
- Client網站向認證服務網站發出請求。
http://localhost:8007/grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
上面 URL 中,grant_type參數等於client_credentials表示采用憑證式,client_id和client_secret用來讓認證服務確認Client的身份。
- 認證服務網站驗證通過以后,直接返回令牌。這種方式給出的令牌,是針對第三方應用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。
- Client網站拿到令牌以后,就可以向資源服務網站(資源服務網站和認證服務網站可以是一個)的 API 請求數據。此時,每個發到 API 的請求,都必須帶有令牌。具體做法是在請求的頭信息,加上一個
Authorization字段,令牌就放在這個字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \ "http://localhost:8008/api/Values"
上面命令中,ACCESS_TOKEN就是拿到的令牌。
2、適應場景
- 肯定不能用作登錄認證!因為登錄認證后需要得到用戶的一些基本信息,如昵稱,頭像之類,這些信息是屬於User的;
- 適用於一些對於權限要求不強的資源認證,比如:僅用於區分用戶是否登錄,排除匿名用戶獲取資源
3、示例說明
(1)新建資源項目:ResourceServer

引用owin:install-package Microsoft.Owin -Version 2.1.0
新增Startup.cs
[assembly: OwinStartup(typeof(ResourceServer.Startup))] namespace ResourceServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新增Startup.Auth.cs
namespace ResourceServer { public partial class Startup { public void ConfigureAuth(IAppBuilder app) {
// 這句是資源服務器認證token的關鍵,認證邏輯在里邊封裝好了,我們看不到 app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()); } } }
新增ValuesController.cs
namespace ResourceServer.Controllers { [Authorize] public class ValuesController : ApiController { public string Get() { return "qiuxianhu"; } } }
(2)新建認證服務項目:AuthorizationServer

新增Startup.cs
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(OAuth2._0ClientCredentialService.Startup))] namespace OAuth2._0ClientCredentialService { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新增Startup.Auth.cs
using ConfigHelper; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.OAuth; using Owin; using System; using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; namespace OAuth2._0ClientCredentialService { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { //創建OAuth授權服務器 app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString(ConfigSetting.TOKENPATH),//獲取 access_token 授權服務請求地址,即http://localhost:端口號/token; ApplicationCanDisplayErrors = true, AccessTokenExpireTimeSpan=TimeSpan.FromDays(10),//access_token 過期時間 #if DEBUG AllowInsecureHttp = true, #endif // Authorization server provider which controls the lifecycle of Authorization Server Provider = new OAuthAuthorizationServerProvider { OnValidateClientAuthentication = ValidateClientAuthentication, OnGrantClientCredentials = GrantClientCredetails } }); } /// <summary> /// ValidateClientAuthentication方法用來對third party application 認證, /// 獲取客戶端的 client_id 與 client_secret 進行驗證 /// context.Validated(); 表示允許此third party application請求。 /// </summary> /// <param name="context"></param> /// <returns></returns> private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId=null; string clientSecret=null; if (context.TryGetBasicCredentials(out clientId, out clientSecret) || context.TryGetFormCredentials(out clientId, out clientSecret)) { if (clientId == "123456" && clientSecret == "abcdef") { context.Validated(); } } return Task.FromResult(0); } /// <summary> /// 該方法是對客戶端模式進行授權的時候使用的 /// 對客戶端進行授權,授了權就能發 access token 。 /// 只有這兩個方法(ValidateClientAuthentication和GrantClientCredetails)同時認證通過才會頒發token。 private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context) { GenericIdentity genericIdentity = new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType); ClaimsIdentity claimsIdentity = new ClaimsIdentity(genericIdentity, context.Scope.Select(x => new Claim("urn:oauth:scope", x))); context.Validated(claimsIdentity); return Task.FromResult(0); } } }
自此,認證服務項目算是建好了,因為對於客戶端模式,認證服務器只需要返回token
(3)新增Client項目

using ConfigHelper; using DotNetOpenAuth.OAuth2; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; namespace ClientIDSecretCredentialGrant { class Program { private static string OAUTH_SERVER_URL = ConfigSetting.OAUTH_SERVER_URL; private static string OAUTH_TOKEN_PATH = ConfigSetting.OAUTH_TOKEN_PATH; private static string RESOURCE_SERVER_URL = ConfigSetting.RESOURCE_SERVER_URL; private static string RESOURCE_ME_PATH = ConfigSetting.RESOURCE_ME_PATH; private static readonly string ClientID = ConfigSetting.CLIENTID; private static readonly string Secret = ConfigSetting.SECRET; private static WebServerClient _WebServerClient; private static string _AccessToken; static void Main(string[] args) { #region 方式一 Call_WebAPI_By_Access_Token(); #endregion #region 方式二 DotNetOpenAuth.OAuth2 //InitializeWebServerClient(); //Console.WriteLine("Requesting Token..."); //RequestToken(); //Console.WriteLine("Access Token: {0}", _AccessToken); //Console.WriteLine("Access Protected Resource"); //AccessProtectedResource(); Console.ReadKey(); #endregion } public static void Call_WebAPI_By_Access_Token() { HttpClient _httpClient = new HttpClient(); Dictionary<string, string> parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "client_credentials"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(ClientID + ":" + Secret)) ); var response = _httpClient.PostAsync(new Uri(new Uri(OAUTH_SERVER_URL), OAUTH_TOKEN_PATH), new FormUrlEncodedContent(parameters)).Result; var responseValue = response.Content.ReadAsStringAsync().Result; if (response.StatusCode == System.Net.HttpStatusCode.OK) { _AccessToken = JObject.Parse(responseValue)["access_token"].Value<string>(); } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _AccessToken); Console.WriteLine(_httpClient.GetAsync(new Uri(new Uri(RESOURCE_SERVER_URL), RESOURCE_ME_PATH)).Result.Content.ReadAsStringAsync().Result); Console.ReadKey(); } private static void InitializeWebServerClient() { Uri authorizationServerUri = new Uri(OAUTH_SERVER_URL); AuthorizationServerDescription authorizationServer = new AuthorizationServerDescription { TokenEndpoint = new Uri(authorizationServerUri, OAUTH_TOKEN_PATH) }; _WebServerClient = new WebServerClient(authorizationServer, ClientID, Secret); } private static void RequestToken() { IAuthorizationState state = _WebServerClient.GetClientAccessToken(); Console.WriteLine(state); _AccessToken = state.AccessToken; } private static void AccessProtectedResource() { Uri resourceServerUri = new Uri(RESOURCE_SERVER_URL); HttpClient client = new HttpClient(_WebServerClient.CreateAuthorizingHandler(_AccessToken)); string body = client.GetStringAsync(new Uri(resourceServerUri, RESOURCE_ME_PATH)).Result; Console.WriteLine(body); } } }
OK,Client環境搭好了,我們來運行下試試

DEMO地址: https://github.com/qiuxianhu/OAuth2.0ClientCredential
五、OWIN實現OAuth 2.0 之密碼模式
1、原理
用戶向客戶端提供用戶名和密碼,客戶端使用這些信息向認證服務進行認證,密碼模式的流程圖:

2、過程
如果你高度信任某個應用,RFC 6749 也允許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌,這種方式稱為"密碼式"(password)。
- A 網站要求用戶提供 B 網站的用戶名和密碼。拿到以后,A 就直接向 B 請求令牌。
https://oauth.b.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID
上面 URL 中,grant_type參數是授權方式,這里的password表示"密碼式",username和password是 B 的用戶名和密碼。
- B 網站驗證身份通過后,直接給出令牌。注意,這時不需要跳轉,而是把令牌放在 JSON 數據里面,作為 HTTP 回應,A 因此拿到令牌。這種方式需要用戶給出自己的用戶名/密碼,顯然風險很大,因此只適用於其他授權方式都無法采用的情況,而且必須是用戶高度信任的應用。
3、示例
(1)新建客戶端Client

using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; namespace Client { class Program { static void Main(string[] args) { string server_url = "http://localhost:8005/"; string resource_url = "http://localhost:8006/"; string clientid = "123456"; string secret = "abcdef"; HttpClient _httpClient = new HttpClient(); Dictionary<string, string> parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "password"); parameters.Add("UserName", "qiuxianhu"); parameters.Add("Password", "123456"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientid + ":" + secret)) ); var response = _httpClient.PostAsync(new Uri(new Uri(server_url), "/Token"), new FormUrlEncodedContent(parameters)).Result; var responseValue = response.Content.ReadAsStringAsync().Result; if (response.StatusCode == System.Net.HttpStatusCode.OK) { string token = JObject.Parse(responseValue)["access_token"].Value<string>(); Console.WriteLine(token); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); Console.WriteLine(_httpClient.GetAsync(new Uri(new Uri(resource_url), "/api/values")).Result.Content.ReadAsStringAsync().Result); } Console.ReadKey(); } } }
(2)認證服務

新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(OAuthService.Startup))] namespace OAuthService { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建Startup.Auth.cs文件
using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.OAuth; using Owin; using System; using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; namespace OAuthService { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { //創建OAuth授權服務器 app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/Token"),//獲取 access_token 授權服務請求地址,即http://localhost:端口號/token; ApplicationCanDisplayErrors = true, AccessTokenExpireTimeSpan = TimeSpan.FromDays(10),//access_token 過期時間 #if DEBUG AllowInsecureHttp = true, #endif // Authorization server provider which controls the lifecycle of Authorization Server Provider = new OAuthAuthorizationServerProvider { OnValidateClientAuthentication = ValidateClientAuthentication, //這個方法就是后台處理密碼模式認證關鍵的地方 OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials } }); } /// <summary> /// ValidateClientAuthentication方法用來對third party application 認證, /// 獲取客戶端的 client_id 與 client_secret 進行驗證 /// context.Validated(); 表示允許此third party application請求。 /// </summary> /// <param name="context"></param> /// <returns></returns> private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId = null; string clientSecret = null; if (context.TryGetBasicCredentials(out clientId, out clientSecret) || context.TryGetFormCredentials(out clientId, out clientSecret)) { if (clientId == "123456" && clientSecret == "abcdef") { context.Validated(); } } return Task.FromResult(0); } /// <summary> /// 這個方法就是后台處理密碼模式認證關鍵的地方 /// 認證服務判斷查詢數據庫判斷該用戶的用戶名和密碼是否正確。如果正確就會授權,產生token。 /// </summary> /// <param name="context"></param> /// <returns></returns> private async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { if (context.UserName!="qiuxianhu"||context.Password!="123456") { context.SetError("invalid_grant","The user name or password is incorrect."); context.Rejected(); return; } else { ClaimsIdentity claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); var ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties()); context.Validated(ticket); } } } }
(3)建立資源服務

新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(ResourceServer.Startup))] namespace ResourceServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建Startup.Auth.cs文件
using Owin; namespace ResourceServer { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // 這句是資源服務器認證token的關鍵,認證邏輯在里邊封裝好了,我們看不到 app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()); } } }
大功告成!
Demo下載:https://github.com/qiuxianhu/OAuth2.0PasswordCredential
六、OWIN實現OAuth 2.0 之授權碼模式
1、原理

通過客戶端的后台服務器,與“服務提供商”的認證服務器進行認證。
- 用戶訪問客戶端,后者將前者導向認證服務器。
- 用戶選擇是否給予客戶端授權。
- 假設用戶給予授權,認證服務器首先生成一個授權碼,並返回給用戶,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
- 客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的后台的服務器上完成的,對用戶不可見。
- 認證服務器核對了授權碼和重定向URI,確認無誤后,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
- Client拿着access token去訪問Resource資源
2、示例
(1)新建客戶端

using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth2;
using System;
using System.Net.Http;
using System.Web.Mvc;
namespace AuthorizationCodeGrantClient.Controllers
{
public class HomeController : Controller
{
static string accessToken = null;
static string refreshToken = null;
public ActionResult Index()
{
Uri authorizationServerUri = new Uri("http://localhost:8011/");
AuthorizationServerDescription authorizationServerDescription = new AuthorizationServerDescription
{
AuthorizationEndpoint = new Uri(authorizationServerUri, "OAuth/Authorize"),
TokenEndpoint = new Uri(authorizationServerUri, "OAuth/Token")
};
WebServerClient webServerClient = new WebServerClient(authorizationServerDescription, "123456", "abcdef");
if (string.IsNullOrEmpty(accessToken))
{
IAuthorizationState authorizationState = webServerClient.ProcessUserAuthorization(Request);
if (authorizationState != null)
{
accessToken = authorizationState.AccessToken;
refreshToken = authorizationState.RefreshToken;
}
}
// 授權申請
if (!string.IsNullOrEmpty(Request.Form.Get("btnRequestAuthorize")))
{
//這里 new[] { "scopes1", "scopes2" }為需要申請的scopes,或者說是Resource Server的接口標識,或者說是接口權限。然后Send(HttpContext)即重定向。
OutgoingWebResponse grantRequest = webServerClient.PrepareRequestUserAuthorization(new[] { "scopes1", "scopes2" });
grantRequest.Send(HttpContext);
Response.End();
}
// 申請資源
if (!string.IsNullOrEmpty(Request.Form.Get("btnRequestResource")))
{
var resourceServerUri = new Uri("http://localhost:8012/");
var resourceRequest = new HttpClient(webServerClient.CreateAuthorizingHandler(accessToken));
ViewBag.ResourceResponse = resourceRequest.GetStringAsync(new Uri(resourceServerUri, "api/Values")).Result;
}
return View();
}
}
}
(2)新建認證服務

新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(AuthorizationCodeGrantServer.Startup))] namespace AuthorizationCodeGrantServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建OAuthController.cs文件
using Microsoft.AspNet.Identity; using Microsoft.Owin.Security; using System.Security.Claims; using System.Web; using System.Web.Mvc; namespace AuthorizationCodeGrantServer.Controllers { public class OAuthController : Controller { public ActionResult Authorize() { IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication; AuthenticateResult authenticateResult = authenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie).Result; ClaimsIdentity claimsIdentity = authenticateResult != null ? authenticateResult.Identity : null; if (claimsIdentity == null) { authenticationManager.Challenge(DefaultAuthenticationTypes.ApplicationCookie); //用戶登錄憑證失效就報401錯誤,並且跳轉至AccountController中的Login中 return new HttpUnauthorizedResult(); } ViewBag.IdentityName = claimsIdentity.Name; ViewBag.Scopes = (Request.QueryString.Get("scope") ?? "").Split(' '); if (Request.HttpMethod == "POST") { // 點擊btnGrant就確認授權,返回token等信息 if (!string.IsNullOrEmpty(Request.Form.Get("btnGrant"))) { claimsIdentity = new ClaimsIdentity(claimsIdentity.Claims, "Bearer", claimsIdentity.NameClaimType, claimsIdentity.RoleClaimType); foreach (var scope in ViewBag.Scopes) { claimsIdentity.AddClaim(new Claim("urn:oauth:scope", scope)); } authenticationManager.SignIn(claimsIdentity); } if (!string.IsNullOrEmpty(Request.Form.Get("btnOtherLogin"))) { authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.Challenge(DefaultAuthenticationTypes.ApplicationCookie); return new HttpUnauthorizedResult(); } } return View(); } } }
(3)新建資源服務
新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(ResourceServer.Startup))] namespace ResourceServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建Startup.Auth.cs文件
using Owin; namespace ResourceServer { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // 這句是資源服務器認證token的關鍵,認證邏輯在里邊封裝好了,我們看不到 app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()); } } }
新建ValuesController,提供資源
using System.Security.Claims; using System.Threading; using System.Web.Http; namespace ResourceServer.Controllers { [Authorize] public class ValuesController : ApiController { public string Get() { ClaimsPrincipal principal = Thread.CurrentPrincipal as ClaimsPrincipal; var isInRole = principal.IsInRole("scopes1"); return "qiuxianhu"; } } }
Demo下載https://github.com/qiuxianhu/OAuth2.0CodeCredential
七、基於JWT的Token認證機制實現
1、什么是JWT
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
2、傳統的session認證方式
http協議本身是一種無狀態的協議,這就意味着如果用戶向我們的應用提供了用戶名和密碼來進行用戶認證,那么下一次請求時,用戶還要再一次進行用戶認證才行,因為根據http協議,我們並不能知道是哪個用戶發出的請求,所以為了讓我們的應用能識別是哪個用戶發出的請求,我們只能在服務器存儲一份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存為cookie,以便下次請求時發送給我們的應用,這樣我們的應用就能識別請求來自哪個用戶了,這就是傳統的基於session認證。但是這種基於session的認證使應用本身很難得到擴展,隨着不同客戶端用戶的增加,獨立的服務器已無法承載更多的用戶,而這時候基於session認證應用的問題就會暴露出來。缺點主要體現在以下兩個方面:
1、session:每個用戶經過我們的應用認證之后,我們的應用都要在服務端做一次記錄,以方便用戶下次請求的鑒別,通常而言session都是保存在內存中,而隨着認證用戶的增多,服務端的開銷會明顯增大。
2、擴展性:用戶認證之后,服務端做認證記錄,如果認證的記錄被保存在內存中的話,這意味着用戶下次請求還必須要請求在這台服務器上,這樣才能拿到授權的資源,這樣在分布式的應用上,相應的限制了負載均衡器的能力。這也意味着限制了應用的擴展能力。
3、CSRF:因為是基於cookie來進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊。
3、基於token的鑒權
基於token的鑒權機制類似於http協議也是無狀態的,它不需要在服務端去保留用戶的認證信息或者會話信息。這就意味着基於token認證機制的應用不需要去考慮用戶在哪一台服務器登錄了,這就為應用的擴展提供了便利。
流程上是這樣的:
- 用戶使用用戶名密碼來請求服務器
- 服務器進行驗證用戶的信息
- 服務器通過驗證發送給用戶一個token
- 客戶端存儲token,並在每次請求時附送上這個token值
- 服務端驗證token值,並返回數據
這個token必須要在每次請求時傳遞給服務端,它應該保存在請求頭里, 另外,服務端要支持CORS(跨來源資源共享)策略,一般我們在服務端這么做就可以了Access-Control-Allow-Origin:*。
4、JWT組成

JWT是由三段信息構成的,將這三段信息文本連接起來就構成了Jwt字符串。第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload),第三部分是簽證(signature),下面是個JWT實例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOiJBZG1pbiIsIkV4cGlyZSI6IjIwMjAtMDctMTEgMTY6NDc6MTYifQ.9ev6IGc1K3xvYaEfmMYeyFz5oHCM57fRGOvSZ-jvArw
(1)header
頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象。jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
在這里,我們說明了這是一個JWT,並且我們所用的簽名算法是HS256算法。對它也要進行Base64編碼,之后的字符串就成了JWT的Header(頭部)。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
(2)playload
載荷就是存放有效信息的地方,這些有效信息包含三個部分
- 標准中注冊的聲明
- 公共的聲明
- 私有的聲明
標准中注冊的聲明
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的聲明 :公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有的聲明 :私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
{
"iss": "John Wu JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"from_user": "B",
"target_user": "A"
}
將上面的JSON對象進行[base64編碼]可以得到下面的字符串。這個字符串我們將它稱作JWT的Payload(載荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
(3)signature
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header (base64后的)
- payload (base64后的)
- secret
這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行secret組合加密,然后就構成了jwt的第三部分。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
(5)JWT簽名的目的
最后一步簽名的過程,實際上是對頭部以及載荷內容進行簽名。一般而言,加密算法對於不同的輸入產生的輸出總是不一樣的。所以,如果有人對頭部以及載荷的內容解碼之后進行修改,再進行編碼的話,那么新的頭部和載荷的簽名和之前的簽名就將是不一樣的。而且,如果不知道服務器加密的時候用的密鑰的話,得出來的簽名也一定會是不一樣的。

服務器應用在接受到JWT后,會首先對頭部和載荷的內容用同一算法再次簽名。那么服務器應用是怎么知道我們用的是哪一種算法呢?別忘了,我們在JWT的頭部中已經用alg字段指明了我們的加密算法了。如果服務器應用對頭部和載荷再次以同樣方法簽名之后發現,自己計算出來的簽名和接受到的簽名不一樣,那么就說明這個Token的內容被別人動過的,我們應該拒絕這個Token,返回一個HTTP 401 Unauthorized響應。
(6)JWT安全性
使用JWT會暴露信息嗎?是的。所以,在JWT中,不應該在載荷里面加入任何敏感的數據。在上面的例子中,我們傳輸的是用戶的User ID。這個值實際上不是什么敏感內容,一般情況下被知道也是安全的。但是像密碼這樣的內容就不能被放在JWT中了。如果將用戶的密碼放在了JWT中,那么懷有惡意的第三方通過Base64解碼就能很快地知道你的密碼了。總結如下:
- 不應該在jwt的payload部分存放敏感信息,因為該部分是客戶端可解密的部分。
- 保護好secret私鑰,該私鑰非常重要。
- 如果可以,請使用https協議
(7)JWT優點
- 因為json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
- 因為有了payload部分,所以JWT可以在自身存儲一些其他業務邏輯所必要的非敏感信息。
- 便於傳輸,jwt的構成非常簡單,字節占用很小,所以它是非常便於傳輸的。
- 它不需要在服務端保存會話信息, 所以它易於應用的擴展
(8)JWT的適用場景
我們可以看到,JWT適合用於向Web應用傳遞一些非敏感信息。其實JWT還經常用於設計用戶認證和授權系統,甚至實現Web應用的單點登錄。
(9)如何應用
一般是在請求頭里加入Authorization,並加上Bearer標注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
參考資料
https://www.cnblogs.com/lanxiaoke/category/941651.html
http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

