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

相关推荐
十重幻想2 小时前
PTA6-5 使用函数求1到10的阶乘和(C)
java·c语言·算法
考虑考虑2 小时前
fastjson调用is方法开头注意
java·后端·java ee
小蒜学长3 小时前
springboot基于javaweb的小零食销售系统的设计与实现(代码+数据库+LW)
java·开发语言·数据库·spring boot·后端
brzhang3 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
TT哇3 小时前
【多线程-进阶】常⻅的锁策略
java
EnCi Zheng3 小时前
JPA 连接 PostgreSQL 数据库完全指南
java·数据库·spring boot·后端·postgresql
brzhang3 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
LucianaiB4 小时前
从玩具到工业:基于 CodeBuddy code CLI 构建电力变压器绕组短路智能诊断系统
后端
tuokuac4 小时前
MVC的含义
java·mvc