• IdentityServer4-从数据库获取User进行授权验证(五)


           本节将在第四节基础上介绍如何实现IdentityServer4从数据库获取User进行验证,并对Claim进行权限设置。


    一、新建Web API资源服务,命名为ResourceAPI

          (1)新建API项目,用来进行user的身份验证服务。

           

          (2)配置端口为5001

           安装Microsoft.EntityFrameworkCore

           安装Microsoft.EntityFrameworkCore.SqlServer

           安装Microsoft.EntityFrameworkCore.Tools

          (3)我们在项目添加一个 Entities文件夹。

           新建一个User类,存放用户基本信息,其中Claims为一对多的关系。

           其中UserId的值是唯一的。

     public class User
        {
            [Key]
            [MaxLength(32)]
            public string UserId { get; set; }
    
            [MaxLength(32)]
            public string UserName { get; set; }
    
            [MaxLength(50)]
            public string Password { get; set; }
    
            public bool IsActive { get; set; }//是否可用
    
            public virtual ICollection<Claims> Claims { get; set; }
    
    }

           新建Claims类

    public class Claims
        {
            [MaxLength(32)]
            public int ClaimsId { get; set; }
    
            [MaxLength(32)]
            public string Type { get; set; }
    
            [MaxLength(32)]
            public string Value { get; set; }
    
            public virtual User User { get; set; }
    
        }

           继续新建 UserContext.cs

    public class UserContext:DbContext
        {
    
            public UserContext(DbContextOptions<UserContext> options)
                : base(options)
            {
            }
            public DbSet<User> Users { get; set; }
            public DbSet<Claims> UserClaims { get; set; }
    }

          (4)修改startup.cs中的ConfigureServices方法,添加SQL Server配置。

    public void ConfigureServices(IServiceCollection services)
            {
                var connection = "Data Source=localhost;Initial Catalog=UserAuth;User ID=sa;Password=Pwd";
                services.AddDbContext<UserContext>(options => options.UseSqlServer(connection));
                // Add framework services.
                services.AddMvc();
            }

           完成后在程序包管理器控制台运行:Add-Migration InitUserAuth

           生成迁移文件。

          (5)添加Models文件夹,定义User的model类和Claims的model类。

           在Models文件夹中新建User类:

    public class User
        {
            public string UserId { get; set; }
    
            public string UserName { get; set; }
    
            public string Password { get; set; }
    
            public bool IsActive { get; set; }
    
            public ICollection<Claims> Claims { get; set; } = new HashSet<Claims>();
    }

           新建Claims类:

    public class Claims
        {
            public Claims(string type,string value)
            {
                Type = type;
                Value = value;
            }
            public string Type { get; set; }
            public string Value { get; set; }
        }

           做Model和Entity之前的映射。

           添加类UserMappers:

    public static class UserMappers
        {
            static UserMappers()
            {
                Mapper = new MapperConfiguration(cfg => cfg.AddProfile<UserContextProfile>())
                    .CreateMapper();
            }
            internal static IMapper Mapper { get; }
    
            /// <summary>
            /// Maps an entity to a model.
            /// </summary>
            /// <param name="entity">The entity.</param>
            /// <returns></returns>
            public static Models.User ToModel(this User entity)
            {
                return Mapper.Map<Models.User>(entity);
            }
    
            /// <summary>
            /// Maps a model to an entity.
            /// </summary>
            /// <param name="model">The model.</param>
            /// <returns></returns>
            public static User ToEntity(this Models.User model)
            {
                return Mapper.Map<User>(model);
            }
        }

            类UserContextProfile:

    public class UserContextProfile: Profile
        {
            public UserContextProfile()
            {
                //entity to model
                CreateMap<User, Models.User>(MemberList.Destination)
                    .ForMember(x => x.Claims, opt => opt.MapFrom(src => src.Claims.Select(x => new Models.Claims(x.Type, x.Value))));
    
                //model to entity
                CreateMap<Models.User, User>(MemberList.Source)
                    .ForMember(x => x.Claims,
                        opt => opt.MapFrom(src => src.Claims.Select(x => new Claims { Type = x.Type, Value = x.Value })));
            }
        }

          (6)在startup.cs中添加初始化数据库的方法InitDataBase方法,对User和Claim做级联插入。

     public void InitDataBase(IApplicationBuilder app)
            {
    
                using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
                {
                    serviceScope.ServiceProvider.GetRequiredService<Entities.UserContext>().Database.Migrate();
    
                    var context = serviceScope.ServiceProvider.GetRequiredService<Entities.UserContext>();
                    context.Database.Migrate();
                    if (!context.Users.Any())
                    {
                        User user = new User()
                        {
                            UserId = "1",
                            UserName = "zhubingjian",
                            Password = "123",
                            IsActive = true,
                            Claims = new List<Claims>
                            {
                                new Claims("role","admin")
                            }
                        };
                        context.Users.Add(user.ToEntity());
                        context.SaveChanges();
                    }
                }
            }

          (7)在startup.cs中添加InitDataBase方法的引用。

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                InitDataBase(app);
                app.UseMvc();
            }

           运行程序,这时候数据生成数据库UserAuth,表Users中有一条UserName=zhubingjian,Password=123的数据。

           


     

    二、实现获取User接口,进行身份验证

          (1)先对API进行保护,在Startup.cs的ConfigureServices方法中添加:

                //protect API
                services.AddMvcCore()
                .AddAuthorization()
                .AddJsonFormatters();
    
                services.AddAuthentication("Bearer")
                    .AddIdentityServerAuthentication(options =>
                    {
                        options.Authority = "http://localhost:5000";
                        options.RequireHttpsMetadata = false;
    
                        options.ApiName = "api1";
                    });

           并在Configure中,将UseAuthentication身份验证中间件添加到管道中,以便在每次调用主机时自动执行身份验证。

           app.UseAuthentication();

          (2)接着,实现获取User的接口。

           在ValuesController控制中,添加如下代码:

    UserContext context;
            public ValuesController(UserContext _context)
            {
                context = _context;
            }
    
    //只接受role为AuthServer授权服务的请求
    [Authorize(Roles = "AuthServer")]
            [HttpGet("{userName}/{password}")]
            public IActionResult AuthUser(string userName, string password)
            {
               var res = context.Users.Where(p => p.UserName == userName && p.Password == password)
                    .Include(p=>p.Claims)
                    .FirstOrDefault();
                return Ok(res.ToModel());
            }

           

           好了,资源服务器获取User的接口完成了。

          (3)接着回到AuthServer项目,把User改成从数据库进行验证。

           

           找到AccountController控制器,把从内存验证User部分修改成从数据库验证。

           主要修改Login方法,代码给出了简要注释:

            public async Task<IActionResult> Login(LoginInputModel model, string button)
            {
                // check if we are in the context of an authorization request
                AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
    
                // the user clicked the "cancel" button
                if (button != "login")
                {
                    if (context != null)
                    {
                        // if the user cancels, send a result back into IdentityServer as if they 
                        // denied the consent (even if this client does not require consent).
                        // this will send back an access denied OIDC error response to the client.
                        await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
    
                        // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                        if (await _clientStore.IsPkceClientAsync(context.ClientId))
                        {
                            // if the client is PKCE then we assume it's native, so this change in how to
                            // return the response is for better UX for the end user.
                            return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                        }
    
                        return Redirect(model.ReturnUrl);
                    }
                    else
                    {
                        // since we don't have a valid context, then we just go back to the home page
                        return Redirect("~/");
                    }
                }
    
                if (ModelState.IsValid)
                {
                    //从数据库获取User并进行验证
                    var client = _httpClientFactory.CreateClient();
                    //已过时
                    //DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                    //TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                    //var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
                    DiscoveryResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
                    var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                    {
                        Address = disco.TokenEndpoint,
                        ClientId = "AuthServer",
                        ClientSecret = "secret",
                        Scope = "api1"
                    });
                    if (tokenResponse.IsError)
                        throw new Exception(tokenResponse.Error);
    
                    client.SetBearerToken(tokenResponse.AccessToken);
                    try
                    {
                        var response = await client.GetAsync("http://localhost:5001/api/values/" + model.Username + "/" + model.Password);
                        if (!response.IsSuccessStatusCode)
                        {
                            throw new Exception("Resource server is not working!");
                        }
                        else
                        {
                            var content = await response.Content.ReadAsStringAsync();
                            User user = JsonConvert.DeserializeObject<User>(content);
                            if (user != null)
                            {
                                await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.UserId, user.UserName));
    
                                // only set explicit expiration here if user chooses "remember me". 
                                // otherwise we rely upon expiration configured in cookie middleware.
                                AuthenticationProperties props = null;
                                if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                                {
                                    props = new AuthenticationProperties
                                    {
                                        IsPersistent = true,
                                        ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                                    };
                                };
    
                                //             context.Result = new GrantValidationResult(
                                //user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)),
                                //OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime,
                                //user.Claims);
    
                                // issue authentication cookie with subject ID and username
                                await HttpContext.SignInAsync(user.UserId, user.UserName, props);
    
                                if (context != null)
                                {
                                    if (await _clientStore.IsPkceClientAsync(context.ClientId))
                                    {
                                        // if the client is PKCE then we assume it's native, so this change in how to
                                        // return the response is for better UX for the end user.
                                        return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                                    }
    
                                    // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                                    return Redirect(model.ReturnUrl);
                                }
    
                                // request for a local page
                                if (Url.IsLocalUrl(model.ReturnUrl))
                                {
                                    return Redirect(model.ReturnUrl);
                                }
                                else if (string.IsNullOrEmpty(model.ReturnUrl))
                                {
                                    return Redirect("~/");
                                }
                                else
                                {
                                    // user might have clicked on a malicious link - should be logged
                                    throw new Exception("invalid return URL");
                                }
                            }
    
                            await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                            ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
                        }
                    }
                    catch (Exception ex)
                    {
                        await _events.RaiseAsync(new UserLoginFailureEvent("Resource server", "is not working!"));
                        ModelState.AddModelError("", "Resource server is not working");
                    }
    
                }
    
                // something went wrong, show form with error
                var vm = await BuildLoginViewModelAsync(model);
                return View(vm);
            }

           可以看到,在IdentityServer4更新后,旧版获取tokenResponse的方法已过时,按官网文档的说明,使用新方法。

           官网链接:https://identitymodel.readthedocs.io/en/latest/client/token.htm

          (4)到这步后,可以把Startup中ConfigureServices方法里面的AddTestUsers去掉了。

            

           运行程序,已经可以从数据进行User验证了。

            

           

           点击进入About页面时候,出现没有权限提示,我们会发现从数据库获取的User中的Claims不起作用了。

           


     

    三、使用数据数据自定义Claim

           为了让获取的Claims起作用,我们来实现IresourceOwnerPasswordValidator接口和IprofileService接口。

          (1)在AuthServer中添加类ResourceOwnerPasswordValidator,继承IresourceOwnerPasswordValidator接口。

     public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
        {
            private readonly IHttpClientFactory _httpClientFactory;
            public ResourceOwnerPasswordValidator(IHttpClientFactory httpClientFactory)
            {
                _httpClientFactory = httpClientFactory;
            }
            public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
            {
                try
                {
                    var client = _httpClientFactory.CreateClient();
                    //已过时
                    //DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                    //TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                    //var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
    
                    DiscoveryResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
                    var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                    {
                        Address = disco.TokenEndpoint,
                        ClientId = "AuthServer",
                        ClientSecret = "secret",
                        Scope = "api1"
                    });
                    if (tokenResponse.IsError)
                        throw new Exception(tokenResponse.Error);
    
                    client.SetBearerToken(tokenResponse.AccessToken);
                    var response = await client.GetAsync("http://localhost:5001/api/values/" + context.UserName + "/" + context.Password);
                    if (!response.IsSuccessStatusCode)
                    {
                        throw new Exception("Resource server is not working!");
                    }
                    else
                    {
                        var content = await response.Content.ReadAsStringAsync();
                        User user = JsonConvert.DeserializeObject<User>(content);
                        //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)
                {
    
                }
    
            }
            public static Claim[] GetUserClaims(User user)
            {
                List<Claim> claims = new List<Claim>();
                Claim claim;
                foreach (var itemClaim in user.Claims)
                {
                    claim = new Claim(itemClaim.Type, itemClaim.Value);
                    claims.Add(claim);
                }
                return claims.ToArray();
            }
        }

           (2)ProfileService类实现IprofileService接口:

     public class ProfileService : IProfileService
        {
            private readonly IHttpClientFactory _httpClientFactory;
            public ProfileService(IHttpClientFactory httpClientFactory)
            {
                _httpClientFactory = httpClientFactory;
            }
            ////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.
                               var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");
                        //获取User_Id
                        if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                        {
                            var client = _httpClientFactory.CreateClient();
                            //已过时
                            //DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                            //TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                            //var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
    
                            DiscoveryResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
                            var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                            {
                                 Address = disco.TokenEndpoint,
                                 ClientId = "AuthServer",
                                 ClientSecret = "secret",
                                 Scope = "api1"
                            });
                            if (tokenResponse.IsError)
                                 throw new Exception(tokenResponse.Error);
                            client.SetBearerToken(tokenResponse.AccessToken);
    
                            //根据User_Id获取user
                            var response = await client.GetAsync("http://localhost:5001/api/values/" + long.Parse(userId.Value));
                            //get user from db (find user by user id)
                            //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                            var content = await response.Content.ReadAsStringAsync();
                            User user = JsonConvert.DeserializeObject<User>(content);
                            // issue the claims for the user
                            if (user != null)
                            {
                                //获取user中的Claims
                                var claims = GetUserClaims(user);
                                //context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
                                context.IssuedClaims = claims.ToList();
                            }
                      }
                }
                catch (Exception ex)
                {
                    //log your error
                }
            }
    
            //check if user account is active.
            public async Task IsActiveAsync(IsActiveContext context)
            {
                try
                {
                    var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");
    
                            if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                            {
                                //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                                var client = _httpClientFactory.CreateClient();
                                //已过时
                                //DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                                //TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                                //ar tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
    
                                DiscoveryResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
                                var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                                {
                                   Address = disco.TokenEndpoint,
                                   ClientId = "AuthServer",
                                   ClientSecret = "secret",
                                   Scope = "api1"
                                });
                                if (tokenResponse.IsError)
                                   throw new Exception(tokenResponse.Error);
                                client.SetBearerToken(tokenResponse.AccessToken);
    
                                //根据User_Id获取user
                                var response = await client.GetAsync("http://localhost:5001/api/values/" + long.Parse(userId.Value));
                                //get user from db (find user by user id)
                                //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                                var content = await response.Content.ReadAsStringAsync();
                                User user = JsonConvert.DeserializeObject<User>(content);
                                if (user != null)
                                {
                                    if (user.IsActive)
                                    {
                                        context.IsActive = user.IsActive;
                                    }
                                }                
                            }
                }
                catch (Exception ex)
                {
                    //handle error logging
                }
            }
            public static Claim[] GetUserClaims(User user)
            {
                List<Claim> claims = new List<Claim>();
                Claim claim;
                foreach (var itemClaim in user.Claims)
                {
                    claim = new Claim(itemClaim.Type, itemClaim.Value);
                    claims.Add(claim);
                }
                return claims.ToArray();
            }
        }

          (3)发现代码里面需要在ResourceAPI项目的ValuesController控制器中

           添加根据UserId获取User的Claims的接口。

            Authorize(Roles = "AuthServer")]
            [HttpGet("{userId}")]
            public ActionResult<string> Get(string userId)
            {
                var user = context.Users.Where(p => p.UserId == userId)
               .Include(p => p.Claims)
               .FirstOrDefault();
                return Ok(user.ToModel());
            }

          (4)修改AuthServer中的Config中GetIdentityResources方法,定义从数据获取的Claims为role的信息。

     public static IEnumerable<IdentityResource> GetIdentityResources()
            {
                var customProfile = new IdentityResource(
                    name: "mvc.profile",
                    displayName: "Mvc profile",
                    claimTypes: new[] { "role" });
                return new List<IdentityResource>
                {
                    new IdentityResources.OpenId(),
                    new IdentityResources.Profile(),
                    //new IdentityResource("roles","role",new List<string>{ "role"}),
                    customProfile
                };
            }

          (5)在GetClients中把定义的mvc.profile加到Scope配置

          (6)最后记得在Startup的ConfigureServices方法加上

           .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()

           .AddProfileService<ProfileService>();

           

           运行后,出现熟悉的About页面(Access Token后面加上去的,源码上有添加方法)

           


            本节介绍的IdentityServer4通过访问接口的形式验证从数据库获取的User信息。当然,也可以写成AuthServer授权服务通过连接数据库进行验证。

           另外,授权服务访问资源服务API,用的是ClientCredentials模式(服务与服务之间访问)。

           参考博客:https://stackoverflow.com/questions/35304038/identityserver4-register-userservice-and-get-users-from-database-in-asp-net-core

           源码地址:https://github.com/Bingjian-Zhu/Mvc-HybridFlow.git

  • 相关阅读:
    Day 19
    Day 18
    Day17
    Day 16
    正则表达式(仅可输入中文英文数字以及英文逗号)
    Linux安装Nginx 并搭建图片服务器
    Mysql创建用户表并利用存储过程添加100万条随机用户数据
    Dubbo(高性能、轻量级的开源Java RPC框架) & RPC(远程过程调用)
    Redis
    OSI (开放系统互联(Open System Interconnection))
  • 原文地址:https://www.cnblogs.com/FireworksEasyCool/p/10181681.html
Copyright © 2020-2023  润新知