Spring Security实现动态权限设置(二)—— 动态权限设置

Spring Security实现动态权限设置(一)——基于数据库登录一文中已经介绍了Spring Security是如何实现基于数据库登录的,上文中提到要创建RoleUser实例,为了实现动态权限我们需要一个Menu实例,这个实例是用来查找数据库中路径与所需角色的,创建Menu实例也需要成员变量与数据库中Menu表的字段相对应,除此之外,还需要一个Role类型的List用来存储路径所需角色,显然这里MenuRole有一个一对多的关系,后面在做查询要注意这一点。
Menu实例

public class Menu {
    private int id;
    private String pattern;
    private List<Role> roles;

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }
}

动态权限设置思想
动态权限设置是基于数据库验证的,所有在这个“动态”是由数据库中用户权限变更保证的,不同于将用户权限写死在代码中,这里动态权限设置的大致思想是:当用户登录后,由获取用户访问路径并对其进行解析,查看数据库中访问该路径所需要的用户角色,并对比当前用户所拥有的角色,如果相匹配则可以访问;还有一些路径,只需要用户登录即可访问,无关用户角色,则可以在解析路径时返回默认标识以表示该路径无需用户角色;

MenuService和MenuMapper
在实现基于数据库登录的基础上,除了UserService还需要一个MenuServiceMenuServic中的getAllMenus方法用来获取访问menu表中路径所需的角色,那么对应的需要MenuMapper接口和XML去操作数据库:
MenuService

@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;
    public List<Menu> getAllMenus(){
        return menuMapper.getAllMenus();
    }
}

MenuMapper.xml

<mapper namespace="org.yc.security_dynamic.mapper.MenuMapper">
    <resultMap id="BaseMap" type="org.yc.security_dynamic.bean.Menu">
        <id property="id" column="id"/>
        <result property="pattern" column="pattern"/>
<collection property="roles" ofType="org.yc.security_dynamic.bean.Role">
            <id column="rid" property="id"/>
            <result column="rname" property="name"/>
            <result column="rnameZh" property="nameZh"/>
        </collection>
    </resultMap>
    <select id="getAllMenus" resultMap="BaseMap">
        select m.*,r.id as rid ,r.name as rname , r.nameZh as rnameZh from menu m left join menu_role mr on m.id = mr.mid left join role r  on mr.rid = r.id
    </select>
</mapper>

XML中的SELECT查询是一个三表联合查询,查询结果应当是每个路径所需角色,由于每个路径所需角色可能不止有一个(这里数据库表里是每一个路径只有一个所需角色,但实际项目可能不是这样),所以路径和角色应该是一对多的关系,那么回到Mybatis那一套,select标签中就不能写resultType,而应该是resultMap
完成MenuService之后可以在Test中做Service测试SQL查询结果是否是我们所期待的;这里提供一个简单测试:

@SpringBootTest
class SecurityDynamicApplicationTests {
    @Autowired
    MenuService menuService;
    @Test
    void contextLoads() {
        System.out.println(menuService.getAllMenus());
    }
}

返回结果如图:
在这里插入图片描述
FilterInvocationSecurityMetadataSource的实现类
针对menu表中的路径所需角色,需要在访问路径时,做路径解析,并获取路径角色。编写MyFileter类实现FilterInvocationSecurityMetadataSource接口的作用就是这个。

@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    //路径匹配符 直接用以匹配路径
    AntPathMatcher pathMatcher = new AntPathMatcher();
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        /*根据请求地址 分析请求该地址需要什么角色*/
        String url = ((FilterInvocation) o).getRequestUrl();    //获取请求地址
        List<Menu> allMenus =  menuService.getAllMenus();
        for (Menu menu:allMenus
             ) {
            if(pathMatcher.match(menu.getPattern() , url)){
                List<Role> roles = menu.getRoles();
                String[] rolesStr = new String[roles.size()];
                for(int i = 0;i<roles.size();i++){
                    rolesStr[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(rolesStr);     //返回请求地址所需的角色
            }
        }
        //请求地址没有匹配数据库中的地址则返回默认值,以作标识
        return SecurityConfig.createList("ROLE_login");
    }

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

    @Override
    public boolean supports(Class<?> aClass) {
        //是否支持该方法,返回true即可
        return true;
    }
}

该Filter中的getAttributes方法获取到的是访问每条路径所需要的角色,并将其存储在类型为ConfigAttributeCollection中作为返回值;接下来就要判断当前用户角色是否与访问路径所需角色相匹配,实现AccessDecisionManager接口的MyAccessDecisionManager类就是完成这件事的。

AccessDecisionManager的实现

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        /*
        * authentication: 当前登录用户信息
        * o:当前请求对象(FilterInvocation对象)
        *collection:FilterInvocationSecurityMetadataSource接口实现类中getAttributes方法的返回值
        * */
        for (ConfigAttribute attribute:collection
             ) {
            if("ROLE_login".equals(attribute.getAttribute())){      //路径不在数据库配置范围内,返回标志ROLE_login
                if(authentication instanceof AnonymousAuthenticationToken){ //用户未登录
                    throw new AccessDeniedException("非法请求!");
                }else{
                    return;      //用户已经登录 无需判断 方法到此结束
                }
            }
            //获取当前登录用户的角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority:authorities
                 ) {
                if(authority.getAuthority().equals(attribute.getAttribute())) {
                    return;      //当前登录用户具备所需的角色 则无需判断
                }
            }
        }
        throw new AccessDeniedException("非法请求!");
    }

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

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

decide方法中的以前面filter中返回的类型为ConfigAttributecollection做变量的for循环会首先判断collection中的值是不是“ROLE_login”默认标识,表示该路径只要认证(登录)就可访问,如果是,那么只要判断当前用户对象authentication不是未登录状态就可以访问(放行,直接退出方法);如果collection不是ROLE_login,则会以用户角色做for循环,只要用户角色中有能匹配当前路径的角色则退出方法。如果整个for循环都做完了仍没有退出方法,表示该访问非法,抛出异常即可。注意后面两个support方法表示是否支持该方法,返回true即可。

Security的配置类
编写完两个重要类之后,如何让它们生效呢?这就需要回到Security的配置类中,做配置:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;
    @Autowired
    MyFilter myFilter;
    @Autowired
    MyAccessDecisionManager myAccessDecisionManager;
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @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()
                .permitAll()
                .and()
                .csrf()
                .disable();
    }
}

只需在实现登录配置的基础上注入上面编写的两个类,并将这两个类配置在继承自FilterSecurityInterceptor对象中即可生效。

Controller接口
接下来就编写controller接口来测试权限访问是否成功:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello security!";
    }
    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin!";
    }
    @GetMapping("/db/hello")
    public String db(){
        return "hello db!";
    }
    @GetMapping("/user/hello")
    public String user(){
        return "hello user!";
    }
}

总结
动态权限设置,关键在于“动态”,而这个动态的实现靠的是修改数据库,那么我们要如何在Spring Security中实现动态获取数据库数据并进行判断就成了实现“动态”的关键所在。

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值