SpringBoot — 安全框架 Spring Security 詳解四
阿新 • • 發佈:2020-12-14
技術標籤:SpringBootSpringBootSpringSecurit
雖然前面我們實現了通過資料庫來配置使用者與角色,但認證規則仍然是使用HttpSecurity進行配置,還是不夠靈活,無法實現資源和角色之間的動態調整。這篇文章我們就介紹一下通過資料庫查詢某個URL資源的訪問角色。
四、基於資料庫的URL許可權規則配置
1、資料庫設計
這裡在上一篇文章的基礎上再新增 資源表和資源許可權表 兩種資料表,表結構如下所示:
- 資源表,儲存每個選單的URL
CREATE TABLE `menus` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -- 主鍵 `menu` varchar(20) NOT NULL, -- 選單路徑 `menu_name` varchar(30) NOT NULL, -- 選單名稱 `enabled` tinyint(1) DEFAULT '1', -- 是否啟用 1-啟用,0-未啟用 PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
- 資源角色表,主要儲存 每個URL允許哪幾個角色訪問
CREATE TABLE `menu_role` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -- 主鍵 `m_id` bigint(20) unsigned DEFAULT NULL, -- 資源ID `r_id` bigint(20) unsigned DEFAULT NULL, -- 角色ID PRIMARY KEY (`id`), UNIQUE KEY `m_r_idx` (`m_id`,`r_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
2、建立實體類及資料訪問層
(1)、建立menus表的實體類 Menus
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Menus {
private Long id;
private String menu;
private String menuName;
private short enabled;
}
(2)、建立menus表的資料訪問層 MenusMapper
@Repository public interface MenusMapper { @Select("select * from `menus`") @Results(value = { @Result(property = "id", column = "id"), @Result(property = "menu", column = "menu"), @Result(property = "menu_name", column = "menuName"), @Result(property = "enabled", column = "enabled"), @Result(property = "roles", column = "id", many = @Many(select = "com.yuange.www.mapper.MenusMapper.queryRoleByMenuId")) }) public List<Menus> queryAllMenus(); @Select("select * from `role` as r, menu_role as mr where r.id = mr.r_id and mr.m_id = #{id}") @ResultType(Role.class) List<Role> queryRoleByMenuId(Long id); }
3、自定義 FilterInvocationSecurityMetadataSource
要實現動態配置許可權,首先需要自定義FilterInvocationSecurityMetadataSource:
注意:自定義FilterInvocationSecurityMetadataSource主要實現該介面中的getAttributes方法,該方法用來確定一個請求需要哪些角色。
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
// 建立一個AnipathMatcher,主要用來實現ant風格的URL匹配。
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Resource
private MenusMapper menusMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//從引數中獲取請求URL
String url = ((FilterInvocation) object).getRequestUrl();
//查詢所有的選單需要的許可權,實際專案中可以先載入到快取中
List<Menus> menus = menusMapper.queryAllMenus();
Collection<String> roles = null;
try {
//匹配出符合條件的URL,並把URL所需要的許可權返回
roles = menus.stream()
.filter(menu -> antPathMatcher.match(menu.getMenu(), url))
.findFirst().orElse(new Menus()).getRoles()
.stream().map(Role::getName).collect(Collectors.toList());
} catch (Exception e) {
System.err.printf("解析url[%s]需要的角色時出現異常: %s \n", url, e.getMessage());
}
//如果沒有匹配到需要重新登入
if(CollectionUtils.isEmpty(roles)){
roles = Collections.singleton("ROLE_LOGIN");
}
return SecurityConfig.createList(roles.toArray(new String[]{}));
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
4、自定義AccessDecisionManager
當一個請求走完FilterInvocationSecurityMetadataSource中的getAttributes方法後,接下來就會來到AccessDecisionManager類中進行角色資訊的對比,自定義AccessDecisionManager程式碼如下:
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//獲取當前登入使用者的角色資訊
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute ca: configAttributes){
//如果此時是 ROLE_LOGIN 角色並且使用者登入操作,則直接放開
if("ROLE_LOGIN".equals(ca.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken){
return;
}
for(GrantedAuthority ga: authorities){
//如使用者角色中有匹配的角色則直接結束
if(ca.getAttribute().equals(ga.getAuthority())){
return;
}
}
}
//沒有匹配的角色說明沒有許可權訪問此資源
throw new AccessDeniedException("許可權不足!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
5、配置Security
這裡與前文的配置相比,主要是修改了configure(HttpSecurity http)方法的實現並注入了兩個Bean。至此我們邊實現了動態許可權配置,許可權和資源的關係可以在menu_role表中動態調整。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//注入userDetailsService
@Resource
private UserDetailsService userDetailsService;
@Resource
@Qualifier("MyAccessDecisionManager")
private AccessDecisionManager accessDecisionManager;
@Resource
@Qualifier("MySecurityMetadataSource")
private FilterInvocationSecurityMetadataSource metadataSource;
//配置記憶體使用者
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService) //修改預設的 userDetailsService
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
// URL訪問許可權配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(metadataSource);
return object;
}
})
.and().formLogin().loginProcessingUrl("/login").permitAll()
.and().csrf().disable();
}
}
6、重啟執行測試
(1)、用 admin 使用者登入訪問 “/add” 可以正常訪問
(2)、用 user 使用者登入訪問 “/add”則出現如下錯誤