RBAC模型与权限系统的梳理(附案例源码)

一、介绍

简介

RBAC(Role-BasedAccessControl )基于角色的访问控制。
RBAC 认为权限的过程可以抽象概括为:
判断【Who 是否可以对 What 进行 How 的访问操作(Operator)】
Who:权限的拥用者或主体
What:权限针对的对象或资源
How:具体的权限
Operator:操作。表明对 What 的 How 操作。也就是 Privilege+Resource
Role:角色,一定数量的权限的集合。 权限分配的单位与载体,目的是隔离User与Privilege 的逻辑关系
RBAC支持公认的安全原则:
最小特权原则、责任分离原则和数据抽象原则。

RBAC96模型

结构图

在这里插入图片描述

相关介绍

RBAC96是一个模型族,其中包括RBAC0~RBAC3四个概念性模型。

  • 1、基本模型RBAC0定义了完全支持RBAC概念的任何系统的最低需求。
  • 2、RBAC1和RBAC2两者都包含RBAC0,但各自都增加了独立的特点,它们被称为高级模型。
    RBAC1中增加了角色分级的概念,一个角色可以从另一个角色继承许可权。
    RBAC2中增加了一些限制,强调在RBAC的不同组件中在配置方面的一些限制。
  • 3、RBAC3称为统一模型,它包含了RBAC1和RBAC2,利用传递性,也把RBAC0包括在内。这些模型构成了RBAC96模型族。

二、数据库设计

该数据库设计适用于绝大部分采取该模型的编程 , 但不排除个别特殊情况

数据库表结构设计

模型图
在这里插入图片描述

数据库表

users:用户表;
roles:权限表;
menus菜单表;
funs:功能表.
roles_menus每个角色对应的功能菜单表(中间表)

数据库表的关系

一个角色对应多个用户,一个用户对应一个角色。用户和角色是多对一的关系。
一个角色对应多个菜单,角色和菜单是多对多的关系,需要中间表将角色和菜单关联起来。一个菜单有多个功能,菜单和功能是一对多的关系。

设计原则

设计时,出现多对一的情况,少的一方的主键作为多的一方的外键,方便调用, 即多对一中一的主键作为多的外键

创建表

用户表

-- 用户表:主键username外键role_id
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`userpwd` varchar(50) DEFAULT NULL,
`role_id` int(11) DEFAULT NULL,
PRIMARY KEY (`username`),
KEY `users_fk` (`role_id`),
CONSTRAINT `users_fk` FOREIGN KEY (`role_id`) REFERENCES `roles` (`roleid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

角色表

-- 角色表:主键roleid
CREATE TABLE `roles` (
`roleid` int(11) NOT NULL AUTO_INCREMENT,
`rolename` varchar(50) DEFAULT NULL,
PRIMARY KEY (`roleid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

菜单表

-- 菜单表:主键menuid
CREATE TABLE `menus` (
`menuid` int(11) NOT NULL AUTO_INCREMENT,
`menuname` varchar(50) DEFAULT NULL,
`menuurl` varchar(50) DEFAULT NULL,
`fatherid` int(11) DEFAULT NULL,
PRIMARY KEY (`menuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

中间表

-- 角色-菜单中间表:主键roles_id`,`menus_id
CREATE TABLE `roles_menus` (
`roles_id` int(11) NOT NULL,
`menus_id` int(11) NOT NULL,
PRIMARY KEY (`roles_id`,`menus_id`),
KEY `roles_menus_fk2` (`menus_id`),
CONSTRAINT `roles_menus_fk1` FOREIGN KEY (`roles_id`) REFERENCES `roles`(`roleid`),
CONSTRAINT `roles_menus_fk2` FOREIGN KEY (`menus_id`) REFERENCES `menus`(`menuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

功能表

-- 功能表:主键 funid,外键menu_id
CREATE TABLE `fu多s` (
`funid` int(11) NOT NULL AUTO_INCREMENT,
`funname` varchar(50) DEFAULT NULL,
`funurl` varchar(50) DEFAULT NULL,
`menu_id` int(11) DEFAULT NULL,
PRIMARY KEY (`funid`),
KEY `menus_fk` (`menu_id`),
CONSTRAINT `menus_fk` FOREIGN KEY (`menu_id`) REFERENCES `menus`
(`menuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加基础数据

介绍

提供令系统能够运行的最底层的数据
这些数据并不是业务数据,业务数据时随着系统运行而产生的数据,而基础数据就是系统开发时就已经存在的数据

添加内容1

角色础数据(管理员\客服人员等);
用户基础数据(初始用户名以及密码);
中间表(每个角色对应的菜单表)

四、项目环境搭建

开发环境

开发环境搭建,搭建SSM环境如下1

实体类

实体类属性如下,只需要手动添加get/set以及toString方法即可

public class Users {
	private String username;//用户名
	private String userpwd;//密码
	private Roles roles;//角色信息
	private List<Menus>menus=new ArrayList<>();//关联菜单
	private List<Funs> funs = new ArrayList<>();//用于在resultMap中关联对象
	
public class Roles {
	private int roleid;//角色id
	private String rolename;//角色姓名
	private List<Menus>menus=new ArrayList<>();//角色:菜单      多对多
public class Menus {
	private int menuid;//菜单id
	private String menuname;//菜单名称
	private String menuurl;//菜单url
	private int fatherid;//父id
	private List<Funs>funs=new ArrayList<>();//菜单:功能   多对多
	
public class Funs {
	private int funid;//功能id
	private String funname;//功能名称
	private String funurl;//功能url

关系

一个角色对应多个用户,一个用户对应一个角色。用户和角色是多对一的关系。
一个角色对应多个菜单,角色和菜单是多对多的关系,需要中间表将角色和菜单关联起来。一个菜单有多个功能,菜单和功能是一对多的关系。

定义resultMap

查询对于返回的结果集,Mybatis不知道如何将结果映射到实体类Users中
这样就用到了resultMap, 常用在在多表连接查询
在resultMap中,
引用型对象使用的是association 属性进行关联
而集合类型(list)的对象使用的是collection 属性进行管理
id标记的是主键元素,result标记的是其他元素!!!

userMapper.xml
<?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="com.bjsxt.mapper.UserMapper" >

	<!-- 返回的结果集是多个,所以使用resultMap,id代表主键元素 -->
	<resultMap type="com.bjsxt.pojo.Users" id="userMapper">
		<id property="username" column="username"/>
		<result property="userpwd" column="userpwd"/>
		<!-- 配置关联对象Roles -->
		<association property="roles" javaType="com.bjsxt.pojo.Roles">
			<id property="roleid" column="roleid"/>
			<result property="rolename" column="rolename"/>
		</association>
		<!-- 配置关联对象Menus -->
		<collection property="menus" ofType="com.bjsxt.pojo.Menus">
			<id property="menuid" column="menuid"/>
			<result property="menuname" column="menuname"/>
			<result property="menuurl" column="menuurl"/>
			<result property="fatherid" column="fatherid"/>
		</collection>
		<!-- 配置关联对象Funs -->
		<collection property="funs" ofType="com.bjsxt.pojo.Funs">
			<id property="funid" column="funid"/>
			<result property="funname" column="funname"/>
			<result property="funurl" column="funurl"/>
		</collection>
	</resultMap>
	<!-- string是mybaties为java.lang.String起的别名 -->
	<select id="selUserByName" parameterType="string" resultMap="userMapper">
		<!-- select * from users u ,roles r ,roles_menus rm ,menus m 
		where u.role_id = r.roleid 
		and r.roleid = rm.roles_id 
		and rm.menus_id = m.menuid 
		and u.username =#{username} -->
		SELECT
			*
		FROM
			users u,
			roles r,
			roles_menus rm,
			menus m left JOIN funs f ON m.menuid = f.menu_id
		WHERE
			u.role_id = r.roleid 
		AND r.roleid = rm.roles_id 
		AND rm.menus_id = m.menuid 
		AND u.username =#{username}
	</select>
</mapper>

五、用户登陆

  1. 根据用户名查询用户的角色/所拥有的的菜单以及功能
  2. 根据用户名查询它的密码.如果查询不到抛出自定义的异常
  3. 后端控制器根据查询到的结果返回到前端页面,如果出错显示错误信息

用户登陆的核心代码

@Service
public class UserServiceImpl implements UsersService{
	@Autowired
	private UserMapper usersMapper;
	
	/**
	 * 用户登陆
	 */
	public Users userLogin(String username, String userpwd) {
		
		Users users=this.usersMapper.selUserByName(username);
		if (users==null) {
			//用户不存在
			throw new UsersException("用户不存在或密码有误");
		} else if (!users.getUserpwd().equals(userpwd)) {
				//密码有误
				throw new UsersException("用户不存在或密码有误");
			}
		return users;
			
		}

在进行查询时,自定义了一个异常类, 用于在用户查询不到是,打印出"用户不存在或密码有误"的异常

public class UsersException extends RuntimeException{
	public  UsersException() {
		
	}
	public  UsersException(String msg) {
			super(msg);
		}
	public  UsersException(String msg,Throwable t) {
		super(msg,t);
	}
}
@Controller
public class LoginController {
	@Autowired
	private UsersService userService;

	@RequestMapping("/userlogin")
	public String userLogin(Users users, Model model, HttpServletRequest request) {
		try {
			Users u = this.userService.userLogin(users.getUsername(),users.getUserpwd());
			
			//遍历这些功能的名称f,直接打印f会出现的是这些功能的地址
			List<Funs>funs=u.getFuns();
			for (Funs f : funs) {
				System.out.println(f.getFunname()+"   "+f.getFunurl());
			}
			
			HttpSession session = request.getSession();
			session.setAttribute("user", u);
			List<Menus> menus = u.getMenus();
			for (Menus menus2 : menus) {
				System.out.println(menus2);
			}
		
		} catch (UsersException e) {
			e.printStackTrace();
			model.addAttribute("msg", e.getMessage());
			return "/login";
		}
		return "redirect:/index";
	}
}

其他代码见底部分享

用户登陆检测

介绍

防止用户出现不通过登陆也能访问到内部资源的情况
避免用户绕过登陆环节访问导致的资源泄露

业务逻辑

获取用户请求的uri,根据uri判断是否为登陆操作,
如果是登录请求则放行,否则查看session中是否有用户数据,有则已登录,放行,没有登录就跳转到登录页面。
放行原则,使用过滤器继承Filter,通过实现接口的FilterChain的dofilter()方法进行放行

代码实现

public class UserLoginFilter implements Filter{

	@Override
	public void destroy() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void doFilter(ServletRequest arg0, ServletResponse arg1,
			FilterChain chain) throws IOException, ServletException {
		
		//获取用户访问的uri(统一资源标识符)
		HttpServletRequest req=(HttpServletRequest) arg0;
		String uri = req.getRequestURI();
		//System.out.println(uri);
	
		//判断当前访问的uri是否是用户登陆资源,如果是则放行
		//indexOf:比较uri这个字符串中是否存在login字符串。-1为indexOf的返回值,返回不到返回-1
		if (uri.indexOf("login")!=-1 || uri.indexOf("userlogin")!= -1) {
			chain.doFilter(arg0, arg1);
		}else {
			//用户是否登录的判断
			HttpSession session = req.getSession();
			Users user = (Users) session.getAttribute("user");
			if (user!=null && user.getUsername().length()>0) {
				chain.doFilter(arg0, arg1);//放行
			}else {
				req.setAttribute("msg", "请登录");
				req.getRequestDispatcher("/login").forward(arg0, arg1);
			}
		}
	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		// TODO Auto-generated method stub
		
	}

}

六、越级访问的解决

介绍&解决

通过浏览器地址栏访问越级的功能。
使用RBAC的控制对象功能。

权限过滤器

业务逻辑

判断当前用户是否有权限访问该资源,避免用户的越级访问。
先对静态资源放行,在对用户登录的资源进行放行,再判断当前访问的uri是否在用户的权限之内。使用for循环遍历user对象中存储的功能信息,判断当前uri与功能是否匹配。匹配则放行。不匹配则提示权限不足。

步骤

修改功能表
创建权限过滤器
配置权限过滤器

代码

public class SafeFilter implements Filter {

	@Override
	public void destroy() {
		// TODO Auto-generated method stub

	}

	@Override
	public void doFilter(ServletRequest arg0, ServletResponse arg1,
			FilterChain chain) throws IOException, ServletException {

		HttpServletRequest req = (HttpServletRequest) arg0;
		HttpServletResponse res = (HttpServletResponse) arg1;
		String uri = req.getRequestURI();
		// 对静态资源做放行处理
		if (uri.endsWith(".js") || uri.endsWith(".css") || uri.endsWith(".gif")) {
			chain.doFilter(arg0, arg1);
		} else {
			// 对用户登录资源做放行
			if (uri.indexOf("login") != -1 || uri.indexOf("userLogin") != -1) {
				chain.doFilter(arg0, arg1);
			} else {
				HttpSession session = req.getSession();
				Users user = (Users) session.getAttribute("user");
				List<Funs> funs = user.getFuns();
				// 开关
				boolean flag = false;
				for (Funs f : funs) {
					// 判断当前访问的 URI 是否在功能数据中包含
					if (uri.indexOf(f.getFunurl()) != -1) {
						flag = true;
						break;
					}
				}
				// 根据开关的值来进行跳转
				if (flag) {
					chain.doFilter(arg0, arg1);
				} else {
					res.sendRedirect("roleerror");
				}
			}
		}

	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		// TODO Auto-generated method stub

	}

}

基础数据库表、环境搭建的基础源码、以及项目整合完成后的源码已经分享至百度云

链接:https://pan.baidu.com/s/1lQDzlNorTNcHPkTdT8IaIQ
提取码:nnaw
复制这段内容后打开百度网盘手机App,操作更方便哦


  1. ↩︎ ↩︎
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页