主頁 > .NET開發 > AspNetCore3.1_Secutiry原始碼決議_6_Authentication_OpenIdConnect

AspNetCore3.1_Secutiry原始碼決議_6_Authentication_OpenIdConnect

2020-09-16 04:00:43 .NET開發

文章目錄

  • AspNetCore3.1_Secutiry原始碼決議_1_目錄
  • AspNetCore3.1_Secutiry原始碼決議_2_Authentication_核心專案
  • AspNetCore3.1_Secutiry原始碼決議_3_Authentication_Cookies
  • AspNetCore3.1_Secutiry原始碼決議_4_Authentication_JwtBear
  • AspNetCore3.1_Secutiry原始碼決議_5_Authentication_OAuth
  • AspNetCore3.1_Secutiry原始碼決議_6_Authentication_OpenIdConnect
  • AspNetCore3.1_Secutiry原始碼決議_7_Authentication_其他
  • AspNetCore3.1_Secutiry原始碼決議_8_Authorization_授權框架

oidc簡介

oidc是基于oauth2.0的上層協議,

OAuth有點像賣電影票的,只關心用戶能不能進電影院,不關心用戶是誰,而oidc則像身份證,掃描就可以上飛機,一次掃描,機場不僅能知道你是否能上飛機,還可以知道你的身份資訊,

oidc兼容OAuth2.0, 可以實作跨頂級域的SSO(單點登錄、登出),下個系列要學習的IdentityServer4就是對oidc協議族的一個具體實作框架,

更多理論知識看下面的參考資料,本系列主要過下原始碼脈絡

博客園

https://www.cnblogs.com/linianhui/p/openid-connect-core.html

協議

https://openid.net/connect/

依賴注入

默認架構名稱是OpenIdConnect,處理器類是OpenIdConnectHandler,配置類是OpenIdConnectOptions

public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder)
        => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, _ => { });

    public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, Action<OpenIdConnectOptions> configureOptions)
        => builder.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);

    public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions> configureOptions)
        => builder.AddOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);

    public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions)
    {
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
        return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler>(authenticationScheme, displayName, configureOptions);
    }

配置類 - OpenIdConnectOptions

建構式

CallbackPath: 回呼地址,即遠程認證之后跳回的地址
SignedOutCallbackPath:登出后的回呼地址
RemoteSignOutPath:遠程登出地址

scope添加openid(用戶id),profile(用戶基本資訊),所以如果client沒有這兩個基本的權限是會被遠程認證拒絕的,

洗掉了nonce,aud等claim,添加了sub(用戶id,必須有),name,profile,email等claim,MapUniqueJsonKey方法的意思是如果某claim無值,遠程認證服務回傳的用戶json資料中中存在此key且有值,則將值插入claim中,否則什么也不做,

然后new了防重放攻擊的nonce cookie,

public OpenIdConnectOptions()
{
    CallbackPath = new PathString("/signin-oidc");
    SignedOutCallbackPath = new PathString("/signout-callback-oidc");
    RemoteSignOutPath = new PathString("/signout-oidc");

    Events = new OpenIdConnectEvents();
    Scope.Add("openid");
    Scope.Add("profile");

    ClaimActions.DeleteClaim("nonce");
    ClaimActions.DeleteClaim("aud");
    ClaimActions.DeleteClaim("azp");
    ClaimActions.DeleteClaim("acr");
    ClaimActions.DeleteClaim("iss");
    ClaimActions.DeleteClaim("iat");
    ClaimActions.DeleteClaim("nbf");
    ClaimActions.DeleteClaim("exp");
    ClaimActions.DeleteClaim("at_hash");
    ClaimActions.DeleteClaim("c_hash");
    ClaimActions.DeleteClaim("ipaddr");
    ClaimActions.DeleteClaim("platf");
    ClaimActions.DeleteClaim("ver");

    // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
    ClaimActions.MapUniqueJsonKey("sub", "sub");
    ClaimActions.MapUniqueJsonKey("name", "name");
    ClaimActions.MapUniqueJsonKey("given_name", "given_name");
    ClaimActions.MapUniqueJsonKey("family_name", "family_name");
    ClaimActions.MapUniqueJsonKey("profile", "profile");
    ClaimActions.MapUniqueJsonKey("email", "email");

    _nonceCookieBuilder = new OpenIdConnectNonceCookieBuilder(this)
    {
        Name = OpenIdConnectDefaults.CookieNoncePrefix,
        HttpOnly = true,
        SameSite = SameSiteMode.None,
        SecurePolicy = CookieSecurePolicy.SameAsRequest,
        IsEssential = true,
    };
}

配置校驗 - Validate

父類RemoteAuthenticationOptions會校驗SignInSchema不允許與當前Schema相同(SignInSchema微軟只提供了Cookie的實作,登錄似乎除了Cookie沒有別的方式可以維持登錄態?)

校驗max-age不能為負數

ClientId不能為空

CallbackPath必須有值

ConfigurationManager不能為null

public override void Validate()
{
    base.Validate();

    if (MaxAge.HasValue && MaxAge.Value < TimeSpan.Zero)
    {
        throw new ArgumentOutOfRangeException(nameof(MaxAge), MaxAge.Value, "The value must not be a negative TimeSpan.");
    }

    if (string.IsNullOrEmpty(ClientId))
    {
        throw new ArgumentException("Options.ClientId must be provided", nameof(ClientId));
    }

    if (!CallbackPath.HasValue)
    {
        throw new ArgumentException("Options.CallbackPath must be provided.", nameof(CallbackPath));
    }

    if (ConfigurationManager == null)
    {
        throw new InvalidOperationException($"Provide {nameof(Authority)}, {nameof(MetadataAddress)}, "
        + $"{nameof(Configuration)}, or {nameof(ConfigurationManager)} to {nameof(OpenIdConnectOptions)}");
    }
}

屬性

/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with the remote identity provider.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60);

/// <summary>
/// The HttpMessageHandler used to communicate with remote identity provider.
/// This cannot be set at the same time as BackchannelCertificateValidator unless the value 
/// can be downcast to a WebRequestHandler.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }

/// <summary>
/// Used to communicate with the remote identity provider.
/// </summary>
public HttpClient Backchannel { get; set; }

/// <summary>
/// Gets or sets the type used to secure data.
/// </summary>
public IDataProtectionProvider DataProtectionProvider { get; set; }

/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }

/// <summary>
/// Gets or sets the optional path the user agent is redirected to if the user
/// doesn't approve the authorization demand requested by the remote server.
/// This property is not set by default. In this case, an exception is thrown
/// if an access_denied response is returned by the remote authorization server.
/// </summary>
public PathString AccessDeniedPath { get; set; }

/// <summary>
/// Gets or sets the name of the parameter used to convey the original location
/// of the user before the remote challenge was triggered up to the access denied page.
/// This property is only used when the <see cref="AccessDeniedPath"/> is explicitly specified.
/// </summary>
// Note: this deliberately matches the default parameter name used by the cookie handler.
public string ReturnUrlParameter { get; set; } = "ReturnUrl";

/// <summary>
/// Gets or sets the authentication scheme corresponding to the middleware
/// responsible of persisting user's identity after a successful authentication.
/// This value typically corresponds to a cookie middleware registered in the Startup class.
/// When omitted, <see cref="AuthenticationOptions.DefaultSignInScheme"/> is used as a fallback value.
/// </summary>
public string SignInScheme { get; set; }

/// <summary>
/// Gets or sets the time limit for completing the authentication flow (15 minutes by default).
/// </summary>
public TimeSpan RemoteAuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(15);

public new RemoteAuthenticationEvents Events
{
    get => (RemoteAuthenticationEvents)base.Events;
    set => base.Events = value;
}

/// <summary>
/// Defines whether access and refresh tokens should be stored in the
/// <see cref="AuthenticationProperties"/> after a successful authorization.
/// This property is set to <c>false</c> by default to reduce
/// the size of the final authentication cookie.
/// </summary>
public bool SaveTokens { get; set; }

/// <summary>
/// Determines the settings used to create the correlation cookie before the
/// cookie gets added to the response.
/// </summary>
public CookieBuilder CorrelationCookie
{
    get => _correlationCookieBuilder;
    set => _correlationCookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
}

配置后處理邏輯 - OpenIdConnectPostConfigureOptions

主要處理如果DataProtectionProvider,StateDataFormat等物件沒有配置的話,則構造默認實作類,options.MetadataAddress += ".well-known/openid-configuration",這是配置的元資料地址,描述了oidc的所有介面地址和其他資訊,

public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions<OpenIdConnectOptions>
{
    private readonly IDataProtectionProvider _dp;

    public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection)
    {
        _dp = dataProtection;
    }

    /// <summary>
    /// Invoked to post configure a TOptions instance.
    /// </summary>
    /// <param name="name">The name of the options instance being configured.</param>
    /// <param name="options">The options instance to configure.</param>
    public void PostConfigure(string name, OpenIdConnectOptions options)
    {
        options.DataProtectionProvider = options.DataProtectionProvider ?? _dp;

        if (string.IsNullOrEmpty(options.SignOutScheme))
        {
            options.SignOutScheme = options.SignInScheme;
        }

        if (options.StateDataFormat == null)
        {
            var dataProtector = options.DataProtectionProvider.CreateProtector(
                typeof(OpenIdConnectHandler).FullName, name, "v1");
            options.StateDataFormat = new PropertiesDataFormat(dataProtector);
        }

        if (options.StringDataFormat == null)
        {
            var dataProtector = options.DataProtectionProvider.CreateProtector(
                typeof(OpenIdConnectHandler).FullName,
                typeof(string).FullName,
                name,
                "v1");

            options.StringDataFormat = new SecureDataFormat<string>(new StringSerializer(), dataProtector);
        }

        if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.ClientId))
        {
            options.TokenValidationParameters.ValidAudience = options.ClientId;
        }

        if (options.Backchannel == null)
        {
            options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
            options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OpenIdConnect handler");
            options.Backchannel.Timeout = options.BackchannelTimeout;
            options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
        }

        if (options.ConfigurationManager == null)
        {
            if (options.Configuration != null)
            {
                options.ConfigurationManager = new StaticConfigurationManager<OpenIdConnectConfiguration>(options.Configuration);
            }
            else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority)))
            {
                if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority))
                {
                    options.MetadataAddress = options.Authority;
                    if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal))
                    {
                        options.MetadataAddress += "/";
                    }

                    options.MetadataAddress += ".well-known/openid-configuration";
                }

                if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
                {
                    throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=https://www.cnblogs.com/holdengong/p/false.");
                }

                options.ConfigurationManager = new ConfigurationManager(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(),
                    new HttpDocumentRetriever(options.Backchannel) { RequireHttps = options.RequireHttpsMetadata });
            }
        }
    }

    private class StringSerializer : IDataSerializer
    {
        public string Deserialize(byte[] data)
        {
            return Encoding.UTF8.GetString(data);
        }

        public byte[] Serialize(string model)
        {
            return Encoding.UTF8.GetBytes(model);
        }
    }

處理器類 - OpenIdConnectHandler

處理認證 - HandRemoteAuthenticate

oidc登錄示例圖

sequenceDiagram mysite->>sso: GET connect/authorize?callback(clientId,redirect_uri,response_type)scope,state,nonce sso->>mysite: Form.POST mysite/signin-oidc (code,id_token,scope,state)

代碼決議

mysite向oidc的認證節點地址/connect/authorize發送請求,oidc站點根據response_mode用get或者form_post方式呼叫mysite的回呼地址mysite/signin-oidc,HandleRemoteAuthenticateAsync就是處理oidc站點的回應的方法,

  • 判斷GET/POST,從請求中提取引數,如果是get請求,id_token,access_token不允許放在query中
  • 從state引數讀取資訊放到properties
  • 校驗correlationId,防跨站偽造攻擊
  • 如果回傳了id_token,校驗token,將資訊寫入HttpContext
  • 如果回傳了授權碼code的處理

代碼量還是比較多,有些地方目前還不是特別理解,需求后面熟悉協議內容在回過頭來看下,總體上就是對oidc站點回傳資訊的校驗和處理,

/// <summary>
/// Invoked to process incoming OpenIdConnect messages.
/// </summary>
/// <returns>An <see cref="HandleRequestResult"/>.</returns>
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
    Logger.EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(GetType().FullName);

    OpenIdConnectMessage authorizationResponse = null;

    if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
    {
        authorizationResponse = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));

        // response_mode=query (explicit or not) and a response_type containing id_token
        // or token are not considered as a safe combination and MUST be rejected.
        // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
        if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken))
        {
            if (Options.SkipUnrecognizedRequests)
            {
                // Not for us?
                return HandleRequestResult.SkipHandler();
            }
            return HandleRequestResult.Fail("An OpenID Connect response cannot contain an " +
                    "identity token or an access token when using response_mode=query");
        }
    }
    // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
    else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
        && !string.IsNullOrEmpty(Request.ContentType)
        // May have media/type; charset=utf-8, allow partial match.
        && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
        && Request.Body.CanRead)
    {
        var form = await Request.ReadFormAsync();
        authorizationResponse = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
    }

    if (authorizationResponse == null)
    {
        if (Options.SkipUnrecognizedRequests)
        {
            // Not for us?
            return HandleRequestResult.SkipHandler();
        }
        return HandleRequestResult.Fail("No message.");
    }

    AuthenticationProperties properties = null;
    try
    {
        properties = ReadPropertiesAndClearState(authorizationResponse);

        var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse, properties);
        if (messageReceivedContext.Result != null)
        {
            return messageReceivedContext.Result;
        }
        authorizationResponse = messageReceivedContext.ProtocolMessage;
        properties = messageReceivedContext.Properties;

        if (properties == null || properties.Items.Count == 0)
        {
            // Fail if state is missing, it's required for the correlation id.
            if (string.IsNullOrEmpty(authorizationResponse.State))
            {
                // This wasn't a valid OIDC message, it may not have been intended for us.
                Logger.NullOrEmptyAuthorizationResponseState();
                if (Options.SkipUnrecognizedRequests)
                {
                    return HandleRequestResult.SkipHandler();
                }
                return HandleRequestResult.Fail(Resources.MessageStateIsNullOrEmpty);
            }

            properties = ReadPropertiesAndClearState(authorizationResponse);
        }

        if (properties == null)
        {
            Logger.UnableToReadAuthorizationResponseState();
            if (Options.SkipUnrecognizedRequests)
            {
                // Not for us?
                return HandleRequestResult.SkipHandler();
            }

            // if state exists and we failed to 'unprotect' this is not a message we should process.
            return HandleRequestResult.Fail(Resources.MessageStateIsInvalid);
        }

        if (!ValidateCorrelationId(properties))
        {
            return HandleRequestResult.Fail("Correlation failed.", properties);
        }

        // if any of the error fields are set, throw error null
        if (!string.IsNullOrEmpty(authorizationResponse.Error))
        {
            // Note: access_denied errors are special protocol errors indicating the user didn't
            // approve the authorization demand requested by the remote authorization server.
            // Since it's a frequent scenario (that is not caused by incorrect configuration),
            // denied errors are handled differently using HandleAccessDeniedErrorAsync().
            // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
            if (string.Equals(authorizationResponse.Error, "access_denied", StringComparison.Ordinal))
            {
                var result = await HandleAccessDeniedErrorAsync(properties);
                if (!result.None)
                {
                    return result;
                }
            }

            return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties);
        }

        if (_configuration == null && Options.ConfigurationManager != null)
        {
            Logger.UpdatingConfiguration();
            _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
        }

        PopulateSessionProperties(authorizationResponse, properties);

        ClaimsPrincipal user = null;
        JwtSecurityToken jwt = null;
        string nonce = null;
        var validationParameters = Options.TokenValidationParameters.Clone();

        // Hybrid or Implicit flow
        if (!string.IsNullOrEmpty(authorizationResponse.IdToken))
        {
            Logger.ReceivedIdToken();
            user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt);

            nonce = jwt.Payload.Nonce;
            if (!string.IsNullOrEmpty(nonce))
            {
                nonce = ReadNonceCookie(nonce);
            }

            var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, user, properties, jwt, nonce);
            if (tokenValidatedContext.Result != null)
            {
                return tokenValidatedContext.Result;
            }
            authorizationResponse = tokenValidatedContext.ProtocolMessage;
            user = tokenValidatedContext.Principal;
            properties = tokenValidatedContext.Properties;
            jwt = tokenValidatedContext.SecurityToken;
            nonce = tokenValidatedContext.Nonce;
        }

        Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
        {
            ClientId = Options.ClientId,
            ProtocolMessage = authorizationResponse,
            ValidatedIdToken = jwt,
            Nonce = nonce
        });

        OpenIdConnectMessage tokenEndpointResponse = null;

        // Authorization Code or Hybrid flow
        if (!string.IsNullOrEmpty(authorizationResponse.Code))
        {
            var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties, jwt);
            if (authorizationCodeReceivedContext.Result != null)
            {
                return authorizationCodeReceivedContext.Result;
            }
            authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage;
            user = authorizationCodeReceivedContext.Principal;
            properties = authorizationCodeReceivedContext.Properties;
            var tokenEndpointRequest = authorizationCodeReceivedContext.TokenEndpointRequest;
            // If the developer redeemed the code themselves...
            tokenEndpointResponse = authorizationCodeReceivedContext.TokenEndpointResponse;
            jwt = authorizationCodeReceivedContext.JwtSecurityToken;

            if (!authorizationCodeReceivedContext.HandledCodeRedemption)
            {
                tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest);
            }

            var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, user, properties);
            if (tokenResponseReceivedContext.Result != null)
            {
                return tokenResponseReceivedContext.Result;
            }

            authorizationResponse = tokenResponseReceivedContext.ProtocolMessage;
            tokenEndpointResponse = tokenResponseReceivedContext.TokenEndpointResponse;
            user = tokenResponseReceivedContext.Principal;
            properties = tokenResponseReceivedContext.Properties;

            // no need to validate signature when token is received using "code flow" as per spec
            // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation].
            validationParameters.RequireSignedTokens = false;

            // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response.
            // And we'll want to validate the new JWT in ValidateTokenResponse.
            var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out var tokenEndpointJwt);

            // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation.
            if (user == null)
            {
                nonce = tokenEndpointJwt.Payload.Nonce;
                if (!string.IsNullOrEmpty(nonce))
                {
                    nonce = ReadNonceCookie(nonce);
                }

                var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, tokenEndpointUser, properties, tokenEndpointJwt, nonce);
                if (tokenValidatedContext.Result != null)
                {
                    return tokenValidatedContext.Result;
                }
                authorizationResponse = tokenValidatedContext.ProtocolMessage;
                tokenEndpointResponse = tokenValidatedContext.TokenEndpointResponse;
                user = tokenValidatedContext.Principal;
                properties = tokenValidatedContext.Properties;
                jwt = tokenValidatedContext.SecurityToken;
                nonce = tokenValidatedContext.Nonce;
            }
            else
            {
                if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal))
                {
                    throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints.");
                }

                jwt = tokenEndpointJwt;
            }

            // Validate the token response if it wasn't provided manually
            if (!authorizationCodeReceivedContext.HandledCodeRedemption)
            {
                Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext()
                {
                    ClientId = Options.ClientId,
                    ProtocolMessage = tokenEndpointResponse,
                    ValidatedIdToken = jwt,
                    Nonce = nonce
                });
            }
        }

        if (Options.SaveTokens)
        {
            SaveTokens(properties, tokenEndpointResponse ?? authorizationResponse);
        }

        if (Options.GetClaimsFromUserInfoEndpoint)
        {
            return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, user, properties);
        }
        else
        {
            using (var payload = JsonDocument.Parse("{}"))
            {
                var identity = (ClaimsIdentity)user.Identity;
                foreach (var action in Options.ClaimActions)
                {
                    action.Run(payload.RootElement, identity, ClaimsIssuer);
                }
            }
        }

        return HandleRequestResult.Success(new AuthenticationTicket(user, properties, Scheme.Name));
    }
    catch (Exception exception)
    {
        Logger.ExceptionProcessingMessage(exception);

        // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
        if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException)
        {
            if (Options.ConfigurationManager != null)
            {
                Logger.ConfigurationManagerRequestRefreshCalled();
                Options.ConfigurationManager.RequestRefresh();
            }
        }

        var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception);
        if (authenticationFailedContext.Result != null)
        {
            return authenticationFailedContext.Result;
        }

        return HandleRequestResult.Fail(exception, properties);
    }
}

處理遠程登出 - HandleRemoteSignOutAsync

OpenIdConectHandler跟OAuthHandler一樣,繼承自RemoteAuthenticationHandler,但是OpenId還實作了IAuthenticationSignOutHandler介面,因為OpenId是支持單點登錄登出的,本地登出之后需要通知認證服務遠程登出(注銷本地站點Cookie),這樣實作帳號的同步登出(注銷sso站點cookie),

  • 遠程登出支持GET和Form-Post兩種提交方式,客戶端根據請求方式,將報文拼裝好,
  • 觸發遠程登出事件
  • 使用SignOutScheme認證,得到身份資訊 - Context.AuthenticateAsync(Options.SignOutScheme)
  • Context.Proerties中必須有iss資訊,issuer就是提供認證方
  • 呼叫本地登出方法 - Context.SignOutAsync(Options.SignOutScheme)
protected virtual async Task<bool> HandleRemoteSignOutAsync()
{
    OpenIdConnectMessage message = null;

    if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
    {
        message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
    }

    // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
    else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)
        && !string.IsNullOrEmpty(Request.ContentType)
        // May have media/type; charset=utf-8, allow partial match.
        && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
        && Request.Body.CanRead)
    {
        var form = await Request.ReadFormAsync();
        message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
    }

    var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message);
    await Events.RemoteSignOut(remoteSignOutContext);

    if (remoteSignOutContext.Result != null)
    {
        if (remoteSignOutContext.Result.Handled)
        {
            Logger.RemoteSignOutHandledResponse();
            return true;
        }
        if (remoteSignOutContext.Result.Skipped)
        {
            Logger.RemoteSignOutSkipped();
            return false;
        }
        if (remoteSignOutContext.Result.Failure != null)
        {
            throw new InvalidOperationException("An error was returned from the RemoteSignOut event.", remoteSignOutContext.Result.Failure);
        }
    }

    if (message == null)
    {
        return false;
    }

    // Try to extract the session identifier from the authentication ticket persisted by the sign-in handler.
    // If the identifier cannot be found, bypass the session identifier checks: this may indicate that the
    // authentication cookie was already cleared, that the session identifier was lost because of a lossy
    // external/application cookie conversion or that the identity provider doesn't support sessions.
    var principal = (await Context.AuthenticateAsync(Options.SignOutScheme))?.Principal;

    var sid = principal?.FindFirst(JwtRegisteredClaimNames.Sid)?.Value;
    if (!string.IsNullOrEmpty(sid))
    {
        // Ensure a 'sid' parameter was sent by the identity provider.
        if (string.IsNullOrEmpty(message.Sid))
        {
            Logger.RemoteSignOutSessionIdMissing();
            return true;
        }
        // Ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket.
        if (!string.Equals(sid, message.Sid, StringComparison.Ordinal))
        {
            Logger.RemoteSignOutSessionIdInvalid();
            return true;
        }
    }

    var iss = principal?.FindFirst(JwtRegisteredClaimNames.Iss)?.Value;
    if (!string.IsNullOrEmpty(iss))
    {
        // Ensure a 'iss' parameter was sent by the identity provider.
        if (string.IsNullOrEmpty(message.Iss))
        {
            Logger.RemoteSignOutIssuerMissing();
            return true;
        }
        // Ensure the 'iss' parameter corresponds to the 'iss' stored in the authentication ticket.
        if (!string.Equals(iss, message.Iss, StringComparison.Ordinal))
        {
            Logger.RemoteSignOutIssuerInvalid();
            return true;
        }
    }

    Logger.RemoteSignOut();

    // We've received a remote sign-out request
    await Context.SignOutAsync(Options.SignOutScheme);
    return true;
}

處理本地登出 - Context.SignOutAsync(Options.SignOutScheme)

方法的注釋:將用戶重定向到身份認證站點登出,

  • ForwardXXX是所有認證配置項的基類,可以攔截使用自己配置的Scheme,
  • 構造要發送給oidc服務的報文,包括IssuerAddress(EndSessionEndpoint:即結束會話節點地址),PostLogoutRedirectUri(登出回跳地址)等,
  • 構造RedirectUri(登錄流程結束最侄訓到的地址):優先使用HttpContext.Properties中的RedirectUri,然后使用配置中的SignedOutRedirectUri,最后使用請求源地址,
  • 獲取IdToken,放到登出請求中
  • state欄位加密后(包含了redirecturi等資訊),放入請求訊息
  • 給oidc站點發送GET或者FormPost請求
/// <summary>
/// Redirect user to the identity provider for sign out
/// </summary>
/// <returns>A task executing the sign out procedure</returns>
public async virtual Task SignOutAsync(AuthenticationProperties properties)
{
    var target = ResolveTarget(Options.ForwardSignOut);
    if (target != null)
    {
        await Context.SignOutAsync(target, properties);
        return;
    }

    properties = properties ?? new AuthenticationProperties();

    Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName);

    if (_configuration == null && Options.ConfigurationManager != null)
    {
        _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
    }

    var message = new OpenIdConnectMessage()
    {
        EnableTelemetryParameters = !Options.DisableTelemetry,
        IssuerAddress = _configuration?.EndSessionEndpoint ?? string.Empty,

        // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri
        PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath)
    };

    // Get the post redirect URI.
    if (string.IsNullOrEmpty(properties.RedirectUri))
    {
        properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri);
        if (string.IsNullOrWhiteSpace(properties.RedirectUri))
        {
            properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
        }
    }
    Logger.PostSignOutRedirect(properties.RedirectUri);

    // Attach the identity token to the logout request when possible.
    message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);

    var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
    {
        ProtocolMessage = message
    };

    await Events.RedirectToIdentityProviderForSignOut(redirectContext);
    if (redirectContext.Handled)
    {
        Logger.RedirectToIdentityProviderForSignOutHandledResponse();
        return;
    }

    message = redirectContext.ProtocolMessage;

    if (!string.IsNullOrEmpty(message.State))
    {
        properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
    }

    message.State = Options.StateDataFormat.Protect(properties);

    if (string.IsNullOrEmpty(message.IssuerAddress))
    {
        throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.");
    }

    if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
    {
        var redirectUri = message.CreateLogoutRequestUrl();
        if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
        {
            Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri);
        }

        Response.Redirect(redirectUri);
    }
    else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
    {
        var content = message.BuildFormPost();
        var buffer = Encoding.UTF8.GetBytes(content);

        Response.ContentLength = buffer.Length;
        Response.ContentType = "text/html;charset=UTF-8";

        // Emit Cache-Control=no-cache to prevent client caching.
        Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
        Response.Headers[HeaderNames.Pragma] = "no-cache";
        Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;

        await Response.Body.WriteAsync(buffer, 0, buffer.Length);
    }
    else
    {
        throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
    }

    Logger.AuthenticationSchemeSignedOut(Scheme.Name);
}

oidc處理完后跳到回呼地址

oidc站點處理完登出請求之后(怎么處理的,應該是清除了oidc的cookie,或許回收了token?目前不清楚,后面看identitserver怎么實作的),回跳到callback地址,執行下面的callback方法

callback方法很簡單,就是將state欄位解碼,將redirect_uri拿到,然后跳過去,

/// <summary>
/// Response to the callback from OpenId provider after session ended.
/// </summary>
/// <returns>A task executing the callback procedure</returns>
protected async virtual Task<bool> HandleSignOutCallbackAsync()
{
    var message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
    AuthenticationProperties properties = null;
    if (!string.IsNullOrEmpty(message.State))
    {
        properties = Options.StateDataFormat.Unprotect(message.State);
    }

    var signOut = new RemoteSignOutContext(Context, Scheme, Options, message)
    {
        Properties = properties,
    };

    await Events.SignedOutCallbackRedirect(signOut);
    if (signOut.Result != null)
    {
        if (signOut.Result.Handled)
        {
            Logger.SignOutCallbackRedirectHandledResponse();
            return true;
        }
        if (signOut.Result.Skipped)
        {
            Logger.SignOutCallbackRedirectSkipped();
            return false;
        }
        if (signOut.Result.Failure != null)
        {
            throw new InvalidOperationException("An error was returned from the SignedOutCallbackRedirect event.", signOut.Result.Failure);
        }
    }

    properties = signOut.Properties;
    if (!string.IsNullOrEmpty(properties?.RedirectUri))
    {
        Response.Redirect(properties.RedirectUri);
    }

    return true;
}

登出時序圖

sequenceDiagram mysite->>sso: GET/FormPost mysite/connect/endsession?params... sso->>mysite: 302,移除sso站點cookie,回呼到signout-callback地址 mysite->>mysite: 從state中決議redirect_uri,回跳redirect_uri

可以看到,oidc的登出只處理了oidc認證站點的cookie,mysite本地的cookie是沒有處理的,因為當前schema是OpenIdConnnect,本地Cookie是SignInSchema的事情,所以登出需要掉兩次SignOut方法

HttpContext.SignOutAsync("Cookies"); //清除本地cookie
HttpContext.SignOutAsync("OpenIdConnect") //清除遠程sso站點cookie

處理質詢 - HandleChallengeAsync

  • OAuth&PKCE的處理,PKCE = Proof Key for Code Exchange,主要用于NativeApp防跨站攻擊的,因為NativeApp沒有Cookie支持,無法使用state欄位,所以需要其他的安全保障,

https://tools.ietf.org/html/rfc7636

  • 拼裝請求引數,根據配置,如果是GET,302跳轉到oidc站點;如果是Form-POST,提交表單到oidc站點,
/// <summary>
/// Responds to a 401 Challenge. Sends an OpenIdConnect message to the 'identity authority' to obtain an identity.
/// </summary>
/// <returns></returns>
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
    await HandleChallengeAsyncInternal(properties);
    var location = Context.Response.Headers[HeaderNames.Location];
    if (location == StringValues.Empty)
    {
        location = "(not set)";
    }
    var cookie = Context.Response.Headers[HeaderNames.SetCookie];
    if (cookie == StringValues.Empty)
    {
        cookie = "(not set)";
    }
    Logger.HandleChallenge(location, cookie);
}

private async Task HandleChallengeAsyncInternal(AuthenticationProperties properties)
{
    Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName);

    // order for local RedirectUri
    // 1. challenge.Properties.RedirectUri
    // 2. CurrentUri if RedirectUri is not set)
    if (string.IsNullOrEmpty(properties.RedirectUri))
    {
        properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
    }
    Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);

    if (_configuration == null && Options.ConfigurationManager != null)
    {
        _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
    }

    var message = new OpenIdConnectMessage
    {
        ClientId = Options.ClientId,
        EnableTelemetryParameters = !Options.DisableTelemetry,
        IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty,
        RedirectUri = BuildRedirectUri(Options.CallbackPath),
        Resource = Options.Resource,
        ResponseType = Options.ResponseType,
        Prompt = properties.GetParameter<string>(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt,
        Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
    };

    // https://tools.ietf.org/html/rfc7636
    if (Options.UsePkce && Options.ResponseType == OpenIdConnectResponseType.Code)
    {
        var bytes = new byte[32];
        CryptoRandom.GetBytes(bytes);
        var codeVerifier = Base64UrlTextEncoder.Encode(bytes);

        // Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
        properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);

        using var sha256 = SHA256.Create();
        var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
        var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);

        message.Parameters.Add(OAuthConstants.CodeChallengeKey, codeChallenge);
        message.Parameters.Add(OAuthConstants.CodeChallengeMethodKey, OAuthConstants.CodeChallengeMethodS256);
    }

    // Add the 'max_age' parameter to the authentication request if MaxAge is not null.
    // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
    var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
    if (maxAge.HasValue)
    {
        message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds))
            .ToString(CultureInfo.InvariantCulture);
    }

    // Omitting the response_mode parameter when it already corresponds to the default
    // response_mode used for the specified response_type is recommended by the specifications.
    // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
    if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) ||
        !string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal))
    {
        message.ResponseMode = Options.ResponseMode;
    }

    if (Options.ProtocolValidator.RequireNonce)
    {
        message.Nonce = Options.ProtocolValidator.GenerateNonce();
        WriteNonceCookie(message.Nonce);
    }

    GenerateCorrelationId(properties);

    var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
    {
        ProtocolMessage = message
    };

    await Events.RedirectToIdentityProvider(redirectContext);
    if (redirectContext.Handled)
    {
        Logger.RedirectToIdentityProviderHandledResponse();
        return;
    }

    message = redirectContext.ProtocolMessage;

    if (!string.IsNullOrEmpty(message.State))
    {
        properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
    }

    // When redeeming a 'code' for an AccessToken, this value is needed
    properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);

    message.State = Options.StateDataFormat.Protect(properties);

    if (string.IsNullOrEmpty(message.IssuerAddress))
    {
        throw new InvalidOperationException(
            "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
    }

    if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
    {
        var redirectUri = message.CreateAuthenticationRequestUrl();
        if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
        {
            Logger.InvalidAuthenticationRequestUrl(redirectUri);
        }

        Response.Redirect(redirectUri);
        return;
    }
    else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
    {
        var content = message.BuildFormPost();
        var buffer = Encoding.UTF8.GetBytes(content);

        Response.ContentLength = buffer.Length;
        Response.ContentType = "text/html;charset=UTF-8";

        // Emit Cache-Control=no-cache to prevent client caching.
        Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
        Response.Headers[HeaderNames.Pragma] = "no-cache";
        Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;

        await Response.Body.WriteAsync(buffer, 0, buffer.Length);
        return;
    }

    throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
}

OpenIdConnect的代碼還是有點復雜的,很多細節無法覆寫到,后面學習了協議再回頭梳理一下,

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/52862.html

標籤:.NET Core

上一篇:參考.net Core類時T4模板無法加載檔案或程式集“ System.Runtime,版本= 4.2.2.0”

下一篇:.Net Core對MongoDB執行多條件查詢

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more