1. 程式人生 > 實用技巧 >訪問安全控制解決方案

訪問安全控制解決方案

本文是《輕量級 Java Web 框架架構設計》的系列博文。

今天想和大家簡單的分享一下,在 Smart 中是如何做到訪問安全控制的。也就是說,當沒有登入或 Session 過期時所做的操作,會自動退回到首頁(例如:登入頁面),以防止使用者進行非法操作。

這件事情或許是每個具備安全性考慮的系統都需要的功能,我們可以分兩種情況來處理使用者的請求:

  1. 普通請求
  2. AJAX 請求

下面是具體的實現過程,您別忘了繫好安全帶,我們這就出發了!

第一步:在 Smart Framework 中定義一個認證異常類AuthException

public class AuthException extends RuntimeException {

    public AuthException() {
        super();
    }

    public AuthException(String message) {
        super(message);
    }

    public AuthException(String message, Throwable cause) {
        super(message, cause);
    }

    public AuthException(Throwable cause) {
        super(cause);
    }
}

沒啥內容,就一個普通的RuntimeException 而已,其實就是想自定義一種異常型別,以區別於其他的異常,方便在框架中進行處理。

第二步:在 Smart Sample 中定義一個 AuthAspect,用於攔截 Action 的所有請求

@Bean
@Aspect(pkg = "com.smart.sample.action")
@Order(0)
public class AuthAspect extends BaseAspect {

    @Override
    public boolean filter(Class<?> cls, Method method, Object[] params) {
        String className = cls.getSimpleName();
        String methodName = method.getName();
        return !(
            className.equals("UserAction") &&
                (methodName.equals("login") || methodName.equals("logout")
            )
        );
    }

    @Override
    public void before(Class<?> cls, Method method, Object[] params) throws Exception {
        User user = DataContext.Session.get("user");
        if (user == null) {
            throw new AuthException();
        }
    }
}

以上程式碼中需注意一下幾點:

  1. 攔截到 action 包,即攔截所有的 Action 類。
  2. 使用 Order 註解定義 AOP 攔截順序,為 0 表示最先攔截。【新特性】
  3. 在 filter 方法中排除掉幾個特殊的 Action 方法,例如:login 與 logout,因為它們是無需進行安全控制的。
  4. 在 before 方法中從 Session 中獲取 User 資料(已在 login 時放入到 Session),若為空,說明 Session 已過期,則主動丟擲 AuthException。

這樣 AOP 框架(也就是 BaseAspect 類及其相關 Proxy Chain 等)就可以處理這個自定義異常了。

還記得 Order 註解嗎?它曾經在單元測試中出現過,現在還可以用於定義 AOP 順序。

友情提示:

  • 對單元測試感興趣的朋友,請閱讀《像這樣做單元測試》。
  • 若您還不熟悉 Smart AOP 的話,請閱讀《使用“鏈式代理”實現 AOP》。

第三步:在 BaseAspect 中將異常繼續往上拋,拋給它的呼叫者

public abstract class BaseAspect implements Proxy {

    @Override
    public final void doProxy(ProxyChain proxyChain) throws Exception {
        ...

        begin();
        try {
            if (filter(cls, method, params)) {
                before(cls, method, params);
                Object result = proxyChain.doProxyChain();
                after(cls, method, params, result);
            } else {
                proxyChain.doProxyChain();
            }
        } catch (Exception e) {
            error(cls, method, params, e);
            throw e; // 將異常繼續往上拋,拋給它的呼叫者
        } finally {
            end();
        }
    }

    ...
}

那麼 Aspect 的呼叫者是誰呢?也就是說,誰用 Aspect 呢?至少 Action 是一個關鍵性使用者。

那麼 Action 又是誰來呼叫呢?當然就是 DispatchServlet 了。

於是,順騰摸瓜,找到了呼叫的起源。

第四步:修改 DispatchServlet,處理 AuthException

@WebServlet("/*")
public class DispatcherServlet extends HttpServlet {
    ...

    private void handleActionMethod(HttpServletRequest request, HttpServletResponse response, ActionBean actionBean, List<Object> paramList) {
        // 從 ActionBean 中獲取 Action 相關屬性
        Class<?> actionClass = actionBean.getActionClass();
        Method actionMethod = actionBean.getActionMethod();
        // 從 BeanHelper 中建立 Action 例項
        Object actionInstance = BeanHelper.getInstance().getBean(actionClass);
        // 呼叫 Action 方法
        Object actionMethodResult;
        try {
            actionMethod.setAccessible(true); // 取消型別安全檢測(可提高反射效能)
            actionMethodResult = actionMethod.invoke(actionInstance, paramList.toArray());
        } catch (Exception e) {
            // 處理 Action 方法異常【★】
            handleActionMethodException(request, response, e);
            // 直接返回
            return;
        }
        // 處理 Action 方法返回值
        handleActionMethodReturn(request, response, actionMethodResult);
    }

    private void handleActionMethodException(HttpServletRequest request, HttpServletResponse response, Exception e) {
        if (e.getCause() instanceof AuthException) {
            // 若為認證異常,則分兩種情況進行處理
            if (WebUtil.isAJAX(request)) {
                // 若為 AJAX 請求,則傳送 403 錯誤
                WebUtil.sendError(403, response);
            } else {
                // 否則重定向到首頁
                WebUtil.redirectRequest(request.getContextPath() + "/", response);
            }
        } else {
            // 若為其他異常,則記錄錯誤日誌
            logger.error("呼叫 Action 方法出錯!", e);
        }
    }

    ...
}

大家可以看到以上程式碼的【★】處,也就是呼叫 Action 方法時的異常處理程式碼,請見下面的 handleActionMethodException 方法。邏輯如下:

  1. 首先判斷該異常是否為 AuthException,如果是,則需要分兩種情況來考慮。
  2. 若為 AJAX 請求,則向 Response 中傳送 403 錯誤程式碼。後面的事情就交給前端的 AJAX 回撥函式來做吧,請見下文。
  3. 否則(即為普通請求),重定向到首頁,即重定向傳送“/”請求。
  4. 若不是 AuthException,則輸出錯誤日誌。

以上程式碼中涉及到了幾個關鍵的 WebUtil 方法,其實這些都是對 Servlet API 的一個簡單的封裝。程式碼片段如下:

public class WebUtil {

    ...

    // 重定向請求
    public static void redirectRequest(String path, HttpServletResponse response) {
        try {
            response.sendRedirect(path);
        } catch (Exception e) {
            logger.error("重定向請求出錯!", e);
            throw new RuntimeException(e);
        }
    }

    // 傳送錯誤程式碼
    public static void sendError(int code, HttpServletResponse response) {
        try {
            response.sendError(code);
        } catch (Exception e) {
            logger.error("傳送錯誤程式碼出錯!", e);
            throw new RuntimeException(e);
        }
    }

    // 判斷是否為 AJAX 請求
    public static boolean isAJAX(HttpServletRequest request) {
        return request.getHeader("X-Requested-With") != null;
    }
}

以上可以看到,普通請求非常簡單,直接 redirect 就行了。而對於 AJAX 請求回撥問題,我們可使用 jQuery 來輕鬆實現。

第五步:使用 jQuery 來處理 AJAX 非法訪問

/* 全域性變數 */
var BASE = '/smart-sample'; // 應用 Context 名稱(若為空字串表示應用以 ROOT 來發布)

...

$(function() {
    $.ajaxSetup({
        cache: false,
        error: function(jqXHR, textStatus, errorThrown) {
            switch (jqXHR.status) {
                case 403:
                    document.write('');
                    location.href = BASE + '/';
                    break;
                case 503:
                    alert(errorThrown);
                    break;
            }
        }
    });

    ...
}

首先,定義一個全域性變數 BASE,代表應用的 Context 名稱,使用者可自行修改。

然後,進行 AJAX 全域性設定(使用了 jQuery 的 $.ajaxSetup 方法),在 error 回撥函式中處理 403 錯誤,故意清空頁面內容,並返回到應用首頁(效果與普通請求非法訪問時相同)。

以上就是 Smart Framework 關於安全性控制的解決方案,能否使用更簡單更高效的方式來實現?等候您的評論。

來源:資陽網站建設