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,其它无关代码全部清除,最终剩下如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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 ,<br> RefreshTokenProvider = new RefreshTokenProvider() //应用RefreshTokenProvider,刷新Token的程序 }; app.UseOAuthBearerTokens(OAuthOptions); } } } |
刷新Token的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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() 这两个方法。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 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)然后写客户端调用代码测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 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 ; } } } |
返回结果:
1 | { "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行代码:
1 2 3 4 | // 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】