RPC框架简单实践

创建一个简单示例,原理如下: 运行一个RPC服务器,服务器中使用ConcurrentHashMap<String, Class>存储服务实现类的类名和实例 服务提供者Provider将自己的实现类加入该Map中去,实现服务注册 服务消费者Consumer发送HTTP的服务请求,其中包含服务类名、方法名、参数类型、参数列表四个参数 RPC服务器接收到服务请求,根据解析的请求内容到Map中取相对应的实例,通过反射执行对应方法 RPC服务器将执行方法后的结果封装到响应中返回Consumer 关于Consumer发送请求如果每次都手动构造请求体就太麻烦了,有两个实现: 编写静态的代理类,也实现了服务接口,实现内部是构造相应的请求体发送给RPC服务器,获取响应后反序列化结果返回 不过这个静态代理类需要针对每个服务接口方法硬编码请求体,比如.methodName(“getUser”)这种都要手动设置 编写动态代理类,可以基于JDK动态代理实现,这样就不用手动实现每个服务接口,invoke()方法会获取到各种调用参数信息,将这些信息封装好发送就行 共享接口 RPC服务器、服务提供者、服务消费者都需要得到服务接口信息,如此才能实例化或实现功能 用户User: public class User implements Serializable { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } 用户服务接口UserService: public interface UserService { /** * 获取用户 * * @param user * @return */ User getUser(User user); } RPC服务器 核心Handler中,先反序列化请求对象,从中取出请求的服务信息,从注册中心Map获取实现类实例后,通过反射执行方法,然后将执行结果封装为响应返回: public class HttpServerHandler implements Handler<HttpServerRequest> { @Override public void handle(HttpServerRequest request) { // 创建序列化器 final Serializer serializer = new JdkSerializer(); // 记录日志 System.out.println("Received request: " + request.method() + " " + request.uri()); // 异步处理请求 request.bodyHandler(body -> { byte[] bytes = body.getBytes(); RpcRequest rpcRequest = null; try { // 反序列化请求参数为RpcRequest对象 rpcRequest = serializer.deserialize(bytes, RpcRequest.class); } catch (Exception e) { e.printStackTrace(); } RpcResponse rpcResponse = new RpcResponse(); // 如果请求为空则返回错误信息 if (rpcRequest == null) { rpcResponse.setMessage("Invalid request, request is null"); doResponse(request, rpcResponse, serializer); return; } try { // 从请求取服务名,然后从注册中心获取服务实现类 Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName()); // 获取服务实现类的方法并执行 Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes()); Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs()); // 封装响应结果 rpcResponse.setData(result); rpcResponse.setDataType(method.getReturnType()); rpcResponse.setMessage("ok"); } catch (Exception e) { e.printStackTrace(); rpcResponse.setMessage(e.getMessage()); rpcResponse.setException(e); } doResponse(request, rpcResponse, serializer); }); } private void doResponse(HttpServerRequest request, RpcResponse rpcResponse, Serializer serializer) { HttpServerResponse httpServerResponse = request.response().putHeader("content-type", "application/json"); try { // 序列化响应结果为字节数组 byte[] bytes = serializer.serialize(rpcResponse); // 发送响应 httpServerResponse.end(Buffer.buffer(bytes)); } catch (IOException e) { e.printStackTrace(); httpServerResponse.end(Buffer.buffer()); } } } 关于注册中心,维护该Map即可: ...

March 22, 2026

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 操作逻辑部分 数据模型部分负责实现对数据模型的各种操作,因此其结构应为: ...

September 4, 2025

Keycloak自定义条件执行器开发经验

介绍 Keycloak中的自定义SPI种类有很多,在设计认证流时,可添加三种类型: 执行器(Execution) 在主流程中标签为execution;在子流程中标签为step 条件(Condition) 标签为condition 子流程(Sub-flow) 标签为flow 执行器用于直接执行某项认证,例如要求用户输入账户名密码、要求用户进行TOTP认证等,通常包括一个或多个用于认证的前端页面以与用户交互 条件用于根据某项信息进行决策判断,决定是否执行所在的流程或子流程,一般不包括前端页面(因为只需要与系统内信息交互,不会参与用户操作过程) 子流程用于划分更精细的认证流程,通常与条件共同构成认证流的某一个条件分支 接下来开发一个最简单的自定义条件类型SPI,作用是负责判断当前上下文中的属性registeringDevice的值是否为true,如果为true,则条件成立,否则不成立 开发 不同于开发自定义认证器SPI需要继承、实现多个类或接口,自定义条件SPI只需要一个实现了ConditionalAuthenticator接口的类以及相应地实现了ConditionalAuthenticatorFactory接口的工厂类即可 编写RegisterDeviceConditionAuthenticator类,实现ConditionalAuthenticator接口: public class RegisterDeviceConditionAuthenticator implements ConditionalAuthenticator { @Override public boolean matchCondition(AuthenticationFlowContext authenticationFlowContext) { String registeringDevice = authenticationFlowContext.getAuthenticationSession().getClientNote("registeringDevice"); return "true".equals(registeringDevice); } @Override public void action(AuthenticationFlowContext authenticationFlowContext) {} @Override public boolean requiresUser() { return false; } @Override public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {} @Override public void close() {} } 这个类唯一需要关注的就是matchCondition()方法,这个方法定义了【何种条件下条件成立】,在这里表现为读取registeringDevice属性并返回是否为"true" 接下来编写RegisterDeviceConditionAuthenticatorFactory类,实现ConditionalAuthenticatorFactory接口: public class RegisterDeviceConditionAuthenticatorFactory implements ConditionalAuthenticatorFactory { public static final String PROVIDER_ID = "register-device-condition"; private static final ConditionalAuthenticator SINGLETON = new RegisterDeviceConditionAuthenticator(); private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED }; @Override public String getDisplayType() { return "Condition - 注册设备时触发"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public String getHelpText() { return "只有在注册设备流程中才会执行子认证器"; } @Override public List<ProviderConfigProperty> getConfigProperties() { return null; } @Override public void init(Config.Scope scope) {} @Override public void postInit(KeycloakSessionFactory keycloakSessionFactory) {} @Override public void close() {} @Override public String getId() { return PROVIDER_ID; } // create()实际上是执行了getSingleton(),因此重写create()与重写getSingleton()是等价的 @Override public ConditionalAuthenticator getSingleton() { return SINGLETON; } } 这个工厂类定义了该条件SPI将如何展示于Keycloak系统中,并设置了“必需”、“禁用”两种可选选项,用于开关功能 ...

September 3, 2025

将Java项目打包为可执行文件

本实例的Java工程构建了一个简单的服务器,监听本地的12345端口,当接收到GET请求:/get_cpuid时,返回当前运行终端的CPUID信息 整个流程分为两部分: 工程打包为jar文件 jar文件转换为exe安装程序,安装后得到exe可执行程序 在打包为jar之前需要指定主类,Maven设置: <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>org.example.LocalHardwareService</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> 执行mvn package后会打包为一个fat jar,文件名GetDeviceInfo-1.0-SNAPSHOT-shaded.jar 警告 若不使用fat jar,则安装的exe会提示启动JVM失败 执行以下jpackage命令: jpackage --input target/ --name LocalHWService --main-jar GetDeviceInfo-1.0-SNAPSHOT-shaded.jar --main-class org.example.LocalHardwareService --type exe --vendor "FICN" --description "Local Hardware Info Service" input:输入目录。因为jar文件是被打包存储于项目的/target目录中的; main-jar:jar文件名; main-class:主类名; vendor:发行公司名; description:文件描述,会显示在任务管理器中 说明 一般来说,vendor和description属性是无需特别指定的,但由于执行这jpackage指令会使用WiX,而我所安装的WiX(v3.14.1)的这两个属性默认会使用非ASCII字符,这就导致了jpackage命令报错,所以需要手动指定这两个属性,保证是纯英文 命令完成后在当前目录生成LocalHWService-1.0.exe文件,这个文件并不能直接运行服务,而是一个安装程序 双击打开后自动安装,一般会安装到C:\Program Files中,进入子目录\LocalHWService后,可以看到文件: LocalHWService.exe就是服务文件了,双击运行,打开任务管理器可以看到其在后台正常运行: 在浏览器中进行测试,此时请求正常得到响应:

August 5, 2025

Keycloak安装自定义插件及代码解析

参考官方开发示例,使用Java为Keycloak系统编写一套个人隐私问题的认证模块 代码编写完成后,借助Maven将项目打包为jar文件,传输至集群主节点准备部署 注意在打包时需要添加resources/META-INF/services目录,并注册Factory类 导入插件 首先创建初始ConfigMap: kubectl create configmap secret-question-plugin --from-file=SecretQuestion.jar=./kc-plugins/SecretQuestion.jar -n keycloak 然后执行kubectl edit statefulset keycloak -n keycloak对keycloak的StatefulSet进行修改: spec: template: spec: volumes: - name: plugins-volume configMap: name: secret-question-plugin containers: - name: keycloak # 其他配置... volumeMounts: - name: plugins-volume mountPath: /opt/keycloak/providers/SecretQuestion.jar subPath: SecretQuestion.jar 更新插件 之后每次更新迭代插件时,需要先删除原ConfigMap,然后创建新ConfigMap,最后重启Pod即可 # 删除旧的ConfigMap kubectl delete configmap secret-question-plugin -n keycloak # 创建新的ConfigMap kubectl create configmap secret-question-plugin --from-file=SecretQuestion.jar=./kc-plugins/SecretQuestion.jar -n keycloak # 重启Keycloak Pod kubectl delete pod keycloak-0 -n keycloak 插件代码解析 将插件导入Keycloak系统后,需要先在【身份验证】【必需的操作】处将之开启,然后将自定义的认证执行器插入流程之中,如此才能使新认证功能发挥作用 这里将新认证器放置在“账户密码验证”之后且作为必需行动 当用户首先进入认证界面(账户密码界面)时,仅账户密码认证执行器在发挥作用 ...

July 13, 2025