本节将在第四节基础上介绍如何实现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. mysql 原理 ~ binlog

    一 简介:我们会持续对binlog进行分析,但是不深入代码二 版本 5.6    格式    GTID和传统格式    传统格式     一 binlog针对具体事务注意点-1         1 u ...

  2. vc++调用exe获取输出信息

    目的 调用命令行程序,返回结果. 思路 把命令行结果输入到管道中,exe的输出信息都存在了strOutput这个变量里. 实现代码 CString strCmd = L"yara64.exe ...

  3. centos6安装python3.6.4

    安装Python依赖包: [root@Python src]# yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlit ...

  4. 移动端rem单位适配使用

    1.适配方法 //缩放比例!function(e,t){function i(){o=1,e.devicePixelRatioValue=o,s=1/o;var t=a.createElement(& ...

  5. Python3学习笔记11-循环语句

    条件判断使用if,需要加上冒号,当条件判断为True时,执行if下的代码块,为false就什么也不做 只要var1不是0,非空字符串,非空list等,就判断为True.否则为False var1 = ...

  6. java中printf()方法简单用法

    %n 换行 相当于 \n %c 单个字符 %d 十进制整数 %u 无符号十进制数 %f 十进制浮点数 %o 八进制数 %x 十六进制数 %s 字符串 %% 输出百分号 > 在printf()方法 ...

  7. 记录entityframework生成的sql语句

    Interceptors (EF6.1 Onwards) Starting with EF6.1 you can register interceptors in the config file. I ...

  8. sqlserver 日志传送

    可以使用日志传送将事务日志不间断地从一个数据库(主数据库)发送到另一个数据库(辅助数据库).不间断地备份主数据库中的事务日志,然后将它们复制并还原到辅助数据库,这将使辅助数据库与主数据库基本保持同步. ...

  9. 整理一下odoo10在windows系统下部署的流程

    odoo10环境搭建 所需依赖: Python3.5 odoo10.0 Node.js PostgreSQL 9.5 PyCharm 专业版 1.首先先安装好Python3.5,并设置好环境变量 2. ...

  10. 通达OA系统myisam转innodb引擎

    OA系统切换到linux环境后,性能提升了2-3倍左右,随着公司的发展壮大,办公人员也会越来越多,当人数达到一定数量级别时如1500在线人数已无法支撑公司业务,就需要对系统进行性能提升优化. 当前OA ...