从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的方法已过时,但我按官网文档的说明,使用新方法(注释的代码),获取不到信息,还望大家指点。
官网链接:
所以这里还是按老方法来获取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