spring security 動態權限管理

寫在前面

前一篇博客:最簡實例:springboot+springsecurity+JPA+mysql實現登陸限制,這一個示例裏,用戶的用戶名、密碼以及角色表配置在數據庫裏。但是角色訪問的路徑在程序裏寫死了,如下:

.antMatchers("/favicon.ico","/css/**","/common/**","/js/**","/images/**","/login","/userLogin","/login-error").permitAll()//靜態資源,都可訪問
                .antMatchers("/root/**").hasRole("root")//"/root/**"只有root能訪問
                .antMatchers("user/**").hasRole("user")
                .anyRequest().authenticated() //其它請求只需登陸即可
                .and()
                .formLogin();

現在將角色的訪問權限也寫入數據庫,就可以動態的改變用戶的權限了,config下多配置了兩個組件。
在這裏插入圖片描述

思路

一句話概括: 訪問時,在數據庫內檢索你訪問的url需要什麼角色,判斷是否與自己的登陸角色相同,相同則可以訪問,不相同則禁止訪問。

訪問時,攔截請求得到請求的url,然後檢索出數據庫中存放的所有url地址(如下圖),與攔截的url對比,看是否匹配。
在這裏插入圖片描述
如果匹配(比如我訪問的是“/root/hello”,那麼就跟上圖第一條匹配),接下來使用“/root/**”在數據庫內進行查詢,查詢的目的是找到“能夠訪問該地址的角色(role)”。這就看你怎麼設置你的數據表了。
最簡單的,你可以直接把url地址和role值放在一個表內,不用一次去查詢多個表。如下:
在這裏插入圖片描述
但是有更專業的做法:
在這裏插入圖片描述
menu表就保存所有的需要權限管理的url,role表保存所有的角色,中間這個menu_role表保存兩個表之間的映射。比如menu_role中這條數據意思爲menu表內id爲1的數據對應着role表內id爲1的數據。那麼現在根據url找到角色的順序爲:

  1. 根據url得到id值(menu表)
  2. 根據pid=1在“menu_role”中檢索出所有的rid(實際上可能有多個,多個即代表有多個角色)。
  3. 根據rid在role表內檢索出所有角色。

爲什麼說這種方法更專業,可以看看我上篇博客,實際上user(報錯登陸的用戶名密碼)和role也需要映射關係,如果按照上述簡單方法,那麼一張表內要同時存放用戶的用戶名、密碼,角色,以及可訪問的地址。然而用戶本身跟可訪問地址是不存在直接關係的,可訪問地址是由擁有的角色決定的。用後述方法結構將會更清晰,改動起來也會比較方便。所以整個的spring security動態權限管理動態其實需要下面5張表:(user、menu分別與role存在對應關係)
在這裏插入圖片描述

代碼實現

新建MenuService

根據上述的思路,service需要提供的方法有兩個:1.檢索出所有需要權限管理的url。2根據url檢索出角色信息

public class MenuService {
    @Resource
    MenuRepository menuRepository;
    @Resource
    MenuRoleRepository menuRoleRepository;
    @Resource
    RoleRepository roleRepository;
	
	// 根據根據url檢索出角色信息
    public List<String> requestedRoles(String pattern){
        Menu menu = menuRepository.findByPattern(pattern);
        List<String> roleList=new ArrayList<>();
        // 在menu_role表內根據mid查出所有rid,再在role表內根據rid檢索出所有角色
        for (MenuRole menuRole : menuRoleRepository.findByMid((menu.getId()))) {
            Role role = roleRepository.findById(menuRole.getRid());
            roleList.add(role.getName());
        }
        return roleList;
    }

	// 檢索出所有需要權限管理的url
    public List<String> getAll(){
        List<String> patterns=new ArrayList<>();
        for (Menu menu : menuRepository.findAll()) {
            patterns.add(menu.getPattern());
        }
        return patterns;
    }

}

新建 MyFilter

在config下新建 MyFilter,需要實現FilterInvocationSecurityMetadataSource,所以要重寫三個方法。
將supports方法的返回值改爲true。
這一個類的目的是:根據訪問的url,向數據庫進行操作,最終返回“訪問該url所需要的角色信息”。

@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
    AntPathMatcher pathMatcher=new AntPathMatcher();
    @Autowired
    MenuService menuService;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
    	// 得到請求的url
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        System.out.println(requestUrl);
        List<String> roleList = new ArrayList<>();
        for (String pattern : menuService.getAll()) {
            if (pathMatcher.match(pattern,requestUrl)){
                roleList = menuService.requestedRoles(pattern);
                // 以爲SecurityConfig.createList()的參數類型是String... 
                // 所以這裏吧List<String>換成String[]
                String[] roleArray=new String[roleList.size()];
                for (int i=0;i<roleList.size();i++){
                    roleArray[i]=roleList.get(0);
                }
                System.out.println(1);
                return SecurityConfig.createList(roleArray);
            }

        }
        System.out.println(2);
        // 如果沒有匹配成功,返回一個角色叫做"ROLE_Login"
        // 這個名字自定義的,只是爲了在後續做判斷:如果是ROLE_Login,則頁面只需登陸就能訪問
        return SecurityConfig.createList("ROLE_Login");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

新建MyAccessDecisionManager

需實現AccessDecisionManager,將下面兩個方法返回值改爲true
這個類接受上一個類的返回值——即需求的角色信息,判斷:1.如果是"ROLE_Login",則只要登陸就能訪問;未登錄則拋異常,拒絕訪問。2.是否與自身角色相同,相同則正常訪問,不同則拋異常,禁止訪問。

主要改動第一個方法,Authentication包含登陸信息,Collection< ConfigAttribute>爲上一個類返回值。

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : collection) {
            if("ROLE_Login".equals(configAttribute.getAttribute())){
            	// 如果未登錄
                if (authentication instanceof AnonymousAuthenticationToken) {
                    System.out.println("error1");
                    throw new AccessDeniedException("非法請求");
                }
                else {
                    System.out.println(3);
                    return;
                }
            }
            for (GrantedAuthority authority : authentication.getAuthorities()) {
                System.out.println(authority.getAuthority());
                // 判斷要求的角色是否與自身角色相同
                if (authority.getAuthority().equals(configAttribute.getAttribute())) {
                    System.out.println(4);
                    return;
                }
            }
        }
        System.out.println("error2");
        throw new AccessDeniedException("非法請求");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

SecurityConfig內配置組件

重寫configure(HttpSecurity http),配置我們前面定義的兩個類。

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(myAccessDecisionManager);
                        o.setSecurityMetadataSource(myFilter);
                        return o;
                    }
                })
                .and()
                .formLogin();
    }

到這就配置好了。(省略了一些model、repository的代碼,比較簡單。)

寫在後面

記錄學習,歡迎交流,多多指教!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章