identityserver mysql_IdentityServer4-从数据库获取User登录并对Claims授权验证(五)
本节将在第四节基础上介绍如何实现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 { 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 options)
: base(options)
{
} public DbSet Users { get; set; } public DbSet 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(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 { get; set; } = new HashSet();
}

新建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())
.CreateMapper();
} internal static IMapper Mapper { get; } ///
/// Maps an entity to a model. ///
/// The entity.
///
public static Models.User ToModel(this User entity)
{ return Mapper.Map(entity);
} ///
/// Maps a model to an entity. ///
/// The model.
///
public static User ToEntity(this Models.User model)
{ return Mapper.Map(model);
}
}

类UserContextProfile:
public class UserContextProfile: Profile
{ public UserContextProfile()
{ //entity to model
CreateMap(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(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().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService().Database.Migrate(); var context = serviceScope.ServiceProvider.GetRequiredService();
context.Database.Migrate(); if (!context.Users.Any())
{
User user = new User()
{
UserId = "1",
UserName = "zhubingjian",
Password = "123",
IsActive = true,
Claims = new List
{ 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 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(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的方法已过时,但我按官网文档的说明,使用新方法(注释的代码),获取不到信息,还望大家指点。

所以这里还是按老方法来获取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(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 claims = new List();
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(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(content); if (user != null)
{ if (user.IsActive)
{
context.IsActive = user.IsActive;
}
}
}
} catch (Exception ex)
{ //handle error logging }
} public static Claim[] GetUserClaims(User user)
{
List claims = new List();
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 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 GetIdentityResources()
{ var customProfile = new IdentityResource(
name: "mvc.profile",
displayName: "Mvc profile",
claimTypes: new[] { "role" }); return new List
{ new IdentityResources.OpenId(), new IdentityResources.Profile(), //new IdentityResource("roles","role",new List{ "role"}), customProfile
};
}

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

(6)最后记得在Startup的ConfigureServices方法加上
.AddResourceOwnerValidator()
.AddProfileService();

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

本节介绍的IdentityServer4通过访问接口的形式验证从数据库获取的User信息。当然,也可以写成AuthServer授权服务通过连接数据库进行验证。
另外,授权服务访问资源服务API,用的是ClientCredentials模式(服务与服务之间访问)。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
