本節將在第四節基礎上介紹如何實現IdentityServer4從數據庫獲取User進行驗證,並對Claim進行權限設置。html
(1)新建API項目,用來進行user的身份驗證服務。git
(2)配置端口爲5001github
安裝Microsoft.EntityFrameworkCore包數據庫
安裝Microsoft.EntityFrameworkCore.SqlServer包api
安裝Microsoft.EntityFrameworkCore.Tools包服務器
(3)咱們在項目添加一個 Entities文件夾。cookie
新建一個User類,存放用戶基本信息,其中Claims爲一對多的關係。mvc
其中UserId的值是惟一的。app
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類async
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的數據。
(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不起做用了。
爲了讓獲取的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模式(服務與服務之間訪問)。