SSM整合Shiro实现RBAC(八)

戒色诗: 二八佳人体似酥,腰间仗剑斩凡夫。虽然不见人头落,暗里教君骨髓枯。

在学习这一章节之前,一定要先学习一下老蝴蝶写的这篇文章: SSM实现权限管理(三)

一. 前期准备

一.一 数据库准备

数据库仍然使用的是 rbac数据库, 将 privilage 表数据改变一下, 为 菜单网址也添加 percode 属性。

有图片

一.二 前端页面准备

有图片

与前面基本一致 。

一.三 SSM框架准备

有图片

web.xml 和依赖,都需要添加。

二. SSM 整合Shiro 实现RBAC

通过 SSM 整合Shiro, 就是将 以前 shiro.ini 里面的配置内容,放置到 spring.xml 配置文件里面,

同样,要注入自定义的Realm, 设置securityManager,密码, 过滤器等内容。

但 spring整合 Shiro, 远比利用 shiro.ini 配置文件功能强大得多。

为了比较区别, 将上一章节的 shiro.ini 配置文件拿过来

[main]
#加密类
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#加密方式
credentialsMatcher.hashAlgorithmName=md5
#加密次数
credentialsMatcher.hashIterations=10
#存储散列后的密码是否为16进制 
credentialsMatcher.storedCredentialsHexEncoded=false
#配置自定义realm
myRealm=com.yjl.shiro.MyRealm
#配置加密
myRealm.credentialsMatcher=$credentialsMatcher
#注入自定义的realm
securityManager.realm=$myRealm

#配置权限
authc.loginUrl=/User/toLogin
#跳转到权限不足的路径
roles.unauthorizedUrl=/NoPermission/NoPermission
perms.unauthorizedUrl=/NoPermission/NoPermission

[urls]
#静态页面可以访问
/static/**=anon
#跳转到登录页面和登录方法可以访问
/User/toLogin=anon
/User/login=anon
#跳转到主页,需要认证
/Main/toMain=authc
/Privilege/getPrivilegeByUId=authc
#执行方法,不仅需要认证,还需要有相应的方法
/Dept/add=authc,perms["dept:add"]
/Dept/update=authc,perms["dept:update"]
/Dept/list=authc,perms["dept:list"]
/Dept/delete=authc,perms["dept:delete"]
/User/toList=authc,perms["user:toList"]
#退出登录
/User/logout=logout
#其他的一切资源,都需要认证
/**=authc

二.一 添加 pom.xml 依赖

相比较以前,只多了一个 shiro-all 的依赖信息。

<!-- shiro依赖 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-all</artifactId>
	<version>1.2.2</version>
</dependency>

二.二 web.xml 配置过滤器

<!-- 配置shiro 的过滤器 ,注意 filter-name的名称-->
<filter>
	<filter-name>shiroFilter</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	<!-- 配置生命周期,由web控制 -->
	<init-param>
		<param-name>targetFilterLifecycle</param-name>
		<param-value>true</param-value>
	</init-param>
	<!--一般不配置 targetBeanName参数,默认与filter-name 相同即可 -->
</filter>

<filter-mapping>
	<filter-name>shiroFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

二.三 编写后端代码

目录结构如下:

有图片

与前面的基本一致,没有太大的改动。

二.三.一 PrivilgeMapper 信息

注意, PrivilegeMapper.java 和 对应的 .xml 文件里面 仍然有一个根据员工编号和类型查询员工的权限方法

/*
     * 根据员工编号,查询员工的权限信息
     * */
	List<Privilege> findByUid(@Param(value="uId") Integer uId,@Param(value="type") Integer type);

对应 xml配置文件:

<select id="findByUid" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from privilege a where a.id in (
		select rp.pid from user_role ur left join role_privilege rp
		on ur.rid=rp.rid
		where ur.uid=#{uId,jdbcType=INTEGER}
	)
	<if test="type != null and type !=''" >
      and a.type=#{type,jdbcType=INTEGER}
    </if>
  </select>

这也是与前面一样的, 比较重要,故单独列一下。

二.三.二 登录和注销功能

@RequestMapping("/login")
	@ResponseBody
	public Map<String,Object> login(User userInfo){
		Map<String,Object> dataMap=new HashMap<String,Object>();
		
		//1. 获取 Subject
		Subject subject=SecurityUtils.getSubject();
		//2. 配置Token
		//配置用户名和密码
		UsernamePasswordToken token=new UsernamePasswordToken(userInfo.getCode(), userInfo.getPassword());
		try{
			//进行登录
			subject.login(token);
			
			Session session=subject.getSession();
			User user=(User)subject.getPrincipal();
			session.setAttribute("loginUser", user);
			dataMap.put("response_status",true);
		}catch(Exception e){
			e.printStackTrace();
			dataMap.put("response_status",false);
			dataMap.put("error_msg","001");
		}
		
		return dataMap;
		
	} 
	@RequestMapping("/logout")
	//退出登录
	public String logout(HttpSession session){
		//注销
		session.invalidate();
		return "login";
	}

二.四 编写自定义 Realm

这个跟前面也一样, 路径为: com.yjl.shiro.MyRealm

package com.yjl.shiro;

import java.util.List;

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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.yjl.mapper.PrivilegeMapper;
import com.yjl.mapper.UserMapper;
import com.yjl.pojo.Privilege;
import com.yjl.pojo.User;

public class MyRealm extends AuthorizingRealm{
	@Autowired
	private UserMapper userMapper;
	@Autowired
	private PrivilegeMapper privilegeMapper;
	@Override
	public String getName() {
		return "myRealm";
	}
	//授权
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection paramPrincipalCollection) {
		//获取当前的登录用户
		System.out.println("获取权限");
		User user=(User)paramPrincipalCollection.getPrimaryPrincipal();
	
		if(user==null){
			return null;
		}
		//根据 id,获取相应的权限信息
		
		List<Privilege> privilegeList=privilegeMapper.findByUid(user.getId(),null);
		
		SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
		
		for(Privilege priInfo:privilegeList){
			//仍然放置的是权限的标识 , 注意,这儿获取的是所有的非一级 权限,包括菜单和按钮。
			if(priInfo!=null&&priInfo.getPercode()!=null){
				info.addStringPermission(priInfo.getPercode());
			}
		}
		return info;
		
	}
	//认证
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken paramAuthenticationToken)
			throws AuthenticationException {
		System.out.println("进入认证");
		String code=(String)paramAuthenticationToken.getPrincipal();
		//根据用户名,去查询相应的数据
		User user=userMapper.selectInfoByCode(code);	
		if(user==null){
			//没有查询出来
			return null;
		}
		SimpleAuthenticationInfo simpleAuthenticationInfo=
				new SimpleAuthenticationInfo(user,user.getPassword(),
						//传入转换后的盐
						ByteSource.Util.bytes(user.getSalt()),getName());
		return simpleAuthenticationInfo;
		
	}
}

二.五 配置shiro 文件

文件的名称是 applicationContext-shiro.xml ,在读取 spring配置文件时,也读取进来了。 不用单独配置。

web.xml 文件中:

<!-- 启动spring -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/applicationContext-*.xml</param-value>
  </context-param>

二.五.一 配置密码加密验证

<!-- 凭证匹配器 -->  
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">  
    <property name="hashAlgorithmName" value="md5"/>  
    <property name="hashIterations" value="10"/>  
    <property name="storedCredentialsHexEncoded" value="false"/>  
</bean> 

与 以前的 shiro.ini 文件

#加密类
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#加密方式
credentialsMatcher.hashAlgorithmName=md5
#加密次数
credentialsMatcher.hashIterations=10
#存储散列后的密码是否为16进制 
credentialsMatcher.storedCredentialsHexEncoded=false

是匹配的。

二.五.二 配置自定义realm, 注入密码匹配器

<!-- 配置自定义的ream -->
<bean id="myRealm" class="com.yjl.shiro.MyRealm">
	<!-- 配置密码管理 -->
	<property name="credentialsMatcher" ref="credentialsMatcher"></property>
</bean>

与以前的

#配置自定义realm
myRealm=com.yjl.shiro.MyRealm
#配置加密
myRealm.credentialsMatcher=$credentialsMatcher

是匹配的。

二.五.三 配置 securityManager 对象,注入自定义Realm

<!-- 配置securityManager, 将一些信息注入到里面 -->

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
	<property name="realm" ref="myRealm"></property>
</bean>

与以前的:

#配置自定义realm
myRealm=com.yjl.shiro.MyRealm
#配置加密
myRealm.credentialsMatcher=$credentialsMatcher
#注入自定义的realm
securityManager.realm=$myRealm

是匹配的。

securityManager 不仅可以注入自定义Realm,还可以注入缓存,session管理,记住我等功能。

二.五.四 配置 生命周期

<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->  
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> 

与 web.xml 中 shiroFilter 部分的 targetFilterLifecycle 属性为 true 相对应。

二.五.五 配置 shiroFilter 过滤器

<!-- 配置shiroFilter 规则 ,与web.xml的过滤器中的 targetBeanName 名称保持一致-->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager"></property>	
		
		<!-- 配置登录,登录成功和没有权限的三个路径 ,以前的main 部分-->
		<property name="loginUrl" value="/User/toLogin"></property>
		<!-- <property name="successUrl" value="/Main/toMain"></property>  -->
		<property name="unauthorizedUrl" value="/Privilege/noPrivilege"></property>
		
		<!-- 配置规则 ,即urls部分-->
		<property name="filterChainDefinitions">
			<value>
				<!-- 配置拦截器 -->
				/static/**=anon
		 		/User/toLogin=anon
		 		/User/login=anon
		 		/User/logout=logout
		 		/Main/toMain=authc
		 		<!-- 没有配置的路径走认证 -->
		 		/**=authc
			</value>
		</property>
	</bean>

常用的,主要分为五个部分,下面依次讲解。

注意点: 该工厂 ShiroFilterFactoryBean 实例化bean 的 必须是 shiroFilter ,这个名称必须要与 web.xml中关于shiro配置的 filter-name 的名称相同。

二.五.五.一 注入 securityManager 对象

	<property name="securityManager" ref="securityManager"></property>	

二.五.五.二 配置 loginUrl 路径

	<property name="loginUrl" value="/User/toLogin"></property>

loginUrl 指的是 ,如果用户没有认证,或者没有通过认证,跳转到哪?

一般跳转到去登录页面的那个方法。

与以前 [main] 里面的

#配置权限
authc.loginUrl=/User/toLogin

对应。

二.五.五.三 配置 successUrl 路径

一般不配置这一个路径。

这个路径指的是,如果员工认证通过,跳转到哪? 认证通过,会跳转到主页, 但跳转到主页,一般都是通过程序代码固化下来,并不在这儿进行配置。 这个路径的实际意义是, 如果认证通过,就跳转到上一次要访问的页面。

即,如果用户 admin 想访问 用户的界面, /项目名/User/toList, 当用户手动输入这个网址时,发现没有认证,就会跳转到登录的界面,当用户登录成功后, 这个时候,会跳转到哪呢? 并不会跳转到主页那,而是跳转到上一次想访问的那个页面, 即 /项目名/User/toList 页面, 展示用户的相应信息。 这个功能还是非常好的,所以一般不配置这个属性值。

二.五.五.四 配置 unauthorizedUrl 路径

<property name="unauthorizedUrl" value="/Privilege/noPrivilege"></property>

unauthorizedUrl 指的是权限不足时,跳转到哪?

与以前 [main] 部分的

#跳转到权限不足的路径
roles.unauthorizedUrl=/NoPermission/NoPermission
perms.unauthorizedUrl=/NoPermission/NoPermission

相对应。

二.五.五.五 filterChainDefinitions 配置拦截器

<!-- 配置规则 ,即urls部分-->
<property name="filterChainDefinitions">
	<value>
		<!-- 配置拦截器 -->
		/static/**=anon
 		/User/toLogin=anon
 		/User/login=anon
 		/User/logout=logout
 		/Main/toMain=authc
 		<!-- 没有配置的路径走认证 -->
 		/**=authc
	</value>
</property>

这个与以前的 [urls] 部分

[urls]
#静态页面可以访问
/static/**=anon
#跳转到登录页面和登录方法可以访问
/User/toLogin=anon
/User/login=anon
#跳转到主页,需要认证
/Main/toMain=authc
/Privilege/getPrivilegeByUId=authc
#执行方法,不仅需要认证,还需要有相应的方法
/Dept/add=authc,perms["dept:add"]
/Dept/update=authc,perms["dept:update"]
/Dept/list=authc,perms["dept:list"]
/Dept/delete=authc,perms["dept:delete"]
/User/toList=authc,perms["user:toList"]
#退出登录
/User/logout=logout
#其他的一切资源,都需要认证
/**=authc

相对应。

发现,并没有 /Dept/add,/Dept/list ... 等权限的配置。

一个系统中,会有很多的权限,不能添加一个就在 filterChainDefinitions 部分配置一个,那么就太烦繁了, 一般都是通过 注解的方式进行配置权限。

关于注解方式配置权限,老蝴蝶下面会讲解。

二.五.六 applicationContext-shiro.xml 配置文件合并

将上面的这些部分,进行合并,得到一个总的配置信息。

<!-- 凭证匹配器 -->  
	<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">  
	    <property name="hashAlgorithmName" value="md5"/>  
	    <property name="hashIterations" value="10"/>  
	    <property name="storedCredentialsHexEncoded" value="false"/>  
	</bean>  
	
	
	<!-- 配置自定义的ream -->
	<bean id="myRealm" class="com.yjl.shiro.MyRealm">
		<!-- 配置密码管理 -->
		<property name="credentialsMatcher" ref="credentialsMatcher"></property>
	</bean>
	
	
	<!-- 配置securityManager, 将一些信息注入到里面 -->
	
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="realm" ref="myRealm"></property>
	</bean>
	
	<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->  
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> 

	<!-- 配置shiroFilter 规则 ,与web.xml的过滤器中的 targetBeanName 名称保持一致-->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager"></property>	
		
		<!-- 配置登录,登录成功和没有权限的三个路径 ,以前的main 部分-->
		<property name="loginUrl" value="/User/toLogin"></property>
		<!-- <property name="successUrl" value="/Main/toMain"></property>  -->
		<property name="unauthorizedUrl" value="/Privilege/noPrivilege"></property>
		
		<!-- 配置规则 ,即urls部分-->
		<property name="filterChainDefinitions">
			<value>
				<!-- 配置拦截器 -->
				/static/**=anon
		 		/User/toLogin=anon
		 		/User/login=anon
		 		/User/logout=logout
		 		/Main/toMain=authc
		 		<!-- 没有配置的路径走认证 -->
		 		/**=authc
			</value>
		</property>
	</bean>

二.六 前端页面

前端页面的配置信息,与以前基本一致,故不重复写了。

重点提一下, dept.jsp 页面的按钮标识控制部分。

dept.jsp 页面设置标识:

<shiro:hasPermission name="dept:add">
		<script>
			 sessionStorage.setItem("dept_update",true);
		</script>
	</shiro:hasPermission>
	<shiro:hasPermission name="dept:update">
		<script>
			 sessionStorage.setItem("dept_update",true);
		</script>
	</shiro:hasPermission>
	<shiro:hasPermission name="dept:delete">
		<script>
			 sessionStorage.setItem("dept_delete","true")
		</script>
	</shiro:hasPermission>
<script type="text/javascript" src="${pageContext.request.contextPath}/static/js/dept.js"></script>

dept.js 脚本里面获取标识:

//看是否有添加的权限
var add=sessionStorage.getItem("dept_add");
if(add){
	$("#add").show();
}else{
	$("#add").hide();
}
//看修改和删除是否显示和隐藏
function operateFormatter(value, row, index) {
	var update=sessionStorage.getItem("dept_update");
	var del=sessionStorage.getItem("dept_delete");
	//console.log("update:"+update+",del:"+del);
	var udpateIcon="";
	if(update){
		udpateIcon='<a class="update text-primary" href="javascript:void(0)" data-toggle="tooltip" title="修改">'
			+'<i class="fa fa-pencil"></i>&nbsp修改&nbsp;&nbsp;&nbsp;&nbsp;</a>';
	}
	var delIcon="";
	if(del){
		 delIcon='<shiro:hasPermission name="dept:delete"><a class="delete text-danger" href="javascript:void(0)" data-toggle="tooltip" title="撤销">'
			+'<i class="fa fa-minus"></i>&nbsp;删除&nbsp;</a></shiro:hasPermission">';
	}
	return udpateIcon+delIcon;
}

二.七 测试验证

输入网址: http://localhost:8080/SSM_Shiro_RBAC/User/toLogin

输入admin,1234 进行测试 (错误的密码情况就不测试了)

有图片

有图片

看起来,是正常的,

然后,当手动输入 部门的添加功能呢, http://localhost:8080/SSM_Shiro_RBAC/Dept/add

有图片

响应是正常的, 这是因为我们没有配置具体的权限验证呢。

三. 注解式配置具体权限

在 applicationContext-shiro.xml 中的 filterChainDefinitions 属性中,虽然可以像 shiro.ini 那样,配置具体的权限,但是当权限过多时,再那样配置,就不太好了。

所以 shiro 提供了注解式配置。

有图片

注解名称 意义
@RequiresPermissions 必须具有权限
@RequiresAuthentication 必须通过认证
@RequiresGuest 匿名访客
@RequiresRoles 必须具有角色
@RequiresUser 是否被记忆,两个含义,一个成功认证的,二是记住我登录的

只需要在某个具体的方法上面加入相应的注解即可。

如: 在 部门的 add() 方法上面 ,添加 @RequiresPermissions("Dept:add") 注解

@RequiresPermissions("dept:add")
@RequestMapping("/add")
@ResponseBody
public Map<String,Object> add(){
	System.out.println("实现部门添加的方法");
	Map<String,Object> map=new HashMap<String,Object>();
	map.put("response_status",true);
	return map;
}

shiro 会先看一下,当前会话登录的用户,是否具有 权限 Dept:add ,如果有才执行 add 方法,如果没有,不执行,抛出异常。

会从 MyRealm 里面的 doGetAuthorizationInfo() 返回对象里面取数据进行比较。

一般常用的,就是 权限验证。

@Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions
{
  String[] value();
  
  Logical logical() default Logical.AND;
}

默认是 AND

配置单个权限:

@RequiresPermissions("dept:add")

表示具有权限 dept:add 才执行

配置多个权限与:

@RequiresPermissions({"dept:add","dept:update"})

表示具有权限 dept:add和 dept:update, 两个权限同时具有才执行

配置多个权限或:

@RequiresPermissions(value={"dept:add","dept:update"},logical=Logical.OR)

表示具有权限 dept:add 或者 dept:update 任意一个均可。

四. 注解式声明 Shiro 权限

直接在方法上 添加 @RequiresPermissions("dept:add") 注解是不生效的,必须要动态代理一下。

由于 权限注解被放置在 Action类的方法上,所以配置动态代理,必须要配置在 springmvc.xml 文件里面。

applicationContext-shiro.xml 文件不用动。

四.一 配置注解

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
		<property name="securityManager" ref="securityManager"></property>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" 
	depends-on="lifecycleBeanPostProcessor">
	<property name="proxyTargetClass" value="true"></property>
</bean> 

四.二 验证

admin 没有 部门的 添加权限, 当手动输入添加的地址时,会报错。

有图片

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [dept:add]

当输入 delete 网址时:

有图片

说明,注解权限是可以的。

需要处理一下异常,如果没有权限的话,就跳转到 无权限那个提示页面。

四.三 springmvc.xml 捕获异常

关于 SpringMVC 的异常处理,可以看老蝴蝶以前写的 SpringMVC系列文章。

<!-- 配置异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
	<property name="exceptionMappings">
		 <props>
		 	<!-- 每个异常类,对应一个jsp -->
		 	 <!-- 没有权限,到哪个具体的页面 -->
        	<prop key="org.apache.shiro.authz.AuthorizationException">noPrivilege</prop>
        	<!-- 没有认证通过 -->
        	<prop key="org.apache.shiro.authc.AuthenticationException">login</prop>
			<!-- 可继续添加多个 ,如StudentException -->
		</props>
	</property>
</bean>

配置好之后,重新测试

当登录成功后,再输入 部门添加的网址后,

有图片

注意观察控制台的打印信息:

有图片

发现,自定义Realm 中的授权方法,被执行了很多次, 每一个注解方法,每一个前端的 shiro:hasPermission 判断,都需要执行一下这个方法,频繁地取数据, 影响数据库的效率, 所以我们需要做缓存的处理。 老蝴蝶下一章节再讲缓存。


本章节代码链接为:

链接:https://pan.baidu.com/s/1qjZcQfKQa4f_SdCjXvTtxQ 
提取码:ko9l 

posted @ 2021-01-29 09:28  两个蝴蝶飞  阅读(190)  评论(0编辑  收藏  举报