四、Spring Security认证和授权-动态授权解决方案

本文介绍了如何基于Spring Security实现动态权限控制。通过介绍RBAC模型、数据库设计,提供两种动态权限解决方案,包括自定义过滤器和access()方法设置动态权限,并给出资源管理系统演示链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上面两期已经学习了认证和授权的相关知识并且对源码进行了解析,相信这些知识点应付简单系统权限控制已经足够,但是如果想要实现复杂的权限控制,就需要自己进自定义开发了。今天我们来学习一下如何基于Spring Security,实时从数据库里查询用户权限进行权限校验,实现动态权限控制。

一、RBAC简介

动态权限开发以前,我们必须要有一套明确的权限系统。下面我们基于RBAC来讲解具体实现流程。
RBAC(role-based access control),基于角色的权限控制系统,是指对于不同角色的用户,拥有不同的权限 。用户绑定角色,角色绑定菜单权限和资源权限,形成用户-角色-权限的关系,如下图所示。
用户角色权限关系图:
在这里插入图片描述

二、RBAC数据库设计

用户角色权限关系数据库模型
在这里插入图片描述
建表语句

/*
 Navicat Premium Data Transfer

 Source Server         : MysqlLocal
 Source Server Type    : MySQL
 Source Server Version : 50728
 Source Host           : localhost:3306
 Source Schema         : auth

 Target Server Type    : MySQL
 Target Server Version : 50728
 File Encoding         : 65001

 Date: 03/09/2020 11:49:20
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for auth_menu
-- ----------------------------
DROP TABLE IF EXISTS `auth_menu`;
CREATE TABLE `auth_menu`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) NULL DEFAULT NULL COMMENT '父级ID',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '前端名称',
  `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单名称',
  `level` int(4) NULL DEFAULT NULL COMMENT '菜单级数',
  `icon` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '前端图标',
  `sort` int(4) NULL DEFAULT NULL COMMENT '菜单排序',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `hidden` int(1) NULL DEFAULT NULL COMMENT '前端隐藏',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 33 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_resource
-- ----------------------------
DROP TABLE IF EXISTS `auth_resource`;
CREATE TABLE `auth_resource`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `category_id` bigint(20) NULL DEFAULT NULL COMMENT '资源分类ID',
  `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源名称',
  `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
  `url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源URL',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `pk_ar_category_id`(`category_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 35 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_resource_category
-- ----------------------------
DROP TABLE IF EXISTS `auth_resource_category`;
CREATE TABLE `auth_resource_category`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类名称',
  `sort` int(4) NULL DEFAULT NULL COMMENT '排序',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '资源分类' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_role
-- ----------------------------
DROP TABLE IF EXISTS `auth_role`;
CREATE TABLE `auth_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
  `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
  `user_count` int(11) NULL DEFAULT NULL COMMENT '后台用户数量',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `status` int(1) NULL DEFAULT 1 COMMENT '启用状态:0->禁用;1->启用',
  `sort` int(11) NULL DEFAULT 0 COMMENT '排序',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `auth_role_menu`;
CREATE TABLE `auth_role_menu`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NULL DEFAULT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NULL DEFAULT NULL COMMENT '菜单ID',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `pk_rm_role_id`(`role_id`) USING BTREE,
  INDEX `pk_rm_menu_id`(`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 240 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_role_resource
-- ----------------------------
DROP TABLE IF EXISTS `auth_role_resource`;
CREATE TABLE `auth_role_resource`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NULL DEFAULT NULL COMMENT '角色ID',
  `resource_id` bigint(20) NULL DEFAULT NULL COMMENT '资源ID',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `pk_rc_role_id`(`role_id`) USING BTREE,
  INDEX `pk_rc_resource_id`(`resource_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 344 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_user
-- ----------------------------
DROP TABLE IF EXISTS `auth_user`;
CREATE TABLE `auth_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
  `nickname` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `id_number` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '身份证号',
  `sex` int(1) NULL DEFAULT NULL COMMENT '性别:0->女;1->男',
  `birthday` datetime(0) NULL DEFAULT NULL COMMENT '出生日期',
  `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系方式',
  `icon` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
  `note` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注信息',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `login_time` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间',
  `status` int(1) NULL DEFAULT 1 COMMENT '帐号启用状态:0->禁用;1->启用',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for auth_user_role
-- ----------------------------
DROP TABLE IF EXISTS `auth_user_role`;
CREATE TABLE `auth_user_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NULL DEFAULT NULL,
  `role_id` bigint(20) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `pk_ur_user_id`(`user_id`) USING BTREE,
  INDEX `pk_ur_role_id`(`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 58 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

三、动态权限解决方案一

在这里插入图片描述
Spring Security中是通过FilterSecurityInterceptor过滤器实现权限验证的,实现的具体细节上期已经介绍。要实现动态权限校验,可以在FilterSecurityInterceptor前增加一个自定义的过滤器,实现自己的权限校验逻辑。
3.1、定义一个动态权限相关业务接口DynamicSecurityService,有一个方法loadDataSource(),用来加载资源ANT通配符和资源对应MAP。具体实现由业务系统是实现。
接口:

public interface DynamicSecurityService {
    /**
     * 加载资源ANT通配符和资源对应MAP
     */
    Map<String, ConfigAttribute> loadDataSource();
}

实现类:

/**
 * @author FishAndFlower
 * @title: rmsDynamicSecurityService
 * @projectName rms
 * @description: 用户动态权限加载
 * @date 2020/7/30 10:01
 */
@Component
public class AuthDynamicSecurityService implements DynamicSecurityService {
    @Autowired
    private ResourceService resourceService;

    @Override
    public Map<String, ConfigAttribute> loadDataSource() {
        Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
        List<AuthResourceDto> resourceList = resourceService.queryAuthResources(new AuthResourceQueryCondition());
        for (AuthResource resource : resourceList) {
            map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
        }
        return map;
    }
}

3.2、定义一个动态权限数据源(DynamicSecurityMetadataSource),实现FilterInvocationSecurityMetadataSource接口,重写getAttributes()方法,自动装配DynamicSecurityService实现类,用于获取动态权限规则。

@Component
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Map<String, ConfigAttribute> configAttributeMap = null;
    @Autowired
    private DynamicSecurityService dynamicSecurityService;

    @PostConstruct
    public void loadDataSource() {
        configAttributeMap = dynamicSecurityService.loadDataSource();//加载所有的URL和资源map
    }

    /**
     * 获取访问路径所需要的权限
     * @param o
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        if (configAttributeMap == null) this.loadDataSource();
        List<ConfigAttribute>  configAttributes = new ArrayList<>();
        //获取当前访问的路径
        String url = ((FilterInvocation) o).getRequestUrl();
        String path = URLUtil.getPath(url);
        PathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = configAttributeMap.keySet().iterator();
        //获取访问该路径所需资源
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            if (pathMatcher.match(pattern, path)) {
                configAttributes.add(configAttributeMap.get(pattern));
            }
        }
        // 未设置操作请求权限,返回空集合
        return configAttributes;
    }

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

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

}

3.3、定义一个动态权限决策管理器(DynamicAccessDecisionManager),用于判断用户是否有访问权限,实现AccessDecisionManager接口,重写decide()方法和supports()方法,decide用于判断用户是都有去访问权限。

@Component
@Slf4j
public class DynamicAccessDecisionManager implements AccessDecisionManager {

    /**
     * 判断是都有访问权限
     * @param authentication 登录用户授权信息
     * @param object
     * @param configAttributes 该请求需要的访问权限
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        // 当接口未被配置资源时直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            log.info("未配置资源访问限制");
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //将访问所需资源或用户拥有资源进行比对
            String needAuthority = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("抱歉,您没有访问权限");
    }

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

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

}

3.4、定义一个DynamicSecurityFilter过滤器,继承AbstractSecurityInterceptor抽象类,自动装配DynamicAccessDecisionManager和DynamicSecurityMetadataSource,实现动态权限过滤器。

@Component
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //OPTIONS请求直接放行
        if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return dynamicSecurityMetadataSource;
    }

}

3.5、 定义好过滤器后,需要将过滤器配置到FilterSecurityInterceptor之前:
在这里插入图片描述

四、动态权限解决方案二

使用access()方法设置动态权限也是很好的办法,相比方案一更简单明了。只需要编写一个动态权限方法,然后配置一下校验规则即可:

@Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        boolean hasPermission = false;
        //获取请求路径
        String url = request.getRequestURI();
        String path = URLUtil.getPath(url);

        //获取访问该路径所需资源权限
        PathMatcher pathMatcher = new AntPathMatcher();
        Collection<ConfigAttribute> configAttributes = new ArrayList<>();
        List<AuthResourceDto> resourceList = resourceService.queryAuthResources(new AuthResourceQueryCondition());
        for (AuthResource resource : resourceList) {
            if(pathMatcher.match(resource.getUrl(), path)){
                configAttributes.add(new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
            }
        }

        //判断是否有权限
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //将访问所需资源或用户拥有资源进行比对
            String needAuthority = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    hasPermission = true;
                }
            }
        }
        return hasPermission;
    }

然后在自定义的AuthorizeConfigProvider实现类中添加一行权限校验代码:
config.anyRequest().access("@authUserServiceImpl.hasPermission(request, authentication)");

@Component
/**最小值,最先读取这个配置**/
@Order(Integer.MIN_VALUE)
public class AuthAuthorizeConfigProvider implements AuthorizeConfigProvider {

	@Autowired
	private SecurityProperties securityProperties;

	@Override
	public boolean config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
		//支持本地应用查询资源信息
//		config.antMatchers(HttpMethod.GET,"/auth/security/resourceall").access("hasIpAddress('0:0:0:0:0:0:0:1') or hasIpAddress('127.0.0.1')");
		config.antMatchers(HttpMethod.GET,"/auth/security/info","/security/resourceall","/security/user","/security/resources").permitAll();

		//使用access配置动态权限
		config.anyRequest().access("@authUserServiceImpl.hasPermission(request, authentication)");
		return true;
	}

}

其中authUserServiceImpl为AuthUserServiceImpl bean的id,hasPermission为其方法。这样可以在FilterSecurityInterceptor过滤器中进行权限校验,不必再自定义过滤器。

五、资源管理系统演示

项目演示地址:http://175.24.75.121/#/login
用户名:visitor
密码:visitor
在这里插入图片描述
在这里插入图片描述

六、GITHUB

前端工程:https://github.com/STIll-clx/rms-admin-web
后端工程:https://github.com/STIll-clx/rms

七、专题导航

上一节:三、Spring Security认证和授权-授权流程及源码解析
下一节:五、Spring Security认证和授权-实现图片验证码功能

欢迎点赞加关注!(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值