Esempio n. 1
0
        public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IamOptions iamOpt, DingTalkOptions dingOpt, WwOptions wwOpt)
        {
            // 如果想部署到 nginx 的 子目录中,比如 foo 这个目录,那此时 url 为 /foo/api/user,但是 .net core 处理时需要去掉 foo
            string pathBase = iamOpt.PathBase?.Trim();

            pathBase = String.IsNullOrWhiteSpace(iamOpt.PathBase) ? "" : "/" + pathBase.TrimStart('/');

            services.AddAuthentication()
            .AddCookie()
            .AddJwtBearer("Bearer", options =>
            {
                // OpenIam 本身也做为 Api Resource 提供服务,当第三方访问 OpenIam 的 Api 的时候需要对 Token 进行验证

                options.Authority            = iamOpt.Host;
                options.RequireHttpsMetadata = false;

                options.Audience = Constants.IAM_API_SCOPE;

                if (iamOpt.ValidIssuers != null)
                {
                    // OpenIam 可能从外网访问,也可能从内网访问,issuer 不同
                    options.TokenValidationParameters.ValidIssuers = iamOpt.ValidIssuers;
                }
            })
            .AddDingTalk("钉钉登录", opts =>
            {
                opts.AppKey          = dingOpt.AppKey;
                opts.AppSecret       = dingOpt.AppSecret;
                opts.IncludeUserInfo = dingOpt.IncludeUserInfo;
                opts.ClientId        = dingOpt.ClientId;
                opts.ClientSecret    = dingOpt.ClientSecret;

                opts.SignInScheme = IdentityConstants.ExternalScheme;

                // 需要手动加上
                opts.AuthorizationEndpoint = $"{pathBase}/Identity/Account/DingTalkLogin";

                opts.Events.OnCreatingTicket = async ctx =>
                {
                    string json = ctx.User.GetRawText();

                    // 如果 jobNumber 找不到,则可以认为用户不存在
                    await Task.CompletedTask;
                };

                opts.Events.OnRemoteFailure = async ctx =>
                {
                    var tempDataProvider = ctx.HttpContext.RequestServices.GetRequiredService <ITempDataProvider>();

                    tempDataProvider.SaveTempData(ctx.HttpContext, new Dictionary <string, object>
                    {
                        { "ErrorMessage", ctx.Failure.Message }
                    });
                    ctx.Response.Redirect($"{pathBase}/Identity/Account/Login");
                    ctx.HandleResponse();

                    await Task.CompletedTask;
                };
            })
            .AddWw("企业微信登录", opts =>
            {
                opts.ClientId     = wwOpt.ClientId;
                opts.ClientSecret = wwOpt.ClientSecret;
                opts.AgentId      = wwOpt.AgentId;

                opts.SignInScheme = IdentityConstants.ExternalScheme;

                opts.AuthorizationEndpoint = $"{pathBase}/Identity/Account/WwLogin";

                opts.Events.OnCreatingTicket = async ctx =>
                {
                    string json = ctx.User.GetRawText();

                    // 如果 jobNumber 找不到,则可以认为用户不存在
                    await Task.CompletedTask;
                };

                opts.Events.OnRemoteFailure = async ctx =>
                {
                    var tempDataProvider = ctx.HttpContext.RequestServices.GetRequiredService <ITempDataProvider>();

                    tempDataProvider.SaveTempData(ctx.HttpContext, new Dictionary <string, object>
                    {
                        { "ErrorMessage", ctx.Failure.Message }
                    });
                    ctx.Response.Redirect($"{pathBase}/Identity/Account/Login");
                    ctx.HandleResponse();

                    await Task.CompletedTask;
                };
            });

            return(services);
        }
Esempio n. 2
0
        /// <summary>
        /// 增加统一身份认证授权功能
        /// </summary>
        /// <remarks>
        /// AddIam 内部已经调用了 AddAuthentication,不需要再重复调用
        /// </remarks>
        /// <param name="services"></param>
        /// <param name="configureOptions"></param>
        /// <returns></returns>
        public static IServiceCollection AddIam(this IServiceCollection services, Action <IamOptions> configureOptions)
        {
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.ConfigureNonBreakingSameSiteCookies();

            services.AddSingleton <IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
            services.AddSingleton <IAuthorizationHandler, PermissionHandler>();
            services.AddMemoryCache();

            IamOptions opts = new IamOptions
            {
                GetClaimsFromUserInfoEndpoint = false,
                RequireHttpsMetadata          = true
            };

            if (configureOptions != null)
            {
                configureOptions(opts);
            }

            services.AddOptions <IamBasicOptions>().Configure(config =>
            {
                config.Authority = opts.Authority;
            });

            services.AddAuthentication(options =>
            {
                options.DefaultScheme          = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
            {
                if (!String.IsNullOrWhiteSpace(opts.AccessDeniedPath))
                {
                    //403跳转页
                    o.AccessDeniedPath = new PathString(opts.AccessDeniedPath);
                }

                o.Events.OnRedirectToAccessDenied = RedirectIfRequired;
                o.Events.OnRedirectToLogin        = RedirectIfRequired;
                o.Events.OnValidatePrincipal      = async ctx =>
                {
                    var identity = (ClaimsIdentity)ctx.Principal.Identity;

                    var accessToken  = ctx.Properties.GetTokenValue("access_token");
                    var refreshToken = ctx.Properties.GetTokenValue("refresh_token");

                    var expiresAtStr = ctx.Properties.GetTokenValue("expires_at");
                    var expiresAt    = DateTime.ParseExact(expiresAtStr, "o", CultureInfo.InvariantCulture);

                    if (DateTime.Now < expiresAt)
                    {
                        return;
                    }

                    // ATT: 由于此处并非使用 HttpClientFactory 创建的实例,为避免 DNS 切换导致的错误,此处会对 Client 进行回收,
                    // 另外由于 token 超时导致的请求不会很频繁,所以对性能不会造成严重影响
                    using (var client = new HttpClient())
                    {
                        var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
                        {
                            Address = opts.Authority
                        });

                        // 如果已经过期,则使用 refresh token 重新请求 access token
                        var response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
                        {
                            Address      = disco.TokenEndpoint,
                            ClientId     = opts.ClientId,
                            ClientSecret = opts.ClientSecret,
                            RefreshToken = refreshToken
                        });

                        if (!response.IsError)
                        {
                            bool result         = ctx.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
                            result              = ctx.Properties.UpdateTokenValue("access_token", response.AccessToken);
                            string newExpiresAt = expiresAt.AddSeconds(response.ExpiresIn).ToUniversalTime().ToString("o");
                            result              = ctx.Properties.UpdateTokenValue("expires_at", newExpiresAt);

                            await ctx.HttpContext.SignInAsync(ctx.Principal, ctx.Properties);
                        }
                        else
                        {
                            // 如果换取不到,则需要用户重新登录
                            ctx.RejectPrincipal();
                            await ctx.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                        }
                    }
                };

                // 对于 Api 的请求不应该 Redirect
                Task RedirectIfRequired(RedirectContext <CookieAuthenticationOptions> ctx)
                {
                    if (!ctx.Request.Path.Value.Contains("/api/"))
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                    }
                    return(Task.FromResult(0));
                }
            })
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority    = opts.Authority;
                options.ClientId     = opts.ClientId;
                options.ClientSecret = opts.ClientSecret;
                options.GetClaimsFromUserInfoEndpoint = opts.GetClaimsFromUserInfoEndpoint;
                options.ResponseType         = ResponseTypes.Code;
                options.RequireHttpsMetadata = opts.RequireHttpsMetadata;

                // 确保使用 Code 的时候必须使用 Pkce
                options.UsePkce = true;

                // 必须是 true,否则 Client 端将无法从 Cookie 中得到 Access Token
                options.SaveTokens = true;

                if (opts.Scopes != null)
                {
                    options.Scope.Clear();
                    foreach (var scope in opts.Scopes)
                    {
                        options.Scope.Add(scope);
                    }
                }

                // 用于使用 Iam Identity Resource 相关接口
                if (!options.Scope.Contains("openid"))
                {
                    options.Scope.Add("openid");
                }

                if (!options.Scope.Contains(Constants.IAM_ID_SCOPE))
                {
                    options.Scope.Add(Constants.IAM_ID_SCOPE);
                }

                if (!options.Scope.Contains(Constants.IAM_API_SCOPE))
                {
                    options.Scope.Add(Constants.IAM_API_SCOPE);
                }

                // 用于获取 refresh token
                if (!options.Scope.Contains("offline_access"))
                {
                    options.Scope.Add("offline_access");
                }

                options.ClaimActions.MapUniqueJsonKey("saler", "saler");
            });
            services.AddHttpContextAccessor();

            services.AddSingleton <IGeneralPermissionService, SdkPermissionService>();
            services.AddHttpClient <IamApi>()
            .ConfigurePrimaryHttpMessageHandler(() =>
            {
                // 避免跳转导致隐藏了最初的 http 状态码
                return(new HttpClientHandler()
                {
                    AllowAutoRedirect = false
                });
            });

            return(services);
        }