在WebApi中基于Owin OAuth使用授權發(fā)放Token

OWIN的全稱是Open Web Interface For .Net, 是MS在VS2013期間引入的全新的概念, 網(wǎng)上已經(jīng)有不少的關于它的信息, 這里我就談下我自己的理解:

  • OWIN****是一種規(guī)范和標準, ****不代表特定技術. MS最新出現(xiàn)的一些新的技術, 比如Kanata, Identity, SignalR, 它們只是基于OWIN的不同實現(xiàn).
  • OWIN****的核心理念是解耦,協(xié)作和開放---這和MS以前的風格大相徑庭牍鞠,值得引起大家的注意殿遂。
  • OWIN****是MS****未來Web****開發(fā)的方向贩汉,想跟著MS路線繼續(xù)開發(fā)Web應用误辑,OWIN是大勢所趨库正。

在這篇博文中杨拐,我們將以 OAuth 的** Resource Owner Password Credentials Grant **的授權方式( grant_type=password )獲取 Access Token,并以這個 Token 調(diào)用與用戶相關的 Web API闸盔。
Resource Owner Password Credentials 這種模式要求用戶提供用戶名和密碼來交換訪問令牌(access_token)。該模式僅用于非常值得信任的用戶琳省,例如API提供者本人所寫的移動應用迎吵。雖然用戶也要求提供密碼,但并不需要存儲在設備上针贬。因為初始驗證之后击费,只需將OAuth的令牌記錄下來即可。如果用戶希望取消授權桦他,因為其真實密碼并沒有被記錄蔫巩,因此無需修改密碼就可以立即取消授權。token本身也只是得到有限的授權,因此相比最傳統(tǒng)的username/password授權批幌,該模式依然更為安全础锐。

對應的應用場景是:為自家的網(wǎng)站開發(fā)手機 App(非第三方 App),只需用戶在 App 上登錄荧缘,無需用戶對 App 所能訪問的數(shù)據(jù)進行授權皆警。

基本流程

365537-20150923105340553-785686353.jpg
  • A. 向用戶索要認證信息
    首先,我們必須得讓用戶將認證信息提供給應用程序截粗。對于應用方來說信姓,如果用戶處于不可信的網(wǎng)絡中時,除了需要輸入用戶名和密碼外绸罗,還需要用戶提供一個安全令牌作為用戶的第三個輸入意推。
  • B. 交換訪問令牌
    這里的訪問令牌交換過程與授權碼類型的驗證授權(authorization code)很相似。我們要做的就是向認證服務器提交一個POST請求并在其中提供相應的認證和客戶信息珊蟀。

所需的POST參數(shù):
**grant_type 該模式下為"password" **
scope 業(yè)務訪問控制范圍菊值,是一個可選參數(shù)
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會關聯(lián)一定的用戶權限,如果用戶授權更改了磅崭,這個accesstoken需要被刷新以關聯(lián)新的權限儿子。
    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,服務調(diào)用也很簡單

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 實現(xiàn)一個簡單的客戶端蜂桶,代碼如下:

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 實現(xiàn)一個簡單的客戶端扑媚,代碼如下:

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)
        {
            // 將數(shù)據(jù)庫上下文和用戶管理器配置為對每個請求使用單個實例
            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),
                //在生產(chǎn)模式下設 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>
        /// 構造函數(shù)
        /// </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 
                // 調(diào)用后臺的登錄服務驗證用戶名與密碼
                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頒發(fā)appKey和appSecrect,在此省略了頒發(fā)appKey和appSecrect的環(huán)節(jié),  
        /// 認為所有的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>
        /// 構造函數(shù)
        /// </summary>
        /// <param name="userService">授權服務</param>
        public TdfRefreshTokenProvider(IUserAppService userService)
        {
            this._userService = userService;
        }

        /// <summary>
        /// 創(chuàng)建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 現(xiàn)有的安全機制,借助 OWIN 的威力人弓,Microsoft.Owin.Security.OAuth 的確讓開發(fā)基于 OAuth 的 Web API 變得更簡單沼死。

更多資料和資源

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市崔赌,隨后出現(xiàn)的幾起案子意蛀,更是在濱河造成了極大的恐慌,老刑警劉巖健芭,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件县钥,死亡現(xiàn)場離奇詭異,居然都是意外死亡慈迈,警方通過查閱死者的電腦和手機若贮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痒留,“玉大人兜看,你說我怎么就攤上這事∠料梗” “怎么了细移?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長熊锭。 經(jīng)常有香客問我弧轧,道長,這世上最難降的妖魔是什么碗殷? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任精绎,我火速辦了婚禮,結果婚禮上锌妻,老公的妹妹穿的比我還像新娘代乃。我一直安慰自己,他們只是感情好仿粹,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布搁吓。 她就那樣靜靜地躺著,像睡著了一般吭历。 火紅的嫁衣襯著肌膚如雪堕仔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天晌区,我揣著相機與錄音摩骨,去河邊找鬼通贞。 笑死,一個胖子當著我的面吹牛恼五,可吹牛的內(nèi)容都是我干的昌罩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灾馒,長吁一口氣:“原來是場噩夢啊……” “哼峡迷!你這毒婦竟也來了?” 一聲冷哼從身側響起你虹,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绘搞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后傅物,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夯辖,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年董饰,在試婚紗的時候發(fā)現(xiàn)自己被綠了蒿褂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡卒暂,死狀恐怖啄栓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情也祠,我是刑警寧澤昙楚,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站诈嘿,受9級特大地震影響堪旧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奖亚,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一淳梦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧昔字,春花似錦爆袍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至所坯,卻和暖如春谆扎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芹助。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工堂湖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人状土。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓无蜂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒙谓。 傳聞我的和親對象是個殘疾皇子斥季,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容