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

这个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"));
    }
}
点击展开

 

Realm 概念

在 Shiro 中存在 Realm 这么个概念, Realm 这个单词翻译为 域,其实是非常难以理解的。 
域 是什么鬼?和权限有什么毛关系? 这个单词Shiro的作者用的非常不好,让人很难理解。 
那么 Realm 在 Shiro里到底扮演什么角色呢? 
当应用程序向 Shiro 提供了 账号和密码之后, Shiro 就会问 Realm 这个账号密码是否对, 如果对的话,其所对应的用户拥有哪些角色,哪些权限。 
所以Realm 是什么? 其实就是个中介。 Realm 得到了 Shiro 给的用户和密码后,有可能去找 ini 文件,就像Shiro 入门中的 shiro.ini,也可以去找数据库,就如同本知识点中的 DAO 查询信息。

Realm 就是干这个用的,它才是真正进行用户认证和授权的关键地方。

DatabaseRealm

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

前面准备了DatabaseRealm,那么在配置文件里,就指定当前的realm 是 他。 默认情况下是找 IniRealm。
shiro.ini 中原本的数据信息,都删除掉了
 
[main]
databaseRealm=com.how2java.DatabaseRealm
securityManager.realms=$databaseRealm

 

TestRealm

TestRealm 没任何变化,运行效果和基于 ini的效果一模一样。
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

 

 

 
 

 

posted @ 2019-02-19 15:30  风雨渡客  阅读(2580)  评论(0编辑  收藏  举报