IdentityServer4 在 asp.net core 中注册 UserService 并从数据库获取用户

92

我在各个地方搜索了如何在asp.net core中使用IdentityServer4注册UserService,但似乎找不到正确的方法。

这是注册InMemoryUsers的代码,可以在这里找到,但我想从我的MSSQL数据库中访问用户而不是访问示例中定义的静态用户。

var builder = services.AddIdentityServer(options =>
{
    options.SigningCertificate = cert;
});

builder.AddInMemoryClients(Clients.Get());
builder.AddInMemoryScopes(Scopes.Get());
builder.AddInMemoryUsers(Users.Get());

那么我看了一下IdentityServer3这个

var factory = new IdentityServerServiceFactory()
                .UseInMemoryClients(Clients.Get())
                .UseInMemoryScopes(Scopes.Get());

var userService = new UserService();
factory.UserService = new Registration<IUserService>(resolver => userService);

从网上的阅读中,似乎我需要使用 DI 系统来注册 UserService,但我不确定它如何绑定到 IdentityServer。

services.AddScoped<IUserService, UserService>();

那么我的问题是:

我如何将我的 UserService绑定到生成器(IdentityServer4 Users)中? 我该如何调用我的数据库来访问和验证我的现有db用户在UserService中(我使用仓储库连接到db)?

考虑到这必须与asp.net core一起使用。

谢谢!

3个回答

121

更新 - IdentityServer 4已更改并用IResourceOwnerPasswordValidatorIProfileService替换了IUserService

我使用我的UserRepository从数据库中获取所有用户数据。这是通过依赖注入(DI)注入到构造函数中,并在Startup.cs中定义。我还创建了以下类来处理身份验证服务器(也进行了注入):

首先定义ResourceOwnerPasswordValidator.cs

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    //repository to get user from db
    private readonly IUserRepository _userRepository;

    public ResourceOwnerPasswordValidator(IUserRepository userRepository)
    {
        _userRepository = userRepository; //DI
    }

    //this is used to validate your user account with provided grant at /connect/token
    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        try
        {
            //get your user model from db (by username - in my case its email)
            var user = await _userRepository.FindAsync(context.UserName);
            if (user != null)
            {
                //check if password match - remember to hash password if stored as hash in db
                if (user.Password == context.Password) {
                    //set the result
                    context.Result = new GrantValidationResult(
                        subject: user.UserId.ToString(),
                        authenticationMethod: "custom", 
                        claims: GetUserClaims(user));

                    return;
                } 

                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Incorrect password");
                return;
            }
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not exist.");
            return;
        }
        catch (Exception ex)
        {
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password");
        }
    }
    
    //build claims array from user data
    public static Claim[] GetUserClaims(User user)
    {
        return new Claim[]
        {
            new Claim("user_id", user.UserId.ToString() ?? ""),
            new Claim(JwtClaimTypes.Name, (!string.IsNullOrEmpty(user.Firstname) && !string.IsNullOrEmpty(user.Lastname)) ? (user.Firstname + " " + user.Lastname) : ""),
            new Claim(JwtClaimTypes.GivenName, user.Firstname  ?? ""),
            new Claim(JwtClaimTypes.FamilyName, user.Lastname  ?? ""),
            new Claim(JwtClaimTypes.Email, user.Email  ?? ""),
            new Claim("some_claim_you_want_to_see", user.Some_Data_From_User ?? ""),

            //roles
            new Claim(JwtClaimTypes.Role, user.Role)
        };
}

以及 ProfileService.cs 文件:

public class ProfileService : IProfileService
{
    //services
    private readonly IUserRepository _userRepository;

    public ProfileService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    //Get user profile date in terms of claims when calling /connect/userinfo
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        try
        {
            //depending on the scope accessing the user data.
            if (!string.IsNullOrEmpty(context.Subject.Identity.Name))
            {
                //get user from db (in my case this is by email)
                var user = await _userRepository.FindAsync(context.Subject.Identity.Name);

                if (user != null)
                {
                    var claims = GetUserClaims(user);

                    //set issued claims to return
                    context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
                }
            }
            else
            {
                //get subject from context (this was set ResourceOwnerPasswordValidator.ValidateAsync),
                //where and subject was set to my user id.
                var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");

                if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                {
                    //get user from db (find user by user id)
                    var user = await _userRepository.FindAsync(long.Parse(userId.Value));

                    // issue the claims for the user
                    if (user != null)
                    {
                        var claims = ResourceOwnerPasswordValidator.GetUserClaims(user);

                        context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
                    }
                }
            }
        }
        catch (Exception ex)
        {
            //log your error
        }
    }

    //check if user account is active.
    public async Task IsActiveAsync(IsActiveContext context)
    {
        try
        {
            //get subject from context (set in ResourceOwnerPasswordValidator.ValidateAsync),
            var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "user_id");

            if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
            {
                var user = await _userRepository.FindAsync(long.Parse(userId.Value));

                if (user != null)
                {
                    if (user.IsActive)
                    {
                        context.IsActive = user.IsActive;
                    }
                }
            }
        }
        catch (Exception ex)
        {
            //handle error logging
        }
    }
}

然后在Startup.cs中我进行了以下操作:

public void ConfigureServices(IServiceCollection services)
{
    //...

    //identity server 4 cert
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "idsrv4test.pfx"), "your_cert_password");

    //DI DBContext inject connection string
    services.AddScoped(_ => new YourDbContext(Configuration.GetConnectionString("DefaultConnection")));

    //my user repository
    services.AddScoped<IUserRepository, UserRepository>();

    //add identity server 4
    services.AddIdentityServer()
        .AddSigningCredential(cert)
        .AddInMemoryIdentityResources(Config.GetIdentityResources()) //check below
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients())
        .AddProfileService<ProfileService>();

    //Inject the classes we just created
    services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
    services.AddTransient<IProfileService, ProfileService>();

    //...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    //...

    app.UseIdentityServer();

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    IdentityServerAuthenticationOptions identityServerValidationOptions = new IdentityServerAuthenticationOptions
    {
        //move host url into appsettings.json
        Authority = "http://localhost:50000/",
        ApiSecret = "secret",
        ApiName = "my.api.resource",
        AutomaticAuthenticate = true,
        SupportedTokens = SupportedTokens.Both,

        // required if you want to return a 403 and not a 401 for forbidden responses
        AutomaticChallenge = true,

        //change this to true for SLL
        RequireHttpsMetadata = false
    };

    app.UseIdentityServerAuthentication(identityServerValidationOptions);

    //...
}

您还需要 Config.cs,其中定义了您的客户端、API 和资源。您可以在此处找到示例:https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

现在您应该能够调用 IdentityServer /connect/token 了。

enter image description here

有关更多信息,请查阅文档:https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf


旧答案(这对于更新的 IdentityServer4 不再适用)

一旦您理解了流程,就会变得非常简单。

像这样配置您的 IdentityService(在 Startup.cs - ConfigureServices() 中):

var builder = services.AddIdentityServer(options =>
{
    options.SigningCertificate = cert;
});

builder.AddInMemoryClients(Clients.Get());
builder.AddInMemoryScopes(Scopes.Get());

//** this piece of code DI's the UserService into IdentityServer **
builder.Services.AddTransient<IUserService, UserService>();

//for clarity of the next piece of code
services.AddTransient<IUserRepository, UserRepository>();

然后设置您的UserService

public class UserService : IUserService
{
    //DI the repository from Startup.cs - see previous code block
    private IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public Task AuthenticateLocalAsync(LocalAuthenticationContext context)
    {
        var user = _userRepository.Find(context.UserName);

        //check if passwords match against user column 
        //My password was hashed, 
        //so I had to hash it with the saved salt first and then compare.
        if (user.Password == context.Password)
        {
            context.AuthenticateResult = new AuthenticateResult(
                user.UserId.ToString(),
                user.Email,

                //I set up some claims 
                new Claim[]
                {
                    //Firstname and Surname are DB columns mapped to User object (from table [User])
                    new Claim(Constants.ClaimTypes.Name, user.Firstname + " " + user.Surname),
                    new Claim(Constants.ClaimTypes.Email, user.Email),
                    new Claim(Constants.ClaimTypes.Role, user.Role.ToString()),
                    //custom claim
                    new Claim("company", user.Company)
                }
            );
        }

        return Task.FromResult(0);
    }

    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        //find method in my repository to check my user email
        var user = _userRepository.Find(context.Subject.Identity.Name);

        if (user != null)
        {
            var claims = new Claim[]
                {
                    new Claim(Constants.ClaimTypes.Name, user.Firstname + " " + user.Surname),
                    new Claim(Constants.ClaimTypes.Email, user.Email),
                    new Claim(Constants.ClaimTypes.Role, user.Role.ToString(), ClaimValueTypes.Integer),
                    new Claim("company", user.Company)
            };

            context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type));
        }

        return Task.FromResult(0);
    }

    public Task IsActiveAsync(IsActiveContext context)
    {
        var user = _userRepository.Find(context.Subject.Identity.Name);

        return Task.FromResult(user != null);
    }
}
基本上,通过将 UserService 注入到类型为 IdentityServerBuilderbuilderServices 中,使其能够在 auth 上调用 UserService。

10
嗯,从我所看到的情况来看,在 IdSvr4 上(适用于 ASP.NET Core 1.0),IUserService 不再存在。它已被两个接口/服务 IProfileServiceIResourceOwnerPasswordValidator 取代。 - Frank Fajardo
3
好的,从现在开始,它们将被分开。关注不同的事情。 - leastprivilege
3
非常抱歉,此答案发布后IdentityServer4已更新,不再使用IUserService。我已更新我的答案,希望对您有所帮助。 - Nick De Beer
3
在GetProfileData中,你应该只需调用context.Subject.Claims.ToList();来设置context.IssuedClaims。这取决于你是否需要隐藏某些声明或在查看配置文件数据时需要进行一些中间逻辑处理。 - Nick De Beer
3
这需要更新为.NET Core 2吗?我实现了IProfileServiece和IResourceOwnerPasswordValidator,但身份验证服务器没有调用它们中的任何一个。 - dragonfly02
显示剩余5条评论

67
在IdentityServer4中,IUserService已不再可用,现在您需要使用IResourceOwnerPasswordValidator进行身份验证,并使用IProfileService获取声明。
在我的情况下,我使用资源所有者授权类型,我只需要根据用户名和密码获取用户的声明,以便为我的Web API执行基于角色的授权。我假设主题对于每个用户是唯一的。
我已经发布了我的代码,并且它可以正常工作;有人能告诉我我的代码是否有任何问题吗?
在startup.cs中注册这两个服务。
public void ConfigureServices(IServiceCollection services)
{
    var builder = services.AddIdentityServer();
    builder.AddInMemoryClients(Clients.Get());
    builder.AddInMemoryScopes(Scopes.Get());
    builder.Services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
    builder.Services.AddTransient<IProfileService, ProfileService>();
}

实现 IResourceOwnerPasswordValidator 接口。

public class ResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator
{
    public Task<customgrantvalidationresult> ValidateAsync(string userName, string password, ValidatedTokenRequest request)
    {
        // Check The UserName And Password In Database, Return The Subject If Correct, Return Null Otherwise
        // subject = ......
        if (subject == null)
        {
            var result = new CustomGrantValidationResult("Username Or Password Incorrect");
            return Task.FromResult(result);
        }
        else {
            var result = new CustomGrantValidationResult(subject, "password");
            return Task.FromResult(result);
        }
    }
}

实现ProfileService接口。

public class ProfileService : IProfileService
{
    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        string subject = context.Subject.Claims.ToList().Find(s => s.Type == "sub").Value;
        try
        {
            // Get Claims From Database, And Use Subject To Find The Related Claims, As A Subject Is An Unique Identity Of User
            //List<string> claimStringList = ......
            if (claimStringList == null)
            {
                return Task.FromResult(0);
            }
            else {
                List<Claim> claimList = new List<Claim>();
                for (int i = 0; i < claimStringList.Count; i++)
                {
                    claimList.Add(new Claim("role", claimStringList[i]));
                }
                context.IssuedClaims = claimList.Where(x => context.RequestedClaimTypes.Contains(x.Type));
                return Task.FromResult(0);
            }
        }
        catch
        {
            return Task.FromResult(0);
        }
    }

    public Task IsActiveAsync(IsActiveContext context)
    {
        return Task.FromResult(0);
    }
}

我按照这个答案进行操作,但是我收到了以下错误信息:“附加信息:未指定授权的存储机制。使用'AddInMemoryStores'扩展方法注册开发版本。” 我正在使用“services.AddIdentityServer”创建生成器,IdentitiServer4的版本为1.0.0-rc1-update2。 - fra
值得指出的是,如果您想控制“sub”声明,您必须在此之前的流程中进行一些定制。 - Ben Collins
即使我为两个服务都提供了实现,仍然存在相同的错误! - Hussein Salman
@EternityWYH 你能看一下这个吗 [http://stackoverflow.com/questions/40797993/identityserver-4-no-storage-mechanism-for-grants-specified-use-addinmemorysto] - Hussein Salman
感谢您的回答,对我来说,只需在“IdentityServer4:1.3.1”中实现IResourceOwnerPasswordValidator和IProfileService即可。 - Ilya Chumakov
出于某种原因,我甚至无法使其在密码验证器的第一行命中断点。它自动拒绝密码。日志中没有看到任何错误。此外,IResourceOwnerPasswordValidator的接口定义是不正确的。它只返回Task,而不是Task<T>,并且只有上下文作为参数。 - Sinaesthetic

10

在 IdentityServer4 1.0.0-rc5 版本中,既无法使用 IUserService 也无法使用 CustomGrantValidationResult。

现在,你需要设置上下文的 Result 属性,而不是返回 CustomGrantValidationResult。

 public class ResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator
 {
    private MyUserManager _myUserManager { get; set; }
    public ResourceOwnerPasswordValidator()
    {
        _myUserManager = new MyUserManager();
    }

    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        var user = await _myUserManager.FindByNameAsync(context.UserName);
        if (user != null && await _myUserManager.CheckPasswordAsync(user,context.Password))
        {
             context.Result = new GrantValidationResult(
                 subject: "2", 
                 authenticationMethod: "custom", 
                 claims: someClaimsList);


        }
        else
        {
             context.Result = new GrantValidationResult(
                    TokenRequestErrors.InvalidGrant,
                    "invalid custom credential");
         }


        return;

   }

资源所有者密码验证


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接