背景
后台系统要支持第三方合作伙伴,且每个第三方开通的模块都不相同。以往的权限无法满足需求。这里重新实现一套数据权限。每个门诊维护一套配置。
条件显示处理器
Thymeleaf给我们提供了条件显示处理器(AbstractConditionalVisibilityAttrProcessor
)。我们只需要继承这个处理器即可。
@Component
public class ModuleEnableAttrProcessor extends AbstractConditionalVisibilityAttrProcessor {
//优先级比sec:authorize(300)靠前。保证模块先执行。
public static final int ATTR_PRECEDENCE = AuthorizeAttrProcessor.ATTR_PRECEDENCE - 1;
public static final String ATTR_NAME = "enable";
@Autowired
@Lazy
private ModuleEnableExpressionService moduleEnableExpressionService;
public ModuleEnableAttrProcessor() {
super(ATTR_NAME);
}
//控制显示与否的方法。
@Override
protected boolean isVisible(Arguments arguments, Element element, String attributeName) {
//自定义属性值。
final String attributeValue = element.getAttributeValue(attributeName);
Integer clinicId = ClinicIdContextHolder.getClinicId();
return moduleEnableExpressionService.parseExpressionByClinicId(clinicId, attributeValue);
}
@Override
public int getPrecedence() {
return ATTR_PRECEDENCE;
}
}
这里由于模块权限优先级比较高,故设置precedence=AuthorizeAttrProcessor.ATTR_PRECEDENCE - 1,只需要实现isVisible方法,返回true:显示,false:不显示。
复杂表达式的支持
由于门诊首页状态栏比较特殊,会有多个模块共同决定是否显示,或者一些模块启用,一些模块禁用等复杂情况,这里借鉴security的表达式(不知道有没有其它的更好的方法)。
public Boolean parseExpressionByClinicId(Integer clinicId, String expression){
if(StringUtils.isBlank(expression)) return false;
SpelExpressionParser template = new SpelExpressionParser();
SpelExpression spelExpression = (SpelExpression)template.parseExpression(expression);
return _handleSpelNode(clinicId, spelExpression.getAST());
}
这里取巧,通过SpelExpressionParser
来解析复杂表达式,比如:BOOKING AND !EXAM
表示有BOOKING
配置,且没有EXAM
配置。解析出来SpelNode
再逐步的判断AND
,OR
,!
语法,最后得出结果。代码如下:
private Boolean _handleSpelNode(Integer clinicId, SpelNode ast){
Class<?> clazz = ast.getClass();
if(clazz == OpAnd.class){
return _handleAnd(clinicId, (OpAnd)ast);//处理and
} else if(clazz == OpOr.class){
return _handleOr(clinicId, (OpOr)ast);//处理or
} else if(clazz == OperatorNot.class){
return _handleNot(clinicId, (OperatorNot)ast);//处理!
} else if(clazz == PropertyOrFieldReference.class){
return _handleReference(clinicId, (PropertyOrFieldReference)ast);
} else {
return false;
}
}
private boolean _handleReference(Integer clinicId, PropertyOrFieldReference reference){
String name = reference.getName().toUpperCase();
return clinicModuleService.isModuleEnabled(clinicId, ClinicModule.byName(name));
}
这里的_handleAnd
,_handleOr
,_handleNot
采用递归的方法调用_handleSpelNode
找到PropertyOrFieldReference
里面的值并处理。判断是否设置了模块权限,通过redis将配置缓存起来,减少数据库的压力。
初始化处理器
将条件显示处理器增加到thymeleaf处理器中
@Component
public class ModuleEnableDialect extends AbstractDialect {
public static final String DEFAULT_PREFIX = "module";
@Autowired
private ModuleEnableAttrProcessor moduleEnableAttrProcessor;
@Override
public String getPrefix() {
return DEFAULT_PREFIX;
}
@Override
public Set<IProcessor> getProcessors() {
final Set<IProcessor> processors = new HashSet<IProcessor>();
processors.add(moduleEnableAttrProcessor);
return processors;
}
}
Thymeleaf模板应用
只需要在标签中加入 DEFAULT_PREFIX:ATTR_NAME
属性,属性的值即为条件表达式。
<li data-type="warehouse" module:enable="MEDICINE or EXAM or CONSUMABLE" sec:authorize="hasPermission('warehouse', 'manage')"><a th:href="@{/warehouse/consume}"><i class="icon-inventory"></i>库存</a></li>