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

-- 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);
<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>
<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 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>
@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
@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";
}
}
<%@ 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 | 角色管理-分配權限 | 角色授權頁面 |

authorizationCache TTL 30-60 分鐘new SimpleHash("SHA-256", password, salt, 1024) 防彩虹表攻擊Spring + Shiro 整合實現了企業級細粒度權限控制體系,從資料庫設計到前後端整合形成閉環。掌握 Realm 核心邏輯、權限字串設計規範與快取策略,能構建安全、可維護、高效能的授權系統。