【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式
【.net core项目实战-统一认证平台】开篇及目录索引
上篇文章我介绍了如何强制令牌过期的实现,相信大家对
identityserver4
的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方式来集成到统一认证平台。.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。
一、自定授权源码剖析
当我们需要使用开源项目的某些功能时,最好了解实现的原理,才能正确和熟练使用功能,避免出现各种未知bug问题和出现问题无法解决的被动场面。
在使用此功能前,我们需要了解完整的实现流程,下面我将从源码开始讲解identityserver4
是如何实现自定义的授权方式。
从我之前的文章中我们知道授权方式是通过grant_type
的值来判断的,所以我们自定义的授权方式,也是通过此值来区分,所以需要了解自定义的值处理流程。tokenrequestvalidator
是请求验证的方法,除了常规验证外,还增加了自定义的验证方式。
public async task<tokenrequestvalidationresult> validaterequestasync(namevaluecollection parameters, clientsecretvalidationresult clientvalidationresult) { _logger.logdebug("start token request validation"); _validatedrequest = new validatedtokenrequest { raw = parameters ?? throw new argumentnullexception(nameof(parameters)), options = _options }; if (clientvalidationresult == null) throw new argumentnullexception(nameof(clientvalidationresult)); _validatedrequest.setclient(clientvalidationresult.client, clientvalidationresult.secret, clientvalidationresult.confirmation); ///////////////////////////////////////////// // check client protocol type ///////////////////////////////////////////// if (_validatedrequest.client.protocoltype != identityserverconstants.protocoltypes.openidconnect) { logerror("client {clientid} has invalid protocol type for token endpoint: expected {expectedprotocoltype} but found {protocoltype}", _validatedrequest.client.clientid, identityserverconstants.protocoltypes.openidconnect, _validatedrequest.client.protocoltype); return invalid(oidcconstants.tokenerrors.invalidclient); } ///////////////////////////////////////////// // check grant type ///////////////////////////////////////////// var granttype = parameters.get(oidcconstants.tokenrequest.granttype); if (granttype.ismissing()) { logerror("grant type is missing"); return invalid(oidcconstants.tokenerrors.unsupportedgranttype); } if (granttype.length > _options.inputlengthrestrictions.granttype) { logerror("grant type is too long"); return invalid(oidcconstants.tokenerrors.unsupportedgranttype); } _validatedrequest.granttype = granttype; switch (granttype) { case oidcconstants.granttypes.authorizationcode: return await runvalidationasync(validateauthorizationcoderequestasync, parameters); case oidcconstants.granttypes.clientcredentials: return await runvalidationasync(validateclientcredentialsrequestasync, parameters); case oidcconstants.granttypes.password: return await runvalidationasync(validateresourceownercredentialrequestasync, parameters); case oidcconstants.granttypes.refreshtoken: return await runvalidationasync(validaterefreshtokenrequestasync, parameters); default://统一的自定义的验证方式 return await runvalidationasync(validateextensiongrantrequestasync, parameters); } }
从上面代码可以看出,除了内置的授权方式,其他的都是用validateextensiongrantrequestasync
来进行验证,详细的验证规则继续分析实现过程。
private async task<tokenrequestvalidationresult> validateextensiongrantrequestasync(namevaluecollection parameters) { _logger.logdebug("start validation of custom grant token request"); ///////////////////////////////////////////// // 校验客户端是否开启了此授权方式 ///////////////////////////////////////////// if (!_validatedrequest.client.allowedgranttypes.contains(_validatedrequest.granttype)) { logerror("{clientid} does not have the custom grant type in the allowed list, therefore requested grant is not allowed", _validatedrequest.client.clientid); return invalid(oidcconstants.tokenerrors.unsupportedgranttype); } ///////////////////////////////////////////// // 判断是否注入了此自定义的授权实现 ///////////////////////////////////////////// if (!_extensiongrantvalidator.getavailablegranttypes().contains(_validatedrequest.granttype, stringcomparer.ordinal)) { logerror("no validator is registered for the grant type: {granttype}", _validatedrequest.granttype); return invalid(oidcconstants.tokenerrors.unsupportedgranttype); } ///////////////////////////////////////////// // 校验是否支持scope ///////////////////////////////////////////// if (!await validaterequestedscopesasync(parameters)) { return invalid(oidcconstants.tokenerrors.invalidscope); } ///////////////////////////////////////////// // 调用自定义的验证实现方法 ///////////////////////////////////////////// var result = await _extensiongrantvalidator.validateasync(_validatedrequest); if (result == null) { logerror("invalid extension grant"); return invalid(oidcconstants.tokenerrors.invalidgrant); } if (result.iserror) { if (result.error.ispresent()) { logerror("invalid extension grant: {error}", result.error); return invalid(result.error, result.errordescription, result.customresponse); } else { logerror("invalid extension grant"); return invalid(oidcconstants.tokenerrors.invalidgrant, customresponse: result.customresponse); } } if (result.subject != null) { ///////////////////////////////////////////// // 判断当前的用户是否可用 ///////////////////////////////////////////// var isactivectx = new isactivecontext( result.subject, _validatedrequest.client, identityserverconstants.profileisactivecallers.extensiongrantvalidation); await _profile.isactiveasync(isactivectx); if (isactivectx.isactive == false) { // todo: raise event? logerror("user has been disabled: {subjectid}", result.subject.getsubjectid()); return invalid(oidcconstants.tokenerrors.invalidgrant); } _validatedrequest.subject = result.subject; } _logger.logdebug("validation of extension grant token request success"); return valid(result.customresponse); }
从代码中可以看出,实现流程如下:
- 1、客户端是否配置了自定义的授权方式。
- 2、是否注入了自定义的授权实现。
- 3、授权的scope客户端是否有权限。
- 4、使用自定义的授权验证方式校验请求数据是否合法。
- 5、判断是否有有效数据信息,可自行实现接口。
从源码中,可以发现流程已经非常清晰了,核心类extensiongrantvalidator
实现了自定义授权的校验过程,进一步分析下此类的代码实现。
using identityserver4.models; using microsoft.extensions.logging; using system; using system.collections.generic; using system.linq; using system.threading.tasks; namespace identityserver4.validation { /// <summary> /// validates an extension grant request using the registered validators /// </summary> public class extensiongrantvalidator { private readonly ilogger _logger; private readonly ienumerable<iextensiongrantvalidator> _validators; /// <summary> /// initializes a new instance of the <see cref="extensiongrantvalidator"/> class. /// </summary> /// <param name="validators">the validators.</param> /// <param name="logger">the logger.</param> public extensiongrantvalidator(ienumerable<iextensiongrantvalidator> validators, ilogger<extensiongrantvalidator> logger) { if (validators == null) { _validators = enumerable.empty<iextensiongrantvalidator>(); } else { _validators = validators; } _logger = logger; } /// <summary> /// gets the available grant types. /// </summary> /// <returns></returns> public ienumerable<string> getavailablegranttypes() { return _validators.select(v => v.granttype); } /// <summary> /// validates the request. /// </summary> /// <param name="request">the request.</param> /// <returns></returns> public async task<grantvalidationresult> validateasync(validatedtokenrequest request) { var validator = _validators.firstordefault(v => v.granttype.equals(request.granttype, stringcomparison.ordinal)); if (validator == null) { _logger.logerror("no validator found for grant type"); return new grantvalidationresult(tokenrequesterrors.unsupportedgranttype); } try { _logger.logtrace("calling into custom grant validator: {type}", validator.gettype().fullname); var context = new extensiongrantvalidationcontext { request = request }; await validator.validateasync(context); return context.result; } catch (exception e) { _logger.logerror(1, e, "grant validation error: {message}", e.message); return new grantvalidationresult(tokenrequesterrors.invalidgrant); } } } }
从上面代码可以发现,自定义授权方式,只需要实现iextensiongrantvalidator
接口即可,然后支持多个自定义授权方式的共同使用。
到此整个验证过程解析完毕了,然后再查看下生成token流程,实现方法为tokenresponsegenerator
,这个方法并不陌生,前几篇介绍不同的授权方式都介绍了,所以直接看实现代码。
public virtual async task<tokenresponse> processasync(tokenrequestvalidationresult request) { switch (request.validatedrequest.granttype) { case oidcconstants.granttypes.clientcredentials: return await processclientcredentialsrequestasync(request); case oidcconstants.granttypes.password: return await processpasswordrequestasync(request); case oidcconstants.granttypes.authorizationcode: return await processauthorizationcoderequestasync(request); case oidcconstants.granttypes.refreshtoken: return await processrefreshtokenrequestasync(request); default://自定义授权生成token的方式 return await processextensiongrantrequestasync(request); } } protected virtual task<tokenresponse> processextensiongrantrequestasync(tokenrequestvalidationresult request) { logger.logtrace("creating response for extension grant request"); return processtokenrequestasync(request); }
实现的代码方式和客户端模式及密码模式一样,这里就不多介绍了。
最后我们查看下是如何注入iextensiongrantvalidator
,是否对外提供接入方式,发现identityserver4
提供了addextensiongrantvalidator
扩展方法,我们自己实现自定义授权后添加即可,详细实现代码如下。
public static iidentityserverbuilder addextensiongrantvalidator<t>(this iidentityserverbuilder builder) where t : class, iextensiongrantvalidator { builder.services.addtransient<iextensiongrantvalidator, t>(); return builder; }
二、自定义授权实现
现在开始开发第一个自定义授权方式,granttype
定义为czarcustomuser
,然后实现iextensiongrantvalidator
接口,为了演示方便,我新建一个测试用户表,用来模拟老系统的登录方式。
create table czarcustomuser ( iid int identity, username varchar(50), usertruename varchar(50), userpwd varchar(100) ) --插入测试用户密码信息,测试数据密码不加密 insert into czarcustomuser values('jinyancao','金焰的世界','777777')
然后把实现验证的方法,由于代码太简单,我就直接贴代码如下。
namespace czar.authplatform.web.application.irepository { public interface iczarcustomuserrepository { /// <summary> /// 根据账号密码获取用户实体 /// </summary> /// <param name="uaccount">账号</param> /// <param name="upassword">密码</param> /// <returns></returns> czarcustomuser finduserbyuaccount(string uaccount, string upassword); } } namespace czar.authplatform.web.application.repository { public class czarcustomuserrepository : iczarcustomuserrepository { private readonly string dbconn = ""; public czarcustomuserrepository(ioptions<czarconfig> czarconfig) { dbconn = czarconfig.value.dbconnectionstrings; } /// <summary> /// 根据账号密码获取用户实体 /// </summary> /// <param name="uaccount">账号</param> /// <param name="upassword">密码</param> /// <returns></returns> public czarcustomuser finduserbyuaccount(string uaccount, string upassword) { using (var connection = new sqlconnection(dbconn)) { string sql = @"select * from czarcustomuser where username=@uaccount and userpwd=upassword "; var result = connection.queryfirstordefault<czarcustomuser>(sql, new { uaccount, upassword }); return result; } } } } namespace czar.authplatform.web.application.iservices { public interface iczarcustomuserservices { /// <summary> /// 根据账号密码获取用户实体 /// </summary> /// <param name="uaccount">账号</param> /// <param name="upassword">密码</param> /// <returns></returns> czarcustomuser finduserbyuaccount(string uaccount, string upassword); } } namespace czar.authplatform.web.application.services { public class czarcustomuserservices: iczarcustomuserservices { private readonly iczarcustomuserrepository czarcustomuserrepository; public czarcustomuserservices(iczarcustomuserrepository czarcustomuserrepository) { this.czarcustomuserrepository = czarcustomuserrepository; } /// <summary> /// 根据账号密码获取用户实体 /// </summary> /// <param name="uaccount">账号</param> /// <param name="upassword">密码</param> /// <returns></returns> public czarcustomuser finduserbyuaccount(string uaccount, string upassword) { return czarcustomuserrepository.finduserbyuaccount(uaccount, upassword); } } }
现在可以定义自定义的授权类型了,我起名为czarcustomusergrantvalidator
,实现代码如下。
using czar.authplatform.web.application.iservices; using identityserver4.models; using identityserver4.validation; using system.threading.tasks; namespace czar.authplatform.web.application.ids4 { /// <summary> /// 金焰的世界 /// 2019-01-28 /// 自定义用户授权 /// </summary> public class czarcustomusergrantvalidator : iextensiongrantvalidator { public string granttype => "czarcustomuser"; private readonly iczarcustomuserservices czarcustomuserservices; public czarcustomusergrantvalidator(iczarcustomuserservices czarcustomuserservices) { this.czarcustomuserservices = czarcustomuserservices; } public task validateasync(extensiongrantvalidationcontext context) { var username = context.request.raw.get("czar_name"); var userpassword = context.request.raw.get("czar_password"); if (string.isnullorempty(username) || string.isnullorempty(userpassword)) { context.result = new grantvalidationresult(tokenrequesterrors.invalidgrant); } //校验登录 var result = czarcustomuserservices.finduserbyuaccount(username, userpassword); if (result==null) { context.result = new grantvalidationresult(tokenrequesterrors.invalidgrant); } //添加指定的claims context.result = new grantvalidationresult( subject: result.iid.tostring(), authenticationmethod: granttype, claims: result.claims); return task.completedtask; } } }
这就实现了自定义授权的功能,是不是很简单呢?然后添加此扩展方法。
services.addidentityserver(option => { option.publicorigin = configuration["czarconfig:publicorigin"]; }) .adddevelopersigningcredential() .adddapperstore(option => { option.dbconnectionstrings = configuration["czarconfig:dbconnectionstrings"]; }) .addresourceownervalidator<czarresourceownerpasswordvalidator>() .addprofileservice<czarprofileservice>() .addsecretvalidator<jwtsecretvalidator>() //添加自定义授权 .addextensiongrantvalidator<czarcustomusergrantvalidator>();
现在是不是就可以使用自定义授权的方式了呢?打开postman
测试,按照源码解析和设计参数,测试信息如下,发现报错,原来是还未配置好客户端访问权限,开启权限测试如下。
三、客户端权限配置
在使用identityserver4
时我们一定要理解整个验证流程。根据这次配置,我再梳理下流程如下:
- 1、校验客户端client_id和client_secret。
- 2、校验客户端是否有当前的授权方式。
- 3、校验是否有请求scope权限。
- 4、如果非客户端验证,校验账号密码或自定义规则是否正确。
- 5、非客户端验证,校验授权信息是否有效。
通过此流程会发现我们缺少授权方式配置,所以请求时提示上面的提示,既然知道原因了,那就很简单的来实现,添加客户端自定义授权模式。此信息是在clientgranttypes
表中,字段为客户端id和授权方式。我测试的客户端id为21,授权方式为czarcustomuser
,那直接使用sql语句插入关系,然后再测试。
insert into clientgranttypes values(21,'czarcustomuser');
发现可以获取到预期结果,然后查看access_token是什么内容,显示如下。
显示的信息和我们定义的信息相同,而且可以通过amr
来区分授权类型,不同的业务系统使用不同的认证方式,然后统一集成到认证平台即可。
四、总结与思考
本篇我介绍了自定义授权方式,从源码解析到最后的实现详细讲解了实现原理,并使用测试的用户来实现自定义的认证流程,本篇涉及的知识点不多,但是非常重要,因为我们在使用统一身份认证时经常会遇到多种认证方式的结合,和多套不同应用用户的使用,在掌握了授权原理后,就能在不同的授权方式中切换的游刃有余。
思考下,有了这些知识后,关于短信验证码登录和扫码登录是不是有心理有底了呢?如果自己实现这类登录应该都知道从哪里下手了吧。
下篇我将介绍常用登录的短信验证码授权方式,尽情期待吧。
推荐阅读
-
【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务
-
【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4
-
【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程
-
【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权
-
【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期
-
【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式
-
【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能
-
【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权
-
【.NET Core项目实战-统一认证平台】第五章 网关篇-自定义缓存Redis
-
【.NET Core项目实战-统一认证平台】第七章 网关篇-自定义客户端限流