Client Credentials Grant的授权方式就是只验证客户端(Client),不验证用户(Resource Owner),只要客户端通过验证就发access token。
举一个对应的应用场景例子,比如我们想提供一个“获取网站首页最新博文列表”的WebAPI给iOS App调用。
由于这个数据与用户无关,所以不涉及用户登录与授权,不需要Resource Owner的参与。
但我们不想任何人都可以调用这个WebAPI,所以要对客户端进行验证,而使用OAuth中的Client Credentials Grant授权方式可以很好地解决这个问题。
1)用Visual Studio 2013/2015创建一个Web API 4项目,VS会生成一堆OAuth相关代码。
2)打开App_Start/Startup.Auth.cs ,精简一下代码,我们只需要实现以Client Credentials Grant授权方式拿到token,其它无关代码全部清除,最终剩下如下代码:
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 进行验证。
在 GrantClientCredentials() 方法中对客户端进行授权,授了权就能发 access token 。
这样,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