当前位置: 移动技术网 > IT编程>开发语言>.net > 从Client应用场景介绍IdentityServer4(五)

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

2018年12月27日  | 移动技术网IT编程  | 我要评论

冬泳图片,丽嘉花园,仙剑5激活码能用几次

本节将在第四节基础上介绍如何实现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

 

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网