Spring Boot整合Apache Shiro权限认证框架(应用篇)

上一篇文章已经介绍了使用Apache Shiro权限认证框架实现简单的认证和授权功能。

Spring Boot整合Apache Shiro权限认证框架(基础篇)https://blog.csdn.net/2501_92713943/article/details/152612858

这篇文章的主要内容是Apache Shiro的更深层次的应用,主要为了解决上篇文章的权限硬编码问题

一、RBAC介绍

权限访问控制通常会使用RBAC(Role-Based Access Control,基于角色的访问控制)模型。

实现访问控制最少需要5张表:

  • 用户表
  • 角色表
  • 权限表
  • 用户-角色表
  • 角色-权限表

权限是分配给角色的,通过用户-角色表关联到用户,从而实现用户权限的访问控制。

权限对应的就是项目中的接口资源信息,包括资源名称、资源URL等信息。

二、准备数据库

sql 复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80043 (8.0.43)
 Source Host           : localhost:3306
 Source Schema         : shiro

 Target Server Type    : MySQL
 Target Server Version : 80043 (8.0.43)
 File Encoding         : 65001

 Date: 07/10/2025 19:57:04
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '接口路径',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限名称',
  `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限值(shiro的权限通配符)',
  `parent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '父级权限ID',
  `anonymity` tinyint UNSIGNED NOT NULL COMMENT '是否允许匿名访问(0-不允许;1-允许)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('UserController', '/user', '用户管理', 'user:*', NULL, 0);
INSERT INTO `permission` VALUES ('UserController_delete', '/user/delete', '删除用户', 'user:delete', 'UserController', 0);
INSERT INTO `permission` VALUES ('UserController_login', '/user/login', '用户登录', 'user:login', 'UserController', 1);
INSERT INTO `permission` VALUES ('UserController_update', '/user/update', '修改用户', 'user:update', 'UserController', 0);

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色名称',
  `note` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色说明',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('system-admin', '超级管理员', '最高权限,拥有系统所有权限');

-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission`  (
  `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `role_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色id,数据来源于role表的主键',
  `permission_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '权限id,数据来源于permission表的主键',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色-权限表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES (1, 'system-admin', 'UserController_login');
INSERT INTO `role_permission` VALUES (2, 'system-admin', 'UserController_delete');
INSERT INTO `role_permission` VALUES (3, 'system-admin', 'UserController_update');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '登录密码',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
  `gender` tinyint UNSIGNED NOT NULL COMMENT '性别(1-男;2-女)',
  `disabled` tinyint UNSIGNED NOT NULL COMMENT '封禁状态(0-正常;1-封禁)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('20241215', 'mumu', 'mhxy1218', '18888888888', 2, 0);

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `role_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色id,数据来源于role表的主键',
  `user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id,数据来源于user表的主键',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户-角色关系表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 'system-admin', '20241215');

SET FOREIGN_KEY_CHECKS = 1;

三、创建实体类

Role.java

在entity包下创建Role类。

java 复制代码
package cn.edu.sgu.www.shiro.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * 角色
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
public class Role implements Serializable {
    private static final long serialVersionUID = 18L;

    private String id;

    /**
     * 角色名称
     */
    private String name;

    /**
     * 角色说明
     */
    private String note;
}

UserRole.java

在entity包下创建UserRole类。

java 复制代码
package cn.edu.sgu.www.shiro.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * 用户-角色
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
public class UserRole implements Serializable {
    private static final long serialVersionUID = 18L;

    private Integer id;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 角色ID
     */
    private String roleId;
}

Permission.java

在entity包下创建Permission类。

java 复制代码
package cn.edu.sgu.www.shiro.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * 权限
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
public class Permission implements Serializable {
    private static final long serialVersionUID = 18L;

    private String id;

    /**
     * 接口路径
     */
    private String url;

    /**
     * 权限名称
     */
    private String name;

    /**
     * 权限值
     */
    private String value;

    /**
     * 父级权限ID
     */
    private String parentId;

    /**
     * 是否允许匿名访问
     * 0-不允许;1-允许
     */
    private Integer anonymity;
}

RolePermission.java

在entity包下创建RolePermission类。

java 复制代码
package cn.edu.sgu.www.shiro.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * 角色-权限
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
public class RolePermission implements Serializable {
    private static final long serialVersionUID = 18L;

    private Integer id;

    /**
     * 角色ID
     */
    private String roleId;

    /**
     * 权限ID
     */
    private String permissionId;
}

四、创建持久层接口

RoleMapper.java

在mapper包下创建RoleMapper接口。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import org.springframework.stereotype.Repository;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface RoleMapper {

}

UserRoleMapper.java

在mapper包下创建UserRoleMapper接口。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import org.springframework.stereotype.Repository;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface UserRoleMapper {

}

PermissionMapper.java

在mapper包下创建PermissionMapper接口。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import org.springframework.stereotype.Repository;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface PermissionMapper {
    
}

RolePermissionMapper.java

在mapper包下创建RolePermissionMapper接口。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import org.springframework.stereotype.Repository;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface RolePermissionMapper {
    
}

五、设置动态权限

因为权限是(通过角色-权限表)直接和角色绑定的,所以用户的权限就是用户拥有的(从用户-角色表查询到的)所有角色的权限的并集。


因此,用户的权限分两步查询:

  • 查询用户的角色
  • 查询角色的权限

1、查询用户的角色

UserRoleMapper.java

增加一个selectUserRoles()方法,接收用户id作为参数。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface UserRoleMapper {

    /**
     * 通过用户ID查询角色列表
     * @param userId 用户ID
     * @return 用户的角色列表
     */
    List<String> selectUserRoles(@Param("userId") Object userId);
}

UserRoleMapper.xml

在src/main/resources目录下创建mapper目录,在mapper目录下创建UserRoleMapper.xml文件。

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="cn.edu.sgu.www.shiro.mapper.UserRoleMapper">

    <select id="selectUserRoles" resultType="String">
        select jsb.id from role jsb
        left join user_role yhjsb on jsb.id = yhjsb.role_id
        left join user yhb on yhjsb.user_id = yhb.id
        where yhb.id = #{userId}
    </select>
</mapper>

application.yml

设置日志级别为debug,控制台将会打印执行的SQL语句。

java 复制代码
logging:
  level: # 日志级别设置
    springfox: error
    cn.edu.sgu.www.shiro: debug

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml # mybatis的mapper.xml文件的位置

2、查询角色的权限

RolePermissionMapper.java

增加一个selectRolePermissions()方法,接收角色id作为参数。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface RolePermissionMapper {

    /**
     * 通过角色ID查询权限列表
     * @param roleId 角色ID
     * @return 角色的权限列表
     */
    List<String> selectRolePermissions(@Param("roleId") String roleId);
}

RolePermissionMapper.xml

在src/main/resources/mapper目录下创建RolePermissionMapper.xml文件。

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="cn.edu.sgu.www.shiro.mapper.RolePermissionMapper">

    <select id="selectRolePermissions" resultType="java.lang.String">
        select qxb.value from permission qxb
        left join role_permission jsqxb on qxb.id = jsqxb.permission_id
        left join role jsb on jsqxb.role_id = jsb.id
        where jsb.id = #{roleId}
    </select>
</mapper>

3、设置角色的权限列表

UsernameRealm.java

注入UserRoleMapper和RolePermissionMapper两个依赖。

  • 根据用户ID查询用户的角色列表
  • 遍历角色列表,根据角色ID查询角色的权限列表
java 复制代码
package cn.edu.sgu.www.shiro.realm;

import cn.edu.sgu.www.shiro.consts.ErrorMessages;
import cn.edu.sgu.www.shiro.entity.User;
import cn.edu.sgu.www.shiro.mapper.RolePermissionMapper;
import cn.edu.sgu.www.shiro.mapper.UserMapper;
import cn.edu.sgu.www.shiro.mapper.UserRoleMapper;
import org.apache.shiro.authc.*;
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 org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Component
public class UsernameRealm extends AuthorizingRealm {

    private final UserMapper userMapper;
    private final UserRoleMapper userRoleMapper;
    private final RolePermissionMapper rolePermissionMapper;

    @Autowired
    public UsernameRealm(
            UserMapper userMapper,
            UserRoleMapper userRoleMapper,
            RolePermissionMapper rolePermissionMapper) {
        this.userMapper = userMapper;
        this.userRoleMapper = userRoleMapper;
        this.rolePermissionMapper = rolePermissionMapper;
    }

    /**
     * 获取认证信息
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取登录时提交的token
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        // 得到用户名
        String username = token.getUsername();

        // 根据用户名查询用户信息
        User user = userMapper.selectByUsername(username);

        // 查询结果为空,则说明用户不存在
        if (user == null) {
            throw new AuthenticationException(ErrorMessages.loginFail);
        } else if (user.isDisabled()) { // 账号被封禁,抛出异常
            throw new AuthenticationException("登录失败,账号状态异常!");
        }

        return new SimpleAuthenticationInfo(user, user.getPassword(), username);
    }

    /**
     * 获取授权信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        // 得到用户ID
        User user = (User) principals.getPrimaryPrincipal();
        String userId = user.getId();

        // 查询用户的角色列表
        List<String> roleList = userRoleMapper.selectUserRoles(userId);

        if (!roleList.isEmpty()) {
            authorizationInfo.setRoles(new HashSet<>(roleList));
            
            Set<String> stringPermissions = new HashSet<>();
            
            for (String roleId : roleList) {
                // 查询角色的权限列表
                List<String> permissions = rolePermissionMapper.selectRolePermissions(roleId);
                
                if (!permissions.isEmpty()) {
                    stringPermissions.addAll(permissions);
                }
            }

            if (!stringPermissions.isEmpty()) {
                authorizationInfo.setStringPermissions(stringPermissions);
            }
        }
        
        return authorizationInfo;
    }

}

六、配置拦截规则

PermissionMapper.java

增加一个selectPermissions()方法,查询所有权限。

java 复制代码
package cn.edu.sgu.www.shiro.mapper;

import cn.edu.sgu.www.shiro.entity.Permission;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Repository
public interface PermissionMapper {

    /**
     * 查询所有权限
     */
    List<Permission> selectPermissions();
}

PermissionMapper.xml

在src/main/resources/mapper目录下创建PermissionMapper.xml文件。

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="cn.edu.sgu.www.shiro.mapper.PermissionMapper">

    <select id="selectPermissions" resultType="cn.edu.sgu.www.shiro.entity.Permission">
        select * from permission
        where parent_id is not null
    </select>
</mapper>

ShiroConfig.java

注入PermissionMapper的依赖,查询所有权限,根据是否匿名接口交给perms或anno过滤器处理。

java 复制代码
package cn.edu.sgu.www.shiro.realm;

import cn.edu.sgu.www.shiro.consts.ErrorMessages;
import cn.edu.sgu.www.shiro.entity.User;
import cn.edu.sgu.www.shiro.mapper.RolePermissionMapper;
import cn.edu.sgu.www.shiro.mapper.UserMapper;
import cn.edu.sgu.www.shiro.mapper.UserRoleMapper;
import org.apache.shiro.authc.*;
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 org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Component
public class UsernameRealm extends AuthorizingRealm {

    private final UserMapper userMapper;
    private final UserRoleMapper userRoleMapper;
    private final RolePermissionMapper rolePermissionMapper;

    @Autowired
    public UsernameRealm(
            UserMapper userMapper,
            UserRoleMapper userRoleMapper,
            RolePermissionMapper rolePermissionMapper) {
        this.userMapper = userMapper;
        this.userRoleMapper = userRoleMapper;
        this.rolePermissionMapper = rolePermissionMapper;
    }

    /**
     * 获取认证信息
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取登录时提交的token
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        // 得到用户名
        String username = token.getUsername();

        // 根据用户名查询用户信息
        User user = userMapper.selectByUsername(username);

        // 查询结果为空,则说明用户不存在
        if (user == null) {
            throw new AuthenticationException(ErrorMessages.loginFail);
        } else if (user.isDisabled()) { // 账号被封禁,抛出异常
            throw new AuthenticationException("登录失败,账号状态异常!");
        }

        return new SimpleAuthenticationInfo(user, user.getPassword(), username);
    }

    /**
     * 获取授权信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        // 得到用户ID
        User user = (User) principals.getPrimaryPrincipal();
        String userId = user.getId();

        // 查询用户的角色列表
        List<String> roleList = userRoleMapper.selectUserRoles(userId);

        if (!roleList.isEmpty()) {
            authorizationInfo.setRoles(new HashSet<>(roleList));

            Set<String> stringPermissions = new HashSet<>();

            for (String roleId : roleList) {
                // 查询角色的权限列表
                List<String> permissions = rolePermissionMapper.selectRolePermissions(roleId);

                if (!permissions.isEmpty()) {
                    stringPermissions.addAll(permissions);
                }
            }

            if (!stringPermissions.isEmpty()) {
                authorizationInfo.setStringPermissions(stringPermissions);
            }
        }

        return authorizationInfo;
    }

}

好了,本章关于Apache Shiro的内容就分享到这里了。

如果这篇文章对你有所帮助,不要忘了点赞+收藏哦~


文章的项目已经上传到Git仓库,需要的可以自行拉取~

Spring Boot整合Apache Shiro案例项目https://gitee.com/muyu-chengfeng/springboot-shiro-v2.git

相关推荐
Lear几秒前
SpringBoot 如何删除清理垃圾文件?
后端
BingoGo1 分钟前
Laravel 新项目避坑指南10 大基础设置让代码半年不崩
后端·php
lang201509282 分钟前
Spring XML AOP配置实战指南
xml·java·spring
桦说编程4 分钟前
深入解析CompletableFuture源码实现(3)———多源输入
java·性能优化·源码阅读
xiaozaq6 分钟前
java 正则表达式 所有的优先级
java·开发语言·正则表达式
JMzz8 分钟前
Rust 中的数据结构选择与性能影响:从算法复杂度到硬件特性 [特殊字符]
开发语言·数据结构·后端·算法·性能优化·rust
鹿邑网爬10 分钟前
Python 制作“满屏浪漫弹窗”教程
后端·python
风一样的美狼子12 分钟前
仓颉语言核心数据结构-高性能与类型安全的工程实践
java·服务器·前端
gustt15 分钟前
让你的 AI 更听话:Prompt 工程的实战秘籍
人工智能·后端·node.js
钟离墨笺21 分钟前
Go语言-->sync.WaitGroup 详细解释
开发语言·后端·golang