Shiro安全框架学习(二) —— shiro结合数据库进行验证和授权

 数据库支持

 在 Shiro安全框架学习(一) 中使用ini 配置文件进行了相关权限数据的配置。

但是实际工作中,我们都会把权限相关的内容放在数据库里。


rbac概念

rbac 是当下权限系统的设计基础,同时有两种解释:

1,Role-Based Access Control,基于角色的访问控制

即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色。(用户拥有该角色,角色拥有该权限)

2,Resource-Based Access Control,基于资源的访问控制

即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限。(用户拥有权限)


权限管理包括用户身份认证和授权两部分,简称认证授权。

对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。


表结构 

 基于 ORAC 概念, 就会存在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,'editeProduct');
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,'editeOrder');
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);

User

 在原来的基础上增加了一个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

 这个DAO提供了和权限相关查询。 但是,并没有提供权限数据本身的维护。 比如没有做用户的增删改,角色和权限表也没有。 因为那些在提供了 表数据 的基础上,就不是必须的了。 
为了专注于 Shiro 和 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",
				"123456");
	}

	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"));
	}
}

方法解析: 

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. 运行dao的主方法测试
运行之后,看到如图所示 zhang3,li4 拥有的角色和权限和  Shiro入门中的shiro.ini 文件 中的数据是一致的:


Reaml 概念简介 

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

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


DatabaseRealm 

 DatabaseRealm 就是用来通过数据库验证用户,和相关授权的类。
两个方法分别做验证和授权:
doGetAuthenticationInfo(),doGetAuthorizationInfo()

  • Authentication(认证):用户身份识别,通常被称为用户“登录”。
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。

细节在代码里都有详细注释,请仔细阅读。

注:

 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
	/**
	 * doGetAuthorizationInfo():授权
	 */
	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
	/**
	 * doGetAuthenticationInfo():验证
	 */
	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

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

前面准备了DatabaseRealm,那么在配置文件里,就指定当前的realm 是 它默认情况下是找 IniRealm
shiro.ini 中原本的数据信息,都删除掉了


TestRealm

 TestRealm 没任何变化,运行效果和基于 ini的效果一模一样。

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方法)
			subject.login(token);
		} catch (AuthenticationException e) {
			//验证错误
			return false;
		}				

		return subject.isAuthenticated();
	} 
    
}

 关于JdbcRealm

Shiro 提供了一个 JdbcRealm,它会默认去寻找 users, roles, permissions 三张表做类似于 DAO 中的查询。
但是本例没有使用它,因为实际工作通常都会有更复杂的权限需要以上3张表不够用。 JdbcRealm 又封装得太严实了,连它执行了 SQL 语句这件事,我都是很久才明白过来,这样不利于初学者消化, 最后 使用 DAO 更符合开发者的习惯,也觉得一切都在自己掌握中,用起来心里更踏实一些。 


rbac概念详细解释

1.3.6 权限控制

用户拥有了权限即可操作权限范围内的资源,系统不知道主体是否具有访问权限需要对用户的访问进行控制。

1.3.6.1 基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:

 

上图中的判断逻辑代码可以理解为:

if(主体.hasRole("总经理角色id")){

    查询工资
}

缺点:以角色进行访问控制粒度较粗,如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断主体的角色是否是总经理或部门经理”,系统可扩展性差

修改代码如下,因此系统可扩展性差:

if(主体.hasRole("总经理角色id") ||  主体.hasRole("部门经理角色id")){

    查询工资
}

1.3.6.2 基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制,比如:主体必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:

上图中的判断逻辑代码可以理解为:

if(主体.hasPermission("查询工资权限标识")){

    查询工资

}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也只需要将“查询工资信息权限”添加到“部门经理角色”的权限列表中,判断逻辑不用修改,系统可扩展性强


参考来源于:

http://how2j.cn/k/shiro/shiro-database/1721.html    源代码来源how2java

分享牛:https://blog.csdn.net/qq_30739519/article/details/51472101

猜你喜欢

转载自blog.csdn.net/weixin_41888813/article/details/81358739