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

Struts 整合 JAAS 實戰:基於 XMLPolicy 的聲明式授權,零 VM 配置修改

     編輯:Java 程式設計入門

前言

Java Authentication and Authorization Service (JAAS) 提供可插拔的認證授權框架,但傳統用法需修改 java.security、java.policy 等 VM 級配置,部署極不靈活。本文實戰示範如何在 Struts Web 應用中,透過程式化方式初始化 JAAS,使用自訂 LoginModule 與 XML 政策檔,實現零 VM 配置修改的聲明式授權。

架構概覽

┌─────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│  Login.jsp  │────▶│  LoginAction     │────▶│  JAAS LoginContext  │
│  (表單)      │     │  (Struts Action) │     │  (認證協調器)        │
└─────────────┘     └──────────────────┘     └──────────┬──────────┘
                                                         │
                              ┌──────────────────────────┼──────────────────────────┐
                              ▼                          ▼                          ▼
                       ┌─────────────┐            ┌─────────────┐            ┌─────────────┐
                       │ CallbackHandler │        │ LoginModule │            │ XMLPolicy   │
                       │ (收集憑證)       │        │ (驗證邏輯)    │            │ (授權政策)   │
                       └─────────────┘            └─────────────┘            └─────────────┘

步驟 1:Struts 入口配置

welcome.jsp - 重導向到索引 Action

<%@ taglib uri="/tags/struts-logic" prefix="logic" %>
<logic:redirect forward="index"/>

index.jsp - 登入表單

<%@ taglib uri="/tags/struts-html" prefix="html" %>
<html:form action="/LoginAction.do">
    <p>User ID: <html:text property="userID" value="tyrone"/></p>
    <p>Password: <html:password property="password" value="password"/></p>
    <html:submit/>
</html:form>

struts-config.xml - Action 映射

<struts-config>
    <form-beans>
        <form-bean name="LoginForm" type="com.nova.colimas.web.form.LoginForm"/>
    </form-beans>

    <global-forwards>
        <forward name="index" path="/index.do"/>
    </global-forwards>

    <action-mappings>
        <action path="/index" 
                type="com.nova.colimas.web.action.StartupServlet"
                forward="/pages/index.jsp"/>
        
        <action path="/LoginAction"
                type="com.nova.colimas.web.action.LoginAction"
                name="LoginForm"
                scope="request"
                validate="true"
                input="/pages/indexcon.jsp">
            <forward name="success" path="/pages/index.jsp"/>
            <forward name="failure" path="/pages/index.jsp"/>
        </action>
    </action-mappings>
</struts-config>

步驟 2:啟動時程式化初始化 JAAS 系統屬性

StartupServlet.java - 應用啟動初始化

package com.nova.colimas.web.action;

import org.apache.struts.action.*;

public class StartupServlet extends Action {
    public ActionForward execute(ActionMapping mapping, ActionForm form,
                                 HttpServletRequest request, HttpServletResponse response) 
            throws Exception {
        initJAAS();
        return mapping.findForward("success");
    }

    private void initJAAS() {
        // 設定 JAAS 登入配置檔路徑(而非修改 java.security)
        System.setProperty("java.security.auth.login.config", 
            JAASConstants.AUTH_SECURITY_LOGINFILE);
        // 若使用自訂 Policy,亦可在此設定:
        // System.setProperty("java.security.policy", JAASConstants.AUTH_SECURITY_POLICYXMLFILE);
    }
}

JAASConstants.java - 配置常數集中管理

package com.nova.colimas.security.auth;

public interface JAASConstants {
    // 使用相對路徑或從 ServletContext 取得絕對路徑,避免硬編碼
    String AUTH_SECURITY_POLICYXMLFILE = "WEB-INF/security-policy.xml";
    String AUTH_SECURITY_LOGINFILE = "WEB-INF/login.config";
    String AUTH_SECURITY_MODULENAME = "ColimasLogin";
}

步驟 3:登入配置檔

login.config

ColimasLogin {
    com.nova.colimas.security.auth.ColimasLoginModule required debug=true;
};

步驟 4:自訂 LoginModule 實現認證與授權邏輯

ColimasLoginModule.java - 核心認證模組

package com.nova.colimas.security.auth;

import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
import javax.security.auth.spi.LoginModule;
import java.security.Principal;
import java.util.*;

public class ColimasLoginModule implements LoginModule {

    private Subject subject;
    private CallbackHandler callbackHandler;
    private boolean debug = false;
    private boolean succeeded = false;
    private boolean commitSucceeded = false;
    private String username;
    private char[] password;

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler,
                           Map<String, ?> sharedState, Map<String, ?> options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.debug = "true".equalsIgnoreCase((String) options.get("debug"));
    }

    @Override
    public boolean login() throws LoginException {
        if (callbackHandler == null) {
            throw new LoginException("CallbackHandler 不可為 null");
        }

        Callback[] callbacks = new Callback[2];
        callbacks[0] = new NameCallback("userid: ");
        callbacks[1] = new PasswordCallback("password: ", false);

        try {
            callbackHandler.handle(callbacks);
            username = ((NameCallback) callbacks[0]).getName();
            char[] tmpPwd = ((PasswordCallback) callbacks[1]).getPassword();
            if (tmpPwd == null) tmpPwd = new char[0];
            password = Arrays.copyOf(tmpPwd, tmpPwd.length);
            ((PasswordCallback) callbacks[1]).clearPassword();
        } catch (IOException | UnsupportedCallbackException e) {
            throw new LoginException("憑證收集失敗: " + e.getMessage());
        }

        if (debug) {
            System.out.println("ColimasLoginModule: 認證用戶 [" + username + "]");
        }

        if (isValidUser(username, password)) {
            succeeded = true;
            return true;
        } else {
            succeeded = false;
            username = null;
            password = null;
            throw new FailedLoginException("用戶名或密碼錯誤");
        }
    }

    @Override
    public boolean commit() throws LoginException {
        if (!succeeded) return false;

        // 建立 Principal 並加入 Subject
        subject.getPrincipals().add(new PrincipalUser(username));
        subject.getPrincipals().addAll(getUserRoles(username));

        username = null;
        password = null;
        commitSucceeded = true;
        return true;
    }

    @Override
    public boolean abort() throws LoginException {
        if (!succeeded) return false;
        if (succeeded && !commitSucceeded) {
            succeeded = false;
            username = null;
            password = null;
        } else {
            logout();
        }
        return true;
    }

    @Override
    public boolean logout() throws LoginException {
        subject.getPrincipals().clear();
        succeeded = false;
        commitSucceeded = false;
        return true;
    }

    // --- 業務邏輯方法 ---
    private boolean isValidUser(String uid, char[] passwd) {
        // 實際專案應連接資料庫/LDAP/SSO 驗證
        String validUser = "tyrone1979";
        String validPwd = "197913";
        return validUser.equals(uid) && validPwd.equals(new String(passwd));
    }

    private Collection<Principal> getUserRoles(String username) {
        List<Principal> roles = new ArrayList<>();
        // 從 DB/快取查詢角色
        roles.add(new PrincipalUser("ROLE_ADMIN"));
        roles.add(new PrincipalUser("ROLE_USER"));
        return roles;
    }
}

PrincipalUser.java - 簡易 Principal 實現

package com.nova.colimas.security.auth;

import java.security.Principal;

public class PrincipalUser implements Principal {
    private final String name;
    public PrincipalUser(String name) { this.name = name; }
    @Override public String getName() { return name; }
    @Override public boolean equals(Object o) { 
        return o instanceof PrincipalUser && name.equals(((PrincipalUser)o).name); 
    }
    @Override public int hashCode() { return name.hashCode(); }
}

步驟 5:CallbackHandler 實現

LoginCallbackHandler.java - 從 Struts Form 取得憑證

package com.nova.colimas.security.auth;

import javax.security.auth.callback.*;
import java.io.IOException;

public class LoginCallbackHandler implements CallbackHandler {
    private final String name;
    private final String password;

    public LoginCallbackHandler(String name, String password) {
        this.name = name;
        this.password = password;
    }

    @Override
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        for (Callback cb : callbacks) {
            if (cb instanceof NameCallback) {
                ((NameCallback) cb).setName(name);
            } else if (cb instanceof PasswordCallback) {
                ((PasswordCallback) cb).setPassword(password.toCharArray());
            } else {
                throw new UnsupportedCallbackException(cb, "不支援的 Callback 類型");
            }
        }
    }
}

步驟 6:LoginAction 執行 JAAS 認證

LoginAction.java

package com.nova.colimas.web.action;

import javax.security.auth.login.*;
import org.apache.struts.action.*;

public class LoginAction extends Action {
    public ActionForward execute(ActionMapping mapping, ActionForm form,
                                 HttpServletRequest request, HttpServletResponse response) 
            throws Exception {
        LoginForm loginForm = (LoginForm) form;

        try {
            LoginContext lc = new LoginContext(
                JAASConstants.AUTH_SECURITY_MODULENAME,
                new LoginCallbackHandler(loginForm.getUserID(), loginForm.getPassword())
            );
            lc.login(); // 觸發 LoginModule.login() -> commit()
            
            // 認證成功,Subject 已包含 Principals
            // 可將 Subject 存入 Session 供後續授權使用
            request.getSession().setAttribute("javax.security.auth.subject", lc.getSubject());
            
            return mapping.findForward("success");
        } catch (FailedLoginException fle) {
            // 登入失敗
            return mapping.findForward("failure");
        } catch (LoginException le) {
            // 系統錯誤
            log.error("JAAS 認證異常", le);
            return mapping.findForward("failure");
        }
    }
}

步驟 7:XMLPolicy 授權配置 (security-policy.xml)

<?xml version="1.0" encoding="UTF-8"?>
<policy xmlns="https://java.sun.com/xml/ns/security/policy">
    <!-- 授予特定 Principal 權限 -->
    <grant principal="com.nova.colimas.security.auth.PrincipalUser \"ROLE_ADMIN\"">
        <permission name="java.security.AllPermission"/>
    </grant>
    
    <grant principal="com.nova.colimas.security.auth.PrincipalUser \"ROLE_USER\"">
        <permission name="com.nova.colimas.permission.BusinessPermission" actions="read,write"/>
    </grant>
    
    <!-- 程式碼基礎授權 -->
    <grant codeBase="file:/WEB-INF/classes/-">
        <permission name="java.util.PropertyPermission" name="*" actions="read"/>
    </grant>
</policy>

關鍵技術點總結

挑戰解決方案
避免修改 java.security、java.policy啟動時用 System.setProperty() 程式化指定配置檔路徑
Web 容器類別載入器隔離配置檔放在 WEB-INF,用相對路徑或 ServletContext.getRealPath() 解析
JAAS Subject 與 Web Session 整合認證成功後將 Subject 存入 Session;後續請求從 Session 取得 Subject 做授權檢查
角色/權限動態管理LoginModule.commit() 階段從 DB 載入角色建立 Principal;XMLPolicy 定義角色權限映射
除錯困難login.config 設定 debug=true;啟動 JVM 參數 -Djava.security.debug=login,policy

現代化替代建議

新專案建議考慮:

  • Spring Security:聲明式安全、OAuth2/JWT 整合、方法級註解授權、CSRF 保護、Session 管理
  • Apache Shiro:輕量級、Realm 抽象支援多種資料源、標籤庫、注解、獨立於 Web 容器
  • Keycloak / Auth0:身分識別即服務 (IDaaS),OpenID Connect、SAML、社交登入、用戶管理主控台

JAAS 適用於需深度整合 Java 安全架構、既有 JAAS 基礎設施的舊系統擴展。

總結

透過 System.setProperty() 程式化初始化、自訂 LoginModule 實現業務認證邏輯、CallbackHandler 橋接 Struts 表單、XMLPolicy 聲明式授權,成功在 Struts 應用中落地 JAAS 且零 VM 配置修改。此模式可移植至任何 Servlet 容器,為舊系統安全升級提供可行路徑。

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