在本系列的上一篇中我们介绍了一个基于cookie的访问控制方法,细心的读者一定会发现,这种方法的实现还是最终基于Struts2的拦截器机制,也就是说它只能保护web应用中的action资源,对于Struts2的应用来说,除了aciton外,一定会有不少的jsp页面。那么,我们又该如何实现对于jsp页面的访问控制呢?其实对于这样的问题,笔者在网络看到过已经不止一次了,下面我们就来介绍一种对于jsp页面的访问控制方法。
首先,笔者要祭出WEB-INF这个目录(呵呵,先不要砸我),这个目录是在servlet规范中定义的,每个web应用都会有,并且用户是访问不到的,难么很自然的一个想法就是把所有的jsp页面都放在这个目录下,而所有的请求都通过action来完成。但是这样也同时引入一个问题,即我们需要为每个页面都写一个对应的action,但是很多情况下,并不需要action中有任何逻辑,仅仅只是做一简单的跳转而已。对于这样的情况,如果我们为每个jsp页面都写一个对应的action就会显得很繁琐。
笔者在介绍一种可以解决这个问题的方法,首先,我们定义一个dispatcherAction,这个action什么也不做,只是为了跳转而存在,但是继承一下actionSupport。就象这样
public class DispatcherAction extends ActionSupport {}
然而在struts.xml中我们却可以这样配
<package name="demo" namespace="/demo" extends="default"> <action name="*" class="com.meidusa.demo.web.action.DispatcherAction "> <result name="success">/WEB-INF/jsp/demo/{1}.jsp</result> </action> </package>
初看上去这种风格的配置有点怪异,不过一旦发现它的强大之处你就会喜欢上它。在这里,action name我们并没有指定一个具体的名字,而是用了一个'*'号来表示,'*'号是一个通配符,当没有全值匹配到一个action name的时候,它可以匹配任何名字,而result页面中的{1}则会被替换成'*'所代表的值。这样一来,我们可以只写一个DispatcherAction(甚至可以不写),实现对任意jsp页面的跳转。假设我们的域名是www.meidusa.com,request请求URL是www.meidusa.com/demo/login.aciton,它会自动跳转到/WEB-INF/jsp/demo/login.jsp页面。这种RESTful风格的action Mapping在Struts2的源码里作者也直言不讳的指出是受到了Ruby on Rails的启发,有兴趣的朋友可以继续深入研究一下Restful2ActionMapper这个类,说不定还可以挖掘到更多的宝贝,这里则不再赘述。
好了,现在我们有了DispatcherAction这个类轻松实现了对jsp的页面的跳转,不过并没有实现任何的访问控制功能,下面我们结合上一篇提到的cookie访问机制,改造一下DispatcherAction这个类。
public class DispatcherAction extends ActionSupport implements ClientCookieAware<Cookie>{ private Cookie cookie; public Cookie getCookie() { return cookie; } public void setClientCookie(Cookie cookie) { this.cookie = cookie; } }
同时我们再写一个DispatcherCookieNotCareAction类
public class DispatcherCookieNotCareAction extends ActionSupport implements ClientCookieNotCare{ }
这样,当我们需要保护某些jsp页面,即只有登录之后才能访问的页面使用DispatcherAction这个类来跳转,而对于不需要保护的页面使用DispatcherCookieNotCareAction类。不过要记得把我们的cookieInterceptor放到拦截器堆栈中。
现在,用我们的DispatcherAction类再结合我们的cookie访问机制又实现了对除aciton之外的jsp页面的控制。不过看到这里,有些朋友可能又要问了,使用'*'通配符那不是匹配了所有的jsp页面吗?如果某些jsp页面仅仅是用作action的返回结果,并不想直接暴露给用户访问该怎么办呢?在解决这个问题前,我们先来研究一下Struts2中的staticParams拦截器。
public class StaticParametersInterceptor extends AbstractInterceptor { private boolean parse; private static final Log LOG = LogFactory.getLog(StaticParametersInterceptor.class); public void setParse(String value) { this.parse = Boolean.valueOf(value).booleanValue(); } public String intercept(ActionInvocation invocation) throws Exception { ActionConfig config = invocation.getProxy().getConfig(); Object action = invocation.getAction(); final Map parameters = config.getParams(); if (LOG.isDebugEnabled()) { LOG.debug("Setting static parameters " + parameters); } // for actions marked as Parameterizable, pass the static parameters directly if (action instanceof Parameterizable) { ((Parameterizable) action).setParams(parameters); } if (parameters != null) { final ValueStack stack = ActionContext.getContext().getValueStack(); for (Iterator iterator = parameters.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); stack.setValue(entry.getKey().toString(), entry.getValue()); Object val = entry.getValue(); if (parse && val instanceof String) { val = TextParseUtil.translateVariables((String) val, stack); } stack.setValue(entry.getKey().toString(), val); } } return invocation.invoke(); } }
这个拦截器已经在默认的拦截器堆栈中,它可以把action配置中的静态属性以map的形式注入到action中,当然前题是这个aciton实现Parameterizable的接口
public interface Parameterizable { public void addParam(String name, Object value); void setParams(Map<String, Object> params); Map getParams(); }
在这里,我们主要定义两个属性,一个includes,一个excludes。includes代表只允许访问的jsp页面,excludes代表剔除不允许访问的页面之外其他页面都允许访问,也就是通常说的黑白名单控制法。这两个属性可以都设置,也可以只设置一个,也可以不设置。我们来看一个例子,假设我们有一个profile.jsp放在了/WEB-INF/jsp/demo目录下。当用户登录后跳转到这个jsp页面上,显然这个页面只有通过login action执行后跳转而不允许直接访问。我们可以在action的配置中设置excludes属性<param name="excludes">profile</param>
<package name="demo" namespace="/demo" extends="default"> <action name="*" class="com.meidusa.demo.web.action.DispatcherAction "> <param name="excludes">profile</param> <result name="success">/WEB-INF/jsp/demo/{1}.jsp</result> <result name="none">/WEB-INF/jsp/demo/error.jsp</result> </action> </package>
现在action已经配置好了,问题是要如何做到对配置好的黑白名单进行控制呢?答案仍旧是用Struts2的拦截器来实现。不知道大家在前面两个例子中有没有发现这样一个有趣的现象,每一个拦截器经常都会有一个配对的由action实现的借口,比如我们的ClientCookieInterceptor拦截器和ClientCookieAware接口(当然了,还有ClientCookieNotCare);StaticParametersInterceptor拦截器和Parameterizable接口。那在我们讨论具体如何实现这个拦截器之前不妨先看一下这个接口应该怎么实现。
public interface Dispatchable { public String getIncludes(); public String getExcludes(); }
其实很简单,呵呵,这个Dispatchable接口只有两个方法getIncludes()和getExcludes()。也就是说这个拦截器在拦截的时候从被拦截的aciton中读取用StaticParametersInterceptor注入的includes和excludes属性。但是要注意一点,在配置拦截器堆栈的时候需要把StaticParametersInterceptor放在我们的DispatcherInterceptor拦截器之前,因为我们的拦截器依赖于它。
最后来看一下我们的DispatcherInterceptor拦截器。通过从aciton读取的includes和excludes属性判断这个aciton是否允许被访问。
public class DispatcherInterceptor extends AbstractInterceptor { @Override public String intercept(ActionInvocation invocation) throws Exception { if (invocation.getAction() instanceof Dispatchable){ Dispatchable action = (Dispatchable)invocation.getAction(); ActionContext context = ActionContext.getContext(); //判断这个aciton是否允许被访问 boolean allow = apply(action.getExcludes(), action.getIncludes(), context.getName()); if (!allow){ return Action.NONE; } } return invocation.invoke(); } //黑白名单控制规则 public static boolean apply(Set excludes, Set includes, String action) { if (((excludes.contains("*") && !includes.contains("*")) || excludes.contains(action)) && !includes.contains(action)) { return false; } return includes.size() == 0 || includes.contains(action) || includes.contains("*"); } public static boolean apply(String excludes, String includes, String action) { Set includesSet = TextParseUtil.commaDelimitedStringToSet(includes == null? "" : includes); Set excludesSet = TextParseUtil.commaDelimitedStringToSet(excludes == null? "" : excludes); return apply(excludesSet, includesSet, action); } }
最后我们再来看一下最终修改后的DispatcherAction
public class DispatcherAction extends ActionSupport implements ClientCookieAware<Cookie>, Dispatchable, Parameterizable{ private Cookie cookie; private Map params; public String getIncludes() { return (String)params.get("includes"); } public String getExcludes() { return (String)params.get("excludes"); } public Cookie getCookie() { return cookie; } public void setClientCookie(Cookie cookie) { this.cookie = cookie; } public void addParam(String name, Object value){ this.params.put(name, value); } public void setParams(Map<String, Object> params){ this.params = params; } public Map getParams(){ return params; } }
最终修改后的DispatcherCookieNotCareAction
public class DispatcherCookieNotCareAction extends ActionSupport implements ClientCookieNotCare, Dispatchable, Parameterizable { private Map params; public String getIncludes() { return (String)params.get("includes"); } public String getExcludes() { return (String)params.get("excludes"); } public void addParam(String name, Object value){ this.params.put(name, value); } public void setParams(Map<String, Object> params){ this.params = params; } public Map getParams(){ return params; } }
至此,我们同样成功实现了对jsp页面的访问控制。