前言:
这是一个系列的文章,这是第八篇,如果只是看了这一篇或者对于Shiro没有基础的人,看到这一篇可能并不会有多大收益。前面几篇文章已经介绍了Shiro+JSP+SpringBoot+Mybatis+mysql的整合,并实现了使用MD5+盐+hash散列的方式对密码进行加密的注册登录功能。这篇是基于之前的文章进行写作的,下面就要说下登录完成后怎么实现授权操作。也就是怎么使用Shiro实现权限管理,前后端的授权是分开的,准确的说是没有关系的,所以这里也是对前后端的授权操作分开讲解。
一.使用Shiro实现对前端的授权
这里介绍的前端授权是基于JSP的,因为Shiro对于集成JSP是比较友好的,所以就拿JSP来学习以及总结了,再加上笔者是个彻底的后端开发,所以就挑了简单的进行学了总结了。但其实对于后端的开发来说,这块是属于可以不用掌握的范围。但是我们要是掌握了,对于我们对前后端全局的感知会更好。况且学习下也不费劲,所以就一起学习了下。
1.基于角色
无论是前端还是后端,权限的管理都是分为两种,一种是基于角色的一种是基于资源的,基于角色的是为用户赋予角色信息,每个角色可以访问的资源我们在页面里进行判断,基于资源的就是为每个用户对应的角色设置资源描述符然后再进行判断。下面说下这个过程的详细实现:
1.导入shiro标签库
首先我们需要为JSP导入集成Shiro的标签库。
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
2.对内容管理按钮进行角色验证
如下,我们可以看出给内容管理按钮增加了admin角色的验证,只有拥有了该角色,才能看到该按钮。
<body> <h1>系统主页</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">用户管理</a></li> <li><a href=""></a>商品管理</li> <li><a href=""></a>商户管理</li> <shiro:hasRole name="admin"> <li><a href=""></a>内容管理</li> </shiro:hasRole> </ul> </body>
3.实现Realm中的授权方法
我们知道,只要我们在调用判断是否拥有角色、是否拥有权限字符串相关方法时,就会触发授权认证的方法,如果不调用是不会触发该方法的(抛开缓存说的)。这里就不演示了,有兴趣的可以测试以下。我们直接实现授权操作。
public class FirstRealm extends AuthorizingRealm { @Autowired ShiroUserService shiroUserService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("触发授权操作了"); String username = (String)principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); List<ShiroUserPermission> shiroUserPermissionList = shiroUserService.queryShiroUserPermissionByUsername(username); shiroUserPermissionList.stream().forEach( shiroUserPermission -> { if(shiroUserPermission.getRole()!=null){ simpleAuthorizationInfo.addRole(shiroUserPermission.getRole()); } if(shiroUserPermission.getPermission()!=null){ simpleAuthorizationInfo.addStringPermission(shiroUserPermission.getPermission()); } } ); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken; ShiroUser shiroUser = shiroUserService.queryUser(usernamePasswordToken.getUsername()); if(shiroUser!=null){ return new SimpleAuthenticationInfo(shiroUser.getUsername(),shiroUser.getPassword(),ByteSource.Util.bytes(shiroUser.getSalt()),this.getName()); } return null; } }
如上面代码中的第一个方法,就是用来获取数据库中的权限信息的,代码也很简单,就是获取到主账户信息,然后去数据库查询该账户下所有的角色以及权限信息。
4.service层接口
public interface ShiroUserService { void insertUser(ShiroUser shiroUser); ShiroUser queryUser(String username); List<ShiroUserPermission> queryShiroUserPermissionByUsername(String username); }
实现类与dao层接口就不展示了,没啥区别,下面展示下mapper.xml的内容
5.对应的mapper.xml内容
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo5.dao.ShiroUserDao"> <resultMap id="ShiroResultMap" type="com.example.demo5.entry.ShiroUserPermission"> <result column="username" property="username" jdbcType="VARCHAR" /> <result column="role" property="role" jdbcType="VARCHAR" /> <result column="permission" property="permission" jdbcType="VARCHAR" /> </resultMap> <sql id="shiro_user_fields"> oid,username,password,salt </sql> <insert id="insertUser" parameterType="ShiroUser" useGeneratedKeys="true" keyProperty="oid"> insert into shiro_user values (#{oid},#{username},#{password},#{salt}) </insert> <select id="queryUser" parameterType="java.lang.String" resultType="ShiroUser"> select <include refid="shiro_user_fields"/> from shiro_user where 1=1 <if test="username != null and username != ''"> and username = #{username} </if> </select> <select id="queryShiroUserPermissionByUsername" resultMap="ShiroResultMap" parameterType="String"> SELECT distinct a.username, c.role, e.permission FROM shiro_user a LEFT JOIN shiro_user_role_relation b ON a.oid = b.shiro_user_oid left join shiro_role c on b.shiro_role_oid = b.oid left join shiro_role_permission d on c.oid = d.shiro_role_oid left join shiro_permission e on d.shiro_permission_oid = e.oid where a.username = #{username} </select> </mapper>
这是对应的mapper文件内容。
6.对应的表的创建
权限管理经典的五张表,用户表、角色表、权限表、用户角色表、角色权限表。如果不清楚这五张表关系,建议看下这篇文章:权限管理的五张表 ,这里就展示下五张表的内容:
用户表:shiro_user
角色表:shiro_role
权限表:shiro_permission
用户角色表:shiro_user_role_relation
角色权限表:shiro_role_permission
分析以上表的内容,我们很容一发现,秦始皇这个用户拥有admin和user两个角色,admin这个角色拥有admin:context:view的资源权限字符串,这样秦始皇这个用户所拥有的权限就清晰了。两个角色admin、user,一个资源描述符admin:context:view。
7.测试授权是否成功
根据以上六个步骤,我们已经将前提条件准备完全了,下面就来测试下, 登录进入首页后能否看到内容管理,如果看得到就说明,授权成功了。
从图中可以看出,内容管理显示了出来,说明验证秦始皇这个用户拥有admin的角色,已经通过了。刚刚我们已经知道秦始皇拥有admin与user两个角色。我们再来验证下,让商户管理拥有两个角色任意一个就可以看到。jsp代码如下:
<body> <h1>系统主页</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">用户管理</a></li> <li><a href=""></a>商品管理</li> <shiro:hasAnyRoles name="admin,user"> <li><a href=""></a>商户管理</li> </shiro:hasAnyRoles> <shiro:hasRole name="admin"> <li><a href=""></a>内容管理</li> </shiro:hasRole> </ul> </body>
刷新网页,展示如下:
依然可以看到商户管理,说明验证通,那么我们再来验证个不存在的角色,若是秦始皇拥有system这个角色,就展示商品管理。很显然当前用户不拥有该角色,理论上会看不到商品管理。jsp代码修改如下:
<body> <h1>系统主页</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">用户管理</a></li> <shiro:hasRole name="system"> <li><a href=""></a>商品管理</li> </shiro:hasRole> <shiro:hasAnyRoles name="admin,user"> <li><a href=""></a>商户管理</li> </shiro:hasAnyRoles> <shiro:hasRole name="admin"> <li><a href=""></a>内容管理</li> </shiro:hasRole> </ul>
~~~~ 刷新页面,结果如下: ![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20210328233203578.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ2ODk3OTIz,size_16,color_FFFFFF,t_70) 我们发现,商品管理以及消失了,因为秦始皇并没有system这个角色,所以当前用户看不到这个菜单。以上这些就是使用JSP时,基于角色实现的权限管理。下面我们就来看下基于资源如何实现权限管理
2.基于资源
在上面基于角色的实现的前端权限控制时,已经详细的介绍了每一步的实现,基于资源与基于角色的唯一区别就是,JSP页面的标签的区别。其他都过程都是相同的,包括使用的service方法,dao方法,mapper文件,数据库表等等都是同一个。所以这里就不重复介绍了。我们直接介绍JSP这部分有区别的部分就好。
我们还是基于商户管理这个按钮来验证,JSP代码修改如下:
<body> <h1>系统主页</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">用户管理</a></li> <li><a href=""></a>商品管理</li> <shiro:hasPermission name="admin:context:view"> <li><a href=""></a>商户管理</li> </shiro:hasPermission> <shiro:hasRole name="admin"> <li><a href=""></a>内容管理</li> </shiro:hasRole> </ul> </body>
根据上面展示的数据库信息我们可以知道,秦始皇是拥有这个资源描述符的,或者说他拥有这个权限,正常情况下我们应该可以看到商户管理。刷新页面展示如下:
很显然结果是正确的,那我们将admin:context:view改成admin:context:delete呢?理论上是看不到商户管理了。JSP改的代码如下:
<body> <h1>系统主页</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">用户管理</a></li> <li><a href=""></a>商品管理</li> <shiro:hasPermission name="admin:context:delete"> <li><a href=""></a>商户管理</li> </shiro:hasPermission> <shiro:hasRole name="admin"> <li><a href=""></a>内容管理</li> </shiro:hasRole> </ul> </body>
刷新页面,结果如下:
我们可以看到,商户管理以及消失了,因为访问商户管理所需的资源描述符admin:context:delete,秦始皇这个用户是没有的,所以自然就看不到了,这就是JSP页面基于资源描述符实现的权限管理。
二.使用Shiro实现对后端的授权
JSP实现权限控制已经说完了,运行时JSP其实也是被编译成class文件运行的,JSP底层也是java代码所写,所以后端的授权操作其实与JSP实现的授权区别不大。出了在请求资源中的权限判断其他部分其实都是相同的。下面我们就来一起看下吧。
1.基于角色
1.首先需要为JSP页面中的内容管理增加一个跳转,修改如下:
<body> <h1>系统主页</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">用户管理</a></li> <li><a href="">商品管理</a></li> <shiro:hasPermission name="admin:context:view"> <li><a href="">商户管理</a></li> </shiro:hasPermission> <shiro:hasRole name="admin"> <li><a href="${pageContext.request.contextPath}/user/context">内容管理</a></li> </shiro:hasRole> </ul> </body>
2.提供一个context的接口,供点击跳转。
@RequestMapping("/context") public String goContext(){ System.out.println("进入内容管理后端接口"); return "redirect:/context.jsp"; }
3.提供一个context的页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>内容管理页面</title> <style type="text/css"> *{margin: 0;padding: 0;} form{margin: 0 auto;padding:15px; width: 300px;height:300px;text-align: center;} #submit{padding: 10px} #submit input{width: 50px;height: 24px;} </style> </head> <body> <h1>内容管理页面</h1> <ul> <a href="${pageContext.request.contextPath}/user/logout">退出登录</a> <li><a href="">内容1</a></li> <li><a href=""></a>内容2</li> <shiro:hasPermission name="admin:context:view"> <li><a href=""></a>内容3</li> </shiro:hasPermission> <shiro:hasRole name="admin"> <li><a href="${pageContext.request.contextPath}/user/context"></a>内容4</li> </shiro:hasRole> </ul> </body> </html>
4.修改后端接口,对角色进行判断,代码如下:
@RequestMapping("/context") public String goContext(){ System.out.println("进入内容管理后端接口"); Subject subject = SecurityUtils.getSubject(); String role = "admin"; if(subject.hasRole(role)){ System.out.println("当前用户"+ subject.getPrincipal()+",拥有该角色:" + role); return "redirect:/context.jsp"; }else{ System.out.println("当前用户"+ subject.getPrincipal()+",未拥有该角色:" + role); return "redirect:/login.jsp"; } }
可以清晰看到,当秦始皇拥有admin这个角色时才能够进入到内容管理页面,否则会重定向到登录页。登录系统点击内容管理展示如下:
说明后端角色验证成功了,下面呢是后端打印的日志:
那么我们换一个秦始皇没有的角色呢,比如system。后台接口代码修改如下:
@RequestMapping("/context") public String goContext(){ System.out.println("进入内容管理后端接口"); Subject subject = SecurityUtils.getSubject(); String role = "system"; if(subject.hasRole(role)){ System.out.println("当前用户"+ subject.getPrincipal()+",拥有该角色:" + role); return "redirect:/context.jsp"; }else{ System.out.println("当前用户"+ subject.getPrincipal()+",未拥有该角色:" + role); return "redirect:/login.jsp"; } }
这种情况理论下点击内容管理按钮会跳转到登录页。我们来测试下。结果如下:
很显然我们跳转到了首页,验证了秦始皇还没有system这个角色的。同时我们可以使用Shiro提供的注解RequiresRoles,不用在代码里加入这些判断,只需要在注解中声明需要校验的角色就行。
上面的动图使用了一款叫GifGam的制图软件。感兴趣可以看下这里:动图制作工具
该工具只有1.6M大小,解压即用。
2.基于资源
那么基于资源我们要怎么实现权限的认证呢,其实和角色没有太大的 区别,无非就是判断下用户拥有的资源描述符是不是与当前校验的一致,或者是否包含当前的资源描述符。我们修改后端代码如下:
@RequiresPermissions("admin:context:view") @RequestMapping("/context") public String goContext(){ System.out.println("进入内容管理后端接口"); return "redirect:/context.jsp"; }
启动项目进行测试,结果如下:
我们可以看到,正常进入到了内容管理页面,说明这个注解式的权限验证是没有问题的,其实在实际开发中,我们使用的一般都是资源描述符,然后通过注解实现权限控制。那么要是没有这个权限呢?
接口代码修改如下:
@RequiresPermissions("admin:context:delete") @RequestMapping("/context") public String goContext(){ System.out.println("进入内容管理后端接口"); return "redirect:/context.jsp"; }
很显然,秦始皇并没有这个资源描述符,肯定是访问不了该接口的。请求结果如下:
可以看到,当用户没有改权限描述符时就会抛出AuthorizationException。让用户看这个肯定是不合适的,我们只需要在配置ShireFilter时配置下,认证失败的返回页面就行。如下:
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); Map<String,String> map = new HashMap<>(); map.put("/user/*","anon");//表示该资源无需认证授权,无需授权的应该写在上面 map.put("/user/logout","anon");//表示该资源无需认证授权 map.put("/register.jsp","anon");//表示该资源无需认证授权 map.put("/test","anon");//表示该资源无需认证授权 map.put("/login.jsp","anon");//表示该资源无需认证授权,此处不写是不能正常访问到登录页面的, //但是看的课程上是可以访问到,并且无其他配置,这块如果不加,我这里访问不到登录页,会陷入循环的重定向。 map.put("/**","authc");//表示所有资源都需要经过认证授权 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); //设置授权失败返回的页面 shiroFilterFactoryBean.setLoginUrl("login.jsp");//这也是默认值 shiroFilterFactoryBean.setUnauthorizedUrl("error.jsp");//认证失败返回页面 return shiroFilterFactoryBean; }
再提供一个无权限的提示页面:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>权限认证</title> <style type="text/css"> *{margin: 0;padding: 0;} form{margin: 0 auto;padding:15px; width: 300px;height:300px;text-align: center;} #submit{padding: 10px} #submit input{width: 50px;height: 24px;} </style> </head> <body> <h1>您没有权限</h1> </body> </html>
然后我们再测试下刚刚的无权限的场景,如下:
这时提示的就是无权限就不会报错了,在代码中实现和角色差不多,这里就不继续演示了。
三.权限管理的总结
1.为什么说前后端的权限是没有关系的
前端我们可以控制到按钮级别,可以控制到资源级别,只要根据角色信息或者是资源描述符进行判断即可。或者是提供一个表,该表用以存储该角色可以访问的菜单,进入菜单之前先查询该表,查到的内容才会在前端展示。
而后端的权限控制都是基于方法的或者说接口,我们每个方法都可以判断用户的角色,以及用户是否拥有相应的资源描述符。前后端的判断是隔离的,互不影响,但是只有前后端一起,才构成了一个完整的权限管理。
2.使用Shiro到底方便了哪些操作
这个总结写到第8篇,其实已经接近完成了。我们这里可以小小的总结下,使用Shiro到底方便了哪些操作呢?使用Shiro我们目的是为了认证授权的便捷。认证自然就是注册与登录场景下使用。我们去实现一个自定义的Realm,然后去实现其中的doGetAuthenticationInfo方法,在这个方法里面我们可以查看数据库的用户信息,然后判断当前用户是否是真正的合法用户。查询到以后,返回给密码匹配器进行匹配,我们在注入自定义Realm时,可以自定义密码匹配器。通常该匹配器使用HashedPriencipalMather,然后告诉该匹配器使用的算法以及散列次数就行。然后我们还需要实现一个配置类,将Security Manager,ShiroFilter等对象注入到Spring容器中,当然了权限管理的5张基础表示少不了的。这样我们就可以在登录进来是进行身份认证了。认证通过以后,如果访问到了需要授权的资源,就会调用自定义Realm中的doGetAuthorizationInfo方法。该方法是去数据库查询权限信息的方法。该方法返回一个SImpleAuthorizationInfo类型,该类型携带了用户的角色以及权限标识符。然后就可以完成权限验证。而且在实际的开发中我们可以使用注解在接口或者方法上进行权限验证,无需使用代码进行判断。整个流程回忆下来,其实没有多少东西。我们平时使用时,只需要使用几个注解就可以完成后端的权限验证。这与我们自己写一个权限管理来说,是大大简化了开发流程,降低了安全隐患。