在ASP.NET中基于Owin OAuth使用Client Credentials Grant授权发放Token

Posted 邢帅杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在ASP.NET中基于Owin OAuth使用Client Credentials Grant授权发放Token相关的知识,希望对你有一定的参考价值。

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

 

以上是关于在ASP.NET中基于Owin OAuth使用Client Credentials Grant授权发放Token的主要内容,如果未能解决你的问题,请参考以下文章

Asp.Net MVC 5 Owin Twitter Auth 抛出 401 异常

ASP.NET Microsoft OWIN 令牌变得无效

使用Owin中间件搭建OAuth2.0认证授权服务器

ASP.NET 5 OAuth 持有者令牌身份验证

没有 OWIN 和 AspNet.Identity 的基于 Asp.Net Web Api 令牌的授权

在WebApi中基于Owin OAuth使用授权发放Token