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