Keycloak两种自定义认证器配合联动
上一篇文章实现了Keycloak中的自定义条件,目的就是为本篇文章将两种自定义认证器(Execution)配合使用的操作提供基础 文章开始前,为了表述方便,提出如下简略说法: 隐私问题认证:SQA 设备指纹认证 / 设备信息认证:DA 流程 设计这样一个流程: 用户输入账户名与密码 要求用户进行设备信息验证 若信息正确,则通过认证,不进行SQA 若信息不正确且用户未选择【注册新设备】,则认证失败 若信息不正确且用户选择【注册新设备】,则转到SQA 要求用户进行SQA 若回答正确,则通过认证并保存新设备信息凭证 若回答错误,则认证失败 容易看出:SQA是否执行,取决于DA的结果与用户彼时的选择;在模块之间,SQA模块需要与DA模块进行联动 SQA是否执行的问题可以由上一篇文章中开发的自定义条件SPI解决,而模块之间进行联动的问题则需要进行进一步的开发、重构工作 在Keycloak管理界面下,设计的认证流程表示为: 注意 用户选择注册新设备时,为了使流程继续下去,DA实际上给出的是无条件通过的认证结果,但并不会就此将新设备信息保存,而是待SQA通过后,由SQA模块将新设备信息保存为凭证 传值 分析整个流程可知,设备信息的存储工作是由SQA模块完成的,而问题在于开发时两个模块并不在同一项目下,因此需要借助Keycloak的上下文环境传递信息,这些信息包括: 用户是否注册新设备 新设备CPUID 新设备浏览器指纹 新设备名称 注意 虽然已有自定义条件SPI负责控制流程分支,但“用户是否注册新设备”这个信息仍然是必要的,这是为了区分当前需要进行纯粹的隐私问题认证还是仅作为其他认证的辅助手段,关系到本模块后续是否需要获取其他相关数据 传递方式:使用authenticationFlowContext.getAuthenticationSession().setClientNote()传出;使用authenticationFlowContext.getAuthenticationSession().getClientNote()接收 解耦 必要性说明 之所以需要传值,就是因为DA模块需要将设备信息交给SQA模块,由SQA模块存储对应的新设备信息凭证,所以,实际上凭证存储这个工作是由隐私认证模块完成的 在原本的设备指纹认证模块中,数据、操作、业务三者是共同存在于同一个jar包里的,在进行凭证存储操作时,DeviceAuthRequiredAction中的代码为: // 从上下文得到Provider实例 DeviceAuthCredentialProvider dacp = (DeviceAuthCredentialProvider) requiredActionContext.getSession().getProvider(CredentialProvider.class, "device-auth"); // 现场构造一个CredentialModel实例,调用Provider实例的createCredential()进行存储 dacp.createCredential(requiredActionContext.getRealm(), requiredActionContext.getUser(), DeviceAuthCredentialModel.createDeviceAuth(hostName, cpuid, visitorId)); 如果SQA模块想要保存设备信息的凭证,则面临着以下难题: SQA模块中并未定义DeviceAuthCredentialProvider、DeviceAuthCredentialModel的类,就算可以从上下文中得到DeviceAuthCredentialProvider的实例,也无法调用设备信息类型的Provider实例中的任何方法,类似地,也根本无法构造设备信息类型的CredentialModel实例 要想解决这个问题,有两个方案: 将设备信息类型的CredentialModel、CredentialProvider、CredentialProviderFactory三个类完全复制到SQA模块中,使得模块可以识别设备信息数据类型。这就相当于使SQA模块从头开始认识了设备信息类型凭证,开发难度不高但代码冗余 将DA模块中的数据操作部分独立出来,DA模块与SQA模块都通过调用这个独立的数据操作部分来完成各自的业务。需要对代码进行重构但能保证代码的可维护性与简洁性,更符合开发规范 考虑到今后还有可能引入更多需要对设备信息凭证进行操作的自定义认证器,我选择方案2,并决定将DA模块分离为三个部分:数据模型、操作逻辑和上层业务 在这三个部分中,数据模型(dto、CredentialModel及其他接口)作为API向其他认证器提供操作入口,操作逻辑(Provider及对应工厂类)仅负责实现需要对数据模型进行的操作,例如存储新凭证和删除凭证,上层业务(DA、SQA的交互部分)处理用户请求,根据需要调用数据模型提供的API完成业务 注意 在这个方案中,我们规定上层业务只能调用数据模型提供的功能接口,而不能直接调用操作逻辑 数据模型部分 数据模型部分负责定义数据类型并为上层业务提供可用的接口API,基于此需求,数据模型部分的结构应为: DTOs Constants类 CredentialModel类 CredentialProvider接口 说明 DTOs和CredentialModel类用于定义凭证数据的格式与类型 Constants类负责存储常量字段,例如ProviderFactory的PROVIDER_ID,这一字段将用于上层业务获取Provider实例 定义CredentialProvider接口的本质就是向上层业务提供API 在规划中,上层业务并不直接调用操作逻辑,上层业务可以直接从上下文获取到Provider,需要解决的问题仅仅是如何承接获取到的Provider实例,这就是定义CredentialProvider接口的目的,它使得上层业务得以承接Provider实例并调用其中的各方法 这里给出CredentialProvider接口代码作为参考: public interface DeviceAuthCredentialProvider extends CredentialProvider<DeviceAuthCredentialModel>, CredentialInputValidator { @Override boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String s); @Override boolean isValid(RealmModel realmModel, UserModel userModel, CredentialInput credentialInput); @Override default void close() { CredentialProvider.super.close(); } @Override String getType(); @Override CredentialModel createCredential(RealmModel realmModel, UserModel userModel, DeviceAuthCredentialModel deviceAuthCredentialModel); @Override boolean deleteCredential(RealmModel realmModel, UserModel userModel, String s); @Override DeviceAuthCredentialModel getCredentialFromModel(CredentialModel credentialModel); @Override default DeviceAuthCredentialModel getDefaultCredential(KeycloakSession session, RealmModel realm, UserModel user) { return CredentialProvider.super.getDefaultCredential(session, realm, user); } @Override CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext credentialTypeMetadataContext); @Override default CredentialMetadata getCredentialMetadata(DeviceAuthCredentialModel credentialModel, CredentialTypeMetadata credentialTypeMetadata) { return CredentialProvider.super.getCredentialMetadata(credentialModel, credentialTypeMetadata); } @Override default boolean supportsCredentialType(CredentialModel credential) { return CredentialProvider.super.supportsCredentialType(credential); } @Override default boolean supportsCredentialType(String type) { return CredentialProvider.super.supportsCredentialType(type); } } 提示 数据模型部分无需在META-INF下进行注册,事实上也并没有能够注册的ProviderFactory 操作逻辑部分 数据模型部分负责实现对数据模型的各种操作,因此其结构应为: ...