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

Maven 整合 Shiro + Struts2 + Spring 3 + MyBatis 完整專案搭建實戰

     編輯:Java 綜合教學

一、專案整體架構

本文延續「Struts2 + Spring 3 + MyBatis Maven 專案」,加入 Apache Shiro 1.2.1 實現認證授權,採用「先跑通、再深入」原則,記錄關鍵整合步驟與坑點。

二、POM 依賴管理

<properties>
    <shiro.version>1.2.1</shiro.version>
</properties>

<dependencies>
    <!-- Shiro 核心 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <!-- 既有 Struts2/Spring/MyBatis 依賴略 -->
</dependencies>

三、web.xml 過濾器順序 (關鍵!)

<!-- 1. Shiro 過濾器必須在 Struts2 之前 -->
<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>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

<!-- 2. Spring ContextLoaderListener 載入所有 applicationContext-*.xml -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext-*.xml</param-value>
</context-param>

<!-- 3. Struts2 過濾器 (次序在 Shiro 之後) -->
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

鐵律:DelegatingFilterProxy (Shiro) 必須映射在 StrutsPrepareAndExecuteFilter 之前,否則 Shiro 無法攔截 Struts2 Action 請求,導致授權失效或無限重導向。

四、Spring-Shiro 整合配置 applicationContext-shiro.xml

<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-3.0.xsd">

    <!-- 1. 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="shiroDbRealm"/>
    </bean>

    <!-- 2. 自定義 Realm (需實作 AuthorizingRealm) -->
    <bean id="shiroDbRealm" class="com.example.shiro.ShiroDbRealm"/>

    <!-- 3. ShiroFilterFactoryBean 定義攔截鏈 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login_loginPage.do"/>
        <property name="successUrl" value="/login_home.do"/>
        <property name="unauthorizedUrl" value="/other_error.do"/>
        <property name="filterChainDefinitions">
            <value>
                /login_loginPage.do = anon
                /login_login.do = anon
                /login_home.do = authc
                /login_hello.do = authc
                /t1/** = roles[aa], perms[aaa]
                /t2/** = roles[bb], perms[baaa]
                /t3/** = roles[dd]
            </value>
        </property>
    </bean>

    <!-- 4. 生命週期 BeanPostProcessor (啟用 Shiro 註解支援) -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 5. AOP 式方法級權限檢查 (需 @RequiresRoles/@RequiresPermissions) -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
        <property name="proxyTargetClass" value="true"/>
    </bean>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

</beans>

filterChainDefinitions 語法速查

五、EhCache 配置 ehcache.xml (放在 classpath 根目錄)

<ehcache>
    <diskStore path="java.io.tmpdir/shiro-spring-sample"/>
    <defaultCache maxElementsInMemory="10000" eternal="false"
                  timeToIdleSeconds="120" timeToLiveSeconds="120"
                  overflowToDisk="false" diskPersistent="false"
                  diskExpiryThreadIntervalSeconds="120"/>
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000" eternal="true"
           overflowToDisk="true" diskPersistent="true"
           diskExpiryThreadIntervalSeconds="600"/>
    <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization"
           maxElementsInMemory="100" eternal="false"
           timeToLiveSeconds="600" overflowToDisk="false"/>
</ehcache>

六、自定義 Realm 實作 ShiroDbRealm

public class ShiroDbRealm extends AuthorizingRealm {
    @Autowired private UserService userService;
    @Autowired private RoleService roleService;
    @Autowired private PermissionService permissionService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        User user = userService.getByUserName(upToken.getUsername());
        if (user != null) {
            // 建立 Principal 物件,攜帶更多資訊
            ShiroUser principal = new ShiroUser(user.getUsername(), user.getNickname());
            return new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
        }
        return null; // 拋出 UnknownAccountException
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 角色
        List<Role> roles = roleService.getByUserName(shiroUser.getUsername());
        info.addRoles(roles.stream().map(Role::getRolename).collect(Collectors.toList()));

        // 權限
        List<Permission> perms = permissionService.getByUserName(shiroUser.getUsername());
        info.setStringPermissions(perms.stream().map(Permission::getPermissionname).collect(Collectors.toSet()));

        return info;
    }

    // 內部 Principal 類別
    public static class ShiroUser implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String username, nickname;
        public ShiroUser(String username, String nickname) { this.username = username; this.nickname = nickname; }
        // getter 略
    }
}

七、Struts2 Action 整合登入/登出

public class LoginAction extends BaseAction {
    private String username, password;
    @Autowired private UserService userService;

    public String login() { // AJAX 登入
        Map<String, Object> map = new HashMap<>();
        User u = userService.check(new User(username, password));
        if ("0".equals(u.getRes())) { // 成功
            Subject subject = SecurityUtils.getSubject();
            subject.login(new UsernamePasswordToken(username, password)); // 觸發 Realm.doGetAuthenticationInfo
            map.put("res", "true");
        } else {
            map.put("res", "false");
        }
        setJsonResult(JSONObject.fromObject(map).toString());
        return "success"; // JSON 結果
    }

    public String loginForm() { // 表單登入 (傳統)
        User u = userService.check(new User(username, password));
        if ("0".equals(u.getRes())) {
            SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
            return "home";
        }
        return "loginPageForm";
    }

    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) subject.logout(); // 清除 Session、快取
        return "loginPage";
    }
    // getter/setter 略
}

八、struts.xml 命名空間對應權限區域

<package name="t1" namespace="/t1" extends="defalutGlobal">
    <action name="t1_*" class="test1Action" method="{1}">
        <result name="t1">/WEB-INF/pages/t1/t1.html</result>
        <result name="toT2" type="redirect">/t2/t2_t2.do</result>
    </action>
</package>
<package name="t2" namespace="/t2" extends="defalutGlobal"> ... </package>
<package name="t3" namespace="/t3" extends="defalutGlobal"> ... </package>

命名空間 /t1、/t2、/t3 直接對應 filterChainDefinitions 的 /t1/** 等路徑模式,實現細粒度角色/權限控制。

九、資料庫表結構 (示範用 5 表模擬)

CREATE TABLE base_user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    createtime CHAR(20),
    username CHAR(20) UNIQUE,
    password CHAR(20),
    nickname CHAR(20),
    t1 CHAR(100), t2 CHAR(30)
);
-- 角色表、權限表、用戶角色關聯表、角色權限關聯表 略
-- Demo 中 RoleService/PermissionService 為模擬實現,實際專案請建立完整關聯表

十、常見坑點總結

  1. 過濾器順序錯誤:Shiro 必須在 Struts2 之前,否則 Action 執行前未攔截。
  2. Realm 未注入 Service:確認 @Autowired 生效 (需 <context:annotation-config/> 或 <context:component-scan> 掃描到 Realm 所在包)。
  3. 權限快取未清理:登出時需 subject.logout(),或實作 SessionListener 監聽 Session 銷毀清理授權快取。
  4. URL 模式不匹配:filterChainDefinitions 路徑需與 Struts2 namespace + action 完全一致 (含 .do 後綴)。
  5. RememberMe 功能:需額外配置 rememberMeManager、Cookie 設定、user 過濾器,後續文章專門介紹。

十一、參考資源

十二、結語

Shiro + Struts2 整合核心在於 DelegatingFilterProxy 過濾器順序、Realm 實作 與 filterChainDefinitions 路徑映射。掌握這三點,即可在傳統 Struts2 專案中引入企業級安全框架,支援 RBAC、方法級註解、Session 管理、快取等完整功能。後續將深入 RememberMe、Session 叢集、OAuth2 整合等進階主題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
過濾器含義範例
anon匿名存取,無需登入/login/** = anon
authc需認證 (登入)/user/** = authc
roles[role1,role2]需具備所有指定角色/admin/** = roles[admin,super]
perms[perm1,perm2]需具備所有指定權限/data/** = perms[data:read,data:write]
rest[user]REST 風格 (自動對應 HTTP 方法)/api/** = rest[user]
port[8443]強制指定埠口/secure/** = port[8443]