亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

使用Owin中間件搭建OAuth2.0認證授權服務器

2019-11-14 14:33:01
字體:
來源:轉載
供稿:網友

前言

這里主要總結下本人最近半個月關于搭建OAuth2.0服務器工作的經驗。至于為何需要OAuth2.0、為何是Owin、什么是Owin等問題,不再贅述。我假定讀者是使用asp.net,并需要搭建OAuth2.0服務器,對于涉及的Asp.Net Identity(Claims Based Authentication)、Owin、OAuth2.0等知識點已有基本了解。若不了解,請先參考以下文章:

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

從何開始?

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

接下來,我們主要看這個demo

Demo:Authorization Server

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

  • Clients
  • Authorization Server
  • Resource Server

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

Client:AuthorizationCodeGrant

AuthorizationCodeGrant項目使用了DotNetOpenAuth.OAuth2封裝的一個WebServerClient類作為和Authorization Server通信的Client。
(這里由于封裝了底層的一些細節,致使不使用這個包和Authorization Server交互時可能會遇到幾個坑,這個稍后再講)
這里主要看幾個關鍵點:

1.運行項目后,出現頁面,點擊【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” } 為需要申請的scopes,或者說是Resource Server的接口標識,或者說是接口權限。然后Send(HttpContext)即重定向。

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

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

我們發現這段代碼在之前點擊Authorize的時候也會觸發,但是那時并沒有code參數(缺少code時,可能_webServerClient.ProcessUserAuthorization(Request)并不會發起請求),所以拿不到AccessToken。

3.拿到AccessToken后,剩下的就是調用api,CallApi,試一下,發現返回的就是剛才用戶登陸Authorization Server所使用的用戶名(Resource Server的具體細節稍后再講)。

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

先易后難,接著看看Resource Server

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

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

然后,唯一的控制器MeController也非常簡單:

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

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

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

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

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

Authorization Server

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

Startup.Auth配置部分

首先來看Authorization Server項目的Startup.Auth.cs文件,關于OAuth2.0服務端的設置就在這里。

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

既然到這里了,先提醒下這個設置:AuthenticationType是用戶登陸Authorization Server后的登陸憑證的標記名,簡單理解為cookie的鍵名就行。為什么要先提醒下呢,因為這和OAuth/Authorize中檢查用戶當前是否已登陸有關系,有時候,這個值的默認設置可能是”ApplicationCookie”。

好,正式看OAuthServer部分的設置:

 // Setup Authorization Serverapp.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions{    AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),    TokenEndpointPath = new PathString(Paths.TokenPath),    ApplicationCanDisplayErrors = true,#if DEBUG    AllowInsecureHttp = true,  //重要!!這里的設置包含整個流程通信環境是否啟用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),...

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

ApplicationCanDisplayErrors = true, #if DEBUG    AllowInsecureHttp = true, //重要?。∵@里的設置包含整個流程通信環境是否啟用ssl#endif

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

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

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

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

具體的這些委托的作用,我們接著看對應的方法的代碼:

//驗證重定向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項目中預設的測試數據。如果我們有Client的注冊機制,那么Clients.Client1.Id對應的Clients.Client1.RedirectUrl就可能是從數據庫中讀取的。而數據庫中讀取的RedirectUrl則可以直接作為字符串參數傳給context.Validated(RedirectUrl)。這樣,這部分邏輯就算結束了。

//驗證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);}

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

  • TryGetBasicCredentials:是指Client可以按照Basic身份驗證的規則提交ClientId和ClientSecret
  • TryGetFormCredentials:是指Client可以把ClientId和ClientSecret放在Post請求的form表單中提交

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

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== //這只是個例子

其中Basic后面部分是 ClientId:ClientSecret 形式的字符串進行Base64編碼后的字符串,Authorization是Http Header 的鍵名,Basic至最后是該Header的值。
Form這種只要注意兩個鍵名是 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);        }    }

這里是對應之前說的用來交換AccessToken的code參數的生成和驗證的,用ConcurrentDictionary是為了線程安全;_authenticationCodes.TryRemove就是之前一直重點強調的code是一次性的,驗證一次后即刪除了。

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

這里處理RefreshToken的生成和接收,只是簡單的調用Token的加密設置和解密的方法。

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

OAuth控制器

OAuthController中只有一個Action,即Authorize。
Authorize方法并沒有區分HttpGet或者HttpPost,主要原因可能是方法簽名引起的(Action同名,除非參數不同,否則即使設置了HttpGet和HttpPost,編譯器也會認為你定義了兩個相同的Action,我們若是硬要拆開,可能會稍微麻煩點)。

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

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

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();}

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

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

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

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

這句只是獲取Client申請的scopes,或者說是權限(用空格分隔感覺有點奇怪,不知道是不是OAuth2.0里的標準)。

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分支就是處理授權的邏輯,其實就是很直觀的向identity中添加Claims。那么Claims都去哪了?有什么用呢?
這需要再回過頭去看ResourceServer,以下是重點內容:

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

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

submit.Login分支就不多說了,意思就是用戶換個賬號登陸。

寫了這么多,基本分析已經結束,我們來看看還需要什么

首先,你需要一個自定義的Authorize屬性,用于在ResourceServer中驗證Scopes,這里要注意兩點:

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

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

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

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

實踐才會真的進步

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

追加

后續實踐發現,由于使用了owin的中間件,ResourceServer依賴Microsoft.Owin.Host.SystemWeb,發布部署的時候不要遺漏該dll。

 

作者:Personball's Blog

 
原文地址:使用Owin中間件搭建OAuth2.0認證授權服務器, 感謝原作者分享。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
在线成人激情视频| 成人美女av在线直播| 国产精品一二三在线| 日韩中文字幕免费| 亚洲老司机av| 欧美激情免费在线| 成人黄色av免费在线观看| 国模叶桐国产精品一区| 国产一区二区欧美日韩| 欧美资源在线观看| 国产欧美日韩视频| 久久精品国产亚洲7777| 全球成人中文在线| 日韩在线中文视频| 日本精品中文字幕| 91精品国产色综合久久不卡98| 亚洲精品国产免费| 国产一区二中文字幕在线看| 岛国av午夜精品| 亚洲加勒比久久88色综合| 亚洲第一精品久久忘忧草社区| 亚洲成色www8888| 亚洲人av在线影院| 亚洲人成网站在线播| 亚洲欧洲日产国产网站| 久久久久成人精品| 精品国产拍在线观看| 久久久精品美女| 色中色综合影院手机版在线观看| 日韩美女视频免费在线观看| 91免费的视频在线播放| 亚洲欧美资源在线| 亚洲影视中文字幕| 亚洲成人动漫在线播放| 欧美日韩亚洲天堂| 国产精品久久久久久超碰| 成人妇女免费播放久久久| 亚洲在线第一页| 欧洲午夜精品久久久| 欧美视频中文字幕在线| 91免费福利视频| 亚洲精品一区二区网址| 国产美女精品免费电影| 亚洲免费视频一区二区| 亚洲精品一区久久久久久| 亚洲少妇激情视频| 欧美激情a在线| 国产精品爽爽ⅴa在线观看| 国外日韩电影在线观看| 国产在线视频2019最新视频| 91天堂在线观看| 综合网中文字幕| 精品国产一区二区三区在线观看| 午夜免费久久久久| 欧美另类交人妖| 色偷偷综合社区| 久久国产精品久久久久| 九九九久久久久久| 亚洲第一男人天堂| 亚洲国产成人一区| 97欧美精品一区二区三区| 亚洲精品大尺度| 亚洲人成电影网| 青青a在线精品免费观看| 国产日韩欧美黄色| 国产精品一区二区三| 国产精品高潮视频| 最近2019中文字幕大全第二页| 国产丝袜一区二区三区免费视频| 97精品视频在线观看| 欧美日韩午夜激情| 欧美成年人视频网站欧美| 国产精品成人观看视频国产奇米| 国产情人节一区| 97欧美精品一区二区三区| 在线中文字幕日韩| 狠狠色噜噜狠狠狠狠97| 欧美日韩电影在线观看| 国产+成+人+亚洲欧洲| 91精品综合视频| 欧美成年人视频网站欧美| 日韩av一区二区在线观看| 亚洲免费高清视频| 中文字幕久热精品视频在线| 国产在线视频一区| 性欧美长视频免费观看不卡| 精品亚洲一区二区三区四区五区| 欧美丝袜第一区| 成人性生交大片免费观看嘿嘿视频| 亚洲一区二区日本| 91在线直播亚洲| 久久久亚洲精品视频| 一本大道亚洲视频| 福利视频第一区| 精品久久久久久中文字幕一区奶水| 亚洲成人黄色网址| 久久精品成人动漫| 精品国产一区二区三区久久狼黑人| 精品动漫一区二区| 国产精品99一区| 国产精品日韩欧美| 7m精品福利视频导航| 成人美女av在线直播| 成人激情av在线| 欧美极品美女视频网站在线观看免费| 久久久久999| 欧美极品美女电影一区| 国产亚洲激情视频在线| 热门国产精品亚洲第一区在线| 亚洲欧美一区二区激情| 欧美高清自拍一区| 国产一区二区三区在线看| 国产欧美一区二区三区久久| 色哟哟网站入口亚洲精品| 97av在线播放| 亚洲精品久久视频| 国产精品h片在线播放| 亚洲激情免费观看| 国产精品久久久久99| 欧美精品在线观看| 日韩精品欧美激情| 美女999久久久精品视频| 国产成人鲁鲁免费视频a| 6080yy精品一区二区三区| 欧美性猛交视频| 国产婷婷97碰碰久久人人蜜臀| 福利二区91精品bt7086| 国产成人av在线播放| 日韩在线视频导航| 国产精品女人久久久久久| 亚洲高清久久网| 国产欧美亚洲视频| 日韩av手机在线观看| 粉嫩av一区二区三区免费野| 国产日韩专区在线| 国产成人精品在线播放| 成人精品久久一区二区三区| 欧美日韩福利在线观看| 热久久美女精品天天吊色| 欧美多人爱爱视频网站| 日产精品久久久一区二区福利| 欧美色视频日本版| 亚洲色图狂野欧美| 亚洲精品日产aⅴ| 欧美极品美女视频网站在线观看免费| 亚洲深夜福利视频| 亚洲精品aⅴ中文字幕乱码| 亚洲精品视频中文字幕| 91日本视频在线| 欧美国产精品人人做人人爱| 国产偷亚洲偷欧美偷精品| 亚洲欧美国产一本综合首页| 欧美激情xxxx性bbbb| 欧美激情亚洲视频| 亚洲男人天堂2024| 91精品视频在线免费观看| 91高清在线免费观看| 国产情人节一区| 中文字幕欧美国内| 日韩欧美在线播放| 韩国视频理论视频久久| 久久久国产影院| 中文字幕精品在线| 91精品国产91久久久久久吃药|