本节将在第四节基础上介绍如何实现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()]
public string UserId { get; set; } [MaxLength()]
public string UserName { get; set; } [MaxLength()]
public string Password { get; set; } public bool IsActive { get; set; }//是否可用 public virtual ICollection<Claims> Claims { get; set; } }

新建Claims类

public class Claims
{
[MaxLength()]
public int ClaimsId { get; set; } [MaxLength()]
public string Type { get; set; } [MaxLength()]
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 = "",
UserName = "zhubingjian",
Password = "",
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");
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不起作用了。


三、使用数据数据自定义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"); 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) > )
{
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) > )
{
//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模式(服务与服务之间访问)。

参考博客: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

IdentityServer4-从数据库获取User进行授权验证(五)的更多相关文章

  1. IdentityServer4-MVC+Hybrid实现Claims授权验证(四)

    上节以对话形式,大概说了几种客户端授权模式的原理,这节重点介绍Hybrid模式在MVC下的使用.且为实现IdentityServer4从数据库获取User进行验证,并对Claim进行权限设置打下基础( ...

  2. 【从零开始搭建自己的.NET Core Api框架】(四)实战!带你半个小时实现接口的JWT授权验证

    系列目录 一.  创建项目并集成swagger 1.1 创建 1.2 完善 二. 搭建项目整体架构 三. 集成轻量级ORM框架——SqlSugar 3.1 搭建环境 3.2 实战篇:利用SqlSuga ...

  3. ASP.NET Core WebApi基于JWT实现接口授权验证

    一.ASP.Net Core WebApi JWT课程前言 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再 ...

  4. 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十四║ Vuex + JWT 实现授权验证登录

    壹周回顾 哈喽,又是元气满满的一个周一,又与大家见面了,周末就是团圆节了,正好咱们的前后端也要团圆了,为什么这么说呢,因为以后的开发可能就需要前后端一起了,两边也终于会师了,还有几天Vue系列就基本告 ...

  5. ASP.NET Core搭建多层网站架构【10-使用JWT进行授权验证】

    2020/01/31, ASP.NET Core 3.1, VS2019, Microsoft.AspNetCore.Authentication.JwtBearer 3.1.1 摘要:基于ASP.N ...

  6. 用Middleware给ASP.NET Core Web API添加自己的授权验证

    Web API,是一个能让前后端分离.解放前后端生产力的好东西.不过大部分公司应该都没能做到完全的前后端分离.API的实现方式有很 多,可以用ASP.NET Core.也可以用ASP.NET Web ...

  7. [转]用Middleware给ASP.NET Core Web API添加自己的授权验证

    本文转自:http://www.cnblogs.com/catcher1994/p/6021046.html Web API,是一个能让前后端分离.解放前后端生产力的好东西.不过大部分公司应该都没能做 ...

  8. yii2 resetful 授权验证

    什么是restful风格的api呢?我们之前有写过大篇的文章来介绍其概念以及基本操作. 既然写过了,那今天是要说点什么吗? 这篇文章主要针对实际场景中api的部署来写. 我们今天就来大大的侃侃那些年a ...

  9. Shrio授权验证详解

    所谓授权,就是控制你是否能访问某个资源,比如说,你可以方位page文件夹下的jsp页面,但是不可以访问page文件夹下的admin文件夹下的jsp页面. 在授权中,有三个核心元素:权限,角色,用户. ...

随机推荐

  1. 关于Linux 虚拟机如何才能ping 通外网

    需要虚拟机能够联网.以前都是用桥接模式让虚拟机跟主机通信,这几天查了好多资料,都没有写得很详细,自己捣鼓了很久,把步骤写下来吧. 虚拟机操作步骤: 点击虚拟机的“菜单栏”上的“编辑”,再点击“虚拟网络 ...

  2. 公共模块定义/草案(Common Module Definition / draft - CMD草案)

    This specification addresses how modules should be written in order to be interoperable in browser-b ...

  3. Udacity并行计算课程 CS344 编程作业答案

    Problem set 1 // Homework 1 // Color to Greyscale Conversion //A common way to represent color image ...

  4. 一套oracle的练习题

    create table student( sno varchar2(10) primary key, sname varchar2(20), sage number(2), ssex varchar ...

  5. Mac下的开发工具

    1.webstrom 淘宝上2块钱就能买一个 WebStorm 是jetbrains公司旗下一款JavaScript 开发工具.被广大中国JS开发者誉为“Web前端开发神器”.“最强大的HTML5编辑 ...

  6. Unity 发送游戏画面到 Winform

    一.首先看一下Unity界面: 设了2个摄像机,位置重叠,旋转相同,父子关系,在父摄像机上加上脚本A.cs,并将子摄像机复制给A脚本中的变量Cam: Cam用于为RenderTexture提供画面,P ...

  7. (常用)re模块

    re模块(正则)#re:一些带有特殊含义的符号或者符号的组合#为什么要用re:一堆字符串中找到你所需要的内容,过滤规则是什么样,通过re模块功能来告诉计算机你的过滤规则#应用:在爬虫中最为常用:使用爬 ...

  8. Springboot分模块开发

    这是个spring cloud项目,service-base:基础服务:service-config:配置中心:service-entity:实体类: service-gateway:服务网关:ser ...

  9. 解执行maven项目出现 SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”. error

    最近再弄maven项目,运行起来没有问题,但是Console控制台会报错,比如说如下的问题异常提示: 由此我们可以看出,报出错误的地方主要是slf4j的jar包,而故障码中“Failed to loa ...

  10. Go语言规格说明书 之 变量声明(Variable/Short variable declarations)

    go version go1.11 windows/amd64 本文为阅读Go语言中文官网的规则说明书(https://golang.google.cn/ref/spec)而做的笔记,完整的介绍Go语 ...