一篇文章带你搞定 SpringSecurity 中的动态权限配置

一、前期准备

1. 数据库脚本

USE `security_dy`;
/*Table structure for table `menu` */
DROP TABLE IF EXISTS `menu`;

CREATE TABLE `menu` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `pattern` VARCHAR(128) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `menu` */

INSERT  INTO `menu`(`id`,`pattern`) VALUES 
(1,'/db/**'),
(2,'/admin/**'),
(3,'/user/**');

/*Table structure for table `menu_role` */

DROP TABLE IF EXISTS `menu_role`;

CREATE TABLE `menu_role` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `mid` INT(11) DEFAULT NULL,
  `rid` INT(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `menu_role` */

INSERT  INTO `menu_role`(`id`,`mid`,`rid`) VALUES 
(1,1,1),
(2,2,2),
(3,3,3);

/*Table structure for table `role` */

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(32) DEFAULT NULL,
  `nameZh` VARCHAR(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `role` */

INSERT  INTO `role`(`id`,`name`,`nameZh`) VALUES 
(1,'ROLE_dba','数据库管理员'),
(2,'ROLE_admin','系统管理员'),
(3,'ROLE_user','用户');

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(32) DEFAULT NULL,
  `password` VARCHAR(255) DEFAULT NULL,
  `enabled` TINYINT(1) DEFAULT NULL,
  `locked` TINYINT(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `user` */

INSERT  INTO `user`(`id`,`username`,`password`,`enabled`,`locked`) VALUES 
(1,'root','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0),
(2,'admin','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0),
(3,'sang','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0);

/*Table structure for table `user_role` */

DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `uid` INT(11) DEFAULT NULL,
  `rid` INT(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

/*Data for the table `user_role` */

INSERT  INTO `user_role`(`id`,`uid`,`rid`) VALUES 
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);

创建成功:

在这里插入图片描述
这里注意,创建角色时已经为其加上了ROLE_,所以下面创建实体类时就不用添加了.

menu 表:存储角色对应的权限

在这里插入图片描述

2. 创建 SpringBoot 项目加入依赖

在这里插入图片描述
锁定mysql 的版本号,同时加入druid 连接池

在这里插入图片描述
配置 application.properties

spring.datasource.password=root
spring.datasource.username=root
spring.datasource.url=jdbc:mysql://localhost:3306/security_dy
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

二、定义 Service

1. UserService

@Service
public class UserService implements UserDetailsService {
    
    
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
    
        User user = userMapper.loadUserByUsername(s);
        if (user == null) {
    
    
            throw new UsernameNotFoundException("用户不存在!");
        }
        user.setRoles(userMapper.getRolesById(user.getId()));
        return user;
    }
}

2. MenuService

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

三、定义 Mapper

由于这里定义的 Mapper 比较多,所以直接把扫描路径加到启动类里:

@MapperScan(basePackages = "org.yolo.securitydy.mapper")

在这里插入图片描述

在 pom 中文件配置资源扫描路径:

		<resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource><directory>src/main/resources</directory></resource>
        </resources>

1. 定义 UserMapper 和 UserMapper.xml

public interface UserMapper {
    
    

    User loadUserByUsername(String s);

    List<Role> getRolesById(Integer id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.yolo.securitydy.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="org.yolo.securitydy.bean.User">
        select * from user where username=#{
    
    username}
    </select>
    <select id="getRolesById" resultType="org.yolo.securitydy.bean.Role">
        select * from role where id in(select rid from user_role where uid=#{
    
    id})
    </select>
</mapper>

2. 定义 MenuMapper 和 MenuMapper.xml

public interface MenuMapper {
    
    
    List<Menu> getAllMenus();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.yolo.securitydy.mapper.MenuMapper">
    <resultMap id="BaseResultMap" type="org.yolo.securitydy.bean.Menu">
        <id property="id" column="id"/>
        <result property="pattern" column="pattern"/>
        <collection property="roles" ofType="org.yolo.securitydy.bean.Role">
            <id column="rid" property="id"/>
            <result column="rname" property="name"/>
            <result column="rnameZh" property="nameZh"/>
        </collection>
    </resultMap>
    <select id="getAllMenus" resultMap="BaseResultMap">
        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>

三、定义 Config

现在想要访问哪个资源就具备那个角色,但是现在资源路径在数据库里:每个角色对应着不同的访问资源路径

在这里插入图片描述

1. MyFilter

定义一个过滤器,主要用于分析出请求地址

@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
    
    
    /**
     *  路径匹配符
     */
    AntPathMatcher pathMatcher = new AntPathMatcher();
    @Autowired
    MenuService menuService;

    /**
     * 这个集合返回的是需要的角色
     * @param o
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
    
    
        //获得请求的地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        //获取数据库中的角色对应的资源路径信息(因为每次都要获取,所以可以结合 redis 将其存储到缓存中)
        List<Menu> allMenus = menuService.getAllMenus();
        //将请求地址信息和数据库中的资源路径信息进行对比
        for (Menu menu : allMenus) {
    
    
            //匹配成功,获取需要的角色
            if (pathMatcher.match(menu.getPattern(), requestUrl)) {
    
    
                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 org.springframework.security.access.SecurityConfig.createList(rolesStr);

            }
        }
        //默认的返回值,相当于一个特殊的标记符,拿到这个标记符单独做处理即可
        return org.springframework.security.access.SecurityConfig.createList("ROLE_login");
    }

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

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

2. MyAccessDecisionManager

主要实现,拿认证的信息和访问资源路径需要的信息进行比对,看是否符合需要的信息要求

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    
    
    /**
     *
     * @param authentication:保存了当前登录用户的认证信息,有哪些角色
     * @param o:获取当前请求对象
     * @param collection:MyFilter 的返回值,访问资源路径需要哪些角色
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
    
    
        //需要的角色
        for (ConfigAttribute attribute : collection) {
    
    
            //对设置的默认值做处理
            if ("ROLE_login".equals(attribute.getAttribute())) {
    
    
                //如果是匿名登录则抛异常
                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;
    }
}

3. SecurityConfig

@Configuration
public class SecurityConfig 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);
    }

    /**
     * 将 MyFilter 和 MyAccessDecisionManager 配置加入
     * @param http
     * @throws Exception
     */
    @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();
    }
}

四、定义 Controller

@RestController
public class HelloController {
    
    
    @GetMapping("/hello")
    public String hello() {
    
    
        return "hello";
    }

    @GetMapping("/db/hello")
    public String db() {
    
    
        return "hello db";
    }

    @GetMapping("/admin/hello")
    public String admin() {
    
    
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user() {
    
    
        return "hello user";
    }
}

对于 hello 接口,并没有存储路径资源,所以都可以访问

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/nanhuaibeian/article/details/108836562