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>
五、用户登陆
- 根据用户名查询用户的角色/所拥有的的菜单以及功能
- 根据用户名查询它的密码.如果查询不到抛出自定义的异常
- 后端控制器根据查询到的结果返回到前端页面,如果出错显示错误信息
用户登陆的核心代码
@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,操作更方便哦