基於OWIN WebAPI 使用OAuth授權服務【客戶端模式(Client Credentials Grant)】
適應範圍
採用Client Credentials方式,即應用公鑰、金鑰方式獲取Access Token,適用於任何型別應用,但通過它所獲取的Access Token只能用於訪問與使用者無關的Open API,並且需要開發者提前向開放平臺申請,成功對接後方能使用。認證伺服器不提供像使用者資料這樣的重要資源,僅僅是有限的只讀資源或者一些開放的 API。例如使用了第三方的靜態檔案服務,如Google Storage或Amazon S3。這樣,你的應用需要通過外部API呼叫並以應用本身而不是單個使用者的身份來讀取或修改這些資源。這樣的場景就很適合使用客戶端證書授權,通過此授權方式獲取Access Token僅可訪問平臺授權類的介面。
比如獲取App首頁最新聞列表,由於這個資料與使用者無關,所以不涉及使用者登入與授權,但又不想任何人都可以呼叫這個WebAPI,這樣場景就適用[例:比如微信公眾平臺授權]。
+---------+ +---------------+ | | | | | |>--(A)- Client Authentication --->| Authorization | | Client | | Server | | |<--(B)---- Access Token ---------<| | | | | | +---------+ +---------------+ Figure 6: Client Credentials Flow
基本流程
A.客戶端提供使用者名稱和密碼交換令牌
B.認證伺服器驗證通過,發放令牌,後面根據這個令牌獲取資源即可
服務實現
Install-Package Microsoft.AspNet.Identity.Owin
Install-Package Microsoft.Owin.Security.OAuth
Install-Package Microsoft.AspNet.WebApi.Owin
Install-Package Microsoft.AspNet.WebApi.WebHost
Install-Package Microsoft.Owin.Host.SystemWeb
OWIN WEBAPI
[assembly: OwinStartup(typeof(Startup))] namespace OAuth2.App_Start { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } } public partial class Startup { public void ConfigureAuth(IAppBuilder app) { /* app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/token"), Provider = new ApplicationOAuthProvider(), AccessTokenExpireTimeSpan = TimeSpan.FromHours(2), AuthenticationMode = AuthenticationMode.Active, //HTTPS is allowed only AllowInsecureHttp = false AllowInsecureHttp = true //ApplicationCanDisplayErrors = false }); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); */ app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/token"), Provider = new ApplicationOAuthProvider(), //RefreshTokenProvider = new ApplicationRefreshTokenProvider(), AccessTokenExpireTimeSpan = TimeSpan.FromHours(2), AuthenticationMode = AuthenticationMode.Active, //HTTPS is allowed only AllowInsecureHttp = false AllowInsecureHttp = true //ApplicationCanDisplayErrors = false }); } } public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider { /* private OAuth2ClientService _oauthClientService; public ApplicationOAuthProvider() { this.OAuth2ClientService = new OAuth2ClientService(); } */ /// <summary> /// 驗證客戶[client_id與client_secret驗證] /// </summary> /// <param name="context"></param> /// <returns></returns> public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { //http://localhost:48339/token //grant_type=client_credentials&client_id=irving&client_secret=123456 string client_id; string client_secret; context.TryGetFormCredentials(out client_id, out client_secret); if (client_id == "irving" && client_secret == "123456") { context.Validated(client_id); } else { //context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.OK); context.SetError("invalid_client", "client is not valid"); } return base.ValidateClientAuthentication(context); } /// <summary> /// 客戶端授權[生成access token] /// </summary> /// <param name="context"></param> /// <returns></returns> public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context) { /* var client = _oauthClientService.GetClient(context.ClientId); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, client.ClientName)); */ var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, "iphone")); var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties() { AllowRefresh = true }); context.Validated(ticket); return base.GrantClientCredentials(context); } /// <summary> /// 重新整理Token[重新整理refresh_token] /// </summary> /// <param name="context"></param> /// <returns></returns> public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context) { //enforce client binding of refresh token if (context.Ticket == null || context.Ticket.Identity == null || !context.Ticket.Identity.IsAuthenticated) { context.SetError("invalid_grant", "Refresh token is not valid"); } else { //Additional claim is needed to separate access token updating from authentication //requests in RefreshTokenProvider.CreateAsync() method } return base.GrantRefreshToken(context); } public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context) { if (context.ClientId == "irving") { var expectedRootUri = new Uri(context.Request.Uri, "/"); if (expectedRootUri.AbsoluteUri == context.RedirectUri) { context.Validated(); } } return Task.FromResult<object>(null); } }
資源服務
/// <summary> ///客戶端模式【Client Credentials Grant】 ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api /// </summary> [RoutePrefix("api/v1/oauth2")] public class OAuth2Controller : ApiController { /// <summary> /// 獲得資訊 /// </summary> /// <returns></returns> [Authorize] [Route("news")] public async Task<IHttpActionResult> GetNewsAsync() { var authentication = HttpContext.Current.GetOwinContext().Authentication; var ticket = authentication.AuthenticateAsync("Bearer").Result; var claimsIdentity = User.Identity as ClaimsIdentity; var data = claimsIdentity.Claims.Where(c => c.Type == "urn:oauth:scope").ToList(); var claims = ((ClaimsIdentity)Thread.CurrentPrincipal.Identity).Claims; return Ok(new { IsError = true, Msg = string.Empty, Data = Thread.CurrentPrincipal.Identity.Name + " It's about news !!! token expires: " + ticket.Properties.Dictionary.ToJson() }); } }
啟用授權驗證[WebApiConfig]
在ASP.NET Web API中啟用Token驗證,需要加上[Authorize]標記,並且配置預設啟用驗證不記名授權方式
// Web API configuration and services // Configure Web API to use only bearer token authentication. config.SuppressDefaultHostAuthentication(); config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
客戶端
獲得票據
服務端[/token]獲取token需要三個引數
POST https://domain.com/token HTTP/1.1
Content-type:application/json;charset=UTF-8
grant_type=client_credentials&client_id=irving&client_secret=123456
{"access_token":"qghSowAcM9Ap7yIiyZ6i52VOk4NWBpgDDJZ6jf-PdAeP4roFMlGhKUV_Kg_ow0QgTXBKaPzIFBLzdc6evBUPVOaV8Op0wsrUwwKUjRluAPAQmw3MIm8MtmtC0Vfp7ZByuvvMy21NbpRBZcajQxzunJGPIqbdPMYs8e279T5UMmgpBVZJuC4N6d-mxk3DMN2-42cOxz-3k6J-7yXCVYroEh6txjZW03ws155LswIg0yw","token_type":"bearer","expires_in":7199}
請求資源
設定HTTP頭 Authorization: Bearer {THE TOKEN}
GET http://localhost:48339/api/v1/oauth2/news HTTP/1.1
Authorization: Bearer qghSowAcM9Ap7yIiyZ6i52VOk4NWBpgDDJZ6jf-PdAeP4roFMlGhKUV_Kg_ow0QgTXBKaPzIFBLzdc6evBUPVOaV8Op0wsrUwwKUjRluAPAQmw3MIm8MtmtC0Vfp7ZByuvvMy21NbpRBZcajQxzunJGPIqbdPMYs8e279T5UMmgpBVZJuC4N6d-mxk3DMN2-42cOxz-3k6J-7yXCVYroEh6txjZW03ws155LswIg0yw
grant_type=client_credentials&client_id=irving&client_secret=123456{"IsError":true,"Msg":"","Data":"iphone It's about news !!! token expires: {\".refresh\":\"True\",\".issued\":\"Mon, 29 Jun 2015 02:47:12 GMT\",\".expires\":\"Mon, 29 Jun 2015 04:47:12 GMT\"}"}
客戶端測試
/// <summary> ///客戶端模式【Client Credentials Grant】 ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api /// </summary> [RoutePrefix("api/v1/oauth2")] public class OAuth2Controller : ApiController { /// <summary> /// 獲取token /// </summary> /// <returns></returns> [Route("token")] public async Task<IHttpActionResult> GetTokenAsync() { //獲得token var dict = new SortedDictionary<string, string>(); dict.Add("client_id", "irving"); dict.Add("client_secret", "123456"); dict.Add("grant_type", "client_credentials"); var data = await (@"http://" + Request.RequestUri.Authority + @"/token").PostUrlEncodedAsync(dict).ReceiveJson<Token>(); //根據token獲得諮詢資訊 [Authorization: Bearer {THE TOKEN}] //var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithHeader("Authorization", "Bearer " + data.access_token).GetAsync().ReceiveString(); var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithOAuthBearerToken(data.access_token).GetAsync().ReceiveString(); return Ok(new { IsError = true, Msg = data, Data = news }); } } public class Token { public string access_token { get; set; } public string token_type { get; set; } public string expires_in { get; set; } }