从Client应用场景介绍IdentityServer4(五)

匿名 (未验证) 提交于 2019-12-02 22:06:11

本节将在第四节基础上介绍如何实现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后面加上去的,源码上有添加方法)


另外,授权服务访问资源服务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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!