欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-25 08:05:30
本节将在第四节基础上介绍如何实现IdentityServer4从数据库获取User进行验证,并对Claim进行权限设置。 一、新建Web API资源服务,命名为ResourceAPI (1)新建API项目,用来进行user的身份验证服务。 (2)配置端口为5001 安装Microsoft.Entit ......

本节将在第四节基础上介绍如何实现identityserver4从数据库获取user进行验证,并对claim进行权限设置。


一、新建web api资源服务,命名为resourceapi

(1)新建api项目,用来进行user的身份验证服务。

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

(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的数据。

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


 

二、实现获取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());
        }

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

好了,资源服务器获取user的接口完成了。

(3)接着回到authserver项目,把user改成从数据库进行验证。

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

找到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的方法已过时,但我按官网文档的说明,使用新方法(注释的代码),获取不到信息,还望大家指点。

官网链接:

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

所以这里还是按老方法来获取tokenresponse。

(4)到这步后,可以把startup中configureservices方法里面的addtestusers去掉了。

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

运行程序,已经可以从数据进行user验证了。

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

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

点击进入about页面时候,出现没有权限提示,我们会发现从数据库获取的user中的claims不起作用了。

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


 

三、使用数据数据自定义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配置

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

(6)最后记得在startup的configureservices方法加上

.addresourceownervalidator<resourceownerpasswordvalidator>()

.addprofileservice<profileservice>();

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

 

运行后,出现熟悉的about页面(access token后面加上去的,源码上有添加方法)

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


 本节介绍的identityserver4通过访问接口的形式验证从数据库获取的user信息。当然,也可以写成authserver授权服务通过连接数据库进行验证。

另外,授权服务访问资源服务api,用的是clientcredentials模式(服务与服务之间访问)。

参考博客:

源码地址:https://github.com/bingjian-zhu/mvc-hybridflow.git