寫在前面
前一篇博客:最簡實例: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找到角色的順序爲:
- 根據url得到id值(menu表)
- 根據pid=1在“menu_role”中檢索出所有的rid(實際上可能有多個,多個即代表有多個角色)。
- 根據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的代碼,比較簡單。)
寫在後面
記錄學習,歡迎交流,多多指教!