如何基于Microsoft.Owin.Security.OAuth,使用Client Credentials
Grant授權方式給客戶(hù)端發(fā)放access token? Client Credentials
Grant的授權方式就是只驗證客戶(hù)端(Client),不驗證用戶(hù)(Resource Owner),只要客戶(hù)端通過(guò)驗證就發(fā)access
token。舉一個(gè)對應的應用場(chǎng)景例子,比如我們想提供一個(gè)“獲取網(wǎng)站首頁(yè)最新博文列表”的WebAPI給客戶(hù)端App調用。由于這個(gè)數據與用戶(hù)無(wú)關(guān),所以不涉及用戶(hù)登錄與授權,不需要Resource
Owner的參與。但我們不想任何人都可以調用這個(gè)WebAPI,所以要對客戶(hù)端進(jìn)行驗證,而使用OAuth中的 Client
Credentials Grant 授權方式可以很好地解決這個(gè)問(wèn)題。
在A(yíng)pp_Start文件夾下新增ApplicationDbInitializer,代碼如下:
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } //創(chuàng )建用戶(hù)名為admin@123.com,密碼為“Admin@123456” public static void InitializeIdentityForEF(ApplicationDbContext dbContext) { var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>(); const string name = "admin@123.com";//用戶(hù)名 const string email = "admin@123.com";//郵箱 const string password = "Admin@123456";//密碼 //如果沒(méi)有admin@123.com用戶(hù)則創(chuàng )建該用戶(hù) var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = email }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } } } 修改Model文件夾下的IdentityModels.cs,添加斜體部分代碼,需添加命名空間:using System.Data.Entity;
public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { // 在第一次啟動(dòng)網(wǎng)站時(shí)初始化數據庫添加管理員用戶(hù)憑據到數據庫 Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer()); }我把WebApi的Controller放到一個(gè)新建的文件夾APIControllers中,TestController的View的js的測試代碼
打開(kāi)Startup.Auth.cs,以下代碼是Oauth相關(guān)的配置代碼
public partial class Startup{ public void ConfigureAuth(IAppBuilder app) { var OAuthOptions = new OAuthAuthorizationServerOptions { //獲取Token的路徑 TokenEndpointPath = new PathString("/Token"), Provider = new ApplicationOAuthProvider(PublicClientId), AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"), //Token 過(guò)期時(shí)間,默認20分鐘 AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), //在生產(chǎn)模式下設 AllowInsecureHttp = false AllowInsecureHttp = true }; app.UseOAuthBearerTokens(OAuthOptions); }}使用Client Credentials Grant的授權方式( grant_type= client_credentials)獲取 Access Token,并以這個(gè) Token 調用與用戶(hù)相關(guān)的 Web API。
我們需要修改部分代碼,修改ValidateClientAuthentication()方法,繼承實(shí)現GrantClientCredentials()方法。代碼如下
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider { public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId, clientSecret; context.TryGetBasicCredentials(out clientId, out clientSecret); if (clientId == "Mobile" && clientSecret == "Xiaomi") { context.Validated(); } return Task.FromResult<object>(null); } public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context) { var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, "Xiaomi")); var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties()); context.Validated(ticket); return base.GrantClientCredentials(context); } }在 ValidateClientAuthentication() 方法中獲取客戶(hù)端的 client_id 與 client_secret 進(jìn)行驗證。在 GrantClientCredentials() 方法中對客戶(hù)端進(jìn)行授權,授了權就能發(fā) access token 。這樣,OAuth的ClientCredentials授權服務(wù)端代碼就完成了。在A(yíng)SP.NET Web API中啟用OAuth的Access Token驗證非常簡(jiǎn)單,只需在相應的Controller或Action加上[Authorize]標記,VS已生成部分代碼,詳細查看APIController文件夾下的ValuesController
下面我們在客戶(hù)端調用一下,添加TestController,生成Index的View,然后在View中添加如下
$(function () { $("#clientCredentials").on("click", function () { GetClientCredentialsAccessToken(); }); }); function GetClientCredentialsAccessToken() { $("#clientResult").html("Requesting"); var clientId = "Mobile"; var clientSecret = "Xiaomi"; $.ajax({ url: "/Token", type: "post", data: { "grant_type": "client_credentials" }, dataType: "json", headers: { "Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret) }, success: function (data) { var accessToken = data.access_token; GetValues(accessToken); } }); } function GetValues(accessToken) { var html = "Token:" + accessToken + "<br/><br/>"; $.ajax({ url: "/api/Values", type: "get", dataType: "json", headers: { "Authorization": "Bearer " + accessToken }, success: function (values) { for (var i = 0; i < values.length; i++) { html += "values[" + i + "] :" + values[i] + "<br/>"; } $("#clientResult").html(html); } }); } function Base64_Encode(str) { var c1, c2, c3; var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var i = 0, len = str.length, string = ''; while (i < len) { c1 = str.charCodeAt(i++) & 0xff; if (i === len) { string += base64EncodeChars.charAt(c1 >> 2); string += base64EncodeChars.charAt((c1 & 0x3) << 4); string += "=="; break; } c2 = str.charCodeAt(i++); if (i === len) { string += base64EncodeChars.charAt(c1 >> 2); string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); string += base64EncodeChars.charAt((c2 & 0xF) << 2); string += "="; break; } c3 = str.charCodeAt(i++); string += base64EncodeChars.charAt(c1 >> 2); string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); string += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); string += base64EncodeChars.charAt(c3 & 0x3F); } return string; }測試結果:
Token:4iIu7HProfJaxRiklsl-ORRO3hdyrsu50pQc1Eh2-Q5lSWK8UJgz6719ZaeeULhwkMPpEFYfk6QDOOMEyFqULULk65Sb0JY29wskyZyQhKJ3_P-eSVQ2PlbKbjH9ZcziAZsVOiNLp8CfUqL5qWUq8ggVAa8KRcnlJ1DIVWnEu0XvTEDZaLDpFqqj2Cex2CX7TmTgfs07RUBdx5_3WDavNAPs:
傳遞clientId與clientSecret有兩種方式,上例使用BasicAuthentication,服務(wù)端使用TryGetBasicCredentials();另外一種方式是普通From的,把參數放到Ajax的data中,如:
{“clientId”: id ,” clientSecret”:”secret”, "grant_type":"client_credentials"}對應服務(wù)端使用TryGetFormCredentials()獲取clientId和clientSecret;
推薦使用Basic Authentication方式;
使用Resource Owner Password Credentials Grant 的授權方式( grant_type=password )獲取 Access Token,并以這個(gè) Token 調用與用戶(hù)相關(guān)的 Web API。
Resource Owner Password Credentials Grant 授權方式(需要驗證登錄用戶(hù))
因為我們剛開(kāi)始時(shí)已經(jīng)初始化EF,添加了一個(gè)用戶(hù)信息。ApplicationOAuthProvider.cs 的GrantResourceOwnerCredentials()方法(VS幫我們自動(dòng)生成了),已經(jīng)實(shí)現了先關(guān)的代碼
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { //調用后臺的登錄服務(wù)驗證用戶(hù)名與密碼 var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>(); ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "用戶(hù)名或密碼不正確。"); return; } ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType); ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = CreateProperties(user.UserName); AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties); context.Validated(ticket); context.Request.Context.Authentication.SignIn(cookiesIdentity); }添加一個(gè)測試用的Controller
public class UsersController : ApiController{ [Authorize] public string GetCurrent() { return User.Identity.Name; //這里可以調用后臺用戶(hù)服務(wù),獲取用戶(hù)相關(guān)數所,或者驗證用戶(hù)權限進(jìn)行相應的操作 }}在Test的index.cshtml 中新增測試的代碼,如下
function GetResourceOwnerCredentialsAccessToken() { $("#resourceOwnerresult").html("Requesting"); var clientId = "Mobile"; var clientSecret = "Xiaomi"; $.ajax({ url: "/Token", type: "post", data: { "grant_type": "password", "username": "admin@123.com", "password": "Admin@123456" }, dataType: "json", headers: { "Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret) }, success: function (data) { var accessToken = data.access_token; GetCurrentUserName(accessToken); } }); } function GetCurrentUserName(accessToken) { var html = "Token:" + accessToken + "<br/><br/>"; $.ajax({ url: "/api/User", type: "get", dataType: "text", headers: { "Authorization": "Bearer " + accessToken }, success: function (userName) { html += "CurrentUserName:" + userName + "<br/>"; $("#resourceOwnerresult").html(html); } }); }測試結果如下
Token:Cvct6BAKix_xLNEEOfidpEG0ymJihOSjdACazP2R2tJSB3TKVnxicgQK27DzDrICUC4A7vITqhkhBRsT5cRgiow--VkbiR4we3yQ54tc6B_W8KRrdGabjase_gpmFv8oYUPGLpI82acDpcZPzCkmgLLwAq8qfkmlK7iHm5tLM6-NRR8tgfEeOVBljHq4smIXw_eVuces3sRQm-PXTD4xmp05JdrJ9zFeRb_SAN0ADqDJfJxk1nNooCtdJyeHB6r1S2D81H6P7bhRK_edneWdkX5QCNBHL8b39UKnnk0ywza6vXcWct4RaATBYOw20iNu0XR6JRx5opP9vqqC2ag8Ux6s3GHl-vAZTaYuwunmWyY0FyJJWpjNnFpPo-pkxZaK1XJxgGPpSV-JJjEZLarnq9O57hQGfbVLCd3KtWuJflo5rMnfkAz2nXlcd3gAgjIhipAIlpsG72StzN0qBL8Ml2XvV9Re1Z8U4QtrE7tzjkECurrentUserName:"admin@123.com"至此,使用WebApi 的兩種授權方式發(fā)放Token和兩種授權方式區別,以及使用Token調用受保護的api已經(jīng)介紹完了,Oauth其實(shí)還有一個(gè)refresh token,refresh token 是專(zhuān)用于刷新 access token 的 token。一是因為 access token 是有過(guò)期時(shí)間的,到了過(guò)期時(shí)間這個(gè) access token 就失效,需要刷新;二是因為一個(gè) access token 會(huì )關(guān)聯(lián)一定的用戶(hù)權限,如果用戶(hù)授權更改了,這個(gè) access token 需要被刷新以關(guān)聯(lián)新的權限。
這個(gè)就不單獨介紹了,有興趣的可以自己研究下。
Asp.Net 高級技術(shù)群 89336052,群共享有源碼感謝:晨風(fēng)的指導和教育,提供demo和文章,我只是一個(gè)發(fā)布文章的戰五渣!
聯(lián)系客服