为了账号安全,请及时绑定邮箱和手机立即绑定

IdentityServer4-从数据库获取User登录并对Claims授权验证(五)

标签:
C#

本节将在第四节基础上介绍如何实现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");                //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest                //{                //    Address = "http://localhost:5000",                //    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.html

所以这里还是按老方法来获取tokenResponse。

(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");                //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest                //{                //    Address = "http://localhost:5000",                //    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");                        //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest                        //{                        //    Address = "http://localhost:5000",                        //    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");                            var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");                            //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest                            //{                            //    Address = "http://localhost:5000",                            //    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

原文出处:https://www.cnblogs.com/FireworksEasyCool/p/10181681.html  

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消