访问安全控制解决方案

本文是《轻量级 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 关于安全性控制的解决方案,能否使用更简单更高效的方式来实现?等候您的评论。

你可能感兴趣的:(访问安全控制解决方案)