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

本节将在第四节基础上介绍如何实现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

从Client应用场景介绍IdentityServer4(五)的更多相关文章

  1. 从Client应用场景介绍IdentityServer4(四)

    原文:从Client应用场景介绍IdentityServer4(四) 上节以对话形式,大概说了几种客户端授权模式的原理,这节重点介绍Hybrid模式在MVC下的使用.且为实现IdentityServe ...

  2. 从Client应用场景介绍IdentityServer4(二)

    原文:从Client应用场景介绍IdentityServer4(二) 本节介绍Client的ClientCredentials客户端模式,先看下画的草图: 一.在Server上添加动态新增Client ...

  3. 从Client应用场景介绍IdentityServer4(三)

    原文:从Client应用场景介绍IdentityServer4(三) 在学习其他应用场景前,需要了解几个客户端的授权模式.首先了解下本节使用的几个名词 Resource Owner:资源拥有者,文中称 ...

  4. 从Client应用场景介绍IdentityServer4(一)

    原文:从Client应用场景介绍IdentityServer4(一) 一.背景 IdentityServer4的介绍将不再叙述,百度下可以找到,且官网的快速入门例子也有翻译的版本.这里主要从Clien ...

  5. ITTC数据挖掘平台介绍(五) 数据导入导出向导和报告生成

    一. 前言 经过了一个多月的努力,软件系统又添加了不少新功能.这些功能包括非常实用的数据导入导出,对触摸进行优化的画布和画笔工具,以及对一些智能分析的报告生成模块等.进一步加强了平台系统级的功能. 马 ...

  6. 消息中间件activemq的使用场景介绍(结合springboot的示例)

    一.消息队列概述 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题.实现高性能,高可用,可伸缩和最终一致性架构.是大型分布式系统不可缺少的中间件. 目前在生产环境,使 ...

  7. Redis 中 5 种数据结构的使用场景介绍

    这篇文章主要介绍了Redis中5种数据结构的使用场景介绍,本文对Redis中的5种数据类型String.Hash.List.Set.Sorted Set做了讲解,需要的朋友可以参考下 一.redis ...

  8. Memcache应用场景介绍

    面临的问题 对于高并发高訪问的Web应用程序来说,数据库存取瓶颈一直是个令人头疼的问题.特别当你的程序架构还是建立在单数据库模式,而一个数据池连接数峰 值已经达到500的时候,那你的程序执行离崩溃的边 ...

  9. 基于Apache Hudi构建数据湖的典型应用场景介绍

    1. 传统数据湖存在的问题与挑战 传统数据湖解决方案中,常用Hive来构建T+1级别的数据仓库,通过HDFS存储实现海量数据的存储与水平扩容,通过Hive实现元数据的管理以及数据操作的SQL化.虽然能 ...

随机推荐

  1. Android Studio设置图片背景及主题设置

    因为Android Studio是基于IDEA的,所以IDEA里面能用的插件Android Studio也能用,这次图片背景是依赖IDEA下的一个插件,名为BackgroundImage的插件,用户可 ...

  2. 使用UIDataDetectorTypes自己主动检測电话、网址和邮箱

    支付宝公布最新版本号9.0.再一次引发一场撕逼大战.微信说支付宝抄袭了它.支付宝说微信一直都在抄袭自己.在我看来.微信和支付宝都抄袭了对方.对于大佬们的抄袭.我们也是司空见惯了. 支付宝这一次更新,真 ...

  3. 原生js螺旋运动

    window.onload=function(){ var oSpiral=document.getElementById('spiral'); var oUl=oSpiral.getElements ...

  4. 什么是网站CDN服务,CDN加速原理?

    转载:http://server.zzidc.com/fwqcjwt/728.html 摘要:在为您的网站打开速度发愁吗?您有没有发现有些大网站每天拥有几十万或者上百万,甚至几亿用户的访问,而且不同用 ...

  5. [转载]Ocelot简易教程(五)之集成IdentityServer认证以及授权

    作者:依乐祝 原文地址:https://www.cnblogs.com/yilezhu/p/9807125.html 最近比较懒,所以隔了N天才来继续更新第五篇Ocelot简易教程,本篇教程会先简单介 ...

  6. PatentTips - Highly-available OSPF routing protocol

    BACKGROUND OF THE INVENTION FIG. 1A is a simplified block diagram schematically representing a typic ...

  7. [Angular] Zones and NgZone

    NgZone, Angular uses it to profiling all the async actions such as setTimeout, http request and anim ...

  8. 忙里偷闲( ˇˍˇ )闲里偷学【C语言篇】——(9)链表

    我们至少可以通过两种结构来存储数据 数组 1.需要一整块连续的存储空间,内存中可能没有 2.插入元素,删除元素效率极低. 3.查找数据快 链表 1.查找效率低 2.不需要一块连续的内存空间 3.插入删 ...

  9. Windows Phone 8.1 控件

    如果你已经开始了 Windows Phone 8.1 的学习,就会发现许多在 8.0 下的控件在 8.1 中都发生了变化,以下就谈谈几个 8.1 下的新控件以及与 8.0 控件的改变. 1. Text ...

  10. svn: E200033: database is locked解决办法

    svn执行update,却被告知database is locked! 执行 svn update,却抛出个错误警报: svn: E200033: database is locked, execut ...