使用Drools实现复杂密码规则校验

796 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

引言

密码是我们使用系统登陆系统不可或缺的一部分。假如你接到一个需求,实现一套校验密码的密码规则,规则如下:

  • 密码长度为10-16位。
  • 包含大写字母、小写字母和数字。
  • 不能与最近一次密码相同。
  • 不能包含敏感词。
  • 不能包含连续数字,如123或345。

并且告诉你密码规则后期可能会根据用户反馈来优化,这个需求你会怎么实现呢?

规则引擎

规则引擎 由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。

规则引擎优势

  • 将那些频繁且通用的业务变化抽象出来,使复杂的业务规则实现变得的简单。
  • 系统代码和业务规则分离,实现规则的统一管理。
  • 可动态修改规则,需求变动快速响应。

Drools简介

常见的Java开源规则引擎有很多,今天我们来介绍一下 Drools

Drools 是一组专注于智能自动化和决策管理的项目,最引人注目的是提供基于前向链和后链推理的规则引擎,DMN决策引擎和其他项目。规则引擎是创建专家系统的基本构建块,在人工智能中,专家系统是模拟人类专家决策能力的计算机系统。Drools是KIE(知识就是一切)开源社区的一部分。

drl 语法

Drools通过编写drl文件实现规则,一个drl文件中可包含多个规则。在实现我们的密码规则需求前,先来了解一下我们的drl语法。

DRL文件基本结构

package

import

function  // Optional

query  // Optional

declare   // Optional

global   // Optional

rule "rule name"
    // Attributes
    when
        // Conditions
    then
        // Actions
end

rule "rule2 name"
...

package

package 和java的package类似,相当于命名空间。提倡package和drl文件的路径保持一致。

import

import 和java的import类似,可导入JDK中的类。

function

function 函数,可以在drl中定义函数,函数中可以编写Java代码,可提高代码的复用。

query

query 的定义可以被Java代码调用获得返回数据。

declare

declare 指定规则使用的语言类型。

global

global 可以定义一些所有规则共享的全局变量。

when 和 then

满足 when 的条件后,会执行 then 定义的代码。when相当于java中的if条件,then 相当于 满足if条件后要执行的代码逻辑。

接下来我们就通过Drools将密码规则的需求来实现一下。

Drools实现密码规则校验

首先引入Drools包(Drools 8 开始最低要求JDK11了,我们这里用JDK8做演示,选用的Drools版本是7.73.0)。

<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-spring</artifactId>
    <version>7.73.0.Final</version>
</dependency>

创建一个我们测试用的实体类。

@Data
public class PasswordRule {

    /**
     * 是否校验通过
     */
    private Boolean verify = Boolean.TRUE;

    /**
     * 密码
     */
    private String password;

    /**
     * 上一次密码
     */
    private String lastPassword = "Shuaijie666";

    /**
     * 敏感词列表
     */
    private List<String> sensitiveWordList = Arrays.asList("sensitive1", "sensitive2", "sensitive3");

    /**
     * 连续数字列表
     */
    private List<String> continuousNumberList = Arrays.asList("123", "234", "345");

    /**
     * 大写字母列表
     */
    private List<String> majusculeList = Arrays.asList("A", "B", "C", "D", "E", "F");

    /**
     * 小写字母列表
     */
    private List<String> minusculeList = Arrays.asList("a", "b", "c", "d", "e", "f");

    /**
     * 数字列表
     */
    private List<String> numberList = Arrays.asList("1", "2", "3", "4", "5", "6", "7");
}

在resources文件夹下创建 rules/password.drl 文件。

image.png

drl 文件内容如下。

package com.shuaijie.rules
dialect "java"

import com.shuaijie.model.PasswordRule
import java.util.List

// 密码长度为10-16位。
rule "rule1"
    // salience 优先级,默认为0
    salience 0
    when
        $passwordRule : PasswordRule( !(password != null && password.length() >= 10 && password.length() <= 16) )
    then
        $passwordRule.setVerify(Boolean.FALSE);
        System.out.println("密码长度为10-16位[校验不通过]");
end

// 判断是否包含大写字母、小写字母和数字
function Boolean checkRule2(PasswordRule passwordRule){
    Boolean containsMajuscule = Boolean.FALSE;
    Boolean containsMinuscule = Boolean.FALSE;
    Boolean containsNumber = Boolean.FALSE;
    for (java.lang.String majuscule : passwordRule.getMajusculeList()) {
        if(Boolean.FALSE.equals(containsMajuscule) && passwordRule.getPassword().contains(majuscule)){
            containsMajuscule = Boolean.TRUE;
        }
    }
    for (java.lang.String minuscule : passwordRule.getMinusculeList()) {
        if(Boolean.FALSE.equals(containsMinuscule) && passwordRule.getPassword().contains(minuscule)){
            containsMinuscule = Boolean.TRUE;
        }
    }
    for (java.lang.String number : passwordRule.getNumberList()) {
        if(Boolean.FALSE.equals(containsNumber) && passwordRule.getPassword().contains(number)){
            containsNumber = Boolean.TRUE;
        }
    }
    return containsMajuscule && containsMinuscule && containsNumber;
}
// 包含大写字母、小写字母和数字。
rule "rule2"
    when
        $passwordRule: PasswordRule()
        // 掉用function
        eval(!checkRule2($passwordRule))
    then
        $passwordRule.setVerify(Boolean.FALSE);
        System.out.println("包含大写字母、小写字母和数字[校验不通过]");
end


// 不能与最近一次密码相同。
rule "rule3"
    when
        $passwordRule : PasswordRule( password == lastPassword )
    then
        $passwordRule.setVerify(Boolean.FALSE);
        System.out.println("不能与最近一次密码相同[校验不通过]");
end

// 判断是否包含敏感词
function Boolean checkRule4(PasswordRule passwordRule){
    for (java.lang.String sensitiveWord : passwordRule.getSensitiveWordList()) {
        if(passwordRule.getPassword().contains(sensitiveWord)){
           return Boolean.TRUE;
        }
    }
    return Boolean.FALSE;
}
// 不能包含敏感词。
rule "rule4"
    when
        $passwordRule: PasswordRule()
        // 掉用function
        eval(checkRule4($passwordRule))
    then
        $passwordRule.setVerify(Boolean.FALSE);
        System.out.println("不能包含敏感词[校验不通过]");
end

// 判断是否包含连续数字
function Boolean checkRule5(PasswordRule passwordRule){
    for (java.lang.String continuousNumber : passwordRule.getContinuousNumberList()) {
        if(passwordRule.getPassword().contains(continuousNumber)){
           return Boolean.TRUE;
        }
    }
    return Boolean.FALSE;
}
// 不能包含连续数字,如123或345。
rule "rule5"
    when
        $passwordRule: PasswordRule()
        // 掉用function
        eval(checkRule5($passwordRule))
    then
        $passwordRule.setVerify(Boolean.FALSE);
        System.out.println("不能包含连续数字[校验不通过]");
end

准备好后,然后我们来单元测试一下。

@Test
void testDrools(){
    Resource resource = ResourceFactory.newClassPathResource("rules/password.drl");
    KieHelper kieHelper = new KieHelper();
    kieHelper.addResource(resource);
    KieBase kieBase = kieHelper.build();
    KieSession kieSession = kieBase.newKieSession();
    PasswordRule passwordRule = new PasswordRule();
    passwordRule.setPassword("123sensitive124567");
    kieSession.insert(passwordRule);
    kieSession.fireAllRules();
    kieSession.dispose();
    System.out.println("密码规则校验结果:"+passwordRule.getVerify());
}

测试执行结果如下。

密码长度为10-16[校验不通过]
包含大写字母、小写字母和数字[校验不通过]
不能包含敏感词[校验不通过]
不能包含连续数字[校验不通过]
密码规则校验结果:false

我们单元测试通过KieHelper简单实现,实际开发过程中可以通过构建Bean的方式来实现。

这一期密码规则的需求我们实现了并且上线了。上线一段时间后,根据用户反馈来优化(新增、修改或删除规则)我们怎么实现呢?

Drools动态化

面对多变的规则,Drools也是有应对措施的。Drools有多种动态化的方式,今天来使用的是 拼装字符串 的方式,这种方式使用方便快捷,但要对drl文件编写有一定程度了解。

来上Demo。(密码规则的需求过于繁琐,代码过长,为了方便阅读理解,Demo只使用 密码长度为10-16位 这条规则来做一个演示)

@Test
void testDynamicDrools(){
    String droolsStr = "package com.shuaijie.rules\n" +
            "dialect "java"\n" +
            "\n" +
            "import com.shuaijie.model.PasswordRule\n" +
            "\n" +
            "\n" +
            "\n" +
            "// 密码长度为10-16位。\n" +
            "rule "rule1"\n" +
            "    // salience 优先级,默认为0\n" +
            "    salience 0\n" +
            "    when\n" +
            "        $passwordRule : PasswordRule( !(password != null && password.length() >= 10 && password.length() <= 16) )\n" +
            "    then\n" +
            "        $passwordRule.setVerify(Boolean.FALSE);\n" +
            "        System.out.println("密码长度为10-16位[校验不通过]");\n" +
            "end";
    KieHelper kieHelper = new KieHelper();
    kieHelper.addContent(droolsStr, ResourceType.DRL);
    KieBase kieBase = kieHelper.build();
    KieSession kieSession = kieBase.newKieSession();
    PasswordRule passwordRule = new PasswordRule();
    passwordRule.setPassword("123sensitive124567");
    kieSession.insert(passwordRule);
    kieSession.fireAllRules();
    kieSession.dispose();
    System.out.println("密码规则校验结果:"+passwordRule.getVerify());
}

单元测试结果。

密码长度为10-16位[校验不通过]
密码规则校验结果:false

我们可以将规则存储到数据库中,使用时查询出来,修改的话修改数据库即可。