首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring 整合 Shiro 權限管理與資料庫設計完整實戰指南

     編輯:Java 相關知識

前言:細粒度權限控制的必要性

現代後台管理系統需實現「從選單到按鈕」的全鏈路權限控制:前端選單顯示/隱藏、頁面元素渲染控制、後端 API 存取驗證。Apache Shiro 作為輕量級安全框架,配合 Spring 整合,能優雅實現基於 RBAC (Role-Based Access Control) 模型的細粒度授權。本文完整記錄資料庫設計、Shiro 核心配置、Realm 實作、前後端整合與實戰技巧。

資料庫設計:RBAC 核心六表結構

ER 關係圖解

資料庫 ER 圖

建表 SQL (MySQL 5.5+)

-- 1. 使用者表
CREATE TABLE sys_user (
    user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    login_account VARCHAR(30) NOT NULL UNIQUE,
    login_pass VARCHAR(65) NOT NULL, -- SHA-256 加密
    user_name VARCHAR(20),
    user_email VARCHAR(30),
    register_time VARCHAR(30) NOT NULL,
    department_key VARCHAR(20)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 2. 角色表
CREATE TABLE sys_role (
    role_id INT PRIMARY KEY AUTO_INCREMENT,
    role_key VARCHAR(30) UNIQUE,     -- 權限標識,如 ROLE_ADMIN
    role_value VARCHAR(40) NOT NULL, -- 顯示名稱
    description VARCHAR(200),
    create_time VARCHAR(30)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 3. 權限/選單表 (統一管理選單與按鈕權限)
CREATE TABLE sys_authority (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    menu_code VARCHAR(50) NOT NULL UNIQUE,      -- 權限編碼
    menu_name VARCHAR(50) NOT NULL,             -- 顯示名稱
    data_url VARCHAR(100) NOT NULL,             -- 路徑/方法標識
    parent_menucode VARCHAR(50),                -- 父選單編碼
    menu_class VARCHAR(50),                     -- 圖標樣式
    sequence BIGINT DEFAULT 0,                  -- 排序
    menu_type VARCHAR(2) DEFAULT '1',           -- 1=選單 2=按鈕
    create_time VARCHAR(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 4. 使用者-角色關聯
CREATE TABLE sys_user_role (
    user_id BIGINT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES sys_user(user_id),
    FOREIGN KEY (role_id) REFERENCES sys_role(role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 5. 角色-選單關聯 (導航選單權限)
CREATE TABLE sys_role_authority (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_key VARCHAR(40) NOT NULL,
    menu_code VARCHAR(50) NOT NULL,
    menu_type INT DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 6. 角色-按鈕權限表 (功能級權限)
CREATE TABLE sys_role_permission (
    role_id INT NOT NULL,
    permissions VARCHAR(1000), -- 逗號分隔權限字串
    FOREIGN KEY (role_id) REFERENCES sys_role(role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 初始測試資料
INSERT INTO sys_role VALUES (1, 'ROLE_USER', '普通用戶', NULL, NULL);
INSERT INTO sys_role VALUES (2, 'ROLE_ADMIN', '系統管理員', NULL, NULL);
INSERT INTO sys_user VALUES (2, 'admin', 'e3b0c44298fc1c149afbf4c8996fb924...', NULL, 'admin@test.com', '2024-01-01', NULL);
INSERT INTO sys_user_role VALUES (2, 2);

Maven 依賴配置

<properties>
    <shiro.version>1.10.0</shiro.version> <!-- 升級至最新穩定版 -->
    <commons-logging.version>1.2</commons-logging.version>
</properties>

<dependencies>
    <!-- Shiro 核心 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <!-- Ehcache 整合 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <!-- Commons Logging -->
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>${commons-logging.version}</version>
    </dependency>
</dependencies>

Shiro 核心配置 (spring-shiro.xml)

<beans xmlns="https://www.springframework.org/schema/beans"
       xmlns:util="https://www.springframework.org/schema/util"
       xsi:schemaLocation="https://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 1. 快取管理器 (Ehcache) -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml" />
    </bean>

    <!-- 2. 安全管理器核心 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="shiroSecurityRealm" />
        <property name="cacheManager" ref="cacheManager" />
        <property name="rememberMeManager" ref="rememberMeManager" />
        <property name="sessionManager" ref="sessionManager" />
    </bean>

    <!-- 3. Realm 實作 (核心邏輯) -->
    <bean id="shiroSecurityRealm" class="com.example.security.ShiroSecurityRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher" />
    </bean>

    <!-- 4. 憑證匹配器 (SHA-256) -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashAlgorithmName" value="SHA-256" />
        <property name="hashIterations" value="1024" />
        <property name="storedCredentialsHexEncoded" value="true" />
    </bean>

    <!-- 5. RememberMe 配置 -->
    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="rememberMe"/>
        <property name="httpOnly" value="true"/>
        <property name="maxAge" value="2592000"/> <!-- 30天 -->
    </bean>
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <property name="cookie" ref="rememberMeCookie"/>
        <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
    </bean>

    <!-- 6. Session 管理 -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="sid"/>
        <property name="httpOnly" value="true"/>
        <property name="maxAge" value="-1"/>
    </bean>
    <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
        <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
    </bean>
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="globalSessionTimeout" value="1800000"/> <!-- 30分鐘 -->
        <property name="sessionDAO" ref="sessionDAO"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <property name="sessionIdCookieEnabled" value="true"/>
    </bean>

    <!-- 7. Shiro Filter 工廠 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/page/main.action"/>
        <property name="unauthorizedUrl" value="/403.jsp"/>
        <property name="filterChainDefinitions">
            <value>
                /static/** = anon
                /login.jsp = anon
                /sysuser/login.action = anon
                /sysuser/logout.action = logout
                /** = user, sysUserFilter
            </value>
        </property>
        <property name="filters">
            <util:map>
                <entry key="sysUserFilter" value-ref="sysUserFilter"/>
            </util:map>
        </property>
    </bean>

    <!-- 8. 自定義過濾器 -->
    <bean id="sysUserFilter" class="com.example.security.SysUserFilter"/>

    <!-- 9. 生命週期 BeanPostProcessor -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans>

Ehcache 配置 (ehcache-shiro.xml)

<ehcache name="shirocache">
    <diskStore path="java.io.tmpdir/shiro-ehcache"/>
    <defaultCache maxElementsInMemory="1000" eternal="false" 
                  overflowToDisk="true" timeToIdleSeconds="300" timeToLiveSeconds="600"/>
    
    <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false"
           timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false"/>
    <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false"
           timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false"/>
    <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false"
           timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false"/>
</ehcache>

核心 Realm 實作

@Component
public class ShiroSecurityRealm extends AuthorizingRealm {

    @Resource private ISysUserService userService;

    public ShiroSecurityRealm() {
        setName("ShiroSecurityRealm");
        setCredentialsMatcher(new HashedCredentialsMatcher("SHA-256"));
    }

    /** 認證:驗證身分 */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SysUser user = userService.findByAccount(upToken.getUsername());
        
        if (user == null) {
            throw new UnknownAccountException("帳號不存在");
        }
        if (user.getStatus() == 0) {
            throw new LockedAccountException("帳號已鎖定");
        }
        
        // 返回 SimpleAuthenticationInfo,Shiro 會自動使用 credentialsMatcher 驗證密碼
        return new SimpleAuthenticationInfo(
            user.getUserId(),           // Principal (身分標識)
            user.getLoginPass(),        // Credentials (資料庫加密密碼)
            getName()                   // Realm 名稱
        );
    }

    /** 授權:查詢角色與權限 */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Long userId = (Long) principals.getPrimaryPrincipal();
        SysUser user = userService.getWithRolesAndPermissions(userId);
        
        if (user == null) return null;
        
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        
        // 角色授權
        for (SysRole role : user.getRoles()) {
            info.addRole(role.getRoleKey());
        }
        
        // 權限字串授權 (格式: 模組:功能:操作)
        for (SysRole role : user.getRoles()) {
            if (role.getPermissions() != null) {
                for (String perm : role.getPermissions()) {
                    info.addStringPermission(perm.trim());
                }
            }
        }
        
        return info;
    }
}

登入 Controller 實作

@Controller
@RequestMapping("/sysuser")
public class SysUserController {

    @Resource private ISysUserService userService;

    @PostMapping("/login")
    @ResponseBody
    public Result login(String loginAccount, String loginPass, boolean rememberMe) {
        Subject subject = SecurityUtils.getSubject();
        
        // 1. 構建 Token (Shiro 會自動調用 Realm.doGetAuthenticationInfo)
        UsernamePasswordToken token = new UsernamePasswordToken(loginAccount, loginPass, rememberMe);
        
        try {
            subject.login(token); // 核心:觸發認證流程
            
            // 2. 登入成功,放入 Session
            SysUser user = userService.findByAccount(loginAccount);
            subject.getSession().setAttribute("CURRENT_USER", user);
            
            return Result.success("登入成功");
        } catch (UnknownAccountException e) {
            return Result.error("帳號不存在");
        } catch (IncorrectCredentialsException e) {
            return Result.error("密碼錯誤");
        } catch (LockedAccountException e) {
            return Result.error("帳號已鎖定");
        } catch (AuthenticationException e) {
            return Result.error("登入失敗: " + e.getMessage());
        }
    }

    @GetMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/login.jsp";
    }
}

前端 JSP 整合 Shiro 標籤庫

<%@ taglib prefix="shiro" uri="https://shiro.apache.org/tags" %>

<!-- 1. 選單權限控制 -->
<shiro:hasPermission name="sys:user:view">
    <a href="/sysuser/list.action">用戶管理</a>
</shiro:hasPermission>

<!-- 2. 按鈕權限控制 -->
<shiro:hasPermission name="sys:user:add">
    <button id="btnAdd" class="btn btn-primary">
        <i class="fa fa-plus"></i> 新增用戶
    </button>
</shiro:hasPermission>

<!-- 3. 角色判斷 -->
<shiro:hasRole name="ROLE_ADMIN">
    <div class="admin-panel">管理員專區</div>
</shiro:hasRole>

<!-- 4. 當前用戶資訊 -->
<shiro:principal property="userName"/> 歡迎您!

後端方法級權限控制

@Service
public class SysUserServiceImpl implements ISysUserService {

    @Resource private ISysUserDao userDao;

    /** 方法級權限攔截 */
    public void addUser(SysUser user) {
        // 方式一:程式化檢查
        Subject subject = SecurityUtils.getSubject();
        subject.checkPermission("sys:user:add"); // 無權限拋 AuthorizationException
        
        // 方式二:註解式 (需啟用 AOP: @EnableGlobalMethodSecurity)
        // @RequiresPermissions("sys:user:add")
        userDao.insert(user);
    }
}

權限字串設計規範

採用 模組:功能:操作 三段式命名:

權限字串含義應用場景
sys:user:view用戶管理-查看列表頁面、詳情頁
sys:user:add用戶管理-新增新增按鈕、POST /user
sys:user:edit用戶管理-編輯編輯按鈕、PUT /user/{id}
sys:user:delete用戶管理-刪除刪除按鈕、DELETE /user/{id}
sys:role:assign角色管理-分配權限角色授權頁面

專案結構圖

專案結構

進階優化建議

  1. 授權快取:Ehcache 減少資料庫查詢,配置 authorizationCache TTL 30-60 分鐘
  2. 密碼加鹽:new SimpleHash("SHA-256", password, salt, 1024) 防彩虹表攻擊
  3. 會話管理:踢出重複登入、強制下線、會話監控
  4. 無狀態 API:JWT Token 替代 Session,支援微服務架構
  5. 動態權限:資料庫維護權限字串,熱加載無需重啟

Spring + Shiro 整合實現了企業級細粒度權限控制體系,從資料庫設計到前後端整合形成閉環。掌握 Realm 核心邏輯、權限字串設計規範與快取策略,能構建安全、可維護、高效能的授權系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved