前言
項目使用mybaits-plus,所以在mybaits-plus的基礎(chǔ)上增加數(shù)據(jù)權(quán)限的過濾
mybaits-plus自帶數(shù)據(jù)權(quán)限支持,但由于系統(tǒng)數(shù)據(jù)權(quán)限相對復(fù)雜,通過查看文檔發(fā)現(xiàn)好像并不適用,且原項目版本低,所以最終還是通過自己的方式實現(xiàn)
1 數(shù)據(jù)范圍
我們系統(tǒng)相對復(fù)雜,比如可以按機(jī)構(gòu)/用戶等多種維度過濾,并且可以指定全局和某個特定接口的過濾方式
其實數(shù)據(jù)范圍過濾落地也不過是:數(shù)據(jù)表的某字段限制在一個范圍內(nèi),即sql中添加column in (1,2,3...)
不管怎么說第一步都是要獲取用戶的數(shù)據(jù)范圍,比如某用戶的數(shù)據(jù)范圍為機(jī)構(gòu)id為(1,2,3)下的數(shù)據(jù),那么先要獲取(1,2,3)
首先建立一個類來存儲用戶的數(shù)據(jù)范圍,由于數(shù)據(jù)權(quán)限是多維度的,所以存儲的是一個Map>結(jié)構(gòu)
?public class GerneralScope extends HashMap> {
?}
存儲的數(shù)據(jù)類似如下
?{
??"org_id": [1,2,3], // 機(jī)構(gòu)id
??"user_id": [], // 為空代表不過濾用戶id
??"xxx_id": [4,8] // 其它為敵
?}
使用ThreadLocal進(jìn)行暫存,并在拼接sql時使用,這樣可以避免代碼侵入
?public class ScopeDataHolder {
??
???public final static ThreadLocal SCOPE_DATA = new ThreadLocal<>();
??
???public static GerneralScope get() {
?????GerneralScope gerneralScope = SCOPE_DATA.get();
?????SCOPE_DATA.remove(); // 獲取一次就刪除
?????return gerneralScope;
??}
??
???public static void set(GerneralScope data) {
?????SCOPE_DATA.set(data);
??}
?}
數(shù)據(jù)結(jié)構(gòu)準(zhǔn)備好了,接下來就是獲取當(dāng)前用戶數(shù)據(jù)范圍存入ScopeDataHolder,采用注解+AOP的方式避免代碼侵入
新增注解@Scope
?@Documented
?@Retention(RetentionPolicy.RUNTIME)
?@Target(ElementType.METHOD)
?public @interface Scope {
???ApiType value() default ApiType.COMMON;
?}
其中加一個參數(shù)value用來區(qū)分不同接口,即可實現(xiàn)特定接口單獨(dú)過濾方式
AOP獲取并設(shè)置數(shù)據(jù)范圍
?@Component
?public class ScopeAspect {
??
??@Pointcut("@annotation(com.xxx.Scope)")
???public void injectScope() {
??}
??
???/**
???* 注入數(shù)據(jù)權(quán)限
???* @param joinPoint
???* @return
???*/
???@Before("injectScope()")
???public void around(JoinPoint joinPoint) {
?????Scope annotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Scope.class);
?????GerneralScope userScopeData = getCurrentUserScopeData(annotation.value()); // 數(shù)據(jù)庫獲取當(dāng)前用戶+當(dāng)前接口的數(shù)據(jù)范圍
?????ScopeDataHolder.set(userScopeData); // 存入ThreadLocal
??}
?}
到此,零侵入代碼情況下,通過ThreadLocal暫存了用戶所配的數(shù)據(jù)范圍
2 修改SQL
獲取到了用戶的數(shù)據(jù)范圍,下一步就是在查詢中加入數(shù)據(jù)范圍的過濾,即修改sql
剛開始本來打算用mybaits-plus的自定義攔截器實現(xiàn)sql的修改,后來發(fā)現(xiàn)有很多坑,主要是當(dāng)sql中存在left join且分頁時,mybaits-plus的分頁器在count查詢時自動把沒有查詢條件的left join表去掉,如果限定數(shù)據(jù)范圍的字段剛好在join表上,就會導(dǎo)致錯誤
所以最終沒有采用攔截器,而是采取重寫mybaits-plus的QueryWrapper類來實現(xiàn),代碼如下
?public class ScopeQueryWrapper extends QueryWrapper {
??
???private final GerneralScope queryScope;
??
???public ScopeQueryWrapper() {
?????this.queryScope = ScopeDataHolder.get(); // 從ThreadLocal獲取數(shù)據(jù)范圍
?????if (this.queryScope==null) {
???????throw new IllegalStateException();
????}
??}
??
???/**
???* 過濾需要篩選的字段
???* @param column
???*/
???@SuppressWarnings("unchecked")
???public void scope(ScopeEnum type, SFunction column) {
?????List els = queryScope.get(type.getValue());
?????if (els!=null && els.size()!=0) {
???????lambda().in(column, els);
????}
??}
??
???/**
???* 過濾需要篩選的字段
???* @param fieldName
???*/
???@SuppressWarnings("unchecked")
???public void scope(ScopeEnum type, String fieldName) {
?????List els = queryScope.get(type.getValue());
?????if (els!=null && els.size()!=0) {
???????in(fieldName, els);
????}
??}
?}
這樣只需在查詢層把QueryWrapper替換為ScopeQueryWrapper,并使用scopeFilter方法來指定界限字段即可,寫法如下
?public Page page(UserQuery query) {
???Page page = new Page<>(query.getPageNum(), query.getPageSize());
???ScopeQueryWrapper wrapper = new ScopeQueryWrapper<>();
???if (query.getName()!=null) {
?????wrapper.lambda().like(User::getName,query.getName());
??}
???/** 數(shù)據(jù)權(quán)限 start **/
???wrapper.scope(ScopeEnum.orgId, User:getOrgId); // 指定機(jī)構(gòu)id字段
???wrapper.scope(ScopeEnum.userId, "user.id"); // 指定用戶id字段,字符串方式可以防止join字段重名
??...省略其它過濾條件
???/** 數(shù)據(jù)權(quán)限 end**/
???wrapper.lambda().orderByDesc(User::getId);
???Page result = page(page, wrapper);
???return result;
?}
如上,需要指定具體需要過濾的字段,由于是多維度,可能會指定很多,ScopeEnum即各維度的枚舉,scope方法中的getValue獲取到的即用戶設(shè)置范圍數(shù)據(jù)的key

ScopeEnum
scope接受字符串形式,可以避免join時字段有歧義
以上代碼出現(xiàn)了代碼的侵入,但自認(rèn)為可以接受,如果不需要多維度可以進(jìn)一步簡略
最終,執(zhí)行的sql大體如下
?select * from user where name like "%pq%" and org_id in (1,2,3) and user.id in (4,8,10)
標(biāo)簽: