从Client应用场景介绍IdentityServer4(五)
原文:从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://github.com/Bingjian-Zhu/Mvc-HybridFlow.git
从Client应用场景介绍IdentityServer4(五)的更多相关文章
- 从Client应用场景介绍IdentityServer4(四)
原文:从Client应用场景介绍IdentityServer4(四) 上节以对话形式,大概说了几种客户端授权模式的原理,这节重点介绍Hybrid模式在MVC下的使用.且为实现IdentityServe ...
- 从Client应用场景介绍IdentityServer4(二)
原文:从Client应用场景介绍IdentityServer4(二) 本节介绍Client的ClientCredentials客户端模式,先看下画的草图: 一.在Server上添加动态新增Client ...
- 从Client应用场景介绍IdentityServer4(三)
原文:从Client应用场景介绍IdentityServer4(三) 在学习其他应用场景前,需要了解几个客户端的授权模式.首先了解下本节使用的几个名词 Resource Owner:资源拥有者,文中称 ...
- 从Client应用场景介绍IdentityServer4(一)
原文:从Client应用场景介绍IdentityServer4(一) 一.背景 IdentityServer4的介绍将不再叙述,百度下可以找到,且官网的快速入门例子也有翻译的版本.这里主要从Clien ...
- ITTC数据挖掘平台介绍(五) 数据导入导出向导和报告生成
一. 前言 经过了一个多月的努力,软件系统又添加了不少新功能.这些功能包括非常实用的数据导入导出,对触摸进行优化的画布和画笔工具,以及对一些智能分析的报告生成模块等.进一步加强了平台系统级的功能. 马 ...
- 消息中间件activemq的使用场景介绍(结合springboot的示例)
一.消息队列概述 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题.实现高性能,高可用,可伸缩和最终一致性架构.是大型分布式系统不可缺少的中间件. 目前在生产环境,使 ...
- Redis 中 5 种数据结构的使用场景介绍
这篇文章主要介绍了Redis中5种数据结构的使用场景介绍,本文对Redis中的5种数据类型String.Hash.List.Set.Sorted Set做了讲解,需要的朋友可以参考下 一.redis ...
- Memcache应用场景介绍
面临的问题 对于高并发高訪问的Web应用程序来说,数据库存取瓶颈一直是个令人头疼的问题.特别当你的程序架构还是建立在单数据库模式,而一个数据池连接数峰 值已经达到500的时候,那你的程序执行离崩溃的边 ...
- 基于Apache Hudi构建数据湖的典型应用场景介绍
1. 传统数据湖存在的问题与挑战 传统数据湖解决方案中,常用Hive来构建T+1级别的数据仓库,通过HDFS存储实现海量数据的存储与水平扩容,通过Hive实现元数据的管理以及数据操作的SQL化.虽然能 ...
随机推荐
- 【习题 3-12 UVA - 11809】Floating-Point Numbers
[链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] \(A*10^B = temp[M]*2^{2^E-1}\) 两边取一下对数 得到 \(lg_A+B = lg_{temp[M]} ...
- WPF学习笔记——概述
如果你选择WPF,多半原因是因为折服于它那震撼性的用户体验.纵观WPF整个知识体系,其内容并不复杂,但却比较细碎,不易理清.以下内容是对WPF部分内容的简单概括,希望读者能够对WPF框架有个大体认识. ...
- 读<阿里亿级日活网关通道架构演进>有感
读<阿里亿级日活网关通道架构演进>时对优化方法有些概念不理解,特意搜索了一下,拓展自己的思路. 其中的优化: 优化方法中1,2比较常见,3,4我知道的比较少,很感兴趣.就继续追踪下去: 于 ...
- Windows下Nginx的下载安装、启动停止和配置浏览
前言: 记录一下今天在Windows下载安装Nginx服务器的过程.因为网上关于Nginx的资料都太复杂了,大多数是在Linux下使用的方法. 1.下载 Nginx官网下载地址:http://ngin ...
- AE中Identify查询工具的实现
原文 AE中Identify查询工具的实现 主要实现点击查询并闪烁显示,并把查询要素的信息通过DataGridView显示出来,主要用到的接口: IIdentity.IArray.IIdentifyO ...
- TCP套接字编程模型及实例
摘要: 本文讲述了TCP套接字编程模块,包括服务器端的创建套接字.绑定.监听.接受.读/写.终止连接,客户端的创建套接字.连接.读/写.终止连接.先给出实例,进而结合代码分析. PS:本文权当 ...
- python3的函数
#摘自廖雪峰的程序教程 函数名是变量: 如abs()是一个求绝对值的函数, >>> x = abs(-10) >>> x 10 变量可以指向函数 用f指向函数abs ...
- eclipse设置jvm
设置参数: -Xverify:none -Xms512m -Xmx512m -Xmn128m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+UseConcMark ...
- jsvc 启动java 在linux下的实现原理
http://blog.csdn.net/raintungli/article/details/8265009 JSVC:http://commons.apache.org/proper/common ...
- OpenGL核心之视差映射
笔者介绍:姜雪伟,IT公司技术合伙人.IT高级讲师,CSDN社区专家,特邀编辑.畅销书作者;已出版书籍:<手把手教你¯的纹理坐标偏移T3来对fragment的纹理坐标进行位移.你能够看到随着深度 ...