<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      PKCE

      一個成功的App背后肯定有一堆后端服務(wù)提供支撐,認證授權(quán)服務(wù)(Authentication and Authorization Service,以下稱AAS)就是其中之一,它是約束App、保障資源安全的必備組件?,F(xiàn)在也有第三方平臺提供此類服務(wù)。如果自己開發(fā)AAS,針對不同場景選擇何種模式是必須要謹慎考慮的問題。這就需要決策者對目前主流的AA協(xié)議有較深理解,若是隨手套用、流于形式,就可能埋下嚴重的安全隱患。我們以OAuth2為例進行說明。
      本文假設(shè)讀者有一定的OAuth2知識,若沒有可先閱讀博主以前寫的一篇博文,或其它資料。

      先說明:以下大部分討論針對的是第三方授權(quán)場景。對于同源系統(tǒng)(Client與AAS同屬一個開發(fā)實體),Username&Password Flow模式和嵌入式(AAS登錄頁面嵌入Client中)PKCE模式都能呈現(xiàn)相同且授權(quán)無感的用戶體驗,相對來說前者簡單,后者能在Client端做一些安全處理,更推薦。

      Why PKCE?

      當我們開發(fā)一個App(本文指的是Native App),若使用的AAS同屬自己開發(fā),一般最簡單能想到的就是在App中畫個登錄框,直連登錄接口成功后返回AccessToken,這便是Username&Password Flow。其實,這種模式在Web領(lǐng)域流傳已久(返回的cookie或JWT可以認為就是AccessToken),但是App卻需要考量兩個問題:

      1. 用戶如何鑒別client是否合法。
      2. AAS如何鑒別登錄請求是否來自于合法client。

      針對這兩個問題,Web應(yīng)用程序依靠多年來各大廠大V對Web安全的重視和發(fā)展,已經(jīng)有相當完備的應(yīng)對手段。比如在用戶側(cè),瀏覽器地址欄上的域名和CA(瀏覽器常以鎖頭圖標顯示)可以給到用戶提醒;同時依靠數(shù)據(jù)隔離(瀏覽器沙盒、cookie等)能讓AAS判斷請求是否來自同域。

      而對于App來說,用戶側(cè)沒有一個標準識別信息能給到用戶,這要求用戶須要憑借自身經(jīng)驗判斷App是否為釣魚App;而本文所涉及的各類授權(quán)模式也無法保證百分之百的安全,只能做一些簡單的防范(其實用戶側(cè)App防偽本身就不屬于AAS的職責(zé)范圍)。所幸各大應(yīng)用商店的審核機制、手機自帶的掃毒進程、App自身的后端防范(e.g.雙因素認證two-factor authentication)等等手段讓做一個釣魚App相當困難。當然,對于小App來說,剛說的這些手段可能都沒法覆蓋到,特別是Android系統(tǒng)Apk隨意下載安裝,用戶其實是很容易中招的,只是仿制一款沒多少人用的App更加沒意義。

      我們把關(guān)注點放到后端,看看OAuth2的幾種模式是否可行。

      Username&Password Flow:適用于AAS高度信任請求的場景,既AAS認為所有請求肯定來自于合法的客戶端———這種情況一般會出現(xiàn)在請求來自于信任域或內(nèi)網(wǎng)系統(tǒng)———即AAS不介意client可以擁有用戶名和密碼。自然該模式不適用于第三方client的場景。另外由于該模式請求一般使用接口形式,惡意方較容易構(gòu)建非法請求,就算每次請求的用戶名密碼錯誤也能讓AAS浪費大量資源(此時后端需要網(wǎng)關(guān)對請求頻率進行限制,并屏蔽惡意請求)。有人會說結(jié)合Client Credentials Flow就能解決client合法性校驗的問題,如果client本身是后端的話確實如此,但在App場景下仍然不夠嚴密。

      Client Credentials Flow:對于外部請求的身份識別,現(xiàn)在較廣泛的解決方案是AAS給App頒發(fā)一個事先約定的身份證明(如client_idclient_secret對),App將它們織入請求(如簽名),AAS以此確定請求合法。然而新的問題又出現(xiàn)了,就是如何保證該身份證明本身的安全性。這個問題在移動端并沒有完美的解決方案。以Android為例,存儲在Sharedpreferences需要在運行時寫入,這就給了不法分子可趁之機;即使存儲在最安全的KeyStore里,使用時還要傳入外部密鑰獲取,那這個外部密鑰的安全如何保障呢:)。
      純粹的Client Credentials Flow是不依賴用戶的,AAS直接向合法client開放指定資源(可能是與所有用戶相關(guān)的資源)。所以在用戶授權(quán)場景下,一般將此模式作為其它模式的輔助。

      既然如此,那干脆不開放接口,而是要求App嵌入AAS自己的登錄界面(與開放接口相比,AAS可借助界面實現(xiàn)一些安全策略比如在客戶端判斷請求頻率是否超出閾值,請求是否由該頁面發(fā)出/cookie判斷),類似于Authorization Code FlowImplicit Flow。因為Implicit Flow可看作Authorization Code Flow的“低配版”,我們先來論證Authorization Code Flow的可行性。

      Authorization Code Flow中關(guān)鍵的一步是client獲取code,是AAS發(fā)送重定向狀態(tài)碼(30X)到瀏覽器告知瀏覽器重定向到預(yù)先指定的URI,并將code附加到query。對Web應(yīng)用程序來說,重定向的URI是屬于client域的URL,而瀏覽器的安全級別也防止了其它進程對code的截獲,且就算截獲了,由于惡意進程不知道Client Credentials(存于client后端),也無法拿去交換AccessToken。而對于App,情況又變得錯漏百出,如前所述,Client Credentials無法得到安全保障,而且code被攔截的風(fēng)險也比Web端高了很多——
      首先,重定向的URI的作用改變了,它起到一個類似“喚起”的作用,系統(tǒng)根據(jù)URI的scheme喚起相應(yīng)App,如果惡意App注冊了相同的scheme就能輕易拿到code;其次,URI從系統(tǒng)瀏覽器傳遞到App的過程走的基本都是不安全通道,這也增加了傳遞過程中被截獲的風(fēng)險。

      于是Authorization Code Flow也被我們否了,那Implicit Flow自然更不用說,而且Implicit Flow無法提供RefreshToken,這對App來說可不能忍。

      另外,用戶側(cè)也不推薦在App中嵌入Web頁面,一個是增加用戶識別的難度,另一個給了App訪問Web隱私數(shù)據(jù)的機會?,F(xiàn)在很多三方登錄并不會提供嵌入式的頁面,都是直接打開它們自己的App的單獨登錄界面,應(yīng)該就是主要避免這個問題。

      看來在App場景下,OAuth2的其中四個授權(quán)模式多少都有安全隱患,那還有什么協(xié)議能支持App的認證授權(quán)呢?難道這么多年的移動互聯(lián)網(wǎng),大家都是在里面裸奔嗎?現(xiàn)實情況雖然沒有如此糟糕,但筆者所接觸到的項目和開發(fā)人員,普遍對所使用的協(xié)議是否真的安全缺少認知,多年運行良好很可能是因為你的App還不起眼:)

      偉大的萊布尼茨·牛頓說過,地球毀滅與普通人無關(guān),因為總會有其他人去解決。有一部分人注意到了App的授權(quán)安全問題,這其中又有一小撮人各種改造了自己的項目,最后有幾個人提出了一個規(guī)范并收錄在RFC文件中,這個規(guī)范就是Proof Key for Code Exchange(即PKCE)。官方說法PKCE是Authorization Code Flow的擴展[和增強],It is primarily used by mobile and JavaScript apps, but the technique can be applied to any client as well.

      之后我們走完一遍授權(quán)流程就會清楚,PKCE和Authorization Code Flow確實無本質(zhì)不同,差別在對CSRF的防范手段上。因為App沒有瀏覽器那樣的cookie支持,AAS沒有session這樣的東西來保存state參數(shù)從而防止CSRF,所以轉(zhuǎn)而使用PKCE的code_verifiercode_challenge,順便解決了無client_secret驗證身份的問題。其實PKCE并沒有解決本節(jié)開頭的那兩個問題,而且它的設(shè)計目標也不是它們(雖然我們可以保證請求是從AAS頁面發(fā)出,但無法保證嵌入AAS頁面的Client是合法的,也許可以通過某些方式檢測?)。任何人只要知道了client_id,就能無障礙地和AAS交互PKCE流程,除非AAS強制要求client_secret(但如前所述在App端這也沒有意義)。它只是彌補了Authorization Code Flow在移動端的一個缺陷,避免請求結(jié)果被惡意App截獲。
      PKCE擴展不會添加任何新響應(yīng),因此即使授權(quán)服務(wù)器不支持,客戶端也可以始終使用PKCE擴展。

      確定好授權(quán)模式,接下來就是要實現(xiàn)或者尋找一個合適的框架,以前用過的DotNetOpenAuth已經(jīng)很久沒更新了,是否支持PKCE尚不清楚。本著與時俱進的想法,為ASP.NET Core量身定制的IdentityServer4進入了視線。它是一款基于OpenID Connect的AAS框架。
      Java開發(fā)的Keycloak在7.0版本也加入了對PKCE的支持

      OIDC(OpenID Connect)

      OpenID Connect是OpenID發(fā)展到第三代的產(chǎn)物,它其實是原來OpenID和OAuth的集合體。本人當年在研究OAuth的時候就被OpenID搞了一頭霧水,總感覺OpenID和AccessToken雖然語義不同,但兩者完全可以歸為一個。多年之后OpenID Connect姍姍來遲,但細究之后發(fā)現(xiàn)還是換湯不換藥,只不過原本的OpenID現(xiàn)在變成了IDToken(一般使用JWT格式來包裝。得益于JWT的自包含性,緊湊性以及防篡改機制,使得ID-Token可以安全的傳遞給第三方客戶端程序并且容易被驗證。),區(qū)別于AccessToken。為什么不干脆合并成一個Token呢?其實從名稱上就能看出來它們各自的職責(zé)不同,從職責(zé)及歷史角度看,如此這般也是合理。

      IDToken只是用于標識用戶,側(cè)重于用戶信息,可用于單點登錄等會話相關(guān)場景,Client也可以把用戶信息保存下來便于用戶行為統(tǒng)計分析。它的標準流程中用戶參與是必須的。AccessToken解決的問題是授權(quán)[給Client],而授權(quán)可能是用戶授權(quán)也可能是AAS直接授權(quán),用戶認證并不是不可或缺的條件,所以授權(quán)過程中并不一定需要用戶參與。在任何模式下,Client也不需要知道用戶信息就能無障礙訪問授權(quán)過的API?,F(xiàn)在看來,AccessToken當然可以在某些模式下包含IDToken,但若沒有IDToken,若只是為了標識用戶使用AccessToken就顯得不那么恰當了。另外,就算有用戶參與,IDToken就一個,但對于不同的Client,AccessToken肯定是不一樣的。


      實戰(zhàn)

      Talk is cheap, show you the code. 我們的目標很簡單,就是實現(xiàn)App的登錄授權(quán),通過后可以拿著token調(diào)用資源Api。用戶對背后的邏輯是無感知的,對他們來說就和普通的登錄一樣。為了達到這個目的,我們還需要把登錄界面嵌入到App中——細心的讀者會發(fā)現(xiàn)這似乎違背了上一節(jié)的提醒,但是上面的建議主要面向的場景是AAS作為第三方登錄平臺,服務(wù)的App不受完全信任——由于我們這個App也是自己從頭開始擼的,所以不用考慮這個問題。

      下面分別就AAS、App、Resource Server列出關(guān)鍵步驟和代碼。

      AAS

      為簡單起見,我們直接使用ASP.NET Core Identity作為用戶體系。

      # 1.創(chuàng)建一個空的mvc項目
      dotnet new mvc -o AAS
      # 2.添加ASP.NET Core Identity相關(guān)依賴包
      dotnet add package Microsoft.Extensions.Identity.Stores
      dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
      # 3.添加IdentityServer4依賴包
      dotnet add package IdentityServer4
      dotnet add package IdentityServer4.EntityFramework
      dotnet add package IdentityServer4.AspNetIdentity  
      
      1. 定義一個繼承自IdentityDbContext<ApplicationUser>DbContext——ApplicationDbContext,其中ApplicationUser繼承須自IdentityUser。
      2. Startup.ConfigureServices,主要是將一些服務(wù)注入到IOC容器中。
              public void ConfigureServices(IServiceCollection services)
              {
                  //Other code...
      
                  //DbContext
                  services.AddDbContext<ApplicationDbContext>(options =>
                      options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
                  //ASP.NET Core Identity
                  services.AddIdentity<ApplicationUser, IdentityRole>()
                      .AddEntityFrameworkStores<ApplicationDbContext>()
                      .AddDefaultTokenProviders();
                  //IdentityServer
                  string migrationsAssembly = typeof(Startup).Assembly.GetName().Name;
                  var builder = services.AddIdentityServer(options =>
                  {
                      options.Events.RaiseErrorEvents = true;
                      options.Events.RaiseInformationEvents = true;
                      options.Events.RaiseFailureEvents = true;
                      options.Events.RaiseSuccessEvents = true;
                      // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                      options.EmitStaticAudienceClaim = true;
                  })
                      .AddConfigurationStore(options =>
                      {
                          options.ConfigureDbContext = builder =>
                             builder.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
                             sql => sql.MigrationsAssembly(migrationsAssembly));
                      })
                      .AddOperationalStore(options =>
                      {
                          options.ConfigureDbContext = builder =>
                              builder.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
                              sql => sql.MigrationsAssembly(migrationsAssembly));
                      })
                      .AddAspNetIdentity<ApplicationUser>()
                      .AddProfileService<ApplicationProfileService>();
      
                      builder.AddDeveloperSigningCredential();
              }
      

      其中AddDefaultTokenProviders和本文所指的Token沒關(guān)系,它主要提供了手機號驗證、郵箱驗證等Api,如何使用可參看ASP.NET CORE IDENTITY TOKEN PROVIDERS – UNDER THE HOOD
      options.Events.RaiseXXXXEvents表示什么級別的事件會被監(jiān)聽到,可以自定義事件和監(jiān)聽器,可參看官方文檔Events。這種模式比到處log應(yīng)該要稍好一點。
      我們使用MySql持久化數(shù)據(jù),AddConfigurationStore表示存儲一些預(yù)配置數(shù)據(jù)如clients、resources等等,AddOperationalStore存儲授權(quán)過程中產(chǎn)生的數(shù)據(jù)如codes、tokens等等。
      AddProfileService<T>用于添加自定義claim,泛型參數(shù)類需要實現(xiàn)IProfileService。

      1. 配置IdentityServer到請求處理管道,在Startup.Configure中增加app.UseIdentityServer()(它間接調(diào)用了app.UseAuthentication()),注意它在管道中和其它處理器的順序。
      2. 在數(shù)據(jù)庫中添加client。由于數(shù)據(jù)表中非關(guān)鍵字段太多,我們一般使用管理后臺或編寫CRUD接口進行維護。以PKCE客戶端為例,我們只需構(gòu)建帶必要屬性的如下client:
                      // interactive client using code flow + pkce
                      new Client
                      {
                          ClientId = "interactive",
                          RequireClientSecret = false,
                          AllowedGrantTypes = GrantTypes.Code,
                          RedirectUris = { "https://localhost:44300/signin-oidc" },
                          AllowOfflineAccess = true,
                          AllowedScopes = { "openid", "profile", "scope2" }
                      },
      

      其中RequireClientSecret設(shè)為false表示交互時不需要client_secret參與;AllowOfflineAccess設(shè)為true表示需要獲取Refresh token;RedirectUris是與client約定好的重定向地址;is4默認開啟對client的PKCE交互的支持(RequirePkce),當然首先要AllowedGrantTypes = GrantTypes.Code

      1. 畫一個登錄頁面

      2. 用戶點擊登錄按鈕時執(zhí)行:

      [HttpPost]
      [ValidateAntiForgeryToken]
      public async Task<IActionResult> Login(LoginInput model)
      {
          // 關(guān)鍵,check if we are in the context of an authorization request
          var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
      
          if (ModelState.IsValid)
          {
              //toetb isPersistent設(shè)為false,應(yīng)該跟AccessToken的有效時間不相干
              var result = await _signInManager.PasswordSignInAsync(model.PhoneNumber, model.Password, false, lockoutOnFailure: true);
              if (result.Succeeded)
              {
                  var user = await _userManager.FindByNameAsync(model.PhoneNumber);
                  await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId));
      
                  if (context != null)
                  {
                      // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                      return Redirect(model.ReturnUrl);
                  }
              }
      
              await _events.RaiseAsync(new UserLoginFailureEvent(model.Password, "invalid credentials", clientId: context?.Client.ClientId));
              ModelState.AddModelError(string.Empty, _localizer["Invalid username or password"]);
          }
      
          // something went wrong, show form with error
          return View(model);
      }
      

      其中_interaction是IIdentityServerInteractionService類型的對象,調(diào)用它的GetAuthorizationContextAsync能區(qū)分用戶操作是處于授權(quán)過程還是普通登錄。

      App

      1. LoginActivity
      class LoginActivity : BaseActivity() {
          private val vm by lazy {
              this.getViewModel(LoginViewModel::class.java)
          }
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_login)
      
              login_page.webViewClient = object : WebViewClient() {
                  override fun shouldOverrideUrlLoading(
                      view: WebView?,
                      request: WebResourceRequest?
                  ): Boolean {
                      val url = request?.url
                      if (OIDC.REDIRECT_URL.indexOf(url?.host!!) > -1) {
                          val code = url!!.getQueryParameter("code")
                          vm.getToken(code!!)
                          return true
                      }
                      return super.shouldOverrideUrlLoading(view, request)
                  }
              }
              login_page.loadUrl(vm.loginUrl)
          }
      }
      

      使用WebViewClient嵌入AAS登錄頁面,并重寫shouldOverrideUrlLoading方法,當發(fā)現(xiàn)瀏覽器redirect約定的URL時即知用戶已登錄成功,此時攔截跳轉(zhuǎn),并發(fā)起token請求。

      1. 交互的流程主要在LoginViewModel
      class LoginViewModel @Inject constructor() : ViewModel() {
          @Inject
          lateinit var authService: AuthorizationService
          var code_verifier: String = getRandomStr(
              64,
              Alphabet.PUREDIG.plus(Alphabet.LOWERCASE).plus(Alphabet.CAPITAL).plus(Alphabet.SPECIAL)
          )
          val redirectUrl = URLEncoder.encode(OIDC.REDIRECT_URL, "UTF-8")
          lateinit var code_challenge: String
          lateinit var loginUrl: String
      
          init {
              code_challenge = getChallengeCode(code_verifier)
              loginUrl = """
                  ${OIDC.AUTHORIZATION_ENDPOINT}
                  ?response_type=code&client_id=${OIDC.CLIENT_ID}&code_challenge=$code_challenge&code_challenge_method=S256
                  &redirect_uri=$redirectUrl&scope=openid%20profile%20offline_access
                  """.trimIndent()
          }
      
          fun getToken(code: String) {
              viewModelScope.launch(Dispatchers.IO) {
                  //取access_token
                  val rsp = authService.authorize(
                      mapOf(
                          "code_verifier" to code_verifier,
                          "code" to code,
                          "redirect_uri" to OIDC.REDIRECT_URL,
                          "client_id" to OIDC.CLIENT_ID,
                          "grant_type" to "authorization_code"
                      )
                  )
      
                  rsp.apply {
                      val token = Token(
                          idToken,
                          accessToken,
                          refreshToken,
                          getCurrentTimeStamp() + expiresIn,
                          scope
                      )
                      Session.storeToken(token)
                  }
              }
          }
      
          private fun getChallengeCode(code_verifier: String): String {
              val bytes = code_verifier.toByteArray(Charset.forName("US-ASCII"))
              val md = MessageDigest.getInstance("SHA-256")
              md.update(bytes, 0, bytes.size)
              val digest = md.digest()
              val challenge =
                  Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
              return challenge
          }
      
          override fun onCleared() {
              viewModelScope.coroutineContext.cancelChildren()
              super.onCleared()
          }
      }
      

      loginUrl查詢字符串各參數(shù)的意義可參看RFC文檔,重點關(guān)注code_challenge
      用戶登錄成功后,就可以用code換token了,重點關(guān)注code_verifier,AAS會拿著這個和上一步的code_challenge進行比對校驗。這里使用Retrofit2封裝了Http請求,注意獲取token時,content-type要設(shè)置為application/x-www-form-urlencoded。

          @POST(TOKEN_ENDPOINT)
          @FormUrlEncoded
          suspend fun authorize(@FieldMap maps: Map<String, String>): TokenRsp
      
      1. 獲取了token之后,就可以訪問Resource Server的資源了。在每次請求時添加請求頭如下:
      reqBuilder.addHeader(
          "Authorization",
          "Bearer ${Session.getAccessToken()}"
      )
      

      Resource Server

      依舊以ASP.NET Core為例。

      1. 添加依賴包Microsoft.AspNetCore.Authentication.JwtBearer
      2. 在Startup.ConfigureServices注冊token校驗服務(wù):
      services.AddAuthentication("Bearer")
      .AddJwtBearer("Bearer", options =>
      {
          options.Authority = "https://localhost:5001";
      });
      

      options.Authority設(shè)置為AAS地址。
      由于AAS頒發(fā)的AccessToken默認是JWT格式,所以這里我們使用JWT校驗。AccessToken還可設(shè)置為Reference Token格式,具體可參看IdentityServer4之JWT簽名(RSA加密證書)及驗簽

      1. 保證Startup.Configure中有
                  app.UseAuthentication();
                  app.UseAuthorization();
      
      1. 在Api上增加[Authorize]特性。

      That's all ! 如果想要更精細的控制,比如控制Api只開放給符合特定要求的AccessToken,那么就要聲明Policy了,如:

      services.AddAuthorization(options =>
      {
          options.AddPolicy("ApiScope", policy =>
          {
              policy.RequireAuthenticatedUser();
              policy.RequireClaim("scope", "api1");
          });
      });
      

      可查閱其它資料獲取更多信息,這里不再贅述。


      參考資料

      OAuth 2.0 for Native Apps
      Asp.Net Core 中IdentityServer4 實戰(zhàn)之 Claim詳解
      淺談SAML, OAuth, OpenID和SSO, JWT和Session

      posted @ 2020-08-28 14:15  萊布尼茨  閱讀(3436)  評論(6)    收藏  舉報
      主站蜘蛛池模板: 国内精品一区二区不卡| 国产日韩乱码精品一区二区| 日韩中文字幕一二三视频| 97久久精品亚洲中文字幕无码| 26uuu另类亚洲欧美日本| 国产69精品久久久久99尤物| 欧美人与性动交ccoo| 国产主播精品福利午夜二区| 久久精品国产99国产精品严洲| 国产精品久久久久7777按摩| 一区二区三区精品自拍视频| 天天做天天爱夜夜爽导航| 久久久久无码精品国产h动漫| 国产一区一一区高清不卡| а天堂中文最新一区二区三区| 亚洲综合无码日韩国产加勒比| 国产成人综合95精品视频| 麻豆国产成人AV在线播放| 欧美一区二区三区欧美日韩亚洲| 在线高清理伦片a| 粉嫩小泬无遮挡久久久久久| 激情亚洲内射一区二区三区| 四虎精品免费永久免费视频| 成人福利一区二区视频在线 | 亚洲国产精品一区二区三| 中文字幕在线亚洲日韩6页 | 精品人妻伦九区久久aaa片69| 日韩av在线一区二区三区| 日韩国产中文字幕精品| 中文字幕av国产精品| 国产一区二区三区在线看| 在线观看潮喷失禁大喷水无码| 亚洲中文字幕一区二区| 2019国产精品青青草原| 少妇人妻偷人免费观看| 伊人春色激情综合激情网| 在线看片免费不卡人成视频| 夜爽8888视频在线观看| 国产午夜影视大全免费观看| 亚洲 制服 丝袜 无码| 99精品国产一区二区三|