Apache Shiro(二)-登录认证和权限管理数据库操作
数据库支持
在 上一篇中使用ini 配置文件进行了相关权限数据的配置。 但是实际工作中,我们都会把权限相关的内容放在数据库里。 所以本知识点讲解如何放在数据库里来撸。
RBAC 概念
RBAC 是当下权限系统的设计基础,同时有两种解释:
一: Role-Based Access Control,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二:Resource-Based Access Control,基于资源的访问控制
即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限
表结构
基于 RBAC 概念, 就会存在3 张基础表: 用户,角色,权限, 以及 2 张中间表来建立 用户与角色的多对多关系,角色与权限的多对多关系。 用户与权限之间也是多对多关系,但是是通过 角色间接建立的。
这里给出了表结构,导入数据库即可。
注: 补充多对多概念: 用户和角色是多对多,即表示:
一个用户可以有多种角色,一个角色也可以赋予多个用户。
一个角色可以包含多种权限,一种权限也可以赋予多个角色。
1 DROP DATABASE IF EXISTS shiro; 2 CREATE DATABASE shiro DEFAULT CHARACTER SET utf8; 3 USE shiro; 4 5 drop table if exists user; 6 drop table if exists role; 7 drop table if exists permission; 8 drop table if exists user_role; 9 drop table if exists role_permission; 10 11 create table user ( 12 id bigint auto_increment, 13 name varchar(100), 14 password varchar(100), 15 constraint pk_users primary key(id) 16 ) charset=utf8 ENGINE=InnoDB; 17 18 create table role ( 19 id bigint auto_increment, 20 name varchar(100), 21 constraint pk_roles primary key(id) 22 ) charset=utf8 ENGINE=InnoDB; 23 24 create table permission ( 25 id bigint auto_increment, 26 name varchar(100), 27 constraint pk_permissions primary key(id) 28 ) charset=utf8 ENGINE=InnoDB; 29 30 create table user_role ( 31 uid bigint, 32 rid bigint, 33 constraint pk_users_roles primary key(uid, rid) 34 ) charset=utf8 ENGINE=InnoDB; 35 36 create table role_permission ( 37 rid bigint, 38 pid bigint, 39 constraint pk_roles_permissions primary key(rid, pid) 40 ) charset=utf8 ENGINE=InnoDB;
表数据
这里基于 Shiro入门中的shiro.ini 文件,插入一样的用户,角色和权限数据。
1 INSERT INTO `permission` VALUES (1,'addProduct'); 2 INSERT INTO `permission` VALUES (2,'deleteProduct'); 3 INSERT INTO `permission` VALUES (3,'editProduct'); 4 INSERT INTO `permission` VALUES (4,'updateProduct'); 5 INSERT INTO `permission` VALUES (5,'listProduct'); 6 INSERT INTO `permission` VALUES (6,'addOrder'); 7 INSERT INTO `permission` VALUES (7,'deleteOrder'); 8 INSERT INTO `permission` VALUES (8,'editOrder'); 9 INSERT INTO `permission` VALUES (9,'updateOrder'); 10 INSERT INTO `permission` VALUES (10,'listOrder'); 11 INSERT INTO `role` VALUES (1,'admin'); 12 INSERT INTO `role` VALUES (2,'productManager'); 13 INSERT INTO `role` VALUES (3,'orderManager'); 14 INSERT INTO `role_permission` VALUES (1,1); 15 INSERT INTO `role_permission` VALUES (1,2); 16 INSERT INTO `role_permission` VALUES (1,3); 17 INSERT INTO `role_permission` VALUES (1,4); 18 INSERT INTO `role_permission` VALUES (1,5); 19 INSERT INTO `role_permission` VALUES (1,6); 20 INSERT INTO `role_permission` VALUES (1,7); 21 INSERT INTO `role_permission` VALUES (1,8); 22 INSERT INTO `role_permission` VALUES (1,9); 23 INSERT INTO `role_permission` VALUES (1,10); 24 INSERT INTO `role_permission` VALUES (2,1); 25 INSERT INTO `role_permission` VALUES (2,2); 26 INSERT INTO `role_permission` VALUES (2,3); 27 INSERT INTO `role_permission` VALUES (2,4); 28 INSERT INTO `role_permission` VALUES (2,5); 29 INSERT INTO `role_permission` VALUES (3,6); 30 INSERT INTO `role_permission` VALUES (3,7); 31 INSERT INTO `role_permission` VALUES (3,8); 32 INSERT INTO `role_permission` VALUES (3,9); 33 INSERT INTO `role_permission` VALUES (3,10); 34 INSERT INTO `user` VALUES (1,'zhang3','12345'); 35 INSERT INTO `user` VALUES (2,'li4','abcde'); 36 INSERT INTO `user_role` VALUES (1,1); 37 INSERT INTO `user_role` VALUES (2,2);
User
在原来的基础上增加了一个id字段。 其实。。。本知识点也没有用到这个id,只是和数据库关联了嘛,数据库里有,还是加上
1 package com.how2java; 2 3 public class User { 4 5 private int id; 6 private String name; 7 private String password; 8 public String getName() { 9 return name; 10 } 11 public void setName(String name) { 12 this.name = name; 13 } 14 public String getPassword() { 15 return password; 16 } 17 public void setPassword(String password) { 18 this.password = password; 19 } 20 public int getId() { 21 return id; 22 } 23 public void setId(int id) { 24 this.id = id; 25 } 26 27 }
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 文件 中的数据是一致的
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")); } }
Realm 概念
在 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; } }
修改 shiro.ini
shiro.ini 中原本的数据信息,都删除掉了
[main] databaseRealm=com.how2java.DatabaseRealm securityManager.realms=$databaseRealm
TestRealm
1 package com.how2java; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import org.apache.shiro.SecurityUtils; 7 import org.apache.shiro.authc.AuthenticationException; 8 import org.apache.shiro.authc.UsernamePasswordToken; 9 import org.apache.shiro.config.IniSecurityManagerFactory; 10 import org.apache.shiro.mgt.SecurityManager; 11 import org.apache.shiro.subject.Subject; 12 import org.apache.shiro.util.Factory; 13 14 public class TestShiro { 15 public static void main(String[] args) { 16 //用户们 17 User zhang3 = new User(); 18 zhang3.setName("zhang3"); 19 zhang3.setPassword("12345"); 20 21 User li4 = new User(); 22 li4.setName("li4"); 23 li4.setPassword("abcde"); 24 25 User wang5 = new User(); 26 wang5.setName("wang5"); 27 wang5.setPassword("wrongpassword"); 28 29 List<User> users = new ArrayList<>(); 30 31 users.add(zhang3); 32 users.add(li4); 33 users.add(wang5); 34 //角色们 35 String roleAdmin = "admin"; 36 String roleProductManager ="productManager"; 37 38 List<String> roles = new ArrayList<>(); 39 roles.add(roleAdmin); 40 roles.add(roleProductManager); 41 42 //权限们 43 String permitAddProduct = "addProduct"; 44 String permitAddOrder = "addOrder"; 45 46 List<String> permits = new ArrayList<>(); 47 permits.add(permitAddProduct); 48 permits.add(permitAddOrder); 49 50 //登陆每个用户 51 for (User user : users) { 52 if(login(user)) 53 System.out.printf("%s \t成功登陆,用的密码是 %s\t %n",user.getName(),user.getPassword()); 54 else 55 System.out.printf("%s \t成功失败,用的密码是 %s\t %n",user.getName(),user.getPassword()); 56 } 57 58 System.out.println("-------how2j 分割线------"); 59 60 //判断能够登录的用户是否拥有某个角色 61 for (User user : users) { 62 for (String role : roles) { 63 if(login(user)) { 64 if(hasRole(user, role)) 65 System.out.printf("%s\t 拥有角色: %s\t%n",user.getName(),role); 66 else 67 System.out.printf("%s\t 不拥有角色: %s\t%n",user.getName(),role); 68 } 69 } 70 } 71 System.out.println("-------how2j 分割线------"); 72 73 //判断能够登录的用户,是否拥有某种权限 74 for (User user : users) { 75 for (String permit : permits) { 76 if(login(user)) { 77 if(isPermitted(user, permit)) 78 System.out.printf("%s\t 拥有权限: %s\t%n",user.getName(),permit); 79 else 80 System.out.printf("%s\t 不拥有权限: %s\t%n",user.getName(),permit); 81 } 82 } 83 } 84 } 85 86 private static boolean hasRole(User user, String role) { 87 Subject subject = getSubject(user); 88 return subject.hasRole(role); 89 } 90 91 private static boolean isPermitted(User user, String permit) { 92 Subject subject = getSubject(user); 93 return subject.isPermitted(permit); 94 } 95 96 private static Subject getSubject(User user) { 97 //加载配置文件,并获取工厂 98 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); 99 //获取安全管理者实例 100 SecurityManager sm = factory.getInstance(); 101 //将安全管理者放入全局对象 102 SecurityUtils.setSecurityManager(sm); 103 //全局对象通过安全管理者生成Subject对象 104 Subject subject = SecurityUtils.getSubject(); 105 106 return subject; 107 } 108 109 private static boolean login(User user) { 110 Subject subject= getSubject(user); 111 //如果已经登录过了,退出 112 if(subject.isAuthenticated()) 113 subject.logout(); 114 115 //封装用户的数据 116 UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword()); 117 try { 118 //将用户的数据token 最终传递到Realm中进行对比 119 subject.login(token); 120 } catch (AuthenticationException e) { 121 //验证错误 122 return false; 123 } 124 125 return subject.isAuthenticated(); 126 } 127 128 }
最后
代码下载地址:https://gitee.com/fengyuduke/my_open_resources/blob/master/shiro-database.zip