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

OWIN的全称是Open Web Interface For .Net, 是MS在VS2013期间引入的全新的概念, 网上已经有不少的关于它的信息, 这里我就谈下我自己的理解:

  • OWIN****是一种规范和标准, ****不代表特定技术. MS最新出现的一些新的技术, 比如Kanata, Identity, SignalR, 它们只是基于OWIN的不同实现.
  • OWIN****的核心理念是解耦,协作和开放---这和MS以前的风格大相径庭,值得引起大家的注意。
  • OWIN****是MS****未来Web****开发的方向,想跟着MS路线继续开发Web应用,OWIN是大势所趋。

在这篇博文中,我们将以 OAuth 的** Resource Owner Password Credentials Grant **的授权方式( grant_type=password )获取 Access Token,并以这个 Token 调用与用户相关的 Web API。
Resource Owner Password Credentials 这种模式要求用户提供用户名和密码来交换访问令牌(access_token)。该模式仅用于非常值得信任的用户,例如API提供者本人所写的移动应用。虽然用户也要求提供密码,但并不需要存储在设备上。因为初始验证之后,只需将OAuth的令牌记录下来即可。如果用户希望取消授权,因为其真实密码并没有被记录,因此无需修改密码就可以立即取消授权。token本身也只是得到有限的授权,因此相比最传统的username/password授权,该模式依然更为安全。

对应的应用场景是:为自家的网站开发手机 App(非第三方 App),只需用户在 App 上登录,无需用户对 App 所能访问的数据进行授权。

基本流程

365537-20150923105340553-785686353.jpg
  • A. 向用户索要认证信息
    首先,我们必须得让用户将认证信息提供给应用程序。对于应用方来说,如果用户处于不可信的网络中时,除了需要输入用户名和密码外,还需要用户提供一个安全令牌作为用户的第三个输入。
  • B. 交换访问令牌
    这里的访问令牌交换过程与授权码类型的验证授权(authorization code)很相似。我们要做的就是向认证服务器提交一个POST请求并在其中提供相应的认证和客户信息。

所需的POST参数:
**grant_type 该模式下为"password" **
scope 业务访问控制范围,是一个可选参数
client_id 应用注册时获得的客户id
client_secret 应用注册时获得的客户密钥
username 用户的用户名
password 用户的密码

POST https://xxx.com/token HTTP/1.1Content-type:application/x-www-form-urlencodedAuthorization Basic Base64(clientId:clientSecret)username=irving&password=123456&grant_type=password
  • C. 刷新Token
    1).accesstoken 是有过期时间的,到了过期时间这个 access token 就失效,需要刷新。
    2).如果accesstoken会关联一定的用户权限,如果用户授权更改了,这个accesstoken需要被刷新以关联新的权限。
    3).为什么要专门用一个 token 去更新 accesstoken 呢?如果没有 refreshtoken,也可以刷新 accesstoken,但每次刷新都要用户输入登录用户名与密码,客户端直接用 refreshtoken 去更新 accesstoken,无需用户进行额外的操作。
POST http://localhost:19923/tokenContent-Type: Application/x-www-form-
urlencodedAuthorization Basic Base64(clientId:clientSecret)username=irving&password=123456&grant_type=refresh_token

备注:
有了前面相关token,服务调用也很简单

GET https://xxx.com/api/v1/account/profile HTTP/1.1Content-type:application/x-www-form-urlencodedAuthorization Authorization: Bearer {THE TOKEN}

WebApi Startup

get access_token

在 C# 中用 HttpClient 实现一个简单的客户端,代码如下:

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace Tdf.OAuthClientTest
{
    public class OAuthClientTest
    {
        private HttpClient _httpClient;

        public OAuthClientTest()
        {
            _httpClient = new HttpClient();
            _httpClient.Timeout = TimeSpan.FromMinutes(30);

            _httpClient.BaseAddress = new Uri("http://localhost:13719");
        }

        public void Get_Accesss_Token_By_Client_Credentials_Grant()
        {
            var parameters = new Dictionary<string, string>();
            parameters.Add("UserName", "Bobby");
            parameters.Add("Password", "123");
            parameters.Add("grant_type", "password");

            Console.WriteLine(_httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters))
                .Result.Content.ReadAsStringAsync().Result);
        }

    }
}

这样,运行客户端程序就可以拿到 Access Token 了。


refresh_token

在 C# 中用 HttpClient 实现一个简单的客户端,代码如下:

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace Tdf.OAuthClientTest
{
    public class OAuthClientTest
    {
        private HttpClient _httpClient;

        public OAuthClientTest()
        {
            _httpClient = new HttpClient();
            _httpClient.Timeout = TimeSpan.FromMinutes(30);

            _httpClient.BaseAddress = new Uri("http://localhost:13719");
        }

        public void Get_Accesss_Token_By_Client_Credentials_Grant()
        {
            var parameters = new Dictionary<string, string>();

            // refresh_token
            parameters.Add("grant_type", "refresh_token");
            parameters.Add("refresh_token", "DAB1FE2B-2F84-4534-A620-F6B9B474B503");

            Console.WriteLine(_httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters))
                .Result.Content.ReadAsStringAsync().Result);
        }

    }
}

这样,运行客户端程序就可以拿到 Access Token 了。


OWIN WEBAPI

默认Startup.Auth.cs

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 Ems.Web.Providers;
using Ems.Web.Models;

namespace Ems.Web
{
    /// <summary>
    /// Startup
    /// </summary>
    public partial class Startup
    {
        /// <summary>
        /// OAuthOptions
        /// </summary>
        public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

        /// <summary>
        /// PublicClientId
        /// </summary>
        public static string PublicClientId { get; private set; }

        /// <summary>
        /// ConfigureAuth
        /// 有关配置身份验证的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkId=301864
        /// </summary>
        /// <param name="app"></param>
        public void ConfigureAuth(IAppBuilder app)
        {
            // 将数据库上下文和用户管理器配置为对每个请求使用单个实例
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

            // 使应用程序可以使用 Cookie 来存储已登录用户的信息
            // 并使用 Cookie 来临时存储有关使用第三方登录提供程序登录的用户的信息
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // 针对基于 OAuth 的流配置应用程序
            PublicClientId = "self";
            OAuthOptions = new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/Token"),
                Provider = new ApplicationOAuthProvider(PublicClientId),
                AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
                //在生产模式下设 AllowInsecureHttp = false
                AllowInsecureHttp = true
            };

            // 使应用程序可以使用不记名令牌来验证用户身份
            app.UseOAuthBearerTokens(OAuthOptions);

            // 取消注释以下行可允许使用第三方登录提供程序登录
            //app.UseMicrosoftAccountAuthentication(
            //    clientId: "",
            //    clientSecret: "");

            //app.UseTwitterAuthentication(
            //    consumerKey: "",
            //    consumerSecret: "");

            //app.UseFacebookAuthentication(
            //    appId: "",
            //    appSecret: "");

            //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
            //{
            //    ClientId = "",
            //    ClientSecret = ""
            //});
        }
    }
}

Startup.cs

using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Practices.Unity;
using Owin;
using System;
using System.Web.Http;
using Tdf.Application.Act.UserMgr;
using Tdf.Utils.Config;
using Tdf.WebApi.Providers;
using Unity.WebApi;

[assembly: OwinStartup(typeof(Tdf.WebApi.Startup))]

namespace Tdf.WebApi
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // 有关如何配置应用程序的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=316888
            var config = new HttpConfiguration();
            WebApiConfig.Register(config);

            IUnityContainer container = UnityConfig.GetConfiguredContainer();
            config.DependencyResolver = new UnityDependencyResolver(container);
            ConfigureOAuth(app, container);

            // 这一行代码必须放在ConfiureOAuth(app)之后
            // Microsoft.AspNet.WebApi.Owin
            app.UseWebApi(config);
        }

        public void ConfigureOAuth(IAppBuilder app, IUnityContainer container)
        {
            IUserAppService userService = container.Resolve<IUserAppService>();
            OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
            {
#if DEBUG
                AllowInsecureHttp = true,
#endif
                TokenEndpointPath = new PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(double.Parse(ConfigHelper.GetValue("TokenExpireMinute", "120"))),
                RefreshTokenProvider = new TdfRefreshTokenProvider(userService),
                Provider = new TdfAuthorizationServerProvider(userService)

            };

            // Token Generation
            app.UseOAuthAuthorizationServer(oAuthServerOptions);

            var opts = new OAuthBearerAuthenticationOptions()
            {
                Provider = new TdfOAuthBearerProvider("Token")
            };
            app.UseOAuthBearerAuthentication(opts);

        }
    }
}

TdfAuthorizationServerProvider.cs

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Tdf.Application.Act.UserMgr;
using Tdf.Application.Act.UserMgr.Dtos;
using Tdf.Domain.Act.Entities;

namespace Tdf.WebApi.Providers
{
    /// <summary>
    /// Resource Owner Password Credentials Grant 授权
    /// </summary>
    public class TdfAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        /// <summary>
        /// Password Grant 授权服务
        /// </summary>
        readonly IUserAppService _userService;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="userService">Password Grant 授权服务</param>
        public TdfAuthorizationServerProvider(IUserAppService userService)
        {
            this._userService = userService;
        }

        /// <summary>
        /// Resource Owner Password Credentials Grant 的授权方式;
        /// 验证用户名与密码 [Resource Owner Password Credentials Grant[username与password]|grant_type=password&username=irving&password=654321]
        /// 重载 OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials() 方法即可
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var user = await _userService.Login(new LoginInput() { UserName = context.UserName, Password = context.Password });
            var userInfo = user.Result as User;
            if (user.ErrCode != 0 || userInfo == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
            else
            {
                // 验证context.UserName与context.Password 
                // 调用后台的登录服务验证用户名与密码
                var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
                oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Id.ToString()));
                oAuthIdentity.AddClaim(new Claim("UserId", userInfo.Id.ToString()));
                oAuthIdentity.AddClaim(new Claim("UserName", userInfo.UserName));

                var props = new AuthenticationProperties(new Dictionary<string, string> {
                    { "user_id", userInfo.Id.ToString() },
                    { "user_name", userInfo.UserName }
                });

                var ticket = new AuthenticationTicket(oAuthIdentity, props);

                context.Validated(ticket);

                await base.GrantResourceOwnerCredentials(context);
            }
        }

        /// <summary>
        /// 把Context中的属性加入到token中
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task TokenEndpoint(OAuthTokenEndpointContext context)
        {
            foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
            {
                context.AdditionalResponseParameters.Add(property.Key, property.Value);
            }

            return Task.FromResult<object>(null);
        }

        /// <summary>  
        /// 验证客户端 [Authorization Basic Base64(clientId:clientSecret)|Authorization: Basic 5zsd8ewF0MqapsWmDwFmQmeF0Mf2gJkW]
        /// 对third party application 认证,  
        /// 为third party application颁发appKey和appSecrect,在此省略了颁发appKey和appSecrect的环节,  
        /// 认为所有的third party application都是合法的  
        /// </summary>  
        /// <param name="context"></param>  
        /// <returns></returns>  
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            // 表示所有允许此third party application请求  
            context.Validated();
            return Task.FromResult<object>(null);
        }

    }
}

TdfRefreshTokenProvider.cs

using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Threading.Tasks;
using Tdf.Application.Act.UserMgr;
using Tdf.Domain.Act.Entities;
using Tdf.Utils.Config;
using Tdf.Utils.GuidHelper;

namespace Tdf.WebApi.Providers
{
    /// <summary>
    /// 刷新Token
    /// 生成与验证Token
    /// </summary>
    public class TdfRefreshTokenProvider : AuthenticationTokenProvider
    {
        /// <summary>
        /// 授权服务
        /// </summary>
        private IUserAppService _userService;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="userService">授权服务</param>
        public TdfRefreshTokenProvider(IUserAppService userService)
        {
            this._userService = userService;
        }

        /// <summary>
        /// 创建refreshToken
        /// </summary>
        /// <param name="context">上下文</param>
        /// <returns></returns>
        public override async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return;

            var refreshTokenLifeTime = ConfigHelper.GetValue("TokenExpireMinute", "120");
            if (string.IsNullOrEmpty(refreshTokenLifeTime)) return;

            // generate access token
            var refreshTokenId = new RegularGuidGenerator().Create();

            context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
            context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddMinutes(double.Parse(refreshTokenLifeTime));

            var refreshToken = new RefreshToken()
            {
                Id = refreshTokenId,
                UserId = new Guid(context.Ticket.Identity.Name),
                IssuedUtc = DateTime.Parse(context.Ticket.Properties.IssuedUtc.ToString()),
                ExpiresUtc = DateTime.Parse(context.Ticket.Properties.ExpiresUtc.ToString()),
                ProtectedTicket = context.SerializeTicket()
            };

            // Token没有过期的情况强行刷新,删除老的Token保存新的Token
            var jsonMsg = await _userService.SaveTokenAsync(refreshToken);
            if (jsonMsg.ErrCode == 0)
            {
                context.SetToken(refreshTokenId.ToString());
            }
        }

        /// <summary>
        /// 刷新refreshToken[刷新access token时,refresh token也会重新生成]
        /// </summary>
        /// <param name="context">上下文</param>
        /// <returns></returns>
        public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            var jsonMsg = await _userService.GetToken(context.Token);
            if (jsonMsg.ErrCode == 0)
            {
                var refreshToken = jsonMsg.Result as RefreshToken;
                if (refreshToken != null)
                {
                    context.DeserializeTicket(refreshToken.ProtectedTicket);
                    await _userService.RemoveToken(context.Token);
                }
            }
        }
    }
}

TdfOAuthBearerProvider.cs

using Microsoft.Owin.Security.OAuth;
using System.Threading.Tasks;

namespace Tdf.WebApi.Providers
{
    public class TdfOAuthBearerProvider : OAuthBearerAuthenticationProvider
    {
        readonly string _name;

        public TdfOAuthBearerProvider(string name)
        {
            _name = name;
        }

        public override Task RequestToken(OAuthRequestTokenContext context)
        {
            var value = context.Request.Query.Get(_name);
            if (!string.IsNullOrEmpty(value))
            {
                context.Token = value;
            }
            return Task.FromResult<object>(null);
        }
    }
}

结合 ASP.NET 现有的安全机制,借助 OWIN 的威力,Microsoft.Owin.Security.OAuth 的确让开发基于 OAuth 的 Web API 变得更简单。

更多资料和资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容