《Spring-Boot-shiro用户认证》中,我们通过继承AuthorizingRealm抽象类实现了doGetAuthenticationInfo()方法完成了用户认证操作。接下来继续实现doGetAuthorizationInfo()方法完成Shiro的权限控制功能。

授权也称为访问控制,是管理资源访问的过程。即根据不同用户的权限判断其是否有访问相应资源的权限。在Shiro中,权限控制有三个核心的元素:权限,角色和用户。

库模型设计

在这里,我们使用RBAC(Role-Based Access Control,基于角色的访问控制)模型设计用户,角色和权限间的关系。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。如下图所示:

Spring Boot Shiro权限控制 - 图1

根据这个模型,设计数据库表,并插入一些测试数据:

  1. -- ----------------------------
  2. -- Table structure for T_PERMISSION
  3. -- ----------------------------
  4. CREATE TABLE T_PERMISSION (
  5. ID NUMBER(10) NOT NULL ,
  6. URL VARCHAR2(256 BYTE) NULL ,
  7. NAME VARCHAR2(64 BYTE) NULL
  8. );
  9. COMMENT ON COLUMN T_PERMISSION.URL IS 'url地址';
  10. COMMENT ON COLUMN T_PERMISSION.NAME IS 'url描述';
  11. -- ----------------------------
  12. -- Records of T_PERMISSION
  13. -- ----------------------------
  14. INSERT INTO T_PERMISSION VALUES ('1', '/user', 'user:user');
  15. INSERT INTO T_PERMISSION VALUES ('2', '/user/add', 'user:add');
  16. INSERT INTO T_PERMISSION VALUES ('3', '/user/delete', 'user:delete');
  17. -- ----------------------------
  18. -- Table structure for T_ROLE
  19. -- ----------------------------
  20. CREATE TABLE T_ROLE (
  21. ID NUMBER NOT NULL ,
  22. NAME VARCHAR2(32 BYTE) NULL ,
  23. MEMO VARCHAR2(32 BYTE) NULL
  24. );
  25. COMMENT ON COLUMN T_ROLE.NAME IS '角色名称';
  26. COMMENT ON COLUMN T_ROLE.MEMO IS '角色描述';
  27. -- ----------------------------
  28. -- Records of T_ROLE
  29. -- ----------------------------
  30. INSERT INTO T_ROLE VALUES ('1', 'admin', '超级管理员');
  31. INSERT INTO T_ROLE VALUES ('2', 'test', '测试账户');
  32. -- ----------------------------
  33. -- Table structure for T_ROLE_PERMISSION
  34. -- ----------------------------
  35. CREATE TABLE T_ROLE_PERMISSION (
  36. RID NUMBER(10) NULL ,
  37. PID NUMBER(10) NULL
  38. );
  39. COMMENT ON COLUMN T_ROLE_PERMISSION.RID IS '角色id';
  40. COMMENT ON COLUMN T_ROLE_PERMISSION.PID IS '权限id';
  41. -- ----------------------------
  42. -- Records of T_ROLE_PERMISSION
  43. -- ----------------------------
  44. INSERT INTO T_ROLE_PERMISSION VALUES ('1', '2');
  45. INSERT INTO T_ROLE_PERMISSION VALUES ('1', '3');
  46. INSERT INTO T_ROLE_PERMISSION VALUES ('2', '1');
  47. INSERT INTO T_ROLE_PERMISSION VALUES ('1', '1');
  48. -- ----------------------------
  49. -- Table structure for T_USER
  50. -- ----------------------------
  51. CREATE TABLE T_USER (
  52. ID NUMBER NOT NULL ,
  53. USERNAME VARCHAR2(20 BYTE) NOT NULL ,
  54. PASSWD VARCHAR2(128 BYTE) NOT NULL ,
  55. CREATE_TIME DATE NULL ,
  56. STATUS CHAR(1 BYTE) NOT NULL
  57. );
  58. COMMENT ON COLUMN T_USER.USERNAME IS '用户名';
  59. COMMENT ON COLUMN T_USER.PASSWD IS '密码';
  60. COMMENT ON COLUMN T_USER.CREATE_TIME IS '创建时间';
  61. COMMENT ON COLUMN T_USER.STATUS IS '是否有效 1:有效 0:锁定';
  62. -- ----------------------------
  63. -- Records of T_USER
  64. -- ----------------------------
  65. INSERT INTO T_USER VALUES ('2', 'tester', '243e29429b340192700677d48c09d992', TO_DATE('2017-12-11 17:20:21', 'YYYY-MM-DD HH24:MI:SS'), '1');
  66. INSERT INTO T_USER VALUES ('1', 'mrbird', '42ee25d1e43e9f57119a00d0a39e5250', TO_DATE('2017-12-11 10:52:48', 'YYYY-MM-DD HH24:MI:SS'), '1');
  67. -- ----------------------------
  68. -- Table structure for T_USER_ROLE
  69. -- ----------------------------
  70. CREATE TABLE T_USER_ROLE (
  71. USER_ID NUMBER(10) NULL ,
  72. RID NUMBER(10) NULL
  73. );
  74. COMMENT ON COLUMN T_USER_ROLE.USER_ID IS '用户id';
  75. COMMENT ON COLUMN T_USER_ROLE.RID IS '角色id';
  76. -- ----------------------------
  77. -- Records of T_USER_ROLE
  78. -- ----------------------------
  79. INSERT INTO T_USER_ROLE VALUES ('1', '1');
  80. INSERT INTO T_USER_ROLE VALUES ('2', '2');

上面的sql创建了五张表:用户表T_USER、角色表T_ROLE、用户角色关联表T_USER_ROLE、权限表T_PERMISSION和权限角色关联表T_ROLE_PERMISSION。用户mrbird角色为admin,用户tester角色为test。admin角色拥有用户的所有权限(user:user,user:add,user:delete),而test角色只拥有用户的查看权限(user:user)。密码都是123456,经过Shiro提供的MD5加密。

Dao层

创建两个实体类,对应用户角色表Role和用户权限表Permission:

Role:

  1. public class Role implements Serializable{
  2. private static final long serialVersionUID = -227437593919820521L;
  3. private Integer id;
  4. private String name;
  5. private String memo;
  6. // get set略
  7. }

Permission:

  1. public class Permission implements Serializable{
  2. private static final long serialVersionUID = 7160557680614732403L;
  3. private Integer id;
  4. private String url;
  5. private String name;
  6. // get,set略
  7. }

创建两个dao接口,分别用户查询用户的所有角色和用户的所有权限:

UserRoleMapper:

  1. @Mapper
  2. public interface UserRoleMapper {
  3. List<Role> findByUserName(String userName);
  4. }

UserPermissionMapper:

  1. @Mapper
  2. public interface UserPermissionMapper {
  3. List<Permission> findByUserName(String userName);
  4. }

其xml实现:

UserRoleMapper.xml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.springboot.dao.UserRoleMapper">
  4. <resultMap type="com.springboot.pojo.Role" id="role">
  5. <id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/>
  6. <id column="name" property="name" javaType="java.lang.String" jdbcType="VARCHAR"/>
  7. <id column="memo" property="memo" javaType="java.lang.String" jdbcType="VARCHAR"/>
  8. </resultMap>
  9. <select id="findByUserName" resultMap="role">
  10. select r.id,r.name,r.memo from t_role r
  11. left join t_user_role ur on(r.id = ur.rid)
  12. left join t_user u on(u.id = ur.user_id)
  13. where u.username = #{userName}
  14. </select>
  15. </mapper>

UserPermissionMapper.xml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.springboot.dao.UserPermissionMapper">
  4. <resultMap type="com.springboot.pojo.Permission" id="permission">
  5. <id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/>
  6. <id column="url" property="url" javaType="java.lang.String" jdbcType="VARCHAR"/>
  7. <id column="name" property="name" javaType="java.lang.String" jdbcType="VARCHAR"/>
  8. </resultMap>
  9. <select id="findByUserName" resultMap="permission">
  10. select p.id,p.url,p.name from t_role r
  11. left join t_user_role ur on(r.id = ur.rid)
  12. left join t_user u on(u.id = ur.user_id)
  13. left join t_role_permission rp on(rp.rid = r.id)
  14. left join t_permission p on(p.id = rp.pid )
  15. where u.username = #{userName}
  16. </select>
  17. </mapper>

数据层准备好后,接下来对Realm进行改造。

Realm

在Shiro中,用户角色和权限的获取是在Realm的doGetAuthorizationInfo()方法中实现的,所以接下来手动实现该方法:

  1. public class ShiroRealm extends AuthorizingRealm {
  2. @Autowired
  3. private UserMapper userMapper;
  4. @Autowired
  5. private UserRoleMapper userRoleMapper;
  6. @Autowired
  7. private UserPermissionMapper userPermissionMapper;
  8. /**
  9. * 获取用户角色和权限
  10. */
  11. @Override
  12. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
  13. User user = (User) SecurityUtils.getSubject().getPrincipal();
  14. String userName = user.getUserName();
  15. System.out.println("用户" + userName + "获取权限-----ShiroRealm.doGetAuthorizationInfo");
  16. SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
  17. // 获取用户角色集
  18. List<Role> roleList = userRoleMapper.findByUserName(userName);
  19. Set<String> roleSet = new HashSet<String>();
  20. for (Role r : roleList) {
  21. roleSet.add(r.getName());
  22. }
  23. simpleAuthorizationInfo.setRoles(roleSet);
  24. // 获取用户权限集
  25. List<Permission> permissionList = userPermissionMapper.findByUserName(userName);
  26. Set<String> permissionSet = new HashSet<String>();
  27. for (Permission p : permissionList) {
  28. permissionSet.add(p.getName());
  29. }
  30. simpleAuthorizationInfo.setStringPermissions(permissionSet);
  31. return simpleAuthorizationInfo;
  32. }
  33. /**
  34. * 登录认证
  35. */
  36. @Override
  37. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  38. // 登录认证已经实现过,这里不再贴代码
  39. }
  40. }

在上述代码中,我们通过方法userRoleMapper.findByUserName(userName)userPermissionMapper.findByUserName(userName)获取了当前登录用户的角色和权限集,然后保存到SimpleAuthorizationInfo对象中,并返回给Shiro,这样Shiro中就存储了当前用户的角色和权限信息了。

除了对Realm进行改造外,我们还需修改ShiroConfig配置。

ShiroConfig

Shiro为我们提供了一些和权限相关的注解,如下所示:

  1. // 表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true。
  2. @RequiresAuthentication
  3. // 表示当前Subject已经身份验证或者通过记住我登录的。
  4. @RequiresUser
  5. // 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
  6. @RequiresGuest
  7. // 表示当前Subject需要角色admin和user。
  8. @RequiresRoles(value={"admin", "user"}, logical= Logical.AND)
  9. // 表示当前Subject需要权限user:a或user:b。
  10. @RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)

要开启这些注解的使用,需要在ShiroConfig中添加如下配置:

  1. ...
  2. @Bean
  3. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
  4. AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
  5. authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
  6. return authorizationAttributeSourceAdvisor;
  7. }
  8. ...

Controller

编写一个UserController,用于处理User类的访问请求,并使用Shiro权限注解控制权限:

  1. @Controller
  2. @RequestMapping("/user")
  3. public class UserController {
  4. @RequiresPermissions("user:user")
  5. @RequestMapping("list")
  6. public String userList(Model model) {
  7. model.addAttribute("value", "获取用户信息");
  8. return "user";
  9. }
  10. @RequiresPermissions("user:add")
  11. @RequestMapping("add")
  12. public String userAdd(Model model) {
  13. model.addAttribute("value", "新增用户");
  14. return "user";
  15. }
  16. @RequiresPermissions("user:delete")
  17. @RequestMapping("delete")
  18. public String userDelete(Model model) {
  19. model.addAttribute("value", "删除用户");
  20. return "user";
  21. }
  22. }

在LoginController中添加一个/403跳转:

  1. @GetMapping("/403")
  2. public String forbid() {
  3. return "403";
  4. }

前端页面

对index.html进行改造,添加三个用户操作的链接:

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>首页</title>
  6. </head>
  7. <body>
  8. <p>你好![[${user.userName}]]</p>
  9. <h3>权限测试链接</h3>
  10. <div>
  11. <a th:href="@{/user/list}">获取用户信息</a>
  12. <a th:href="@{/user/add}">新增用户</a>
  13. <a th:href="@{/user/delete}">删除用户</a>
  14. </div>
  15. <a th:href="@{/logout}">注销</a>
  16. </body>
  17. </html>

当用户对用户的操作有相应权限的时候,跳转到user.html:

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>[[${value}]]</title>
  6. </head>
  7. <body>
  8. <p>[[${value}]]</p>
  9. <a th:href="@{/index}">返回</a>
  10. </body>
  11. </html>

403页面:

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>暂无权限</title>
  6. </head>
  7. <body>
  8. <p>您没有权限访问该资源!!</p>
  9. <a th:href="@{/index}">返回</a>
  10. </body>

测试

启动项目,使用mrbird的账户登录后主页如下图所示:

Spring Boot Shiro权限控制 - 图2

点击”获取用户信息连接”:

Spring Boot Shiro权限控制 - 图3

因为mrbird角色为admin,对着三个链接都由访问权限,所以这里就不演示了。

接着使用tester用户登录。因为tester用户角色为test,只拥有(user:user)权限,所以当其点击”新增用户”和”删除用户”的时候:

Spring Boot Shiro权限控制 - 图4

后台抛出org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method:…异常!!!

这里有点出乎意料,本以为在ShiroConfig中配置了shiroFilterFactoryBean.setUnauthorizedUrl("/403");,没有权限的访问会自动重定向到/403,结果证明并不是这样。后来研究发现,该设置只对filterChain起作用,比如在filterChain中设置了filterChainDefinitionMap.put("/user/update", "perms[user:update]");,如果用户没有user:update权限,那么当其访问/user/update的时候,页面会被重定向到/403。

那么对于上面这个问题,我们可以定义一个全局异常捕获类:

  1. @ControllerAdvice
  2. @Order(value = Ordered.HIGHEST_PRECEDENCE)
  3. public class GlobalExceptionHandler {
  4. @ExceptionHandler(value = AuthorizationException.class)
  5. public String handleAuthorizationException() {
  6. return "403";
  7. }
  8. }

启动项目,再次使用tester的账号点击”新增用户”和”删除用户”链接的时候,页面如下所示:

Spring Boot Shiro权限控制 - 图5

页面已经成功重定向到/403。