Client Credentials Grant的受權方式就是隻驗證客戶端(Client),不驗證用戶(Resource Owner),只要客戶端經過驗證就發access token。html
舉一個對應的應用場景例子,好比咱們想提供一個「獲取網站首頁最新博文列表」的WebAPI給iOS App調用。api
因爲這個數據與用戶無關,因此不涉及用戶登陸與受權,不須要Resource Owner的參與。mvc
但咱們不想任何人均可以調用這個WebAPI,因此要對客戶端進行驗證,而使用OAuth中的Client Credentials Grant受權方式能夠很好地解決這個問題。app
1)用Visual Studio 2013/2015建立一個Web API 4項目,VS會生成一堆OAuth相關代碼。async
2)打開App_Start/Startup.Auth.cs ,精簡一下代碼,咱們只須要實現以Client Credentials Grant受權方式拿到token,其它無關代碼所有清除,最終剩下以下代碼:ide
using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Google; using Microsoft.Owin.Security.OAuth; using Owin; using WebApi4.Providers; using WebApi4.Models; namespace WebApi4 { public partial class Startup { public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; } public static string PublicClientId { get; private set; } // 有關配置身份驗證的詳細信息,請訪問 http://go.microsoft.com/fwlink/?LinkId=301864 public void ConfigureAuth(IAppBuilder app) { var OAuthOptions = new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/token"),//獲取Token的地址 示例:http://localhost:54342/token Provider = new AuthorizationServerProvider(),// AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),//Token有效期 AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()//應用RefreshTokenProvider,刷新Token的程序 }; app.UseOAuthBearerTokens(OAuthOptions); } } }
刷新Token的程序測試
using Microsoft.Owin.Security.Infrastructure; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using System.Web; using WebApi4.Interfaces; using WebApi4.Models; namespace WebApi4.Providers { public class RefreshTokenProvider : AuthenticationTokenProvider { private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>(); public override void Create(AuthenticationTokenCreateContext context) { string tokenValue = Guid.NewGuid().ToString("n"); context.Ticket.Properties.IssuedUtc = DateTime.UtcNow; context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60); _refreshTokens[tokenValue] = context.SerializeTicket(); context.SetToken(tokenValue); } public override void Receive(AuthenticationTokenReceiveContext context) { string value; if (_refreshTokens.TryRemove(context.Token, out value)) { context.DeserializeTicket(value); } } } }
3)建立一個新的類 AuthorizationServerProvider,並繼承自 OAuthAuthorizationServerProvider,重載 OAuthAuthorizationServerProvider() 與 GrantClientCredentials() 這兩個方法。代碼以下:網站
using Microsoft.Owin.Security; using Microsoft.Owin.Security.OAuth; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using System.Web; namespace WebApi4.Providers { public class AuthorizationServerProvider : OAuthAuthorizationServerProvider { public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId; string clientSecret; //省略了return以前context.SetError的代碼 if (!context.TryGetBasicCredentials(out clientId, out clientSecret)) { return; } //保存client_id context.OwinContext.Set<string>("client_id", clientId); //context.OwinContext.Set<string>("clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); context.Validated(clientId); await base.ValidateClientAuthentication(context); } public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context) { var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); var props = new AuthenticationProperties(new Dictionary<string, string> { { "client_id", context.ClientId } }); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.ClientId)); var ticket = new AuthenticationTicket(oAuthIdentity, props); //var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties()); context.Validated(ticket); await base.GrantClientCredentials(context); } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { //驗證context.UserName與context.Password //調用後臺的登陸服務驗證用戶名與密碼 var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); var props = new AuthenticationProperties(new Dictionary<string, string> { { "client_id", context.ClientId } }); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); var ticket = new AuthenticationTicket(oAuthIdentity, props); context.Validated(ticket); await base.GrantResourceOwnerCredentials(context); } public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context) { var originalClient = context.Ticket.Properties.Dictionary["client_id"]; var currentClient = context.ClientId; if (originalClient != currentClient) { context.Rejected(); return; } var oAuthIdentity = new ClaimsIdentity(context.Ticket.Identity); var props = new AuthenticationProperties(new Dictionary<string, string> { { "client_id", context.ClientId } }); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.ClientId));//"newClaim", "refreshToken" var newTicket = new AuthenticationTicket(oAuthIdentity, context.Ticket.Properties); context.Validated(newTicket); await base.GrantRefreshToken(context); } } }
在 ValidateClientAuthentication() 方法中獲取客戶端的 client_id 與 client_secret 進行驗證。ui
在 GrantClientCredentials() 方法中對客戶端進行受權,授了權就能發 access token 。spa
這樣,OAuth的服務端代碼就完成了。
4)而後寫客戶端調用代碼測試一下:
using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Web; using System.Web.Mvc; using System.Web.Script; using System.Web.Script.Serialization; namespace WebApi4.Controllers { public class HomeController : Controller { public ActionResult Index() { ViewBag.Title = "Home Page"; return View(); } /// <summary> /// 使用 client_credentials 方式得到Token /// </summary> /// <returns></returns> public ContentResult Get_Accesss_Token_By_Client_Credentials_Grant() { var clientId = "xsj";//用戶名 var clientSecret = "1989";//密碼 HttpClient _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri("http://localhost:54342"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret))); var parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "client_credentials"); string result = _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters)).Result.Content.ReadAsStringAsync().Result; return Content(result); } /// <summary> /// 使用 password 方式得到Token /// </summary> /// <returns></returns> public ContentResult Get_Accesss_Token_By_Password_Grant() { var clientId = "xsj";//用戶名 var clientSecret = "1989";//密碼 HttpClient _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri("http://localhost:54342"); var parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "password"); parameters.Add("username", clientId); parameters.Add("password", clientSecret); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret))); var response = _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters)); string responseValue = response.Result.Content.ReadAsStringAsync().Result; return Content(responseValue); } /// <summary> /// 根據上一次獲取的 refresh_token 來獲取新 Token /// </summary> /// <param name="refresh_token"></param> /// <returns></returns> public ContentResult Get_Access_Token_By_RefreshToken(string refresh_token) { var clientId = "xsj"; var clientSecret = "1989"; HttpClient _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri("http://localhost:54342"); var parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "refresh_token"); parameters.Add("refresh_token", refresh_token); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret))); var response = _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters)); string responseValue = response.Result.Content.ReadAsStringAsync().Result; return Content(responseValue); } /// <summary> /// 測試用 訪問一個受限的API接口 /// </summary> /// <returns></returns> public ContentResult TokenTest() { HttpClient _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri("http://localhost:54342"); string token = GetAccessToken(); TokenInfo tinfo = new JavaScriptSerializer().Deserialize<TokenInfo>(token); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tinfo.access_token); return Content(_httpClient.GetAsync("/api/Account/Test").Result.Content.ReadAsStringAsync().Result); } /// <summary> /// 測試用 得到一個Token /// </summary> /// <returns></returns> public string GetAccessToken() { var clientId = "xsj";//用戶名 var clientSecret = "1989";//密碼 HttpClient _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri("http://localhost:54342"); var parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "password"); parameters.Add("username", clientId); parameters.Add("password", clientSecret); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret))); var response = _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters)); string responseValue = response.Result.Content.ReadAsStringAsync().Result; return responseValue; } } public class TokenInfo { public string access_token { get; set; } public string token_type { get; set; } public long expires_in { get; set; } public string refresh_token { get; set; } } }
返回結果:
{"access_token":"W2m0pUxHLWpb2p6Ys25g....","token_type":"bearer","expires_in":1209599,"refresh_token":"4b45asdfa5fe1a5e548c0f"}
注:使用Basic Authentication傳遞clientId與clientSecret,服務端AuthorizationServerProvider中的TryGetFormCredentials()改成TryGetBasicCredentials()
使用Fiddler得到Token:
使用獲得的Token訪問受限的接口,須要在Header中加入Token:Authorization: bearer {Token}
Authorization: bearer 9R5KsWyFmOYEbQs9qNCgnpZqDpkLkvjW5aVN6j5c6kDegDg...
受限的Action
在ASP.NET Web API中啓用OAuth的Access Token驗證很是簡單,只需在相應的Controller或Action加上[Authorize]標記,好比:
[AcceptVerbs("GET")]
[Authorize]
public HttpResponseMessage GetUserInfo(int ID){......}
加上[Authorize]以後,若是不使用Access Token,調用API時就會出現以下的錯誤:{"Message":"已拒絕爲此請求受權。"}
這時你也許會問,爲何一加上[Authorize]就會有這個效果?原來的Forms驗證怎麼不起做用了?
緣由是你在用Visual Studio建立ASP.NET Web API項目時,VS自動幫你添加了相應的代碼,打開WebApiConfig.cs,你會看到下面這2行代碼:
// Web API 配置和服務 // 將 Web API 配置爲僅使用不記名令牌身份驗證。 config.SuppressDefaultHostAuthentication(); config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
【參考資料】
http://www.cnblogs.com/dudu/p/4569857.html
http://www.hackered.co.uk/articles/asp-net-mvc-creating-an-oauth-client-credentials-grant-type-token-endpoint
http://www.cnblogs.com/YamatAmain/p/5029466.html
http://www.cnblogs.com/xizz/archive/2015/12/18/5056195.html