利用maven创建webx3项目——实现简单的留言板(八)

github:https://github.com/wade6/messageboard-webx3

增加权限和白名单

8、权限验证

前面已经说过,创建的留言板系统没有权限验证,导致每个页面不用登陆就能访问,那么现在就来加上权限验证,增加留言板系统的安全性。

首先了解一下权限验证的原理,前面也简单的提到过,其实就是利用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>

 修改uri.xml配置"loginLink",如下:

<?xml version="1.0" encoding="UTF-8" ?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:services="http://www.alibaba.com/schema/services"
    xmlns:uris="http://www.alibaba.com/schema/services/uris"
    xmlns="http://www.alibaba.com/schema/services/uris"
    xmlns:uri-interceptors="http://www.alibaba.com/schema/services/uris/interceptors"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.alibaba.com/schema/services http://localhost:8080/schema/services.xsd
        http://www.alibaba.com/schema/services/uris http://localhost:8080/schema/services-uris.xsd
        http://www.alibaba.com/schema/services/uris/interceptors http://localhost:8080/schema/services-uris-interceptors.xsd
        http://www.springframework.org/schema/beans http://localhost:8080/schema/www.springframework.org/schema/beans/spring-beans.xsd
    ">

    <services:uris>
        <uri id="server" requestAware="true" />

        <turbine-uri id="messageBoardLink" exposed="true" extends="server">
            <componentPath>/</componentPath>
        </turbine-uri>
        
        <turbine-content-uri id="loginLink" extends="messageBoardLink" exposed="true">
        	<contentPath>/index.htm</contentPath>
        </turbine-content-uri>
    </services:uris>
</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("/message/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>留言列表&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
	<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就不会加载了,其实不删也没什么关系。

留言板系统也就此结束~

9、总结

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

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>

猜你喜欢

转载自wade6.iteye.com/blog/1255290