简述
RBAC思想是一种用于数据库设计的思想,一般来讲有三种场景:
- 菜单
- 资源可见
- url权限
一个网站可以有多个用户,而用户会有不同的角色,最常见的就是管理员和普通用户,有的情境下用户只有一种角色(有的情境下用户会有不同的角色,这里以只一种角色来进行分析),一种角色一般是对应多种功能,对于角色和功能的关系,就必须要用角色-功能中间表,在整个过程中,角色始终是数据库设计的核心,其他的表都是围绕着角色转的
菜单Demo
- ssm环境下
- 一个用户对应一个角色
- 不同角色不同菜单
- 用户登录后根据用户角色显示菜单
数据库首先创建角色,角色表不需要外键,都是别的表依赖角色表
create table role(
id int(10) primary key auto_increment,
name varchar(20)
);
然后创建用户表,案例设定一个用户一个角色,所以角色id对应用户的外键rid
create table users(
id int(10) primary key auto_increment,
username varchar(20),
password varchar(20),
rid int(10)
)
上边用户和角色的管理完毕,下边是角色和菜单的关联,像多对多或者一对多的关联,就不必也不能使用外键
创建菜单表,其中pid是显示出菜单的层次而设计
create table menu(
id int(10) primary key auto_increment,
name varchar(20),
pid int(10)
)
创建role_menu,将role和menu联系起来,当业务需要查询某个角色的菜单,应将role_menu和menu两个表联合(rm.mid=m.id &where rm.id=?),这样就能查询出某个role的所有menu
create table role_menu(
id int(10) primary key auto_increment,
rid int(10),
mid int(10)
)
mapper
Login
public interface UsersMapper {
Users selByUser(Users users);
}
<?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">
<!-- namesapce:理解成实现类的全路径(包名+类名) -->
<mapper namespace="cn.wit.mapper.UsersMapper" >
<select id="selByUser" resultType="users">
select * from users where username = #{username} and password = #{password}
</select>
</mapper>
查询菜单
public interface MenuMapper {
List<Menu> selByRid(@Param("rid") int rid ,@Param("pid")int pid);
}
菜单联合后,主要是联合了每个menu的role信息,where筛选role,就可以得到想要的某个role的全部menu,菜单需要有层次,进行递归调用(分级菜单案例),
<?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">
<!-- namesapce:理解成实现类的全路径(包名+类名) -->
<mapper namespace="cn.wit.mapper.MenuMapper">
<resultMap type="menu" id="mmap">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="pid" property="pid" />
<collection property="children" select="selByRid" column="{rid=rid,pid=id}">
</collection>
</resultMap>
<select id="selByRid" resultMap="mmap">
select m.* ,#{
rid} rid
from Menu m join
role_menu r on r.mid = m.id
where r.rid = #{
rid} and m.pid=#{
pid}
</select>
</mapper>
</mapper>
pojo
Menu
private int id;
private String name;
private int pid;
Users
private int id;
private String username;
private String password;
private int rid;
private List<Menu> menus;
service
@Service
public class MenuServiceImpl implements MenuService{
@Resource
private MenuMapper menuMapper;
@Override
public List<Menu> showMenu(int rid) {
return menuMapper.selByRid(rid, 0);
}
}
注入登录的Mapper和需要设置菜单的Impl,传入rid设置菜单List,另外user空指针
@Service
public class UsersServiceImpl implements UsersService {
@Resource
private UsersMapper usersMapper;
@Resource
private MenuService menuServiceImpl;
@Override
public Users login(Users users) {
Users user = usersMapper.selByUser(users);
if(user!=null){
user.setMenus(menuServiceImpl.showMenu(user.getRid()));
}
return user;
}
}
controller
基 本 操 作
@Controller
public class UsersController {
@Resource
private UsersService usersServiceImpl;
@RequestMapping("login")
public String login(Users users,HttpSession session){
Users user = usersServiceImpl.login(users);
if(user!=null){
session.setAttribute("user", user);
return "redirect:/main.jsp";
}
return "redirect:/login.jsp";
}
}
jsp
login.jsp
<form action="login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录"/><br/>
</form>
</body>
</html>
main.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<dl>
<c:forEach items="${user.menus }" var="menu">
<dt>${
menu.name }</dt>
<c:forEach items="${menu.children }" var="child">
<dd>${
child.name }</dd>
</c:forEach>
</c:forEach>
</dl>
</body>
</html>
资源可见
在菜单的例子上进行添加,由于rbac良好的扩展性,不管是菜单还是资源可见还是url权限,都是属于功能的部分,都是功能呢围绕角色进行数据库设计的,在具体操作时,只需要添加一个功能表,一个角色_功能表,因为添加了新功能,相当于角色也多了一个新属性,对应的用户自然也要同步这个属性,在业务需要查询角色对应的功能时,SQL语句联合操作即可拿到数据
数据库
- 建立一个资源表element
- 建立一个角色_资源表role_element
DDL
CREATE TABLE `element` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`eleno` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `role_element` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`rid` int(10) DEFAULT NULL,
`eid` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
mapper
资源可见指的是资源对特定的角色可见,对其他角色隐藏,当用户登录时,通过用户的角色(rid)判断是否要显示这一资源,所以,落实到具体的操作上,就应该是selByRid,联合element和role_element来结合rid和具体的资源,筛选rid可以得到某个rid的资源(返回element对象)
<?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="cn.wit.mapper.ElementMapper">
<resultMap type="Element" id="mymap">
<id column="id" property="id"/>
<result column="eleno" property="eleno"/>
</resultMap>
<select id="selByRid" parameterType="int" resultMap="mymap">
select e.*,re.rid from element e
join role_element re on e.id=re.eid
where rid=#{
id}
</select>
</mapper>
Impl
Impl基本操作调用方法
@Service
public class ElementServiceImpl implements ElementService{
@Resource
private ElementMapper elementMapper;
public List<Element> selByRid(int id) {
return elementMapper.selByRid(id);
}
}
将新功能添加到用户中
@Service
public class UsersServiceImpl implements UsersService {
@Resource
private UsersMapper usersMapper;
@Resource
private MenuService menuServiceImpl;
@Resource
private ElementService elementServiceImpl;
@Override
public Users login(Users users) {
Users user = usersMapper.selByUser(users);
if(user!=null){
user.setMenus(menuServiceImpl.showMenu(user.getRid()));
user.setElements(elementServiceImpl.selByRid(user.getRid()));
}
return user;
}
}
jsp
通过验证用户的elements资源属性中的eleno来匹配显示资源,某个资源的eleno字符串匹配,则进行显示
<c:forEach items="${user.elements }" var="ele">
<c:if test="${ele.eleno eq 'grant' }">
<button>授权</button>
</c:if>
</c:forEach>
URL权限验证
权限验证相比前面两个不好理解一点,简单来说就是用户A用户B都能进行登录操作(重定向rbac/main.jsp这个uri),但是用户A能访问demo控制器(经过controller然后跳转到demo.jsp),用户B没有访问demo的权限,访问demo就重定向到登录页面
- 同样的,跟上面两个功能相比,url同样是一种功能,同样是建立两个表,并且用户添加功能属性
- 不一样的是,只能将用户的url添加到Users中,返回值为User,这样查询全部权限的数据无法传到controller中,解决办法可以改返回值,也可以将查询全部权限的操作直接放在controller中,下边的例子就是后者
数据库
- 建立url表
- 建立role_url表
DDL
CREATE TABLE `url` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
CREATE TABLE `role_url` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`rid` int(10) DEFAULT NULL,
`uid` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
mapper
url权限控制需要查询某个角色的权限之外,还需要查询全部权限(当用户访问无权限内容时,应该做出某些动作,比如跳转会登陆界面,或者提醒你充VIP)
public interface UrlMapper {
@Select("select *from url where id in(select uid from role_url where rid=#{rid})")
List<Url> selByRid(int rid);
@Select("select *from url")
List<Url>selAll();
}
Impl
@Service
public class UrlServiceImpl implements UrlService{
@Resource
private UrlMapper urlMapper;
@Override
public List<Url> selByRid(int rid) {
return urlMapper.selByRid(rid);
}
@Override
public List<Url> showAll() {
return urlMapper.selAll();
}
}
@Service
public class UsersServiceImpl implements UsersService {
@Resource
private UsersMapper usersMapper;
@Resource
private MenuService menuServiceImpl;
@Resource
private ElementService elementServiceImpl;
@Resource
private UrlService urlServiceImpl;
@Override
public Users login(Users users) {
Users user = usersMapper.selByUser(users);
if(user!=null){
user.setMenus(menuServiceImpl.showMenu(user.getRid()));
user.setElements(elementServiceImpl.selByRid(user.getRid()));
user.setUrl(urlServiceImpl.selByRid(user.getRid()));
}
return user;
}
}
controller
- 需要注入用户的所有信息,包括usersServiceImpl和url权限的查询全部需要的urlServiceImpl
- 将users和全部url权限数据通过session流转给jsp
@Controller
public class UsersController {
@Resource
private UrlService urlServiceImpl;
@Resource
private UsersService usersServiceImpl;
@RequestMapping("login")
public String login(Users users,HttpSession session){
Users user = usersServiceImpl.login(users);
if(user!=null){
System.out.println(user);
List<Url> urlist=urlServiceImpl.showAll();
session.setAttribute("user", user);
session.setAttribute("urlist", urlist);
return "redirect:/main.jsp";
}
return "redirect:/login.jsp";
}
@RequestMapping("demo")
public String demo(){
return "/a.jsp";
}
}
filter
通过filter或者拦截器来筛选请求
逻辑思路:
uri=session.getURI()//请求uri
if(静态资源){
放行
}else{
所有权限url urlist
if(urlist!=null){
isExist=false//请求uri是否在权限里边
foreach(items=urlist var=url){
if(url.getName==uri){
isExist=true//全部权限中有与uri一致的,需要权限验证
}
}
if(isExist){
//uri在权限里边
isRight//是否属于本用户权限
urls=session.getAttribute()//本用户权限
foreach(items=urls var=url){
if(url.getName()==uri){
isRight=true//用户权限中有与uri一致的
}
}
if(isRight){
//属于本用户权限
放行
}else{
//本用户无该权限
重定向登录页面
}
}else{
//不在权限里边
放行
}
}else{
没有权限url,放行
}
}
@WebFilter("/*")
public class UrlFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req=(HttpServletRequest)request;
HttpServletResponse res=(HttpServletResponse)response;
String uri=req.getRequestURI();
if(uri.endsWith(".js")||uri.endsWith(".css")||uri.endsWith(".jpg")||uri.endsWith(".png")){
//放行
chain.doFilter(request, response);
}else{
if(uri.equals("/rbac/login")||uri.equals("/rbac/login.jsp")){
//不拦截的uri
chain.doFilter(request, response);
}else{
boolean isExist=false;
HttpSession session = req.getSession();
//先判断属不属于有权限的uri
List<Url> urlist = (List<Url>) session.getAttribute("urlist");
if(urlist!=null){
System.out.println("urlist不为空");
for (Url url : urlist) {
System.out.println(url+":"+uri);
if(uri.equals(url.getName())){
isExist=true;
}
}
if(isExist){
System.out.println("存在有权限的uri");
//如果属于有权限的uri,判断是否是当前用户是否有访问该uri的权限
Users user = (Users) session.getAttribute("user");
List<Url> urls = user.getUrl();
boolean isRight=false;
for (Url url : urls) {
System.out.println(uri+":"+url.getName());
if(uri.equals(url.getName())){
isRight=true;
}
}
if(isRight){
//此时是uri属于权限,而且用户拥有该权限,放行
System.out.println("有uri权限");
chain.doFilter(request, response);
}else{
//此时uri属于权限,用户没该权限,无权限访问跳回登陆页面
System.out.println("无uri权限");
res.sendRedirect("/rbac/login.jsp");
}
}else{
//此时是uri不属于权限,放行
chain.doFilter(request, response);
}
}else{
//此时该权限表里边没有数据,所有都放行
chain.doFilter(request, response);
}
}
}
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
}