Apache Shiro 是輕量級安全框架,提供認證、授權、加密、會話管理等功能。本文記錄 Spring MVC 整合 Shiro 的最小化配置步驟,涵蓋 Realm 自定義、權限註解、登錄流程、攔截器鏈等核心環節。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version> <!-- 當前穩定版 -->
</dependency>
<!-- 需額外引入 shiro-ehcache 若用 EhCache -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value> <!-- 委托 Spring 管理生命週期 -->
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
關鍵:DelegatingFilterProxy 從 Spring 容器查找名為 shiroFilter 的 Bean。
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 1. 密碼匹配器 (MD5) -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.Md5CredentialsMatcher"/>
<!-- 2. 自定義 Realm -->
<bean id="myRealm" class="com.example.shiro.MyRealm"/>
<!-- 3. JDBC Realm (可選,直接查表) -->
<bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="dataSource" ref="dataSource"/>
<property name="authenticationQuery" value="SELECT password FROM users WHERE username = ?"/>
<property name="userRolesQuery" value="SELECT role_name FROM user_roles WHERE username = ?"/>
<property name="permissionsQuery" value="SELECT perm FROM roles_perms WHERE role_name = ?"/>
<property name="permissionsLookupEnabled" value="true"/>
</bean>
<!-- 4. 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="myRealm"/> <!-- 多 Realm 按順序嘗試 -->
<ref bean="jdbcRealm"/>
</list>
</property>
</bean>
<!-- 5. ShiroFilterFactoryBean -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="unauthorizedUrl" value="/403.jsp"/>
<property name="filterChainDefinitions">
<value>
/static/** = anon
/login.jsp = anon
/dologin = anon
/logout = logout
/admin/** = roles[admin]
/user/** = authc
/** = authc
</value>
</property>
</bean>
<!-- 6. 註解支持 (AOP) -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
public class MyRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(MyRealm.class);
@Autowired private UserService userService; // 注入業務服務
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) getAvailablePrincipal(principals);
log.info("授權查詢: {}", username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User user = userService.findByUsername(username);
if (user != null) {
info.addRoles(user.getRoleNames()); // 角色集合
info.addStringPermissions(user.getPerms()); // 權限字符串集合
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
log.info("認證用戶: {}", username);
User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException("用戶不存在");
}
// 返回 Principal + Credentials (加密後密碼) + RealmName
return new SimpleAuthenticationInfo(username, user.getPassword(), getName());
}
}
@Controller
@RequestMapping("/")
public class LoginController {
@PostMapping("/dologin")
@ResponseBody
public Map login(HttpServletRequest request,
@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) Boolean rememberMe) {
Map result = new HashMap<>();
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(rememberMe != null && rememberMe);
try {
subject.login(token); // 觸發 Realm.doGetAuthenticationInfo
result.put("success", true);
result.put("msg", "登錄成功");
} catch (UnknownAccountException e) {
result.put("msg", "用戶不存在");
} catch (IncorrectCredentialsException e) {
result.put("msg", "密碼錯誤");
} catch (LockedAccountException e) {
result.put("msg", "賬號已鎖定");
} catch (DisabledAccountException e) {
result.put("msg", "賬號已禁用");
} catch (ExcessiveAttemptsException e) {
result.put("msg", "嘗試次數過多,請稍後再試");
} catch (AuthenticationException e) {
result.put("msg", "登錄失敗: " + e.getMessage());
}
return result;
}
@GetMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "redirect:/login.jsp";
}
}
@Service
public class UserServiceImpl implements UserService {
@RequiresPermissions("user:create")
public void createUser(User user) { ... }
@RequiresRoles("admin")
public void deleteUser(Long id) { ... }
@RequiresPermissions(value = {"user:view", "user:export"}, logical = Logical.OR)
public List listUsers() { ... }
}
<%@ taglib prefix="shiro" uri="https://shiro.apache.org/tags" %>
<shiro:guest>
<a href="/login.jsp">登錄</a>
</shiro:guest>
<shiro:user>
歡迎, <shiro:principal/> | <a href="/logout">退出</a>
</shiro:user>
<shiro:hasRole name="admin">
<a href="/admin/dashboard">管理後台</a>
</shiro:hasRole>
<shiro:hasPermission name="user:export">
<button>導出用戶</button>
</shiro:hasPermission>
@Lazy 或在 doGetAuthenticationInfo 內手動 getBeanfilterChainDefinitions 首行配置 /static/** = anonDefaultAdvisorAutoProxyCreator 和 AuthorizationAttributeSourceAdvisor 都已註冊shiro-redis 或 shiro-spring-sessionnew Md5Hash(password, salt, 1024).toHex(),Realm 配置 Md5CredentialsMatcher 並 setStoredCredentialsHexEncoded(true)