日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

开源项目葫芦藤:IdentityServer4的实现及其运用

發布時間:2023/12/4 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 开源项目葫芦藤:IdentityServer4的实现及其运用 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

本篇文章主要是講解葫蘆藤項目中對IdentityServer的實踐使用,為了使您對本篇文章中所講述的內容有深刻的認識,并且在閱讀時避免感到乏味,文中的內容不會涉及太多的基礎理論知識,而更多的是采用動手實踐的方式進行講解,所以在閱讀此篇文章前假定您已經掌握了OAuth2.0的基礎知識,如您事先并未了解OAuth2.0,請參閱一下阮一峰老師的文章《理解OAuth2.0》(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html), ASP.NET Core 認證與授權,可以看看博客?雨夜朦朧(https://www.cnblogs.com/RainingNight),另外IdentityServer的相關文章也可以參考博客?曉晨Master(https://www.cnblogs.com/stulzq/)。

葫蘆藤前端地址:https://account.suuyuu.cn (驗證碼獲取后,輸入123456即可)

葫蘆藤后端地址:https://account-web.suuyuu.cn

葫蘆藤源碼地址:https://github.com/fuluteam/fulusso (幫忙點個小星星哦)

團隊博文地址:https://www.cnblogs.com/fulu

簽名證書(Signing Credential)

IdentityServer支持X.509證書(包括原始文件和對Windows證書存儲庫的引用)、RSA密鑰和EC密鑰,用于令牌簽名和驗證。每個密鑰都可以配置一個(兼容的)簽名算法,如RS256、RS384、RS512、PS256、PS384、PS512、ES256、ES384或ES512。

通常情況下,我們使用的是針對開發場景創建的臨時證書 AddDeveloperSigningCredential,
生產環境怎么辦呢?IdentityServer還提供了AddSigningCredential用來裝載證書文件,
為此我們需要準備一個X.509證書,下面是在控制臺項目中用于生成證書的代碼,完整代碼請參考項目:https://github.com/fuluteam/ICH.BouncyCastle

//頒發者DNvar issuer = new X509Name(new ArrayList{X509Name.C,X509Name.O,X509Name.OU,X509Name.L,X509Name.ST},new Hashtable{[X509Name.C] = "CN",[X509Name.O] = "Fulu Newwork",[X509Name.OU] = "Fulu RSA CA 2020",[X509Name.L] = "Wuhan",[X509Name.ST] = "Hubei"});//使用者DNvar subject = new X509Name(new ArrayList{X509Name.C,X509Name.O,X509Name.CN}, new Hashtable {[X509Name.C] = "CN",[X509Name.O] = "ICH",[X509Name.CN] = "*.fulu.com"});//生成證書文件CertificateGenerator.GenerateCertificate(newCertificateGenerator.GenerateCertificateOptions { Path = "mypfx.pfx",Issuer = issuer, Subject = subject });

執行代碼后,在項目編譯輸出目錄中,會看到一個mypfx.pfx的文件,此時我們的證書就創建成功啦。

接著怎么使用呢,看下面代碼:

var certificate2 = new X509Certificate2("mypfx.pfx", "password", X509KeyStorageFlags.Exportable);identityServerBuilder.AddSigningCredential(certificate2);

大家可能會問,葫蘆藤中怎么不是這么寫的呢,其實葫蘆藤項目中是將證書文件的流數據轉成了二進制字符串,這樣就可以寫在配置文件中了:

using (var fs = new FileStream(options.Path, FileMode.Open)){var bytes = new byte[fs.Length];fs.Read(bytes, 0, bytes.Length);var pfxHexString = Hex.ToHexString(bytes);}

然后在這么使用:

identityServerBuilder.AddSigningCredential(new X509Certificate2(Hex.Decode(appSettings.X509RawCertData), appSettings.X509CertPwd));

客戶端存儲(Client Store)

在葫蘆藤項目中,我們創建了一個ClientStore類,繼承自接口IClientStore,實現其方法代碼如下:

public class ClientStore : IClientStore{private readonly IClientCacheStrategy _clientInCacheRepository;public ClientStore(IClientCacheStrategy clientInCacheRepository){_clientInCacheRepository = clientInCacheRepository;}public async Task<Client> FindClientByIdAsync(string clientId){var clientEntity = await _clientInCacheRepository.GetClientByIdAsync(clientId.ToInt32());if (clientEntity == null){return null;}return new Client{ClientId = clientId,AllowedScopes = new[] { "api", "get_user_info" },ClientSecrets = new[] { new Secret(clientEntity.ClientSecret.Sha256()) },AllowedGrantTypes = new[]{GrantType.AuthorizationCode, //授權碼模式GrantType.ClientCredentials, //客戶端模式GrantType.ResourceOwnerPassword, //密碼模式CustomGrantType.External, //自定義模式——三方(移動端)模式CustomGrantType.Sms //自定義——短信模式},AllowOfflineAccess = false,RedirectUris = string.IsNullOrWhiteSpace(clientEntity.RedirectUri) ? null : clientEntity.RedirectUri.Split(';'),RequireConsent = false,AccessTokenType = AccessTokenType.Jwt,AccessTokenLifetime = 7200,ClientClaimsPrefix = "",Claims = new[] { new Claim(JwtClaimTypes.Role, "Client") }};}}

通過代碼可以看到,通過clientId從緩存中讀取Client的相關信息構建并返回,這里我們為所有的Client簡單的設置了統一的AllowedGrantTypes,這是一種偷懶的做法,應當按需授予GrantType,例如通常情況下我們只應默認給應用分配AuthorizationCode或者ClientCredentials,ResourceOwnerPassword需要謹慎授予(需要用戶對Client高度信任)。

資源存儲(Resource Store)

由于歷史原因,在葫蘆藤中,我們并沒有通過IdentityServer對api資源進行訪問保護(后續會提供我們的實現方式),我們為所有Client設置了相同的Scope。

持久化授權存儲(Persisted Grant Store)

葫蘆藤中,我們使用了Redis來持久化數據,

通過EntityFramework Core持久化配置和操作數據,請參考
https://www.cnblogs.com/stulzq/p/8120518.html
https://github.com/IdentityServer/IdentityServer4.EntityFramework

IPersistedGrantStore接口中定義了如下6個方法:

/// <summary>Interface for persisting any type of grant.</summary>public interface IPersistedGrantStore{/// <summary>Stores the grant.</summary>/// <param name="grant">The grant.</param>/// <returns></returns>Task StoreAsync(PersistedGrant grant);/// <summary>Gets the grant.</summary>/// <param name="key">The key.</param>/// <returns></returns>Task<PersistedGrant> GetAsync(string key);/// <summary>Gets all grants for a given subject id.</summary>/// <param name="subjectId">The subject identifier.</param>/// <returns></returns>Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId);/// <summary>Removes the grant by key.</summary>/// <param name="key">The key.</param>/// <returns></returns>Task RemoveAsync(string key);/// <summary>/// Removes all grants for a given subject id and client id combination./// </summary>/// <param name="subjectId">The subject identifier.</param>/// <param name="clientId">The client identifier.</param>/// <returns></returns>Task RemoveAllAsync(string subjectId, string clientId);/// <summary>/// Removes all grants of a give type for a given subject id and client id combination./// </summary>/// <param name="subjectId">The subject identifier.</param>/// <param name="clientId">The client identifier.</param>/// <param name="type">The type.</param>/// <returns></returns>Task RemoveAllAsync(string subjectId, string clientId, string type);}

PersistedGrant的結構如下:

/// <summary>A model for a persisted grant</summary>public class PersistedGrant{/// <summary>Gets or sets the key.</summary>/// <value>The key.</value>public string Key { get; set; }/// <summary>Gets the type.</summary>/// <value>The type.</value>public string Type { get; set; }/// <summary>Gets the subject identifier.</summary>/// <value>The subject identifier.</value>public string SubjectId { get; set; }/// <summary>Gets the client identifier.</summary>/// <value>The client identifier.</value>public string ClientId { get; set; }/// <summary>Gets or sets the creation time.</summary>/// <value>The creation time.</value>public DateTime CreationTime { get; set; }/// <summary>Gets or sets the expiration.</summary>/// <value>The expiration.</value>public DateTime? Expiration { get; set; }/// <summary>Gets or sets the data.</summary>/// <value>The data.</value>public string Data { get; set; }}

可以看出主要是針對PersistedGrant對象的操作,通過觀察GetAsync和RemoveAsync方法的入參均為key,我們在StoreAsync中將PersistedGrant中的Key作為緩存key,將PersistedGrant對象以hash的方式存入緩存中,并設置過期時間(注意將UTC時間轉換為本地時間)

public async Task StoreAsync(PersistedGrant grant){//var expiresIn = grant.Expiration - DateTimeOffset.UtcNow;var db = await _redisCache.GetDatabaseAsync();var trans = db.CreateTransaction();var expiry = grant.Expiration.Value.ToLocalTime();db.HashSetAsync(grant.Key, GetHashEntries(grant)); //GetHashEntries是將對象PersistedGrant轉換為HashEntry數組db.KeyExpireAsync(grant.Key, expiry);await trans.ExecuteAsync();}

同時,把GetAsync和RemoveAsync的代碼填上:

public async Task<PersistedGrant> GetAsync(string key){var db = await _redisCache.GetDatabaseAsync();var items = await db.HashGetAllAsync(key);return GetPersistedGrant(items); //將HashEntry數組轉換為PersistedGrant對象}public async Task RemoveAsync(string key){var db = await _redisCache.GetDatabaseAsync();await db.KeyDeleteAsync(key);}

接著,GetAllAsync方法,通過subjectId查詢PersistedGrant集合,1對n,因此,我們在StoreAsync中補上這一層關系,以subjectId為緩存key,grant.Key為緩存值存入list集合中;GetAllAsync方法中,通過subjectId取出grant.Key的集合,最終得到PersistedGrant集合。

public async Task StoreAsync(PersistedGrant grant){//var expiresIn = grant.Expiration - DateTimeOffset.UtcNow;var db = await _redisCache.GetDatabaseAsync();var trans = db.CreateTransaction();var expiry = grant.Expiration.Value.ToLocalTime();db.HashSetAsync(grant.Key, GetHashEntries(grant)); //GetHashEntries是將對象PersistedGrant轉換為HashEntry數組db.KeyExpireAsync(grant.Key, expiry);db.ListLeftPushAsync(grant.SubjectId, grant.Key);db.KeyExpireAsync(grant.SubjectId, expiry);await trans.ExecuteAsync();}public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId){if (string.IsNullOrWhiteSpace(subjectId))return new List<PersistedGrant>();var db = await _redisCache.GetDatabaseAsync();var keys = await db.ListRangeAsync(subjectId);var list = new List<PersistedGrant>();foreach (string key in keys){var items = await db.HashGetAllAsync(key);list.Add(GetPersistedGrant(items));}return list;}

類似的,StoreAsync方法中我們只需StoreAsync方法中根據RemoveAllAsync方法參數組裝緩存key,grant.Key為緩存值寫入緩存,對應的RemoveAllAsync中根據參數組裝的key查詢出grant.Key集合,刪除緩存即可。

public async Task StoreAsync(PersistedGrant grant){var db = await _redisCache.GetDatabaseAsync();var trans = db.CreateTransaction();var expiry = grant.Expiration.Value.ToLocalTime();db.HashSetAsync(grant.Key, GetHashEntries(grant));db.KeyExpireAsync(grant.Key, expiry);if (!string.IsNullOrEmpty(grant.SubjectId)){db.ListLeftPushAsync(grant.SubjectId, grant.Key);db.KeyExpireAsync(grant.SubjectId, expiry);var key1 = $"{grant.SubjectId}:{grant.ClientId}";db.ListLeftPushAsync(key1, grant.Key);db.KeyExpireAsync(key1, expiry);var key2 = $"{grant.SubjectId}:{grant.ClientId}:{grant.Type}";db.ListLeftPushAsync(key2, grant.Key);db.KeyExpireAsync(key2, expiry);}await trans.ExecuteAsync();}public async Task RemoveAllAsync(string subjectId, string clientId){if (string.IsNullOrEmpty(subjectId) || string.IsNullOrEmpty(clientId))return;var db = await _redisCache.GetDatabaseAsync();var key = $"{subjectId}:{clientId}";var keys = await db.ListRangeAsync(key);if (!keys.Any()) return;var trans = db.CreateTransaction();db.KeyDeleteAsync(keys.ToRedisKeys());db.KeyDeleteAsync(key);await trans.ExecuteAsync();}public async Task RemoveAllAsync(string subjectId, string clientId, string type){if (string.IsNullOrEmpty(subjectId) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(type)) return;var db = await _redisCache.GetDatabaseAsync();var key = $"{subjectId}:{clientId}:{type}";var keys = await db.ListRangeAsync(key);if (!keys.Any()) return;var trans = db.CreateTransaction();db.KeyDeleteAsync(keys.ToRedisKeys());db.KeyDeleteAsync(key);await trans.ExecuteAsync();}

至此,持久化的代碼填寫完畢;啟動并調試項目,可以看到PersistedGrant對象如下:

資源擁有者驗證器(Resource Owner Validator)

如果要使用OAuth 2.0 密碼模式(Resource Owner Password Credentials Grant),則需要實現并注冊IResourceOwnerPasswordValidator接口:

public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context){var result = await _userService.LoginByPasswordAsync(context.UserName, context.Password);if (result.Code == 0){var claims = await _userService.SaveSuccessLoginInfo(context.Request.ClientId.ToInt32(), result.Data.Id,_contextAccessor.HttpContext.GetIp(), UserLoginModel.Password);context.Result = new GrantValidationResult(result.Data.Id, OidcConstants.AuthenticationMethods.Password, claims);}else{context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, result.Message);}}

重定向地址驗證器(Redirect Uri Validator)

用于驗證重定向(授權碼模式)和注銷后重定向Uri的校驗,葫蘆藤項目中重定向地址驗證只驗證域名(不驗證完整的requestedUri地址),且未進行注銷重定向Uri的校驗。

public class RedirectUriValidator : IRedirectUriValidator{public Task<bool> IsRedirectUriValidAsync(string requestedUri, Client client){if (client.RedirectUris == null || !client.RedirectUris.Any()){return Task.FromResult(false);}var uri = new Uri(requestedUri);return Task.FromResult(client.RedirectUris.Any(x => x.Contains(uri.Host)));}public Task<bool> IsPostLogoutRedirectUriValidAsync(string requestedUri, Client client){return Task.FromResult(true);}}

擴展授權驗證器(Extension Grant Validator)

在IdentityServer4中,通過實現IExtensionGrantValidator接口,可以實現自定義授權。在葫蘆藤項目中,我們有兩個場景需要用到自定義授權:

  • 通過第三方(QQ、微信)的用戶標識(OpenId)進行登錄(頒發用戶令牌)

  • 通過短信驗證碼進行登錄(頒發用戶令牌)

在IdentityServer4中實現短信驗證碼授權模式,我們創建了一個SmsGrantValidator類,繼承自IExtensionGrantValidator接口,然后給屬性GrantType取一個名字,此處名稱為“sms”,實現ValidateAsync方法,方法內進行入參校驗,然后驗證短信驗證碼,驗證通過后取出用戶信息,下面代碼中,當用戶不存在時也可以自動注冊。代碼如下:

public class SmsGrantValidator : IExtensionGrantValidator{private readonly IHttpContextAccessor _contextAccessor;private readonly IValidationComponent _validationComponent;private readonly IUserService _userService;public SmsGrantValidator(IHttpContextAccessor contextAccessor, IValidationComponent validationComponent, IUserService userService){_contextAccessor = contextAccessor;_validationComponent = validationComponent;_userService = userService;GrantType = CustomGrantType.Sms;}public async Task ValidateAsync(ExtensionGrantValidationContext context){var phone = context.Request.Raw.Get("phone");var code = context.Request.Raw.Get("code");if (string.IsNullOrEmpty(phone) || Regex.IsMatch(phone, RegExp.PhoneNumber) == false){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "phone is not valid");return;}if (string.IsNullOrEmpty(code)){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "code is not valid");return;}try{var validSms = await _validationComponent.ValidSmsAsync(phone, code);if (!validSms.Data){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, validSms.Message);return;}var userEntity = await _userService.GetUserByPhoneAsync(phone);if (userEntity == null){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "用戶不存在或未注冊");return;}if (userEntity.Enabled == false){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "您的賬號已被禁止登錄");return;}await _userService.SaveSuccessLoginInfo(context.Request.ClientId.ToInt32(), userEntity.Id, _contextAccessor.HttpContext.GetIp(),UserLoginModel.SmsCode);}catch (Exception ex){context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, ex.Message);}}public string GrantType { get; }}

OAuth2.0的實踐運用場景

基于角色的授權(role-based authorization)

基于角色的授權檢查是聲明性的,開發人員將其嵌入到代碼中、控制器或控制器內的操作,指定當前用戶必須是其成員的角色才能訪問請求的資源,文檔參考《ASP.NET Core 中的基于角色的授權》。

葫蘆藤中定義了兩種角色Claim(聲明),客戶端和用戶,使用客戶端授權模式(client credentials)頒發的令牌,ClaimRole為Client,使用授權碼模式(authorization code)、密碼模式(resource owner password credentials)、自定義授權模式(短信、第三方)頒發的用戶令牌,ClaimRole為User

public static class ClaimRoles{/// <summary>/// 客戶端/// </summary>public const string Client = "Client";/// <summary>/// 用戶/// </summary>public const string User = "User";}

在ClientStore中增加返回Client的Claims,JwtClaimTypes.Role為ClaimRoles.Client,下面是客戶端令牌,可以看到 “role”:”Client”

{"alg":"RS256","kid":"99AA0C1236097972F29789562761D38AAE301918","typ":"JWT","x5t":"maoMEjYJeXLyl4lWJ2HTiq4wGRg"}{"nbf":1608522625,"exp":1608529825,"iss":"http://localhost:80","aud":"api","client_id":"10000001","role":"Client","scope":["api","get_user_info"]}

在用戶登錄成功后返回的Claims中增加JwtClaimTypes.Role為ClaimRoles.User,下面是用戶令牌,可以看到 “role”:”User”

{"alg":"RS256","kid":"99AA0C1236097972F29789562761D38AAE301918","typ":"JWT","x5t":"maoMEjYJeXLyl4lWJ2HTiq4wGRg"}{"nbf":1608522576,"exp":1608529776,"iss":"http://localhost:80","aud":"api","client_id":"10000001","sub":"df09efff-0074-4dca-91c3-e38180c5e4ac","auth_time":1608522576,"idp":"local","id":"df09efff-0074-4dca-91c3-e38180c5e4ac","open_id":"07E8E30B56D256EF8C440019AB6AAA89","name":"1051dfd1-73e5-4e6f-9326-3423bc9b71a3","nickname":"laowang","phone_number":"18627131390","email":"","role":"User","login_ip":"0.0.0.1","login_address":"保留地址","last_login_ip":"0.0.0.1","last_login_address":"保留地址","scope":["api","get_user_info"],"amr":["pwd","mfa"]}

在項目Fulu.Passport.API的Startup文件中,添加對組件Fulu.Service.Authorize的服務注入

services.AddServiceAuthorize(o =>...代碼省略...);services.AddAuthentication(x =>...代碼省略...).AddJwtBearer(o =>{...代碼省略...o.TokenValidationParameters = new TokenValidationParameters{NameClaimType = JwtClaimTypes.Name,RoleClaimType = ClaimTypes.Role, //注意,這里不能使用JwtClaimTypes.Role...代碼省略...}}

接著,只需在Controller或Action上指定屬性即可

[Route("api/[controller]/[action]")][ApiController][Authorize(Roles = ClaimRoles.Client)]public class ClientController : ControllerBase{...省略部分代碼.../// <summary>/// 獲取應用列表/// </summary>/// <returns></returns>[HttpGet][ProducesResponseType(typeof(ActionObjectResult<List<ClientEntity>, Statistic>), 200)]public async Task<IActionResult> GetClients(){var clients = await _clientRepository.TableNoTracking.Where(c => c.Enabled).ToListAsync();return ObjectResponse.Ok(clients);}...省略部分代碼...

客戶端授權模式(client credentials)

通過客戶端授權模式頒發的令牌,可以實現對服務資源進行保護。步驟如下:

(A)客戶端10000001向葫后進行身份認證,并要求一個訪問令牌。(B)葫后驗證客戶端身份后,向客戶端10000001提供訪問令牌。

A步驟中,客戶端10000001發出的HTTP請求,包含以下參數:

  • grant_type:表示授權類型,此處的值固定為”clientcredentials”,必選項。

  • client_id:表示客戶端的ID,必選項。

  • client_secret:表示客戶端密鑰,必選項。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: www.xxx.comContent-Type: application/x-www-form-urlencodedgrant_type=client_credentials&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w

B步驟中,葫蘆藤向客戶端10000001發放令牌,下面是一個例子。

HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-store, no-cache, max-age=0Pragma: no-cache{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk5QUEwQzEyMzYwOTc5NzJGMjk3ODk1NjI3NjFEMzhBQUUzMDE5MTgiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJtYW9NRWpZSmVYTHlsNGxXSjJIVGlxNHdHUmcifQ.eyJuYmYiOjE2MDc0MTQ2MjUsImV4cCI6MTYwNzQyMTgyNSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwicm9sZSI6IkNsaWVudCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkNsaWVudCIsInNjb3BlIjpbImFwaSIsImdldF91c2VyX2luZm8iXX0.ilu1qMxDiXVxsqU6aO-xuyYaLvvj2mxONjYkXtpMs46K7O3_Qc5VsY0ZZaYPoLROAqPulxsWWpxjEiQd10OdRh4IziGAcpYfAfoD80CZxrcuWrWloB5aWncv_PMZcjzKw7Vt3G3g-WkJl4amTta498hZJ3B-N-ReLhl-3ICSMFU8PU_ZVtEB-2lRx93rVyPIaQu_DWmpyW4Bdf2ocYm4RPQAEsvBToEFObbWPG6paLWIjrSN2aQPvsRWziorvlIhyFV5L6oyFIGIrZxdLJTOsvRQaevpV1sbv9pD_Z9PZDbSQiQDbWQv0MfrYB0Npc6VQlIMkL2GPNlQ8NgwyGT1sQ","expires_in": 7200,"token_type": "Bearer","scope": "api get_user_info"}

授權碼模式(authorization code)

葫蘆藤項目通過授權碼模式(authorization code)實現了單點登錄,通過授權碼模式拿到用戶令牌。目前葫蘆藤只有一個應用(葫蘆藤安全中心),這里為了不把概念搞混淆,我們假定百度(客戶端10000002,redirect_uri 為 http://www.baidu.com)接入了咱們的授權體系,當然,百度的前端肯定沒有寫如何構造請求步驟的邏輯代碼,因此,我們下面通過人工模擬請求步驟。

名詞定義

  • 葫蘆藤的client_id是10000001,百度的client_id是10000002

  • 葫蘆藤前端服務,簡稱“葫前”(https://account.suuyuu.cn)

  • 葫蘆藤后端服務,簡稱“葫后”(https://account-web.suuyuu.cn)

  • 百度前端服務,簡稱“百前”(https://www.baidu.com)

  • 百度后端服務,簡稱“百后”(假定地址為?https://api.baidu.com)

(A)用戶訪問“百前”,“百前”將用戶導向“葫后”。 (B)“葫后”檢查用戶是否需要登錄(是否攜帶了有效的登錄Cookie),如需登錄跳轉到“葫前”。 (C)用戶登錄后,“葫后”將用戶導向百度事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。 (D)“百前”收到授權碼,附上早先的"重定向URI",向“百后”申請令牌,“百后”拿到授權碼之后攜帶密鑰client_secret向“葫后”申請令牌。 (E)“葫后”核對了授權碼和重定向URI,確認無誤后,向“百后”頒發訪問令牌(access token)。 (F)“百后”將令牌返回給“百前”。

A步驟中,構造的請求地址包含以下參數:

  • response_type:表示授權類型,必選項,此處的值固定為”code”

  • client_id:表示客戶端的ID,必選項

  • redirect_uri:表示重定向URI,可選項

  • scope:表示申請的權限范圍,可選項

  • state:表示客戶端的當前狀態,可以指定任意值,認證服務器會原封不動地返回這個值

步驟A中開發人員需向前端人員提供client_id,即上面的client_id,下面是一個例子。

構造如下地址,復制到瀏覽器地址欄中并回車,如果跳轉到登錄頁,請進行登錄。

https://account-web.suuyuu.cn/connect/authorize?client_id=10000002&redirect_uri=https%3A%2F%2Fwww.baidu.com&response_type=code&scope=api&state=STATE

登錄后會重定向redirect_uri到如下地址:

https://www.baidu.com/?code=1MlxrvXuD7TfH-s4dLzcw9ymO0SKDbf5xAlh3ZEHlMo&scope=api&state=STATE

D步驟中,我們通過臨時授權碼向“葫后”索取令牌,包含以下參數:

  • grant_type:表示使用的授權模式,必選項,此處的值固定為”authorization_code”。

  • code:表示上一步獲得的授權碼,必選項。

  • redirect_uri:表示重定向URI,必選項,且必須與A步驟中的該參數值保持一致。

  • client_id:表示應用ID,必選項。

  • client_secret:表示應用密鑰,必選項。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: account-web.suuyuu.cnContent-Type: application/x-www-form-urlencodedgrant_type=authorization_code&code=1MlxrvXuD7TfH-s4dLzcw9ymO0SKDbf5xAlh3ZEHlMo&redirect_uri=https%3A%2F%2Fwww.baidu.com&client_id=10000002&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwQzQ3OUY1QUIyQTFERjM2QzE0MkNEQjQ3NjQ1QkEwMzQ1MTg1NUEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjTVI1OWFzcUhmTnNGQ3piUjJSYm9EUlJoVm8ifQ.eyJuYmYiOjE2MDc0MjY0MjcsImV4cCI6MTYwNzQzMzYyNywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwiYXV0aF90aW1lIjoxNjA3NDI2MTk2LCJpZHAiOiJsb2NhbCIsImlkIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwib3Blbl9pZCI6IjA3RThFMzBCNTZEMjU2RUY4QzQ0MDAxOUFCNkFBQTg5IiwibmFtZSI6IjEwNTFkZmQxLTczZTUtNGU2Zi05MzI2LTM0MjNiYzliNzFhMyIsIm5pY2tuYW1lIjoibGFvd2FuZyIsInBob25lX251bWJlciI6IjE4NjI3MTMxMzkwIiwiZW1haWwiOiIiLCJyb2xlIjoiVXNlciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJsb2dpbl9pcCI6IjExMy41Ny4xMTguNTEiLCJsb2dpbl9hZGRyZXNzIjoi5rmW5YyX55yB5q2m5rGJ5biCIiwibGFzdF9sb2dpbl9pcCI6IjExMy41Ny4xMTguNTEiLCJsYXN0X2xvZ2luX2FkZHJlc3MiOiLmuZbljJfnnIHmrabmsYnluIIiLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbIm1mYSJdfQ.ElnHr5Niknq7kzGL8iv1TH0F6NQ21yPrswzSTIZuvetUxztYgQpD-RfgBW2HL6b_rRyQxFjE23gU4lBIEayM8k3M9_sUzZq8E_dFT8LwpsU76-CxepxHft4hn1YG0a5C6QRyjFQoSFVUZXIp663Es7vwRQ6PgsfkHZKXxAqXL-obHj_QLbv6OeciTIRGwYrL9-1_SDQ4esFR2n8LkGGOug55j9QuQEKMCufQLJ-nB3y7A2-0mnNoiuF2BBYSPLamcvMcLe8LbhCITLrHkcUSc6tsSdnEeisS6BMIoiyRq-LR2jJwDD30swTPFd85v6kUBJ3ZnWjeCqsluGGKHrwDLA","expires_in":7200,"token_type":"Bearer","scope":"api"}

密碼模式(resource owner password credentials)

密碼模式主要用于給可信應用頒發用戶令牌,此類應用有個性化的登錄頁(不依賴單點登錄,葫蘆藤的登錄頁面),如app、小程序、h5等。

  • grant_type:表示授權類型,此處的值固定為”password”,必選項。

  • client_id:表示客戶端的ID,必選項。

  • client_secret:表示客戶端密鑰,必選項。

  • username:用戶名,必選項。

  • password:密碼,必選項。(基于密碼原文的rsa加密串)

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: account-web.suuyuu.cnContent-Type: application/x-www-form-urlencodedgrant_type=password&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w&username=18627131390&password=0200f6389afbcbc624811785c9fbbf5c1b6d7b53b1315a1a43021c0733323fab7625bb9e6594cd30758fa700798421bc189dc223bf696d2438530ffab337809b96bb47ee38f3416bf4b57222050d5f4ad66ee052598ea62ff5ec6f991729956cb692f6f48b758564a46aeff86208581cad9063d3ccd71b551fa4b4b4b983fc1a{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwQzQ3OUY1QUIyQTFERjM2QzE0MkNEQjQ3NjQ1QkEwMzQ1MTg1NUEiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjTVI1OWFzcUhmTnNGQ3piUjJSYm9EUlJoVm8ifQ.eyJuYmYiOjE2MDc1MTE2NTEsImV4cCI6MTYwNzUxODg1MSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwiYXV0aF90aW1lIjoxNjA3NTExNjUxLCJpZHAiOiJsb2NhbCIsImlkIjoiZGYwOWVmZmYtMDA3NC00ZGNhLTkxYzMtZTM4MTgwYzVlNGFjIiwib3Blbl9pZCI6IjA3RThFMzBCNTZEMjU2RUY4QzQ0MDAxOUFCNkFBQTg5IiwibmFtZSI6IjEwNTFkZmQxLTczZTUtNGU2Zi05MzI2LTM0MjNiYzliNzFhMyIsIm5pY2tuYW1lIjoibGFvd2FuZyIsInBob25lX251bWJlciI6IjE4NjI3MTMxMzkwIiwiZW1haWwiOiIiLCJyb2xlIjoiVXNlciIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJsb2dpbl9pcCI6IjExMy41Ny4xMTguNjEiLCJsb2dpbl9hZGRyZXNzIjoi5rmW5YyX55yB5q2m5rGJ5biCIiwibGFzdF9sb2dpbl9pcCI6IjExMy41Ny4xMTguNjEiLCJsYXN0X2xvZ2luX2FkZHJlc3MiOiLmuZbljJfnnIHmrabmsYnluIIiLCJzY29wZSI6WyJhcGkiLCJnZXRfdXNlcl9pbmZvIl0sImFtciI6WyJwd2QiLCJtZmEiXX0.d3qvhX6KSdm5EgWpUzbjJX2bB1OiUo-285nZ1qsGKpqTQJUH1VHQoJogB0NI-uVYdgIV-y3CMBhFY_fDYQJto43zDf0gDvYxa2eWnX5MWL7Augigi59Icp0YvNDCGd2iT5ztAWpxk1Jww815TtCFtFFGiQfQC75bKLrTW9QvdXr8t4VHcFKGmz92m8g3WL-0eWqAyvk0YuSBvxOd8P8zoocEiiOgVKTSylphSIQxuC8B4MFNf2DoFWDQjNZmDCs7PLh7sniMmLdfilo7T7gAlq9qjUrmQmav4wbDMT8WZqa01WY-LsWq6mZUnbCytgSu7Xrr90b6LAEGn-hxdQ5VHg","expires_in": 7200,"token_type": "Bearer","scope": "api get_user_info"}

自定義授權模式(短信、第三方)(extension grant)

客戶端通過用戶手機號短信驗證碼或第三方用戶(QQ、WeChat)的用戶唯一標識(OpenId)向認證服務器索要用戶令牌。

以短信驗證碼方式為例,我們定義的流程如下:

用戶向客戶端提供自己的手機號和短信驗證碼。客戶端使用這些信息,向認證服務器索要授權。步驟如下:

(A)用戶向客戶端提供手機號和短信驗證碼。

(B)客戶端將手機號和短信碼發給認證服務器,向后者請求令牌。

(C)認證服務器確認無誤后,向客戶端提供用戶令牌。

B步驟中,客戶端發出的HTTP請求,包含以下參數:

  • grant_type:表示授權類型,此處的值固定為”sms”,必選項。

  • client_id:表示客戶端的ID,必選項。

  • client_secret:表示客戶端的密鑰,必選項。

  • phone:表示手機號,必選項。

  • code:表示短信驗證碼,必選項。

下面是一個請求示例。

POST https://account-web.suuyuu.cn/oauth/token HTTP/1.1Host: account-web.suuyuu.cnContent-Type: application/x-www-form-urlencodedgrant_type=sms&phone=18627131390&code=123456&client_id=10000001&client_secret=14p9ao1gxu4q3sp8ogk8bq4gkct59t9w{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk5QUEwQzEyMzYwOTc5NzJGMjk3ODk1NjI3NjFEMzhBQUUzMDE5MTgiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJtYW9NRWpZSmVYTHlsNGxXSjJIVGlxNHdHUmcifQ.eyJuYmYiOjE2MDczOTU4NTIsImV4cCI6MTYwNzQwMzA1MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MCIsImF1ZCI6ImFwaSIsImNsaWVudF9pZCI6IjEwMDAwMDAxIiwic3ViIjoiMTg2MjcxMzEzOTAiLCJhdXRoX3RpbWUiOjE2MDczOTU4NTIsImlkcCI6ImxvY2FsIiwiaWQiOiJkZjA5ZWZmZi0wMDc0LTRkY2EtOTFjMy1lMzgxODBjNWU0YWMiLCJvcGVuX2lkIjoiMDdFOEUzMEI1NkQyNTZFRjhDNDQwMDE5QUI2QUFBODkiLCJuYW1lIjoiMTA1MWRmZDEtNzNlNS00ZTZmLTkzMjYtMzQyM2JjOWI3MWEzIiwibmlja25hbWUiOiJsYW93YW5nIiwicGhvbmVfbnVtYmVyIjoiMTg2MjcxMzEzOTAiLCJlbWFpbCI6IiIsInJvbGUiOiJVc2VyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiVXNlciIsImxvZ2luX2lwIjoiMC4wLjAuMSIsImxvZ2luX2FkZHJlc3MiOiLkv53nlZnlnLDlnYAiLCJsYXN0X2xvZ2luX2lwIjoiMC4wLjAuMSIsImxhc3RfbG9naW5fYWRkcmVzcyI6IuS_neeVmeWcsOWdgCIsInNjb3BlIjpbImFwaSIsImdldF91c2VyX2luZm8iXSwiYW1yIjpbInBhc3N3b3JkIiwibWZhIl19.ZQklMJMXObc3vL-gMOWnWIS56ck5_XbDfXjw9Vm6BeYjG4dyz05JTN_YHgU-EIJoM04nmFyjNgGYtqL-28-3MQeHfWhvQf_5dyY1w-DBBCKo1EMEm_ujKTDB1QQTN1XmVTgW7bBkEiv4NK5v3uYqh_s7pv8Csusm4oWZThWPlKLtxWVDtawFzvz4Un-2WATytsLNfluutiLVnpN7INhkdglansTTOCUOdCOLBEEbDzTuLyCnhm00xYtg5GrMAkDohqXLKYD2jSFzIyYTA_oryTFXcJpkGYwIRqRX7bXvAlMR5yE_CTtNWpSnaLJ2GtFv_QFe-YItCtSO-bBd6XQBRA","expires_in": 7200,"token_type": "Bearer","scope": "api get_user_info"}

第三方授權登錄的編寫與使用

在葫蘆藤項目中我們提供了釘釘、微信的OAuth組件,并實現了功能,演示地址在 https://account.suuyuu.cn,下面我們以微信為例簡單介紹下如何編寫組件及使用。

首先咱們閱讀一下網站應用微信登錄開發指南,了解一下接入流程。要使用微信登錄,先得在微信·開放平臺注冊成為開發者,并進行資質認證。

微信開放平臺帳號的開發者資質認證提供更安全、更嚴格的真實性認證、也能夠更好的保護企業及用戶的合法權益開發者資質認證通過后,微信開放平臺帳號下的應用,將獲得微信登錄、智能接口、第三方平臺開發等高級能力審核費用:中國大陸地區:300元,非中國大陸地區:99美元

然后在管理中心創建網站應用

對照微信開發指南將需要用到的地址定義到WeChatDefaults.cs中

public static class WeChatDefaults{public const string AuthenticationScheme = "wechat";public static readonly string DisplayName = "wechat";//第一步:請求CODEpublic static readonly string AuthorizationEndpoint = "https://open.weixin.qq.com/connect/qrconnect";//第二步:通過code獲取access_tokenpublic static readonly string TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token";//第三步:獲取用戶個人信息public static readonly string UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo";}

此處唯一要注意的地方,ClaimActions集合的參數來自微信返回的字段

public class WeChatOptions : OAuthOptions{/// <summary>/// Initializes a new <see cref="WeChatOptions"/>./// </summary>public WeChatOptions(){CallbackPath = new PathString("/signin-wechat");AuthorizationEndpoint = WeChatDefaults.AuthorizationEndpoint;TokenEndpoint = WeChatDefaults.TokenEndpoint;UserInformationEndpoint = WeChatDefaults.UserInformationEndpoint;Scope.Add("snsapi_login");ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid");ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname");}/// <summary>/// access_type. Set to 'offline' to request a refresh token./// </summary>public string AccessType { get; set; }}public static class WeChatExtensions{public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder)=> builder.AddWeChat(WeChatDefaults.AuthenticationScheme, _ => { });public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, Action<WeChatOptions> configureOptions)=> builder.AddWeChat(WeChatDefaults.AuthenticationScheme, configureOptions);public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, string authenticationScheme, Action<WeChatOptions> configureOptions)=> builder.AddWeChat(authenticationScheme, WeChatDefaults.DisplayName, configureOptions);public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WeChatOptions> configureOptions)=> builder.AddOAuth<WeChatOptions, WeChatHandler>(authenticationScheme, displayName, configureOptions);}

新增一個類WeChatHandler,繼承自OAuthHandler

BuildChallengeUrl(構造客戶端申請認證的URI)

protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri){var state = Options.StateDataFormat.Protect(properties);var baseUri = $"{Request.Scheme}{Uri.SchemeDelimiter}{Request.Host}{Request.PathBase}";var currentUri = $"{baseUri}{Request.Path}{Request.QueryString}";if (string.IsNullOrEmpty(properties.RedirectUri)){properties.RedirectUri = currentUri;}var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase){{"response_type", "code"},{"appid", Uri.EscapeDataString(Options.ClientId)},{"redirect_uri", redirectUri},{"state", Uri.EscapeDataString(state)}};var scope = string.Join(",", Options.Scope);queryStrings.Add("scope", Uri.EscapeDataString(scope));var authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);return authorizationEndpoint;}

HandleRemoteAuthenticateAsync(向認證服務器申請令牌獲取用戶信息并創建票據)

protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync(){var state = Request.Query["state"];var properties = Options.StateDataFormat.Unprotect(state);if (properties == null)return HandleRequestResult.Fail("The oauth state was missing or invalid.");if (!ValidateCorrelationId(properties))return HandleRequestResult.Fail("Correlation failed.", properties);var code = Request.Query["code"];if (StringValues.IsNullOrEmpty(code))return HandleRequestResult.Fail("Code was not found.", properties);var redirectUri = !string.IsNullOrEmpty(Options.CallbackPath) ?Options.CallbackPath.Value : BuildRedirectUri(Options.CallbackPath);var context = new OAuthCodeExchangeContext(properties, code, redirectUri);var tokens = await ExchangeCodeAsync(context);if (tokens.Error != null)return HandleRequestResult.Fail(tokens.Error, properties);if (string.IsNullOrEmpty(tokens.AccessToken))return HandleRequestResult.Fail("Failed to retrieve access token.", properties);var identity = new ClaimsIdentity(ClaimsIssuer);if (Options.SaveTokens){var authenticationTokenList = new List<AuthenticationToken>{new AuthenticationToken{Name = "access_token",Value = tokens.AccessToken}};if (!string.IsNullOrEmpty(tokens.RefreshToken)){authenticationTokenList.Add(new AuthenticationToken{Name = "refresh_token",Value = tokens.RefreshToken});}if (!string.IsNullOrEmpty(tokens.TokenType)){authenticationTokenList.Add(new AuthenticationToken{Name = "token_type",Value = tokens.TokenType});}if (!string.IsNullOrEmpty(tokens.ExpiresIn) && int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)){var dateTimeOffset = Clock.UtcNow + TimeSpan.FromSeconds(result);authenticationTokenList.Add(new AuthenticationToken(){Name = "expires_at",Value = dateTimeOffset.ToString("o", CultureInfo.InvariantCulture)});}properties.StoreTokens(authenticationTokenList);}var ticket = await CreateTicketAsync(identity, properties, tokens);return ticket == null ? HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties) : HandleRequestResult.Success(ticket);}

此步驟中包含兩個子步驟

ExchangeCodeAsync(交換授權碼Code)

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context){var tokenRequestParameters = new List<KeyValuePair<string, string>>{new KeyValuePair<string, string>("appid", Options.ClientId),new KeyValuePair<string, string>("secret", Options.ClientSecret),new KeyValuePair<string, string>("code", context.Code),new KeyValuePair<string, string>("grant_type", "authorization_code"),};var urlEncodedContent = new FormUrlEncodedContent(tokenRequestParameters);var response =await Backchannel.PostAsync(Options.TokenEndpoint, urlEncodedContent, Context.RequestAborted);return response.IsSuccessStatusCode ? OAuthTokenResponse.Success(JsonDocument.Parse(await response.Content.ReadAsStringAsync())) : OAuthTokenResponse.Failed(new Exception("OAuth token failure"));}

CreateTicketAsync(創建票據)

protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity,AuthenticationProperties properties,OAuthTokenResponse tokens){var openId = tokens.Response.RootElement.GetString("openid");var parameters = new Dictionary<string, string>{{ "openid", openId},{ "access_token", tokens.AccessToken }};var userInfoEndpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, parameters);var response = await Backchannel.GetAsync(userInfoEndpoint, Context.RequestAborted);if (!response.IsSuccessStatusCode){throw new HttpRequestException($"An error occurred when retrieving WeChat user information ({response.StatusCode}). Please check if the authentication information is correct.");}using (var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync())){var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme,Options, Backchannel, tokens, payload.RootElement);context.RunClaimActions();await Events.CreatingTicket(context);context.Properties.ExpiresUtc = DateTimeOffset.Now.AddMinutes(15);return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);}}

組件寫好了,怎么使用呢?在Fulu.Passport.Web項目的Startup.cs文件中添加代碼如下:

public void ConfigureServices(IServiceCollection services){......省略部分代碼......services.AddAuthentication().AddWeChat(o =>{o.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;o.ClientId = Configuration["ExternalWeChat:AppId"];o.ClientSecret = Configuration["ExternalWeChat:Secret"];})}

接著,在UserController.cs中添加如下代碼:

/// <summary>/// 外部賬號登錄/// </summary>/// <param name="model"></param>/// <returns></returns>[HttpGet, AllowAnonymous]public IActionResult ExternalLogin([FromQuery] ExternalLoginModel model){var authenticationProperties = new AuthenticationProperties(){RedirectUri = Url.Action(nameof(ExternalLoginCallback)),Items ={{ "returnUrl", model.ReturnUrl },{ "scheme", model.Provider },}};return Challenge(authenticationProperties, model.Provider);}/// <summary>/// 外部登錄回調/// </summary>/// <returns></returns>[HttpGet][AllowAnonymous]public async Task<IActionResult> ExternalLoginCallback(){//獲取idsrv.external Cookie 對象var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);var returnUrl = result.Properties.Items["returnUrl"];if (result.Succeeded == false){return await RedirectErrorResult("error", "External authentication error", returnUrl);}......省略部分代碼......//刪除 idsrv.external Cookieawait HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);//寫入 .AspNetCore.Cookiesawait SignIn(userEntity, UserLoginModel.External);return Redirect(returnUrl);}

總結

以上是生活随笔為你收集整理的开源项目葫芦藤:IdentityServer4的实现及其运用的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

欧美一级片在线播放 | 国产aaa免费视频 | 免费福利视频导航 | 久草视频在线观 | 日韩成人高清在线 | 最近日本韩国中文字幕 | 久久精品视频观看 | 在线观看免费色 | 青青草国产在线 | 免费在线国产视频 | 国产精品视频线看 | 日韩欧美电影在线 | 日本精品午夜 | 久久99久久精品 | 97成人精品区在线播放 | 日日天天狠狠 | 超碰人人国产 | 很污的网站 | 综合天天色 | 免费日韩电影 | 成人免费观看网站 | 欧美 日韩 久久 | 2022久久国产露脸精品国产 | 免费日韩一级片 | 91最新中文字幕 | 韩国av一区二区 | 国产粉嫩在线观看 | 日韩激情视频在线观看 | 亚洲va欧美va人人爽 | 欧美日韩国产mv | 在线观看 亚洲 | 国产大尺度视频 | 天天看天天干天天操 | 亚洲综合成人婷婷小说 | 国产精品完整版 | 久久综合婷婷国产二区高清 | 久热av| 久久成熟 | 国产精品一区二区av影院萌芽 | 国产精品av在线免费观看 | 在线观看aaa | 91爱爱视频| 玖玖视频免费在线 | 91在线视频免费播放 | 黄色av免费电影 | 精品国产一区二区三区久久久 | 国产黄a三级三级 | 亚洲欧美日韩一级 | 国产精品视频观看 | 超碰个人在线 | 成年美女黄网站色大片免费看 | 欧美精品久 | 97成人在线视频 | 中文字幕在线观看第一区 | 精品国产一区二区三区在线观看 | 久久视频免费在线观看 | 黄色小视频在线观看免费 | av三级在线免费观看 | 黄色的片子 | 国产精品99久久久精品免费观看 | 午夜精品福利一区二区三区蜜桃 | 国产成人精品福利 | 91在线porny国产在线看 | 九九久久婷婷 | av免费在线免费观看 | 亚洲视频精品在线 | 国产成人福利在线观看 | 九九热.com| 久草在线视频首页 | 精品国产一区二 | 中文在线8新资源库 | 97精品国产97久久久久久春色 | 一级欧美一级日韩 | 成人黄色电影免费观看 | 国产精品一区二区三区在线播放 | 国产字幕在线看 | 一级黄色片在线 | 99热免费在线 | 免费观看视频黄 | 日韩高清国产精品 | 91 在线视频播放 | 日日弄天天弄美女bbbb | 在线观看日本高清mv视频 | 黄色一级大片在线观看 | 精品国产久 | 亚洲视频高清 | 热久久国产精品 | 日韩电影中文字幕 | 亚洲一区欧美精品 | 人人爱爱 | 六月色| 色鬼综合网 | 97精品国自产拍在线观看 | 午夜18视频在线观看 | 国产成人三级在线 | 久久精品久久久久 | 国产不卡视频在线播放 | 蜜桃av久久久亚洲精品 | 免费高清在线观看电视网站 | 丁香婷婷久久 | 久久免费电影 | 久久成人免费电影 | 精品少妇一区二区三区在线 | 91精品免费| 国产一区二区高清视频 | 日日干,天天干 | 久久视频在线观看 | www99精品| 成人全视频免费观看在线看 | 国产精品欧美久久久久无广告 | 中文字幕成人网 | 永久精品视频 | 免费福利视频网站 | 一区二区精品在线观看 | 久久久91精品国产一区二区三区 | 亚洲国产av精品毛片鲁大师 | 色多多视频在线 | 国产视频资源 | 黄色影院在线播放 | 久久激情视频 | 91手机视频在线 | 99视频免费在线观看 | 日韩av一区二区三区四区 | 黄色av一级片 | 91在线视频在线观看 | 国产第一福利网 | 国产精品v欧美精品v日韩 | 久久免费看片 | 日韩激情影院 | 亚洲高清视频在线观看 | 免费亚洲成人 | 久久精品久久99精品久久 | 亚洲欧美999 | 久久久久免费视频 | 欧美国产日韩一区二区三区 | 操碰av | 三级免费黄色 | 亚洲免费在线播放视频 | 91看片麻豆 | 久久国产热视频 | 亚洲国产日本 | 国产在线91在线电影 | 国产麻豆视频免费观看 | 97超在线 | 91精品国产电影 | 波多野结衣理论片 | 国产精品美女久久久久久 | 激情网色| 国产精品第十页 | 国产v在线播放 | 欧美激情精品久久久 | 国产精品一区二区久久 | 中文字幕欧美激情 | 在线小视频国产 | 欧美日韩二三区 | 日韩美一区二区三区 | 国产一级片久久 | 欧美一区二视频在线免费观看 | 麻花豆传媒mv在线观看网站 | 亚洲精品女 | www.久久视频 | 亚洲成人av电影在线 | 超碰在线人人97 | 日韩有码在线播放 | 欧美午夜精品久久久久久孕妇 | 成人毛片久久 | 天天干天天天天 | 激情五月激情综合网 | 亚洲五月六月 | 五月天国产精品 | 黄污视频网站 | 日日添夜夜添 | 国产在线精品一区二区 | 欧美福利在线播放 | 欧美嫩草影院 | 亚洲网站在线看 | 久久网站免费 | 国产在线一区二区三区播放 | 久久久国产精品亚洲一区 | 亚州精品在线视频 | 国产黄网站在线观看 | 国内精品99| 亚洲蜜桃在线 | 人人插人人干 | 亚洲一级免费电影 | 国产丝袜在线 | 久久国产成人午夜av影院宅 | 日本精品中文字幕在线观看 | 毛片永久新网址首页 | 激情久久久久 | 日韩a在线看 | 香蕉视频18 | 在线看v片成人 | .精品久久久麻豆国产精品 亚洲va欧美 | 黄色av网站在线观看 | 亚洲禁18久人片 | 91av视频在线播放 | 久草免费资源 | 久久久久久美女 | 最近日本中文字幕 | 国产色视频一区 | 亚洲精品成人免费 | 美女黄色网在线播放 | 97视频网站 | 高清不卡一区二区三区 | 国产精品女人久久久 | 久久视频精品在线观看 | 亚洲小视频在线 | 久久精品亚洲一区二区三区观看模式 | 成人精品一区二区三区中文字幕 | 啪啪动态视频 | 久久这里只有精品首页 | 日韩中文字幕亚洲一区二区va在线 | 国产伦理精品一区二区 | 国产精品久久久久一区二区国产 | 天堂av网在线 | 天天操天天操天天操天天操天天操天天操 | 国产成人黄色av | 一 级 黄 色 片免费看的 | 亚洲精品国产区 | 亚洲国产中文字幕在线观看 | 在线日本看片免费人成视久网 | 在线观看国产日韩 | 超碰在线官网 | 一区二区三区高清 | 亚洲免费在线观看视频 | 国产九九精品视频 | 在线色吧| 四虎在线观看视频 | 免费成人在线电影 | 人人爽爽人人 | 四虎影视国产精品免费久久 | 国产精品一区二区久久精品 | 免费av影视 | 99国内精品久久久久久久 | 国产美女被啪进深处喷白浆视频 | 在线观看成人一级片 | 狠狠操夜夜| 精品欧美在线视频 | 天干啦夜天干天干在线线 | 欧美久久久久久久久久久久久 | 中文字幕一区二区三区乱码不卡 | 高清国产在线一区 | 国产偷v国产偷∨精品视频 在线草 | 久久99国产精品免费网站 | 国产成年免费视频 | 在线视频欧美精品 | 在线免费观看黄 | 国产成人福利片 | 欧美午夜精品久久久久 | 欧美国产日韩一区二区三区 | 久草在线手机视频 | 天天干天天玩天天操 | 成人午夜av电影 | 日韩视频专区 | 国产中文字幕一区二区 | 视频一区二区在线 | 欧美另类xxx | 国产精品18久久久久久不卡孕妇 | 日韩网站在线播放 | 视频国产 | 波多野结衣一区三区 | 波多野结衣电影一区 | 特级毛片aaa| 亚洲黄色免费网站 | 狠狠干婷婷色 | 日韩精品视频在线观看免费 | 在线观看亚洲成人 | 日本久久久影视 | 精品一二三四五区 | 日韩免费av网址 | 一本一本久久a久久 | av在线播放不卡 | 久久免费国产 | 黄色小网站免费看 | 深夜免费网站 | 00av视频| 色偷偷男人的天堂av | 久久久久久久久久毛片 | 免费毛片一区二区三区久久久 | 国产精品久久久久久久久久白浆 | 91aaa在线观看| 黄色avwww | 久久国产精品99久久人人澡 | 九九色在线观看 | 中文字幕亚洲国产 | 狠狠色丁香婷婷综合最新地址 | 黄色亚洲大片免费在线观看 | 色福利网| 欧美专区日韩专区 | 丁香婷婷社区 | 午夜三级影院 | 综合五月 | av电影免费在线播放 | 天天操天天操天天操天天操天天操 | 日韩精品在线看 | 91中文在线视频 | 欧美日韩中文另类 | 日韩精品一区二区三区免费视频观看 | 免费观看成人网 | 91免费高清视频 | 久久久久久久久国产 | 少妇性色午夜淫片aaaze | 亚洲精品白浆高清久久久久久 | 精品久久久久久久久久久久久久久久 | 国产亚洲精品久久久久久电影 | 香蕉免费| 欧美日韩3p | 欧美a级在线 | 天天综合婷婷 | 国产精品欧美激情在线观看 | 九九热在线免费观看 | 天天干天天想 | 午夜三级理论 | 爱爱一区| 国产黄色片免费 | 在线看片91 | 天天操天天色天天射 | a级国产乱理论片在线观看 伊人宗合网 | 成人91免费视频 | 国内精品久久久久久久久久久久 | 免费看片黄色 | 亚洲一区日韩精品 | 精品国产一区二区三区四 | 久久久久久久久国产 | 狠狠狠色狠狠色综合 | 日韩精品免费一线在线观看 | 色网站免费在线看 | 国产专区在线看 | 久久久精品网站 | 亚洲区另类春色综合小说 | 亚洲精品国偷拍自产在线观看蜜桃 | 成人h视频在线 | 欧美日韩在线精品一区二区 | 日本乱视频| 999精品网 | 亚洲国产精品人久久电影 | 婷婷日 | 免费人成在线观看 | 国产高清在线永久 | 日韩激情av在线 | 91x色| 国产精品久久二区 | 91成品视频 | 久久久久久久久久福利 | 狠狠色丁香久久婷婷综 | 亚洲国产欧美在线人成大黄瓜 | 九草视频在线 | 88av色 | 午夜久久美女 | 国产精品免费av | 午夜aaaa| www..com黄色片 | 夜夜澡人模人人添人人看 | 人人插人人插 | www.91成人| 日韩久久久久久久久久久久 | 中文字幕免 | 国内视频一区二区 | 国产成人在线综合 | 美女视频黄免费的久久 | 91精品国产92久久久久 | 久免费视频 | 国产精品视频一二三 | 国产对白av| 国产高h视频 | 国产情侣一区 | 欧美aa级| 999久久久久久久久久久 | 精品一区中文字幕 | 欧洲色吧| 色婷婷一区 | 国产精品女同一区二区三区久久夜 | 四虎最新入口 | 国产精品手机在线 | 天天爽夜夜爽人人爽一区二区 | av在线播放免费 | 国产一区欧美日韩 | 久久夜色精品国产亚洲aⅴ 91chinesexxx | 亚洲最大的av网站 | 中文字幕91 | 国产一区欧美日韩 | 少妇自拍av | 日韩二区在线观看 | av电影在线观看完整版一区二区 | 成人一区在线观看 | av成人在线看 | 婷婷福利影院 | av3级在线 | 国产成人av片 | av电影免费在线看 | 久久久久久久影视 | 丝袜美腿在线 | 欧美视频在线观看免费网址 | 久久午夜羞羞影院 | 日本爱爱免费视频 | 欧美日韩三级 | 国产原创av片 | 国产午夜亚洲精品 | 午夜av片 | 青草视频在线免费 | 午夜视频亚洲 | 国产在线视频导航 | 又黄又刺激的视频 | 黄色www在线观看 | 国产精品麻豆视频 | 国产精品色 | 精品欧美小视频在线观看 | 日韩专区视频 | 午夜精品视频福利 | 精品福利在线观看 | 国产91小视频| caobi视频 | 久久精品91视频 | 国产色就色 | 国产特级毛片aaaaaaa高清 | 91精品蜜桃| 狠狠狠色丁香综合久久天下网 | 久久一精品| 国产精品久久99综合免费观看尤物 | 日本在线观看一区二区 | 日韩在线一级 | av在线不卡观看 | 一级黄色电影网站 | 黄色aa久久 | 日日夜夜干 | 在线播放第一页 | 97国产一区二区 | 96精品视频 | 久久久久久久久久久久电影 | 久久久久久久99精品免费观看 | 99精品国产一区二区三区不卡 | 国产成人一级电影 | 日韩美av在线 | 久久久国产成人 | 午夜国产一区二区 | 亚洲在线网址 | 亚洲久草在线视频 | 亚洲精品国产综合99久久夜夜嗨 | 国产精品一区二区在线观看免费 | 黄色小网站在线观看 | 亚洲精品中文字幕在线 | 国产精品精品 | 欧美一区二区三区在线视频观看 | 人人澡人人草 | 久久综合五月婷婷 | 亚洲理论片在线观看 | 亚洲91中文字幕无线码三区 | 亚洲一区视频在线播放 | 色在线视频 | 国产精品va最新国产精品视频 | 成年人网站免费观看 | 人人狠狠综合久久亚洲婷 | 亚洲一二三区精品 | 国产九九热 | 亚洲人成人99网站 | 91尤物国产尤物福利在线播放 | 成人久久久久久久久 | 日韩免费高清在线观看 | 最新av免费在线 | 久久精品在线视频 | 在线视频久 | 黄色的片子 | 91精品视频免费看 | 欧美日韩综合在线 | 天天干,狠狠干 | 国产精品伦一区二区三区视频 | 91视频免费观看 | 亚洲成人网av | 国产亚洲无 | 深爱五月网 | 综合久久网站 | 欧美成年人在线视频 | 久久人人爽人人爽人人片 | 国产三级视频 | 国产91九色蝌蚪 | 超级碰碰碰免费视频 | 在线观看日韩免费视频 | 综合久久影院 | 天天操狠狠操网站 | 不卡电影一区二区三区 | 少妇性bbb搡bbb爽爽爽欧美 | 亚洲一区美女视频在线观看免费 | 日本婷婷色 | 国产va在线观看免费 | 日韩一级片观看 | 成人午夜网 | 免费看成人av | 日本中文在线 | 日本精品久久久久影院 | 日韩在线观看视频中文字幕 | 三级大片网站 | 在线性视频日韩欧美 | 亚洲国产精品日韩 | av福利网址导航大全 | 精久久久久 | 97在线视 | 色综合久久久久久久久五月 | 欧美黄色成人 | 成人精品国产免费网站 | 欧美日韩中字 | 国产精品一区二区三区免费视频 | 成人精品一区二区三区中文字幕 | 国产资源精品在线观看 | 丁香六月婷婷开心 | 婷婷开心久久网 | a视频在线| 国产精品123 | 一区二区三区日韩在线观看 | 人人舔人人爱 | 欧美日韩视频精品 | 91麻豆精品国产午夜天堂 | 深爱激情综合网 | 亚洲精品在线观看的 | 午夜精品一区二区三区在线视频 | 日韩在线观看电影 | 亚洲精品福利在线 | 色a网| 97成人精品区在线播放 | 国产在线传媒 | 91精品视频一区 | 黄色亚洲片 | 国产在线观看 | 亚洲激情精品 | 不卡的一区二区三区 | 日韩视频中文字幕在线观看 | 亚洲一片黄 | 欧美久久久久久久 | 91成人短视频在线观看 | 日本视频网 | 精品一二三区视频 | 91精品国产九九九久久久亚洲 | www久久久| 国产91学生粉嫩喷水 | 激情片av | 六月丁香在线观看 | 500部大龄熟乱视频使用方法 | 天天爱综合| 97免费在线视频 | 热久久最新地址 | 日日天天| 天天天干天天天操 | 国产高清视频色在线www | 88av网站 | 久久久受www免费人成 | 碰天天操天天 | 成片免费 | 免费97视频 | 欧美婷婷色 | 免费精品人在线二线三线 | 国产区免费在线 | 国产看片 色 | 国产精品淫片 | av在线最新| 色94色欧美 | 曰本免费av | 天天射天天干天天插 | av久久在线| 久久夜色精品国产欧美一区麻豆 | 国产日产精品一区二区三区四区 | 亚洲视频精品在线 | 人人爽人人香蕉 | 国产精品不卡在线播放 | 成人av一区二区在线观看 | 国产麻豆果冻传媒在线观看 | 国产精品一区二区免费视频 | 久久伊人热 | 日韩免费播放 | 日韩在线高清视频 | 丁香六月色 | 玖玖在线播放 | 国产精品久久久久久久久蜜臀 | 黄色毛片一级 | 国产一级电影在线 | 精品亚洲欧美一区 | 亚洲性视频| 日韩专区在线 | 一区二区三区免费在线观看 | 婷婷六月色| 久久久精品综合 | 欧美日韩裸体免费视频 | 国产一级视频在线观看 | 偷拍福利视频一区二区三区 | 日韩精品在线观看视频 | 久草在线最新 | 欧美日本不卡 | 日韩视频在线不卡 | 国内精品小视频 | 国产精品视频资源 | 午夜12点 | 久久久久9999亚洲精品 | 亚洲精品中文字幕在线观看 | 精品成人在线 | 97色噜噜 | 天堂网一区二区三区 | 91亚洲精品国偷拍 | 在线免费性生活片 | 亚洲成人免费观看 | 免费av网址大全 | 精品中文字幕在线播放 | 91日韩在线播放 | 亚洲欧美综合精品久久成人 | 男女靠逼app | 在线免费中文字幕 | 精品国产1区 | 久草视频国产 | 69精品人人人人 | 免费日韩一级片 | 久久久久蜜桃 | 国产91精品看黄网站在线观看动漫 | 欧美久久久一区二区三区 | 色播99| 美女视频黄在线观看 | 操操操av| 亚洲国产高清在线观看视频 | 午夜电影中文字幕 | 精品久久久久久亚洲综合网 | 黄色电影在线免费观看 | 日韩三级视频在线观看 | 国产一区在线精品 | 日日夜夜天天干 | 国产精品免费久久久久久久久久中文 | 97电影院在线观看 | 色综合久久久久综合体桃花网 | 三级性生活视频 | 日韩在线一区二区免费 | 色婷婷视频在线 | 欧美在线视频免费 | 日本99干网 | 色网站在线看 | 不卡视频一区二区三区 | 天堂av高清 | 91av在| 国产日韩精品在线 | 亚洲国产成人精品在线观看 | 中文字幕在线中文 | 久草在线资源网 | 亚洲精品欧美专区 | 亚洲日本三级 | 高清不卡一区二区三区 | 天天se天天cao天天干 | 国产精品综合久久久久久 | 精品成人久久 | 人人插人人玩 | 青青草视频精品 | 99产精品成人啪免费网站 | 99热最新 | a天堂在线看| 国产美女视频黄a视频免费 久久综合九色欧美综合狠狠 | 日本一区二区高清不卡 | 日韩一区二区三区高清免费看看 | 日本黄色大片免费 | 久久久免费视频播放 | 久久久久国产a免费观看rela | 日韩网站在线看片你懂的 | 国产老熟 | 91在线免费观看国产 | 免费手机黄色网址 | 国产精品日韩在线观看 | 久久精品免费观看 | 日韩免费不卡视频 | av在线h| 国产 精品 资源 | 久久国产成人午夜av影院潦草 | 国产香蕉97碰碰碰视频在线观看 | 国产日韩在线看 | 久久精品一级片 | 天堂在线视频中文网 | 日av免费 | 五月婷在线视频 | 亚洲电影免费 | 五月婷婷六月丁香在线观看 | 中文字幕在线网址 | 午夜视频一区二区三区 | 五月精品 | 麻豆视频在线 | 99久久99久国产黄毛片 | 国产成人一级电影 | 欧美一二三专区 | 又湿又紧又大又爽a视频国产 | 正在播放五月婷婷狠狠干 | 亚洲激情在线视频 | 久久免费视频网站 | 久草网站 | 激情综合色图 | 天天操天天色天天 | 中文字幕免费在线看 | 亚洲欧洲一区二区在线观看 | 麻豆91在线看 | 99精品免费 | 九草在线视频 | 黄色国产成人 | 精品欧美乱码久久久久久 | 国产在线观 | 欧美视频一区二 | 日韩精品一卡 | 福利电影久久 | 免费日韩 | 91视频在线网址 | 久久在线免费观看 | 香蕉视频一级 | 五月激情丁香图片 | 欧美在线视频免费 | 日韩精品一区二 | av一级片 | 久久不射网站 | 国产午夜精品一区二区三区在线观看 | 美女天天操 | 成人h视频在线 | 91超级碰| www夜夜操 | 一级α片| 天天操天天射天天舔 | 色国产精品一区在线观看 | 91色网址| 99精品国产在热久久 | 九九热在线免费观看 | 国产精品色视频 | 国产麻豆精品95视频 | 超碰久热| 在线观看亚洲电影 | 午夜精品久久久久99热app | 日韩a级黄色 | 日韩精品一区二区三区免费视频观看 | 99久热精品 | 成人v| 毛片网站免费 | 成年人在线观看免费视频 | 亚洲 欧洲 国产 日本 综合 | 免费看色网站 | 国产成人333kkk | 亚洲国产影院av久久久久 | 免费视频久久 | 国产精品自在线拍国产 | av在线电影网站 | 免费视频18 | 国产高清一区二区 | 成人黄色大片在线观看 | 亚洲永久精品视频 | 成人h视频在线播放 | 中文字幕国产 | 最近字幕在线观看第一季 | 一区二区三区污 | 天天色天天操综合网 | 在线网址你懂得 | 99精品成人 | 免费av在线| 最近乱久中文字幕 | 精品国产一区二区三区不卡 | 国产在线国产 | 中文字幕在线观看视频免费 | 国产一二三精品 | 啪一啪在线 | 综合精品久久久 | 97精品国产 | 国产亚洲字幕 | 久久国产精品99久久久久久老狼 | 久久久国产毛片 | 97在线视频网站 | 网站免费黄 | av中文字幕在线观看网站 | 久久96国产精品久久99漫画 | 91丨九色丨91啦蝌蚪老版 | 中文字幕在线观看完整版 | av一区在线播放 | 久久这里只有精品9 | 我爱av激情网 | 91专区在线观看 | 免费视频黄色 | 精品国产理论 | 婷婷六月综合网 | 国内精品久久久久久久影视麻豆 | 狠狠躁夜夜躁人人爽超碰91 | 免费观看成人av | 亚洲天堂网视频 | 午夜精品久久久久久久久久 | 国产不卡免费 | 色综合天天视频在线观看 | 欧美少妇xxxxxx| 91av免费在线观看 | 黄色在线免费观看网址 | 久草在线欧美 | 91视频这里只有精品 | 国产精品99久久久久 | 免费av黄色 | 9色在线视频 | 亚洲v欧美v国产v在线观看 | av一区二区三区在线观看 | 欧美日韩精品国产 | av电影在线免费观看 | 97涩涩视频 | 国产精品美乳一区二区免费 | 欧美午夜久久 | 久久久精品一区二区三区 | 午夜精品麻豆 | 亚洲成年人在线播放 | 国产中文字幕亚洲 | 亚洲日本va中文字幕 | 国产视频在线观看一区二区 | 久久国产午夜精品理论片最新版本 | 中文字幕日韩高清 | 亚洲黄网站 | 久久精品一区二区三区中文字幕 | 久久免费视频一区 | 激情综合亚洲精品 | 中日韩欧美精彩视频 | 婷婷激情综合 | 中文字幕在线观看完整版电影 | 国产三级香港三韩国三级 | www.狠狠操.com | 天天干夜夜夜操天 | 成人免费网站在线观看 | 天天干人人干 | 六月丁香综合网 | 热久久精品在线 | 99免在线观看免费视频高清 | 国产一区二区精品在线 | 激情综合久久 | 四虎亚洲精品 | 毛片网在线播放 | 99在线视频精品 | 狠狠躁夜夜躁人人爽超碰91 | av超碰在线 | 韩国av在线 | 天堂视频一区 | 色噜噜在线观看 | 欧亚日韩精品一区二区在线 | 亚洲视频在线免费看 | 欧美在线观看小视频 | 国产美女在线免费观看 | 日韩欧美在线视频一区二区三区 | 欧美性天天 | 亚洲精品99久久久久久 | 97视频总站 | 国产精品欧美久久久久无广告 | 精品黄色片 | 亚洲精品国产麻豆 | 精品久久久久久国产 | 国产精品一区二区久久久久 | 成人影视免费看 | 欧美性久久久久久 | www国产一区 | 精品在线观看一区二区 | 免费在线观看亚洲视频 | 美女网站久久 | 天天干,天天射,天天操,天天摸 | 国产男女免费完整视频 | 91探花在线视频 | 国产a级精品 | 日韩毛片在线一区二区毛片 | 久久亚洲专区 | 综合精品在线 | 五月天免费网站 | 久久看片网站 | 欧美一二三区在线观看 | 成人在线播放视频 | 成人va视频 | 亚洲 欧美 91 | 国产精品毛片一区视频播不卡 | 精品视频在线播放 | 国产一区播放 | 亚洲乱码精品 | 婷婷午夜天 | 五月激情站 | 激情视频一区二区三区 | 国产精品99精品 | 999国产| 天天综合色网 | 国产网红在线观看 | 亚洲婷婷综合色高清在线 | 日韩系列在线观看 | 在线观看国产日韩 | 五月丁婷婷 | 久久免费电影 | 黄色片网站大全 | 成年人在线观看 | 51久久成人国产精品麻豆 | 精品亚洲国产视频 | 激情小说网站亚洲综合网 | 九九热精品国产 | 精品999 | 日韩av影视在线 | 久草国产在线观看 | 国产在线2020| www国产亚洲精品久久麻豆 | 久久最新视频 | 欧美大片mv免费 | 夜夜爽88888免费视频4848 | 久草免费福利在线观看 | 天天综合亚洲 | 天天爽天天碰狠狠添 | 国产精品露脸在线 | 久久久精品国产一区二区三区 | 成人黄色在线观看视频 | 欧美日韩99 | 久久亚洲视频 | 亚洲国产精品推荐 | www视频在线播放 | 91精品啪啪| 精品中文字幕视频 | 99久久精品国产亚洲 | 日韩欧美99| 在线观看一区二区精品 | 国产区精品区 | 91秒拍国产福利一区 | 午夜精品一区二区三区在线播放 | 天天爱综合 | 青草视频在线免费 | www国产亚洲精品久久麻豆 | 在线观看国产v片 | 就色干综合 | 玖玖在线播放 | 国产1区在线观看 | 国产精成人品免费观看 | 在线观看成人网 | 亚洲精品国产精品国自产观看 | 久草在线免费色站 | 91最新国产 | 蜜臀av性久久久久蜜臀av | 欧美日韩国产综合一区二区 | 91精品资源 | 丁香五月亚洲综合在线 | 久久无码av一区二区三区电影网 | 国产精品福利在线观看 | 国产一级黄色电影 | 精品国产观看 | 久久久久久久久久久久av | 五月婷婷在线播放 | 亚洲欧美日韩一级 | 久久99国产综合精品 | a午夜在线 | 深爱五月激情五月 | 精品久久网 | 九九精品无码 | 91麻豆精品国产91久久久使用方法 | 夜夜躁狠狠躁日日躁视频黑人 | 中文字幕视频一区二区 | 精品99久久久久久 | 六月丁香在线观看 | 日日摸日日添日日躁av | 天天天天天天天操 | 99精品电影| 91香蕉国产 | www五月婷婷| 伊在线视频 | 色久综合 | 成人在线视频免费 | 中文字幕在线日 | 五月天电影免费在线观看一区 | 99色亚洲 | 91久草视频 | 成人av免费网站 | 久久婷婷视频 | 久久综合久久八八 | 久久草在线视频国产 | 亚洲成人国产精品 | 欧美日韩在线观看一区二区 | 日韩精品一区二区三区丰满 | 国产中文字幕在线免费观看 | 久久99精品久久久久蜜臀 | 日韩精品一区二区三区电影 | 伊人影院99 | 久久成人高清 | 亚洲欧美视频一区二区三区 | 在线观看国产一区二区 | 日韩二区三区在线观看 | 久久中文精品视频 | 在线观看国产日韩欧美 | 97在线观看免费视频 | 成人毛片一区 | 亚洲精品97 | 日韩在线观看免费 | 91精品免费在线 | 国产精品久久久久永久免费 | 黄色毛片网站在线观看 | 日韩亚洲精品电影 | 久久久久久高清 | 午夜久久久久久久 | 久久久精品欧美一区二区免费 | 五月激情亚洲 | 超碰97人人射妻 | 精品91久久久久 | 久久精品毛片基地 | av在线亚洲天堂 | 天堂av网站 | 久久久综合九色合综国产精品 | 天天婷婷 | 蜜臀久久99精品久久久无需会员 | 色香蕉在线 | 九九免费视频 | 五月开心激情网 | 在线观看国产 | 国产精品一区二区三区99 | 91黄色影视 | 欧美精品久久久久性色 | 又黄又爽的免费高潮视频 | 丁香视频全集免费观看 | 日韩高清网站 |