1. 程式人生 > 其它 >SpringBoot — 安全框架 Spring Security 詳解四

SpringBoot — 安全框架 Spring Security 詳解四

技術標籤: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”則出現如下錯誤