RBAC 是当下权限系统的设计基础,同时有两种解释:
一: Role-Based Access Control,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二:Resource-Based Access Control,基于资源的访问控制
即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限
需要用到的jar包:
shiro-all-1.3.2.jar
mysql-connector-java-5.1.38.jar
commons-beanutils-1.8.3.jar
slf4j-api-1.7.2.jar
commons-logging-1.2.jar
基于RBAC概念, 就会存在3 张基础表: 用户,角色,权限, 以及 2 张中间表来建立 用户与角色的多对多关系,角色与权限的多对多关系。 用户与权限之间也是多对多关系,但是是通过 角色间接建立的。
这里给出了表结构,导入数据库即可,新建一个数据库。
create table user (
id bigint auto_increment,
name varchar(100),
password varchar(100),
constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table role (
id bigint auto_increment,
name varchar(100),
constraint pk_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table permission (
id bigint auto_increment,
name varchar(100),
constraint pk_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table user_role (
uid bigint,
rid bigint,
constraint pk_users_roles primary key(uid, rid)
) charset=utf8 ENGINE=InnoDB;
create table role_permission (
rid bigint,
pid bigint,
constraint pk_roles_permissions primary key(rid, pid)
) charset=utf8 ENGINE=InnoDB;
INSERT INTO `permission` VALUES (1,'addProduct');
INSERT INTO `permission` VALUES (2,'deleteProduct');
INSERT INTO `permission` VALUES (3,'editProduct');
INSERT INTO `permission` VALUES (4,'updateProduct');
INSERT INTO `permission` VALUES (5,'listProduct');
INSERT INTO `permission` VALUES (6,'addOrder');
INSERT INTO `permission` VALUES (7,'deleteOrder');
INSERT INTO `permission` VALUES (8,'editOrder');
INSERT INTO `permission` VALUES (9,'updateOrder');
INSERT INTO `permission` VALUES (10,'listOrder');
INSERT INTO `role` VALUES (1,'admin');
INSERT INTO `role` VALUES (2,'productManager');
INSERT INTO `role` VALUES (3,'orderManager');
INSERT INTO `role_permission` VALUES (1,1);
INSERT INTO `role_permission` VALUES (1,2);
INSERT INTO `role_permission` VALUES (1,3);
INSERT INTO `role_permission` VALUES (1,4);
INSERT INTO `role_permission` VALUES (1,5);
INSERT INTO `role_permission` VALUES (1,6);
INSERT INTO `role_permission` VALUES (1,7);
INSERT INTO `role_permission` VALUES (1,8);
INSERT INTO `role_permission` VALUES (1,9);
INSERT INTO `role_permission` VALUES (1,10);
INSERT INTO `role_permission` VALUES (2,1);
INSERT INTO `role_permission` VALUES (2,2);
INSERT INTO `role_permission` VALUES (2,3);
INSERT INTO `role_permission` VALUES (2,4);
INSERT INTO `role_permission` VALUES (2,5);
INSERT INTO `role_permission` VALUES (3,6);
INSERT INTO `role_permission` VALUES (3,7);
INSERT INTO `role_permission` VALUES (3,8);
INSERT INTO `role_permission` VALUES (3,9);
INSERT INTO `role_permission` VALUES (3,10);
INSERT INTO `user` VALUES (1,'zhang3','12345');
INSERT INTO `user` VALUES (2,'li4','abcde');
INSERT INTO `user_role` VALUES (1,1);
INSERT INTO `user_role` VALUES (2,2);
注: 补充多对多概念: 用户和角色是多对多,即表示:
一个用户可以有多种角色,一个角色也可以赋予多个用户。
一个角色可以包含多种权限,一种权限也可以赋予多个角色。
创建数据表实体类:
package Second_Shiro;
public class User {
private int id;
private String name;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
创建对数据表进行操作的DAO类:
package Second_Shiro;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;
public class ShiroDAO {
public ShiroDAO() {
// TODO Auto-generated constructor stub
try {
Class.forName("com.mysql.jdbc.Driver");
}catch (ClassNotFoundException e) {
// TODO: handle exception
e.printStackTrace();
}
}
public Connection getConnect() throws SQLException{
return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root", "123456");
}
/*
1. getPassword 方法:
根据用户名查询密码,这样既能判断用户是否存在,也能判断密码是否正确
String sql = "select password from user where name = ?";
*/
public String getPassword(String username) {
String sql="SELECT password FROM user WHERE name=?";
try (Connection connection = getConnect(); PreparedStatement preparedStatement = connection.prepareStatement(sql);){
preparedStatement.setString(1, username);
ResultSet resultSet= preparedStatement.executeQuery();
if(resultSet.next()) {
return resultSet.getString("password");
}
} catch (SQLException e) {
// TODO: handle exception
e.printStackTrace();
}
return null;
}
/*
2. listRoles 方法:
根据用户名查询此用户有哪些角色,这是3张表的关联
String sql = "select r.name from user u "
+ "left join user_role ur on u.id = ur.uid "
+ "left join Role r on r.id = ur.rid "
+ "where u.name = ?";
*/
public Set listRoles(String username){
Set roles= new HashSet<>();
String sql = "select r.name from user u "
+ "left join user_role ur on u.id = ur.uid "
+ "left join Role r on r.id = ur.rid "
+ "where u.name = ?";
try (Connection connection = getConnect(); PreparedStatement preparedStatement = connection.prepareStatement(sql);){
preparedStatement.setString(1, username);
ResultSet resultSet= preparedStatement.executeQuery();
while(resultSet.next()) {
roles.add(resultSet.getString(1));
}
} catch (SQLException e) {
// TODO: handle exception
e.printStackTrace();
}
return roles;
}
/*
3. listPermissions 方法:
根据用户名查询此用户有哪些权限,这是5张表的关联
String sql =
"select p.name from user u "+
"left join user_role ru on u.id = ru.uid "+
"left join role r on r.id = ru.rid "+
"left join role_permission rp on r.id = rp.rid "+
"left join permission p on p.id = rp.pid "+
"where u.name =?";
*/
public Set listPermissions(String username){
Set permissions = new HashSet<>();
String sql =
"select p.name from user u "+
"left join user_role ru on u.id = ru.uid "+
"left join role r on r.id = ru.rid "+
"left join role_permission rp on r.id = rp.rid "+
"left join permission p on p.id = rp.pid "+
"where u.name =?";
try (Connection connection = getConnect(); PreparedStatement preparedStatement = connection.prepareStatement(sql);){
preparedStatement.setString(1, username);
ResultSet resultSet= preparedStatement.executeQuery();
while(resultSet.next()) {
permissions.add(resultSet.getString(1));
}
} catch (SQLException e) {
// TODO: handle exception
e.printStackTrace();
}
return permissions;
}
public static void main(String[] args) {
System.out.println(new ShiroDAO().listRoles("zhang3"));
System.out.println(new ShiroDAO().listRoles("li4"));
System.out.println(new ShiroDAO().listPermissions("zhang3"));
System.out.println(new ShiroDAO().listPermissions("li4"));
}
}
运行截图:
在 Shiro 中存在 Realm 这么个概念, Realm 这个单词翻译为 域,其实是非常难以理解的。
域 是什么鬼?和权限有什么毛关系? 这个单词Shiro的作者用的非常不好,让人很难理解。
那么 Realm 在 Shiro里到底扮演什么角色呢?
当应用程序向 Shiro 提供了 账号和密码之后, Shiro 就会问 Realm 这个账号密码是否对, 如果对的话,其所对应的用户拥有哪些角色,哪些权限。
所以Realm 是什么? 其实就是个中介。 Realm 得到了 Shiro 给的用户和密码后,有可能去找 ini 文件,就像Shiro 入门中的 shiro.ini,也可以去找数据库,就如同本知识点中的 DAO 查询信息。
Realm 就是干这个用的,它才是真正进行用户认证和授权的关键地方。
DatabaseRealm 就是用来通过数据库 验证用户,和相关授权的类。
两个方法分别做验证和授权:
doGetAuthenticationInfo(), doGetAuthorizationInfo()
细节在代码里都有详细注释,请仔细阅读。
注: DatabaseRealm 这个类,用户提供,但是不由用户自己调用,而是由 Shiro 去调用。 就像Servlet的doPost方法,是被Tomcat调用一样。
那么 Shiro 怎么找到这个 Realm 呢? 那么就需要下一步,修改 shiro.ini
实体类和DAO不变,去除DAO的main函数,创建 DatabaseRealm ,继承自AuthorizingRealm,并实现方法:
package Third_Shiro;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class DatabaseRealm extends AuthorizingRealm{
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// TODO Auto-generated method stub
//能进入这里,表示账号已经验证通过
String username= (String)principalCollection.getPrimaryPrincipal();
//通过ShiroDAO获取角色和权限
Set permissions=new ShiroDAO().listPermissions(username);
Set roles = new ShiroDAO().listRoles(username);
//授权对象
SimpleAuthorizationInfo simpleAuthorizationInfo= new SimpleAuthorizationInfo();
//把获取的居然色和权限放进去
simpleAuthorizationInfo.setStringPermissions(permissions);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// TODO Auto-generated method stub
//获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName= token.getPrincipal().toString();
String password= new String( t.getPassword());
//获取数据库中的密码
String passwordInDB = new ShiroDAO().getPassword(userName);
//如果为空就是账号不存在,如果不相同就是密码错误,但是都抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息
if(null==passwordInDB || !passwordInDB.equals(password))
throw new AuthenticationException();
//认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName());
return a;
}
}
修改shiro.ini:
[main]
databaseRealm=Third_Shiro.DatabaseRealm
securityManager.realms=$databaseRealm
创建测试类:
package Third_Shiro;
import java.util.*;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
public class TestShiroRealm {
public static void main(String[] args) {
//用户们
User zhang3 = new User();
zhang3.setName("zhang3");
zhang3.setPassword("12345");
User li4 = new User();
li4.setName("li4");
li4.setPassword("abcde");
User wang5 = new User();
wang5.setName("wang5");
wang5.setPassword("wrongpassword");
List users = new ArrayList<>();
users.add(zhang3);
users.add(li4);
users.add(wang5);
//角色们
String roleAdmin = "admin";
String roleProductManager ="productManager";
List roles = new ArrayList<>();
roles.add(roleAdmin);
roles.add(roleProductManager);
//权限们
String permitAddProduct = "addProduct";
String permitAddOrder = "addOrder";
List permits = new ArrayList<>();
permits.add(permitAddProduct);
permits.add(permitAddOrder);
//登陆每个用户
for (User user : users) {
if(login(user))
System.out.printf("%s \t成功登陆,用的密码是 %s\t %n",user.getName(),user.getPassword());
else
System.out.printf("%s \t成功失败,用的密码是 %s\t %n",user.getName(),user.getPassword());
}
System.out.println("-------TestShiroRealm 分割线------");
//判断能够登录的用户是否拥有某个角色
for (User user : users) {
for (String role : roles) {
if(login(user)) {
if(hasRole(user, role))
System.out.printf("%s\t 拥有角色: %s\t%n",user.getName(),role);
else
System.out.printf("%s\t 不拥有角色: %s\t%n",user.getName(),role);
}
}
}
System.out.println("-------TestShiroRealm 分割线------");
//判断能够登录的用户,是否拥有某种权限
for (User user : users) {
for (String permit : permits) {
if(login(user)) {
if(isPermitted(user, permit))
System.out.printf("%s\t 拥有权限: %s\t%n",user.getName(),permit);
else
System.out.printf("%s\t 不拥有权限: %s\t%n",user.getName(),permit);
}
}
}
}
private static boolean hasRole(User user, String role) {
Subject subject = getSubject(user);
return subject.hasRole(role);
}
private static boolean isPermitted(User user, String permit) {
Subject subject = getSubject(user);
return subject.isPermitted(permit);
}
private static Subject getSubject(User user) {
//加载配置文件,并获取工厂
Factory factory = new IniSecurityManagerFactory("classpath:Third_Shiro/shiro.ini");
//获取安全管理者实例
SecurityManager sm = factory.getInstance();
//将安全管理者放入全局对象
SecurityUtils.setSecurityManager(sm);
//全局对象通过安全管理者生成Subject对象
Subject subject = SecurityUtils.getSubject();
return subject;
}
private static boolean login(User user) {
Subject subject= getSubject(user);
//如果已经登录过了,退出
if(subject.isAuthenticated())
subject.logout();
//封装用户的数据
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());
try {
//将用户的数据token 最终传递到Realm中进行对比
subject.login(token);
} catch (AuthenticationException e) {
//验证错误
return false;
}
return subject.isAuthenticated();
}
}
运行图