一、RBAC介绍
RBAC:基于角色的权限管理
简单的理解为:谁,扮演什么角色,被允许做什么操作
用户对象: user : 当前操作用户
角色对象:role: 表示权限操作许可权的集合
权限对象: permission:资源操作许可权
例子: 张三(user) 下载(permission) 一个高清无码的种子(资源) , 需要VIP权限(role)
简单的流程就是:
张三现在是普通用户 ,需要授权 成为VIP才可以下载指定的资源。
二、权限校验的几种方式
1、 编程方式
通过写if/else授权代码块完成
if(subject.hasRole("admin")){
//有权限
}
else{
//无权限
}
2、注解方式:
通过再执行的java方法上放置相应的注解完成
@RequiresRoles("admin")
@RequiresPermission("employee:saves")
@RequestMapping("/save")
public void hello(){
//有权限
}
上面中必须要有admin角色,以及对employee这个资源的save操作才可以访问hello方法
三、实例演示
1、ini方式检查用户拥有的角色
建立一个ini文件,命名shiro-permission(随意)
[users]
#用户张三的密码是666 ,此时用户具有的角色是role1,role2
zhangsan=666,role1,role2
lisi=888,role2
[roles]
#role定义的格式: 资源名:操作:实例
#角色role1对资源user拥有create、update权限
role1=user:create,user:update
#角色role2对资源user拥有create,delete权限
role2=user:create,user:delete
#角色role3对资源user拥有create权限
role3=user:create
测试
//验证用户角色
@Test
public void testForShiroRoles(){
Subject subject = ShiroUtils.getSubject("classpath:shiro-permission");
//假设这个是用户输入的用户名
String username = "haizhang";
//假设这个是用户输入的密码
String password = "111";
//设置迭代的次数,迭代次数越大,加密生成的密文就越难以被破解
try {
//模拟用户名和密码
AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
//模拟用户登录,会去shiro.ini中对比
subject.login(authenticationToken);
}
catch (UnknownAccountException e){
System.out.println(e.getMessage());
System.out.println("找不到账号");
}
catch (IncorrectCredentialsException e){
System.out.println(e.getMessage());
System.out.println("密码错误");
}
//只有通过登录认证之后,采用去检查是否对资源拥有操作的角色
if(subject.isAuthenticated()){
//判断登录的用户haizhang,是否拥有role3和role2的角色,返回的是角色对应的boolean数组,true表示拥有这个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("role3", "role2"));
System.out.println(Arrays.toString(booleans));
//判断登录的用户haizhang是否拥有单个角色。
boolean role1 = subject.hasRole("role1");
System.out.println(role1);
//进行checkRoles判断,它没有返回值,但是如果用户不存在指定的某个角色,会抛出异常
try{
// subject.checkRole("role5");
subject.checkRoles("role1","role2","role3");
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
其中上面代码的shiroUtils只是我封装的一个工具类,给出代码使用到的这部分代码:
public class ShiroUtils{
public static Subject getSubject(String iniPath){
Factory<SecurityManager> factory = new IniSecurityManagerFactory(iniPath);
SecurityManager sec = factory.getInstance();
SecurityUtils.setSecurityManager(sec);
return SecurityUtils.getSubject();
}
}
代码运行的结果:
2、 ini方式检查用户拥有的权限
同样引用上面的ini文件,我们这里讲解如何使用subject函数验证用户角色对资源拥有的权限
//验证用户角色
@Test
public void testForShiroRoles2(){
Subject subject = ShiroUtils.getSubject("classpath:shiro-permission");
//假设这个是用户输入的用户名
String username = "haizhang";
//假设这个是用户输入的密码
String password = "111";
//设置迭代的次数,迭代次数越大,加密生成的密文就越难以被破解
try {
//模拟用户名和密码
AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
//模拟用户登录,会去shiro.ini中对比
subject.login(authenticationToken);
}
catch (UnknownAccountException e){
System.out.println(e.getMessage());
System.out.println("找不到账号");
}
catch (IncorrectCredentialsException e){
System.out.println(e.getMessage());
System.out.println("密码错误");
}
//只有通过登录认证之后,采用去检查是否对资源拥有操作的角色
if(subject.isAuthenticated()){
boolean[] permitted = subject.isPermitted("user:delete", "user:create", "user:search");
System.out.println(Arrays.toString(permitted));
try {
subject.checkPermission("user:delete");
subject.checkPermission("user:update");
subject.checkPermission("user:search");
} catch (AuthorizationException e) {
e.printStackTrace();
}
}
}
结果:
说明:
上面还是使用Subject类的checkxxx
和isPermitted
方法来验证该用户拥有的所有的角色中,是否存在一个角色拥有对资源的某个具体的操作权限,其中checkxxx
如果是成功的检测存在某个权限/角色。就不会抛异常。而如果没有到某个指定的权限存在,就会抛异常。
四、采用自定义realm重写权限认证方法
上面的方式还是太过于死板,将用户拥有的所有权限和密码冰冷的写在ini文件中,这样不用于进行扩展和修改。我们使用自定义realm的方式,很方便的结合数据库进行查询。
- 定义自己的reaml类
public class MyRoleRealm extends AuthorizingRealm {
@Override
public String getName() {
return "MyRoleRealm";
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
String username_db = "haizhang";
String password_db ="111";
if(!username_db.equals(username)){
return null;
}
return new SimpleAuthenticationInfo(username_db,password_db,getName());
}
/**
* @param principals 用户认证凭证信息
* @return
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户通过认证的用户名
String username = (String)principals.getPrimaryPrincipal();
//模拟从数据库中查出的角色以及角色拥有的权限
String roles[] = {"role1","role2"};
//* 代表拥有user资源的任何操作权限
String permission[] = {"user:*"};
//将查出的权限角色信息,放入SimpelAuthorization中包装
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(Arrays.asList(roles));
simpleAuthorizationInfo.addStringPermissions(Arrays.asList(permission));
return simpleAuthorizationInfo;
}
}
- 定义ini文件
myRealm= com.haizhang.shirodemo.reaml.MyRoleRealm
securityManager.realms=$myRealm
- 测试类
//验证用户角色
@Test
public void testForShiroRoles2(){
Subject subject = ShiroUtils.getSubject("classpath:shiro-custom-permission");
//假设这个是用户输入的用户名
String username = "haizhang";
//假设这个是用户输入的密码
String password = "111";
try {
//模拟用户名和密码
AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
subject.login(authenticationToken);
}
catch (UnknownAccountException e){
System.out.println(e.getMessage());
System.out.println("找不到账号");
}
catch (IncorrectCredentialsException e){
System.out.println(e.getMessage());
System.out.println("密码错误");
}
//只有通过登录认证之后,采用去检查是否对资源拥有操作的角色
if(subject.isAuthenticated()){
boolean b = subject.hasRole("role1");
System.out.println("拥有角色role1:"+b);
boolean[] permitted = subject.isPermitted("user:delete", "user:create", "user:search");
System.out.println(Arrays.toString(permitted));
try {
subject.checkPermission("user:delete");
subject.checkPermission("user:update");
subject.checkPermission("user:search");
} catch (AuthorizationException e) {
e.printStackTrace();
}
}
}
结果:
五、小结:整个授权流程
- 首先调用
Subject.isPermitted/hasRole
接口,其会委托给SecurityManager
,而SecurityManager接着会委托给Authorizer
Authorizer
是真正的授权者,如果我们调用如isPermitted(“user:view”)
, 其首先会通过PermissionResolver
把字符串转换成相应的Permission
实例- 在进行授权之前,它会调用相应的
Realm
进行获取Subject
相应的角色/权限用于匹配传入的角色/权限; Authorizer
会判断Realm
的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModeularReamlAuthorizer
进行循环判断,如果匹配如isPermitted/hasRole
。会返回true,否则返回false表示授权失败。
六、Shiro的三种授权配置方式
1.applicationContext-shiro.xml配置
在spring中使用xml方式进行配置某个uri需要用户拥有的的角色权限:
<property name=”filterChainDefinitions“>
<value>
/docs/** = authc, perms[document:read]
/admin/** = authc, roles[admin]
</value>
</property>
解释:
- 访问上面/docs/开头的uri接口,需要进行授权校验(authc),必须有document:read权限才可以访问。
- 访问上面/admin/开头的uri接口,需要有admin角色才允许访问。
2.注解方法:
2.1 开启controller类aop支持
- 如果你是spring项目
在springmvc.xml中配置:
<!-- 开启aop,对类代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 开启shiro注解支持 -->
<bean
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
- 如果你是springboot项目,可以如下配置:
定义一个ShiroConfiguration的配置类,配置如下,开启支持shiro注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor
= new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
在controller方法中添加注解
@RequestMapping("/user")
@RequiresPermissions("userInfo:test")
public Result<T> getUser(){}
若还没生效,应该是aop没起作用
解决方法一
ShiroConfiguration中增加
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
解决方法二
在pom中引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
同时application.properties
中补充
spring.aop.proxy-target-class=true
3.JSP授权(页面上根据权限设置菜单显示与否)
Jsp页面添加:
<%@ tagliburi="http://shiro.apache.org/tags" prefix="shiro" %>
其中我们还可以通过property获取对应的用户身份属性:
样例:
七、总结:
当调用controller的一个方法,由于该 方法加了@RequiresPermissions("item:query")
,shiro调用realm获取数据库中的权限信息,看"item:query"是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。
当展示一个jsp页面时,页面中如果遇到<shiro:hasPermission name="item:update">
,shiro调用realm获取数据库中的权限信息,看item:update
是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。
还有一种情况是有时候连接是在Ajax请求之后拼接到页面的,有时候也需要根据权限进行判断,项目中也遇到这种情况:
思路:在页面中定义一个JS全局变量,在shiro权限标签里面,如果有权限修改全局变量的值,在JS中根据全局变量的值判断是否有权限
(1)页面定义全局变量
<script>
var hasOperatingDepart=false;
</script>
(2)页面用shiro标签判断是否有权限:(有权限会执行JS脚本改变全局变量的值)
(3)JS拼接的时候根据全局变量判断是否有权限:
有时候我们需要在代码中判断用户是否有某些权限;
// 获取用户信息
Subject currentUser = SecurityUtils.getSubject();
//是否有权限
boolean permitted = currentUser.isPermitted("exammanager:factory");
// 判断是否有全厂管理的权限,有就不添加部门ID,没有就设为当前Session中的部门ID
String departmentId = permitted ? null : departmentIdSession;
有时候我们需要在代码中判断用户是否有某些角色:
// 获取用户信息
Subject currentUser = SecurityUtils.getSubject();
boolean hasRole = currentUser.hasRole("教研室");
boolean hasRole2 = currentUser.hasRole("院长")
上面获取的主体的权限码是我们在授权的时候塞进去的,当然我们也可以将角色码也塞进去:
package cn.xm.jwxt.shiro;
import cn.xm.jwxt.bean.system.Permission;
import cn.xm.jwxt.bean.system.User;
import cn.xm.jwxt.service.system.UserService;
import cn.xm.jwxt.utils.ValidateCheck;
import org.apache.shiro.SecurityUtils;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @Description 自定义realm。根据上面传下来的token去数据库查信息,查到返回一个SimpleAuthenticationInfo,查不到返回null(用于shiro认证)
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealm");
}
// realm的认证方法,从数据库查询用户信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userCode=(String)token.getPrincipal();//获取token的主身份(登录的username
//自定义User bean
User user = null;
try {
user = userService.getUserByUserCode(userCode);
} catch (Exception e) {
e.printStackTrace();
}
AuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
return authenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//0.下面方法principals.getPrimaryPrincipal()获取的是在上面认证的时候装进AuthenticationInfo的对象
String userId=((User)(principals.getPrimaryPrincipal())).getUserid();
SimpleAuthorizationInfo simpleAuthorizationInfo=null;
try {
simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//1.设置所有的权限(注意权限是以字符串的形式保存的权限码)
List<Permission> permissions1 = userService.selectPermissionsByUserId(userId);//获取所有权限码
Set<String> permissions = new HashSet<>();
for(Permission permission:permissions1){
if(ValidateCheck.isNotNull(permission.getPermissioncode())){
permissions.add(permission.getPermissioncode());
}
}
if (permissions != null && permissions.size()>0) {
simpleAuthorizationInfo.setStringPermissions(permissions);
}
//2.设置角色,角色也是以字符串的形式表示(这里存的是角色名字)
Set<String> userRoleNames = userService.getUserRoleNameByUserId(userId);
if(userRoleNames != null && userRoleNames.size()>0){
simpleAuthorizationInfo.setRoles(userRoleNames);
}
} catch (Exception e) {
e.printStackTrace();
}
return simpleAuthorizationInfo;
}
}
获取用户信息
@RequestMapping("/first.action")
public String first(Model model)throws Exception{
//从shiro的session中取activeUser
Subject subject = SecurityUtils.getSubject();
//取身份信息
User user = (User) subject.getPrincipal();
//通过model传到页面
model.addAttribute("activeUser", user);
return "/first";
}