how2j.cn

下载区
文件名 文件大小
shiro.rar 1m
使用站长秘制下载工具
步骤 1 : 数据库支持   
步骤 2 : RBAC 概念   
步骤 3 : 表结构   
步骤 4 : 表数据   
步骤 5 : 先运行,看到效果,再学习   
步骤 6 : 模仿和排错   
步骤 7 : User   
步骤 8 : DAO   
步骤 9 : Realm 概念   
步骤 10 : DatabaseRealm   
步骤 11 : 修改 shiro.ini   
步骤 12 : TestRealm   
步骤 13 : 关于JdbcRealm   

Shiro 入门 中使用ini 配置文件进行了相关权限数据的配置。 但是实际工作中,我们都会把权限相关的内容放在数据库里。 所以本知识点讲解如何放在数据库里来撸。
RBAC 是当下权限系统的设计基础,同时有两种解释:
一: Role-Based Access Control,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二:Resource-Based Access Control,基于资源的访问控制
即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限
基于 RBAC 概念, 就会存在3 张基础表: 用户,角色,权限, 以及 2 张中间表来建立 用户与角色的多对多关系,角色与权限的多对多关系。 用户与权限之间也是多对多关系,但是是通过 角色间接建立的。

这里给出了表结构,导入数据库即可。

注: 补充多对多概念: 用户和角色是多对多,即表示:
一个用户可以有多种角色,一个角色也可以赋予多个用户。
一个角色可以包含多种权限,一种权限也可以赋予多个角色。
DROP DATABASE IF EXISTS shiro; CREATE DATABASE shiro DEFAULT CHARACTER SET utf8; USE shiro; drop table if exists user; drop table if exists role; drop table if exists permission; drop table if exists user_role; drop table if exists role_permission; 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;
这里基于 Shiro入门中的shiro.ini 文件,插入一样的用户,角色和权限数据。
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);
步骤 5 :

先运行,看到效果,再学习

edit edit
老规矩,先下载右上角的可运行项目,配置运行起来,确认可用之后,再学习做了哪些步骤以达到这样的效果。
运行 TestShiro,可以看到如图所示,其效果和 Shiro 入门中的效果一模一样,而这样的效果,是基于数据库来做的
先运行,看到效果,再学习
在确保可运行项目能够正确无误地运行之后,再严格照着教程的步骤,对代码模仿一遍。
模仿过程难免代码有出入,导致无法得到期望的运行结果,此时此刻通过比较正确答案 ( 可运行项目 ) 和自己的代码,来定位问题所在。
采用这种方式,学习有效果,排错有效率,可以较为明显地提升学习速度,跨过学习路上的各个槛。

推荐使用diffmerge软件,进行文件夹比较。把你自己做的项目文件夹,和我的可运行项目文件夹进行比较。
这个软件很牛逼的,可以知道文件夹里哪两个文件不对,并且很明显地标记出来
这里提供了绿色安装和使用教程:diffmerge 下载和使用教程
在原来的基础上增加了一个id字段。 其实。。。本知识点也没有用到这个id,只是和数据库关联了嘛,数据库里有,还是加上
package com.how2java; public class User { private int id; private String name; private String password; 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; } public int getId() { return id; } public void setId(int id) { this.id = id; } }
这个DAO提供了和权限相关查询。 但是,并没有提供权限数据本身的维护。 比如没有做用户的增删改,角色和权限表也没有。 因为那些在提供了 表数据 的基础上,就不是必须的了。
为了专注于 Shiro 和 DAO 的结合,只提供必要的数据库操作支持。
1. getPassword 方法:
根据用户名查询密码,这样既能判断用户是否存在,也能判断密码是否正确

String sql = "select password from user where name = ?";


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 = ?";



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 =?";


4. 主方法测试
运行之后,看到如图所示 zhang3,li4 拥有的角色和权限和  Shiro入门中的shiro.ini 文件 中的数据是一致的
DAO
package com.how2java; 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 DAO { public DAO() { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public Connection getConnection() throws SQLException { return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root", "admin"); } public String getPassword(String userName) { String sql = "select password from user where name = ?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); if (rs.next()) return rs.getString("password"); } catch (SQLException e) { e.printStackTrace(); } return null; } public Set<String> listRoles(String userName) { Set<String> 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 c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); while (rs.next()) { roles.add(rs.getString(1)); } } catch (SQLException e) { e.printStackTrace(); } return roles; } public Set<String> listPermissions(String userName) { Set<String> 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 c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); while (rs.next()) { permissions.add(rs.getString(1)); } } catch (SQLException e) { e.printStackTrace(); } return permissions; } public static void main(String[] args) { System.out.println(new DAO().listRoles("zhang3")); System.out.println(new DAO().listRoles("li4")); System.out.println(new DAO().listPermissions("zhang3")); System.out.println(new DAO().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
package com.how2java; 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) { //能进入到这里,表示账号已经通过验证了 String userName =(String) principalCollection.getPrimaryPrincipal(); //通过DAO获取角色和权限 Set<String> permissions = new DAO().listPermissions(userName); Set<String> roles = new DAO().listRoles(userName); //授权对象 SimpleAuthorizationInfo s = new SimpleAuthorizationInfo(); //把通过DAO获取到的角色和权限放进去 s.setStringPermissions(permissions); s.setRoles(roles); return s; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取账号密码 UsernamePasswordToken t = (UsernamePasswordToken) token; String userName= token.getPrincipal().toString(); String password= new String( t.getPassword()); //获取数据库中的密码 String passwordInDB = new DAO().getPassword(userName); //如果为空就是账号不存在,如果不相同就是密码错误,但是都抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息 if(null==passwordInDB || !passwordInDB.equals(password)) throw new AuthenticationException(); //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName()); return a; } }

[main]
databaseRealm=com.how2java.DatabaseRealm
securityManager.realms=$databaseRealm

前面准备了DatabaseRealm,那么在配置文件里,就指定当前的realm 是 他。 默认情况下是找 IniRealm。
shiro.ini 中原本的数据信息,都删除掉了
[main] databaseRealm=com.how2java.DatabaseRealm securityManager.realms=$databaseRealm
[main]
databaseRealm=com.how2java.DatabaseRealm
securityManager.realms=$databaseRealm
TestRealm 没任何变化,运行效果和基于 ini的效果一模一样。
TestRealm
package com.how2java; import java.util.ArrayList; import java.util.List; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; 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 TestShiro { 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<User> users = new ArrayList<>(); users.add(zhang3); users.add(li4); users.add(wang5); //角色们 String roleAdmin = "admin"; String roleProductManager ="productManager"; List<String> roles = new ArrayList<>(); roles.add(roleAdmin); roles.add(roleProductManager); //权限们 String permitAddProduct = "addProduct"; String permitAddOrder = "addOrder"; List<String> 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("-------how2j 分割线------"); //判断能够登录的用户是否拥有某个角色 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("-------how2j 分割线------"); //判断能够登录的用户,是否拥有某种权限 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<SecurityManager> factory = new IniSecurityManagerFactory("classpath: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(); } }
Shiro 提供了一个 JdbcRealm,它会默认去寻找 users, roles, permissions 三张表做类似于 DAO 中的查询。
但是本例没有使用它,因为实际工作通常都会有更复杂的权限需要,以上3张表不够用。 JdbcRealm 又封装得太严实了,连它执行了 SQL 语句这件事,我都是很久才明白过来,这样不利于初学者消化, 最后 使用 DAO 更符合开发者的习惯,也觉得一切都在自己掌握中,用起来心里更踏实一些。


HOW2J公众号,关注后实时获知最新的教程和优惠活动,谢谢。


提问已经提交成功,正在审核。 请于 我的提问 处查看提问记录,谢谢
关于 工具和中间件-Shiro-数据库支持 的提问

尽量提供截图代码异常信息,有助于分析和解决问题。 也可进本站QQ群交流: 982790551
提问尽量提供完整的代码,环境描述,越是有利于问题的重现,您的问题越能更快得到解答。
对教程中代码有疑问,请提供是哪个步骤,哪一行有疑问,这样便于快速定位问题,提高问题得到解答的速度
在已经存在的几千个提问里,有相当大的比例,是因为使用了和站长不同版本的开发环境导致的,比如 jdk, eclpise, idea, mysql,tomcat 等等软件的版本不一致。
请使用和站长一样的版本,可以节约自己大量的学习时间。 站长把教学中用的软件版本整理了,都统一放在了这里, 方便大家下载: https://how2j.cn/k/helloworld/helloworld-version/1718.html

上传截图