SpringBoot — 安全框架 Spring Security 详解四

虽然前面我们实现了通过数据库来配置用户与角色,但认证规则仍然是使用 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”则出现如下错误

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值