前面已经说过,创建的留言板系统没有权限验证,导致每个页面不用登陆就能访问,那么现在就来加上权限验证,增加留言板系统的安全性。
首先了解一下权限验证的原理,前面也简单的提到过,其实就是利用cookie和session机制。
cookie实现
只用cookie是可以实现权限验证的,其过程为:
1、当登陆请求到来时,生成一个cookie,将其响应给用户;
Cookie cookie = new Cookie("login","success");
response.addCookie(cookie);
然后客户端浏览器将创建一个cookie;
2、当请求再次到达时,检查请求报头中的cookie信息即可实现权限验证;
3、用户退出时,设置login=flase;
4、可以设置cookie,当浏览器关闭后,cookie马上销毁;也可以设置有效期。
但这种方式的安全性极低,如果人为的修改了cookie,那么系统是完全透明的。
session实现
session一般是保存在服务器中的一个记录请求会话的对象。
利用session实现权限验证,其过程为:
1、请求第一次到达时,新建一个session,将其唯一标识sessionId保存在cookie中,返回给客户端;
2、登录请求到达时,根据cookie中的sessionId,在此session种创建用户的信息<key,value>;
3、用户请求需要权限验证的页面时,根据cookie中的sessionId获取session,然后就可以拿到当前用户的信息。
4、用户退出时,将用户的信息从session中删除。
5、可以在用户退出时调用session方法清除session,也可以设置有效期控制session的生命周期。
这种方式通过服务器主动的进行权限验证,相对cookie来说比较安全。但也存在风险,比如csrf攻击。
webx3可以防御这种攻击,就是在页面中增加一个隐藏的字段。
webx3中的session可以保存在服务器端,也可以保存在客户端的cookie中,只需要在webx.xml中设置一下即可。
如下图:
设置将session保存在客户端的cookie中,还可以对session和cookie进行一些参数的设置,其实webx3实现一个session框架,具体内容参见《webx框架指南》。
设置好session后,就可以在系统中利用session进行权限验证了。
权限验证是通过在pipeline.xml中配置验证valve来实现的。
首先创建一个权限vavle类,如下:
com.alibaba.webx3.messageboard.util.AuthorizationValve.java
package com.alibaba.webx3.messageboard.util; import java.util.List; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import com.alibaba.citrus.service.pipeline.PipelineContext; import com.alibaba.citrus.service.pipeline.support.AbstractValve; import com.alibaba.citrus.service.uribroker.URIBrokerService; import com.alibaba.citrus.service.uribroker.uri.URIBroker; import com.alibaba.citrus.turbine.TurbineRunData; import com.alibaba.citrus.turbine.util.TurbineUtil; import com.alibaba.citrus.util.ServletUtil; import com.alibaba.citrus.util.StringUtil; public class AuthorizationValve extends AbstractValve { @Autowired private HttpServletRequest request; @Autowired private URIBrokerService uriBrokerService; @Resource(name="whiteListForLogin") private List<String> whiteListForLogin; public void invoke(PipelineContext pipelineContext) throws Exception { // 获取session HttpSession session = request.getSession(); TurbineRunData rundata = TurbineUtil.getTurbineRunData(request); String sessionUser=null; //获取session中的用户名,到相应的sessionStore中获取,若设置为cookie,则到cookie中查找 sessionUser = (String) session.getAttribute("login_user"); //取得request所请求的资源路径。 String path = ServletUtil.getResourcePath(request); if (sessionUser == null) { //不是白名单的页面,跳回登陆页面 if (!checkUri(path)) { URIBroker loginUrl = uriBrokerService.getURIBroker("loginLink"); rundata.setRedirectLocation(loginUrl.render()); return; } } pipelineContext.invokeNext(); } //检查白名单 private boolean checkUri(String path) { int lastSlashIndex = path.lastIndexOf("/"); //最后的页面下划线大写处理 if (lastSlashIndex >= 0) { path = path.substring(0, lastSlashIndex) + "/" + StringUtil.toCamelCase(path.substring(lastSlashIndex + 1)); } else { path = StringUtil.toCamelCase(path); } return whiteListForLogin != null && whiteListForLogin.contains(path) ? true : false; } }
其中checkUri()方法是检查设置的白名单,在白名单中的页面可以面登录访问,其中白名单对象就是List<String> whiteListForLogin,是通过注入得到的,其真实的身份是在webx.xml中配置的bean,如下:
...... <!-- 装载模块。 --> <services:module-loader> <ml-factories:class-modules> <ml-factories:search-packages type="$1" packages="com.alibaba.webx3.common.module.*" /> </ml-factories:class-modules> </services:module-loader> <!-- 免登陆访问的白名单 --> <beans:bean id="whiteListForLogin" class="java.util.ArrayList" > <beans:constructor-arg> <beans:list> <beans:value>/index.htm</beans:value> <beans:value>/register.htm</beans:value> </beans:list> </beans:constructor-arg> </beans:bean> </beans:beans>
然后在pipeline.xml中配置该valve,如下:
...... <!-- 检查csrf token,防止csrf攻击和重复提交。假如request和session中的token不匹配,则出错,或显示expired页面。 --> <checkCsrfToken /> <!-- 登陆权限验证 --> <valve class="com.alibaba.webx3.messageboard.util.AuthorizationValve" /> <loop> ......
此时只是完成了验证的功能,但是还能没有给系统增加授权的语句。
在userAction.java中增加授权的语句。
当用户登录时,在session中增加用户信息,退出时清除session。
package com.alibaba.webx3.messageboard.module.action; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import com.alibaba.citrus.turbine.Context; import com.alibaba.citrus.turbine.Navigator; import com.alibaba.citrus.turbine.dataresolver.FormGroup; import com.alibaba.webx3.messageboard.biz.service.UserService; import com.alibaba.webx3.messageboard.dao.object.UserDO; import com.alibaba.webx3.messageboard.module.vo.UserVO; public class UserAction { @Autowired private UserService userService; @Autowired private HttpServletResponse response; @Autowired private HttpServletRequest request; // 登陆 public void doLogin(@FormGroup("login") UserVO user, Context context, Navigator nav, HttpSession session) { String username = user.getUsername(); String password = user.getPassword(); UserDO userdo = null; boolean success; // 根据用户名获得用户记录 userdo = userService.getByUsername(username); if (userdo == null) { context.put("message", "用户名不存在!"); nav.forwardTo("index"); return; } // 校验密码是否正确 if (password.equals(userdo.getPassword())) { success = true; } else { success = false; } // 判断执行转向和重定向 if (success) { session.setAttribute("login_user", user.getUsername()); context.put("username", user.getUsername()); nav.redirectTo("messageLink").withTarget("messageList"); } else { context.put("message", "密码错误!"); nav.forwardTo("index"); } } // 退出 public void doLogout(Navigator nav, HttpSession session) { session.invalidate(); // 转到首页 nav.redirectTo("messageBoardLink").withTarget("index"); } }
在messageList.vm中增加权限的判断,admin用户可以删除和编辑,其他用户只能操作自己添加的留言,如下
<div style="font-size:10pt"> <p>留言列表 <a href="$messageBoardLink.setTarget("message/addMessage")">添加留言</a> </p> <p style="color:red">$!message</p> <p>--------------------------------------------------------</p> <form action="" method="post" target="_self"> $csrfToken.hiddenField <input type="hidden" name="action" value="messageAction"/> #foreach($messageItem in $messageList) <p>标题:$!messageItem.title</p> <p>作者:$!messageItem.author</p> <div style="color:blue"> <p>$!messageItem.content</p> </div> #if($admin==true||$username==$messageItem.author) <a href='#' onclick="deleteMessage($messageItem.id)">删除</a> <a href='$messageBoardLink.setTarget("message/modifyMessage").addQueryData("messageId", $messageItem.id)'>编辑</a> #end <p>--------------------------------------------------------</p> #end <input type="hidden" id="messageId" name="messageId" value="$messageItem.id"> <input type="submit" id="delete" style="display:none;" name="event_submit_do_delete" /> </form> </div> <script> function deleteMessage(id){ if(confirm("确定要删除?")){ document.getElementById("messageId").value=id; document.getElementById("delete").click(); } } </script>
至此,权限验证就完成了。
现在可以把webx-app1.xml删掉,代码可以不用管,这样子应用app1就不会加载了,其实不删也没什么关系。
留言板系统也就此结束~
1、在实现权限系统时出现了一些问题,在配置白名单的bean时,将其配置到webx-messageboard.xml中,启动容器的时候加载app1出错:no bean named whiteListForLogin,折腾了好久才知道,app1和messageboard先前设置的公用同一个pipeline,但是whiteListForLogin 这个vale只配在了messageboard的子容器中,所以会出现上述错误;
解决办法:将whiteListForLogin 配置到webx.xml中
2、解决了上述问题后,又报错:No matching bean of type [java.lang.String] found for dependency [collection of java.lang.String],经过网上搜刮,发现原因是自动注入的问题;.AuthorizationValve.java中注入whiteListForLogin不能用@Autowired,改用@Resource(name="whiteListForLogin")就解决了。具体原因可以参考 http://stackoverflow.com/questions/1363310/auto-wiring-a-list-using-util-schema-gives-nosuchbeandefinitionexception
3、通过这个小实践,加深了对webx3开发的理解,同时学到了不少web开发知识~在此感谢身边的同事,谢谢他们对我的帮助。
4、对webx3的原理还不是很熟悉,下一步准备研究一下~
webx3中实现了一个session框架,可以通过webx.xml中的设置将session保存在不同的地方,常用的有服务器内存和cookie;还可以将session的不同部分分别保存到不同的地方。
session框架
session ID是唯一标示,一般将其保存在cookie中,这样相同cookie值的请求都看作是同一个session的请求。
session的生命周期:第一个请求时创建;在访问期间可以不断地更新;超过配置的最大不活动时间就会结束,还可以通过调用session.invalidate()方法,直接清除session的所有内容;
Session Store是session框架中最核心的部分,定义了session保存的位置,可以设置多个store,这样就可以将session不同的部分保存在不同的地方。
在session框架中,有一个重要的特殊对象,用来保存session生命期的状态。这个对象叫作session model。
Session Model是用来记录当前session的生命期数据的,例如:session的创建时间、最近更新时间等。
SessionModelEncoder
默认情况下,SessionModel对象将被转换成一个JSON字符串,然后这个字符串将被保存在某个session store中;读取时需要解码成SessionModel对象。
默认实现为:
<session-model-encoders>
<model-encoders:default-session-model-encoder />
</session-model-encoders>
Session Interceptor拦截器的作用是拦截特定的事件,甚至干预该事件的执行结果。
Cookie Store
Cookie Store的作用,是将session对象保存在客户端cookie中。Cookie Store减轻了服务器维护session数据的压力,从而提高了应用的扩展性和可用性。
但是读写cookie比较麻烦,还要在代码中设置很多参数:domain、path、httpOnly...等,所以通过操作HttpSession,session框架就帮我们读写cookie了(那些参数在配置文件里配置就ok)。webx主张把一切对cookie的读写,都转换成对session的读写。
Session Encoders
Session里保存的是Java对象,而cookie中只能保存字符串。如何把Java对象转换成合法的cookie字符串(或者将字符串恢复成对象)呢?这就是Session Encoder所要完成的任务。详细见下面。
Cookie Store需要依赖其它两个Request Contexts: <buffered>(将所有的输出到response.getWriter()或getOutputStream()的内容缓存在内存里,直到最后一刻才真正
输出到浏览器) 和 <lazy-commit>(拦截了response对象中引起提交的方法,将它们延迟到最后才执行。)
Cookie Store分为多值和单值;
多值Cookie Store是在一组cookie(如tmp0, tmp1, ...)中保存一组attributes的名称和对象。它所创建的cookie值,只有session框架自己才能解读,如<key,value>的形式。
单值cookie store就是在一个cookie中仅保存一个值或对象,如<object>。
Session Encoders和Session Value Encoder
这两个cookie store的结构是不一样的。因此解码的方法也不一样。单值的cookieStore使用Session Value Encoder解码;多值的cookieStore使用Session Encoder。
Session Encoders
Session Encoder需要转换一组session attributes的key-values。Session框架提供了一种encoder的实现,编码的基本过程为:序列化、加密(可选)、压缩、Base64编码、URL encoding编码。
保存session数据时,session框架将使用第一个encoder来将对象转换成cookie可接受的字符串;
读取session数据时,session框架将依次尝试所有的encoders,直到解码成功为止。
默认实现为:
<session-stores:encoders>
<session-encoders:serialization-encoder />
</session-stores:encoders>
Session Value Encoder
session Value Encoder只转换sessionattribute的值。
和SessionModelEncoder以及SessionEncoder类似,session框架也支持多个session valueencoders同时存在。
• 保存session数据时,session框架将使用第一个encoder来将对象转换成cookie可接受的字符串;
• 读取session数据时,session框架将依次尝试所有的encoders,直到解码成功为止。
这种编码、解码方案可让使用不同session value encoders的系统之间共享cookie数据,也有利于平滑迁移系统。
目前有两种基本的session value encoders实现。<simple-value-encoder>和<mappedvalues-encoder>
Simple Memory Store
SimpleMemoryStore是最简单的session store。它将所有的session对象都保存在内存里面。这种store不支持多台机器的session同步,而且也不关心内存是否被用尽。因此这种简单的store一般只应使用于测试环境。
<stores>
<session-stores:simple-memory-store id="simple" />
</stores>