国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計(jì) > 正文

使用Owin中間件搭建OAuth2.0認(rèn)證授權(quán)服務(wù)器

2019-11-14 14:33:01
字體:
供稿:網(wǎng)友

前言

這里主要總結(jié)下本人最近半個(gè)月關(guān)于搭建OAuth2.0服務(wù)器工作的經(jīng)驗(yàn)。至于為何需要OAuth2.0、為何是Owin、什么是Owin等問題,不再贅述。我假定讀者是使用asp.net,并需要搭建OAuth2.0服務(wù)器,對(duì)于涉及的Asp.Net Identity(Claims Based Authentication)、Owin、OAuth2.0等知識(shí)點(diǎn)已有基本了解。若不了解,請(qǐng)先參考以下文章:

  • MVC5 - ASP.NET Identity登錄原理 - Claims-based認(rèn)證和OWIN
  • 下一代Asp.net開發(fā)規(guī)范OWIN(1)—— OWIN產(chǎn)生的背景以及簡(jiǎn)單介紹
  • 理解OAuth 2.0
  • rfc6749

從何開始?

在對(duì)前言中所列的各知識(shí)點(diǎn)有初步了解之后,我們從何處下手呢?
這里推薦一個(gè)demo:OWIN OAuth 2.0 Authorization Server
除了demo外,還推薦準(zhǔn)備好katanaPRoject的源代碼

接下來,我們主要看這個(gè)demo

Demo:Authorization Server

從OAuth2.0的rfc文檔中,我們知道OAuth有多種授權(quán)模式,這里只關(guān)注授權(quán)碼方式。
首先來看Authorization Server項(xiàng)目,里面有三大塊:

  • Clients
  • Authorization Server
  • Resource Server

以RFC6749圖示:
Clients分別對(duì)應(yīng)各種授權(quán)方式的Client,這里我們只看對(duì)應(yīng)授權(quán)碼方式的AuthorizationCodeGrant項(xiàng)目;
Authorization Server即提供OAuth服務(wù)的認(rèn)證授權(quán)服務(wù)器;
Resource Server即Client拿到accessToken后攜帶AccessToken訪問的資源服務(wù)器(這里僅簡(jiǎn)單提供一個(gè)/api/Me顯示用戶的Name)。
另外需要注意Constants項(xiàng)目,里面設(shè)置了一些關(guān)鍵數(shù)據(jù),包含接口地址以及Client的Id和Secret等。

Client:AuthorizationCodeGrant

AuthorizationCodeGrant項(xiàng)目使用了DotNetOpenAuth.OAuth2封裝的一個(gè)WebServerClient類作為和Authorization Server通信的Client。
(這里由于封裝了底層的一些細(xì)節(jié),致使不使用這個(gè)包和Authorization Server交互時(shí)可能會(huì)遇到幾個(gè)坑,這個(gè)稍后再講)
這里主要看幾個(gè)關(guān)鍵點(diǎn):

1.運(yùn)行項(xiàng)目后,出現(xiàn)頁面,點(diǎn)擊【Authorize】按鈕,第一次重定向用戶至 Authorization Server

if (!string.IsNullOrEmpty(Request.Form.Get("submit.Authorize"))){    var userAuthorization = _webServerClient.PrepareRequestUserAuthorization(new[] { "bio", "notes" });    userAuthorization.Send(HttpContext);    Response.End();}

這里 new[] { “bio”, “notes” } 為需要申請(qǐng)的scopes,或者說是Resource Server的接口標(biāo)識(shí),或者說是接口權(quán)限。然后Send(HttpContext)即重定向。

2.這里暫不論重定向用戶至Authorization Server后的情況,假設(shè)用戶在Authorization Server上完成了授權(quán)操作,那么Authorization Server會(huì)重定向用戶至Client,在這里,具體的回調(diào)地址即之前點(diǎn)擊【Authorize】按鈕的頁面,而url上帶有一個(gè)一次性的code參數(shù),用于Client再次從服務(wù)器端發(fā)起請(qǐng)求到Authorization Server以code交換AccessToken。關(guān)鍵代碼如下:

if (string.IsNullOrEmpty(accessToken)){    var authorizationState = _webServerClient.ProcessUserAuthorization(Request);    if (authorizationState != null)    {        ViewBag.AccessToken = authorizationState.AccessToken;        ViewBag.RefreshToken = authorizationState.RefreshToken;        ViewBag.Action = Request.Path;    }}

我們發(fā)現(xiàn)這段代碼在之前點(diǎn)擊Authorize的時(shí)候也會(huì)觸發(fā),但是那時(shí)并沒有code參數(shù)(缺少code時(shí),可能_webServerClient.ProcessUserAuthorization(Request)并不會(huì)發(fā)起請(qǐng)求),所以拿不到AccessToken。

3.拿到AccessToken后,剩下的就是調(diào)用api,CallApi,試一下,發(fā)現(xiàn)返回的就是剛才用戶登陸Authorization Server所使用的用戶名(Resource Server的具體細(xì)節(jié)稍后再講)。

4.至此,Client端的代碼分析完畢(RefreshToken請(qǐng)自行嘗試,自行領(lǐng)會(huì))。沒有復(fù)雜的內(nèi)容,按RFC6749的設(shè)計(jì),Client所需的就只有這些步驟。對(duì)于Client部分,唯一需要再次鄭重提醒的是,一定不能把AccessToken泄露出去,比如不加密直接放在瀏覽器cookie中。

先易后難,接著看看Resource Server

我們先把Authorization Server放一放,接著看下Resource Server。
Resource Server非常簡(jiǎn)單,App_Start中Startup.Auth配置中只有一句代碼:

app.USEOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());

然后,唯一的控制器MeController也非常簡(jiǎn)單:

[Authorize]public class MeController : ApiController{    public string Get()    {        return this.User.Identity.Name;    }}

有效代碼就這些,就實(shí)現(xiàn)了非用戶授權(quán)下無法訪問,授權(quán)了就能獲取用戶登陸用戶名。(其實(shí)webconfig里還有一項(xiàng)關(guān)鍵配置,稍后再說)

那么,Startup.Auth中的代碼是什么意思呢?為什么Client訪問api,而User.Identity.Name卻是授權(quán)用戶的登陸名而不是Client的登陸名呢?

我們先看第一個(gè)問題,找 UseOAuthBearerAuthentication() 這個(gè)方法。具體怎么找就不廢話了,我直接說明它的源代碼位置在 Katana Project源碼中的Security目錄下的Microsoft.Owin.Security.OAuth項(xiàng)目。OAuthBearerAuthenticationExtensions.cs文件中就這么一個(gè)針對(duì)IAppBuilder的擴(kuò)展方法。而這個(gè)擴(kuò)展方法其實(shí)就是設(shè)置了一個(gè)OAuthBearerAuthenticationMiddleware,以針對(duì)AccessToken進(jìn)行解析。解析的結(jié)果就類似于Client以授權(quán)用戶的身份(即第二個(gè)問題,User.Identity.Name是授權(quán)用戶的登陸名)訪問了api接口,獲取了屬于該用戶的信息數(shù)據(jù)。

關(guān)于Resource Server,目前只需要知道這么多。
(關(guān)于接口驗(yàn)證scopes、獲取用戶主鍵、AccessToken中添加自定義標(biāo)記等,在看過Authorization Server后再進(jìn)行說明)

Authorization Server

Authorization Server是本文的核心,也是最復(fù)雜的一部分。

Startup.Auth配置部分

首先來看Authorization Server項(xiàng)目的Startup.Auth.cs文件,關(guān)于OAuth2.0服務(wù)端的設(shè)置就在這里。

// Enable application Sign In Cookieapp.UseCookieAuthentication(new CookieAuthenticationOptions{    AuthenticationType = "Application", //這里有個(gè)坑,先提醒下    AuthenticationMode = AuthenticationMode.Passive,    LoginPath = new PathString(Paths.LoginPath),    LogoutPath = new PathString(Paths.LogoutPath),});

既然到這里了,先提醒下這個(gè)設(shè)置:AuthenticationType是用戶登陸Authorization Server后的登陸憑證的標(biāo)記名,簡(jiǎn)單理解為cookie的鍵名就行。為什么要先提醒下呢,因?yàn)檫@和OAuth/Authorize中檢查用戶當(dāng)前是否已登陸有關(guān)系,有時(shí)候,這個(gè)值的默認(rèn)設(shè)置可能是”ApplicationCookie”。

好,正式看OAuthServer部分的設(shè)置:

 // Setup Authorization Serverapp.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions{    AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),    TokenEndpointPath = new PathString(Paths.TokenPath),    ApplicationCanDisplayErrors = true,#if DEBUG    AllowInsecureHttp = true,  //重要!!這里的設(shè)置包含整個(gè)流程通信環(huán)境是否啟用ssl#endif    // Authorization server provider which controls the lifecycle of Authorization Server    Provider = new OAuthAuthorizationServerProvider    {        OnValidateClientRedirectUri = ValidateClientRedirectUri,        OnValidateClientAuthentication = ValidateClientAuthentication,        OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,        OnGrantClientCredentials = GrantClientCredetails    },    // Authorization code provider which creates and receives authorization code    AuthorizationCodeProvider = new AuthenticationTokenProvider    {        OnCreate = CreateAuthenticationCode,        OnReceive = ReceiveAuthenticationCode,    },    // Refresh token provider which creates and receives referesh token    RefreshTokenProvider = new AuthenticationTokenProvider    {        OnCreate = CreateRefreshToken,        OnReceive = ReceiveRefreshToken,    }});
我們一段段來看:
...AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),TokenEndpointPath = new PathString(Paths.TokenPath),...

設(shè)置了這兩個(gè)EndpointPath,則無需重寫OAuthAuthorizationServerProvider的MatchEndpoint方法(假如你繼承了它,寫了個(gè)自己的ServerProvider,否則也可以通過設(shè)置OnMatchEndpoint達(dá)到和重寫相同的效果)。
反過來說,如果你的EndpointPath比較復(fù)雜,比如前面可能因?yàn)閲?guó)際化而攜帶culture信息,則可以通過override MatchEndpoint方法實(shí)現(xiàn)定制。
但請(qǐng)記住,重寫了MatchEndpoint(或設(shè)置了OnMatchEndpoint)后,我推薦注釋掉這兩行賦值語句。至于為什么,請(qǐng)看Katana Project源碼中的Security目錄下的Microsoft.Owin.Security.OAuth項(xiàng)目OAuthAuthorizationServerHandler.cs第38行至第46行代碼。
對(duì)了,如果項(xiàng)目使用了某些全局過濾器,請(qǐng)自行判斷是否要避開這兩個(gè)路徑(AuthorizeEndpointPath是對(duì)應(yīng)OAuth控制器中的Authorize方法,而TokenEndpointPath則是完全由這里配置的OAuthAuthorizationServer中間件接管的)。

ApplicationCanDisplayErrors = true, #if DEBUG    AllowInsecureHttp = true, //重要!!這里的設(shè)置包含整個(gè)流程通信環(huán)境是否啟用ssl#endif

這里第一行不多說,字面意思理解下。
重要!!AllowInsecureHttp設(shè)置整個(gè)通信環(huán)境是否啟用ssl,不僅是OAuth服務(wù)端,也包含Client端(當(dāng)設(shè)置為false時(shí),若登記的Client端重定向url未采用https,則不重定向,踩到這個(gè)坑的話,問題很難定位,親身體會(huì))

// Authorization server provider which controls the lifecycle of Authorization ServerProvider = new OAuthAuthorizationServerProvider{    OnValidateClientRedirectUri = ValidateClientRedirectUri,    OnValidateClientAuthentication = ValidateClientAuthentication,    OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,    OnGrantClientCredentials = GrantClientCredetails}

這里是核心Provider,凡是On開頭的,其實(shí)都是委托方法,中間件定義了OAuth2的一套流程,但是它把幾個(gè)關(guān)鍵的事件以委托的方式暴露了出來。

  • OnValidateClientRedirectUri:驗(yàn)證Client的重定向Url,這個(gè)是為了安全,防釣魚
  • OnValidateClientAuthentication:驗(yàn)證Client的身份(ClientId以及ClientSecret)
  • OnGrantResourceOwnerCredentials和OnGrantClientCredentials是這個(gè)demo中提供的另兩種授權(quán)方式,不在本文討論范圍內(nèi)。

具體的這些委托的作用,我們接著看對(duì)應(yīng)的方法的代碼:

//驗(yàn)證重定向url的private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context){    if (context.ClientId == Clients.Client1.Id)    {        context.Validated(Clients.Client1.RedirectUrl);    }    else if (context.ClientId == Clients.Client2.Id)    {        context.Validated(Clients.Client2.RedirectUrl);    }    return Task.FromResult(0);}

這里context.ClientId是OAuth2處理流程上下文中獲取的ClientId,而Clients.Client1.Id是前面說的Constants項(xiàng)目中預(yù)設(shè)的測(cè)試數(shù)據(jù)。如果我們有Client的注冊(cè)機(jī)制,那么Clients.Client1.Id對(duì)應(yīng)的Clients.Client1.RedirectUrl就可能是從數(shù)據(jù)庫中讀取的。而數(shù)據(jù)庫中讀取的RedirectUrl則可以直接作為字符串參數(shù)傳給context.Validated(RedirectUrl)。這樣,這部分邏輯就算結(jié)束了。

//驗(yàn)證Client身份private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context){    string clientId;    string clientSecret;    if (context.TryGetBasicCredentials(out clientId, out clientSecret) ||        context.TryGetFormCredentials(out clientId, out clientSecret))    {        if (clientId == Clients.Client1.Id && clientSecret == Clients.Client1.Secret)        {            context.Validated();        }        else if (clientId == Clients.Client2.Id && clientSecret == Clients.Client2.Secret)        {            context.Validated();        }    }    return Task.FromResult(0);}

和上面驗(yàn)證重定向URL類似,這里是驗(yàn)證Client身份的。但是特別要注意兩個(gè)TryGet方法,這兩個(gè)TryGet方法對(duì)應(yīng)了OAuth2Server如何接收Client身份認(rèn)證信息的方式(這個(gè)demo用了封裝好的客戶端,不會(huì)遇到這個(gè)問題,之前說的在不使用DotNetOpenAuth.OAuth2封裝的一個(gè)WebServerClient類的情況下可能遇到的坑就是這個(gè))。

  • TryGetBasicCredentials:是指Client可以按照Basic身份驗(yàn)證的規(guī)則提交ClientId和ClientSecret
  • TryGetFormCredentials:是指Client可以把ClientId和ClientSecret放在Post請(qǐng)求的form表單中提交

那么什么時(shí)候需要Client提交ClientId和ClientSecret呢?是在前面說到的Client拿著一次性的code參數(shù)去OAuth服務(wù)器端交換AccessToken的時(shí)候。
Basic身份認(rèn)證,參考RFC2617
Basic簡(jiǎn)單說明下就是添加如下的一個(gè)Http Header:

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== //這只是個(gè)例子

其中Basic后面部分是 ClientId:ClientSecret 形式的字符串進(jìn)行Base64編碼后的字符串,Authorization是Http Header 的鍵名,Basic至最后是該Header的值。
Form這種只要注意兩個(gè)鍵名是 client_id 和 client_secret 。

 private readonly ConcurrentDictionary<string, string> _authenticationCodes =        new ConcurrentDictionary<string, string>(StringComparer.Ordinal);    private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)    {        context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));        _authenticationCodes[context.Token] = context.SerializeTicket();    }    private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)    {        string value;        if (_authenticationCodes.TryRemove(context.Token, out value))        {            context.DeserializeTicket(value);        }    }

這里是對(duì)應(yīng)之前說的用來交換AccessToken的code參數(shù)的生成和驗(yàn)證的,用ConcurrentDictionary是為了線程安全;_authenticationCodes.TryRemove就是之前一直重點(diǎn)強(qiáng)調(diào)的code是一次性的,驗(yàn)證一次后即刪除了。

private void CreateRefreshToken(AuthenticationTokenCreateContext context){    context.SetToken(context.SerializeTicket());}private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context){    context.DeserializeTicket(context.Token);}

這里處理RefreshToken的生成和接收,只是簡(jiǎn)單的調(diào)用Token的加密設(shè)置和解密的方法。

至此,Startup.Auth部分的基本結(jié)束,我們接下來看OAuth控制器部分。

OAuth控制器

OAuthController中只有一個(gè)Action,即Authorize。
Authorize方法并沒有區(qū)分HttpGet或者HttpPost,主要原因可能是方法簽名引起的(Action同名,除非參數(shù)不同,否則即使設(shè)置了HttpGet和HttpPost,編譯器也會(huì)認(rèn)為你定義了兩個(gè)相同的Action,我們?nèi)羰怯惨痖_,可能會(huì)稍微麻煩點(diǎn))。

還是一段段來看
if (Response.StatusCode != 200){    return View("AuthorizeError");}

這段說實(shí)話,到現(xiàn)在我還沒搞懂為啥要判斷下200,可能是考慮到owin中間件會(huì)提前處理點(diǎn)什么?去掉了也沒見有什么異常,或者是我沒注意。。。這段可有可無。。

var authentication = HttpContext.GetOwinContext().Authentication;var ticket = authentication.AuthenticateAsync("Application").Result;var identity = ticket != null ? ticket.Identity : null;if (identity == null){    authentication.Challenge("Application");    return new HttpUnauthorizedResult();}

這里就是判斷授權(quán)用戶是否已經(jīng)登陸,這是很簡(jiǎn)單的邏輯,登陸部分可以和AspNet.Identity那套一起使用,而關(guān)鍵就是authentication.AuthenticateAsync(“Application”)中的“Application”,還記得么,就是之前說的那個(gè)cookie名:

...AuthenticationType = "Application", //這里有個(gè)坑,先提醒下...

這個(gè)里要匹配,否則用戶登陸后,到OAuth控制器這里可能依然會(huì)認(rèn)為是未登陸的。
如果用戶登陸,則這里的identity就會(huì)有值。

 var scopes = (Request.QueryString.Get("scope") ?? "").Split(' ');

這句只是獲取Client申請(qǐng)的scopes,或者說是權(quán)限(用空格分隔感覺有點(diǎn)奇怪,不知道是不是OAuth2.0里的標(biāo)準(zhǔn))。

if (Request.HttpMethod == "POST"){    if (!string.IsNullOrEmpty(Request.Form.Get("submit.Grant")))    {        identity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);        foreach (var scope in scopes)        {            identity.AddClaim(new Claim("urn:oauth:scope", scope));        }        authentication.SignIn(identity);    }    if (!string.IsNullOrEmpty(Request.Form.Get("submit.Login")))    {        authentication.SignOut("Application");        authentication.Challenge("Application");        return new HttpUnauthorizedResult();    }}

這里,submit.Grant分支就是處理授權(quán)的邏輯,其實(shí)就是很直觀的向identity中添加Claims。那么Claims都去哪了?有什么用呢?
這需要再回過頭去看ResourceServer,以下是重點(diǎn)內(nèi)容:

其實(shí)Client訪問ResourceServer的api接口的時(shí)候,除了AccessToken,不需要其他任何憑據(jù)。那么ResourceServer是怎么識(shí)別出用戶登陸名的呢?關(guān)鍵就是claims-based identity 這套東西。其實(shí)所有的claims都加密存進(jìn)了AccessToken中,而ResourceServer中的OAuthBearer中間件就是解密了AccessToken,獲取了這些claims。這也是為什么之前強(qiáng)調(diào)AccessToken絕對(duì)不能泄露,對(duì)于ResourceServer來說,訪問者擁有AccessToken,那么就是受信任的,頒發(fā)AccessToken的機(jī)構(gòu)也是受信任的,所以對(duì)于AccessToken中加密的內(nèi)容也是絕對(duì)相信的,所以,ResourceServer這邊甚至不需要再去數(shù)據(jù)庫驗(yàn)證訪問者Client的身份。

這里提到,頒發(fā)AccessToken的機(jī)構(gòu)也是受信任的,這是什么意思呢?我們看到AccessToken是加密過的,那么如何解密?關(guān)鍵在于AuthorizationServer項(xiàng)目和ResourceServer項(xiàng)目的web.config中配置了一致的machineKey
(題外話,有個(gè)在線machineKey生成器:machineKey generator,這里也提一下,如果不喜歡配置machineKey,可以研究下如何重寫AccessToken和RefreshToken的加密解密過程,這里不多說了,提示:OAuthAuthorizationServerOptions中有好幾個(gè)以Format后綴的屬性)
上面說的machineKey即是系統(tǒng)默認(rèn)的AccessToken和RefreshToken的加密解密的密鑰。

submit.Login分支就不多說了,意思就是用戶換個(gè)賬號(hào)登陸。

寫了這么多,基本分析已經(jīng)結(jié)束,我們來看看還需要什么

首先,你需要一個(gè)自定義的Authorize屬性,用于在ResourceServer中驗(yàn)證Scopes,這里要注意兩點(diǎn):

  1. webapi的Authorize和mvc的Authorize不一樣(起碼截至MVC5,這還是兩個(gè)東西,vnext到時(shí)再細(xì)究;
  2. 如何從ResourceServer的User.Identity中挖出自定義的claims。

第一點(diǎn),需要重寫的方法不是AuthorizeCore(具體方法名忘了,不知道有沒有寫錯(cuò)),而是OnAuthorize(同上,有空VS里驗(yàn)證下再來改),且需要調(diào)用 base.OnAuthorize 。
第二點(diǎn),如下:

var claimsIdentity = User.Identity as ClaimsIdentity;claimsIdentity.Claims.Where (c => c.Type == "urn:oauth:scope").ToList();

然后,還有個(gè)ResourceServer常用的東西,就是用戶信息的主鍵,一般可以從User.Identity.GetUserId()獲取,不過這個(gè)方法是個(gè)擴(kuò)展方法,需要using Microsoft.AspNet.Identity。至于為什么這里可以用呢?就是Claims里包含了用戶信息的主鍵,不信可以調(diào)試下看看(注意觀察添加claims那段代碼,將登陸后原有的claims也累加進(jìn)去了,這里就包含了用戶登陸名Name和用戶主鍵UserId)。

實(shí)踐才會(huì)真的進(jìn)步

這次寫的真不少,基本自己踩過的坑應(yīng)該都寫了吧,有空再回顧看下有沒有遺漏的。今天就先到這里,over。

追加

后續(xù)實(shí)踐發(fā)現(xiàn),由于使用了owin的中間件,ResourceServer依賴Microsoft.Owin.Host.SystemWeb,發(fā)布部署的時(shí)候不要遺漏該dll。

 

作者:Personball's Blog

 
原文地址:使用Owin中間件搭建OAuth2.0認(rèn)證授權(quán)服務(wù)器, 感謝原作者分享。

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 囊谦县| 青田县| 鸡泽县| 黄大仙区| 九龙城区| 贞丰县| 临湘市| 诸暨市| 金溪县| 高淳县| 磐石市| 博爱县| 东兴市| 玉屏| 石阡县| 襄城县| 镇原县| 龙口市| 湘阴县| 东乡族自治县| 泰安市| 吉木萨尔县| 莱阳市| 三明市| 固安县| 平陆县| 长岛县| 阳春市| 南溪县| 射洪县| 邵东县| 泸定县| 青海省| 夏津县| 奈曼旗| 陆丰市| 德安县| 湖州市| 丰台区| 泾川县| 邵阳县|