首页 > 代码库 > 【Servlet】3中web重定向的方式

【Servlet】3中web重定向的方式

http是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。当然它知道是哪个客户端地址发过来的,但是对于我们的应用来说,我们是靠用户来管理,而不是靠客户端。所以对我们的应用而言,它是需要有状态管理的,以便服务端能够准确的知道http请求是哪个用户发起的,从而判断他是否有权限继续这个请求。这个过程就是常说的会话管理。它也可以简单理解为一个用户从登录到退出应用的一段期间。本文总结了3种常见的实现web应用会话管理的方式:

1)基于server端session的管理方式

2)cookie-base的管理方式

3)token-base的管理方式

这些内容可以帮助加深对web中用户登录机制的理解,对实际项目开发也有参考价值,欢迎阅读与指正。

1. 基于server端session的管理

在早期web应用中,通常使用服务端session来管理用户的会话。快速了解服务端session:

1) 服务端session是用户第一次访问应用时,服务器就会创建的对象,代表用户的一次会话过程,可以用来存放数据。服务器为每一个session都分配一个唯一的sessionid,以保证每个用户都有一个不同的session对象。

2)服务器在创建完session后,会把sessionid通过cookie返回给用户所在的浏览器,这样当用户第二次及以后向服务器发送请求的时候,就会通过cookie把sessionid传回给服务器,以便服务器能够根据sessionid找到与该用户对应的session对象。

3)session通常有失效时间的设定,比如2个小时。当失效时间到,服务器会销毁之前的session,并创建新的session返回给用户。但是只要用户在失效时间内,有发送新的请求给服务器,通常服务器都会把他对应的session的失效时间根据当前的请求时间再延长2个小时。

4)session在一开始并不具备会话管理的作用。它只有在用户登录认证成功之后,并且往sesssion对象里面放入了用户登录成功的凭证,才能用来管理会话。管理会话的逻辑也很简单,只要拿到用户的session对象,看它里面有没有登录成功的凭证,就能判断这个用户是否已经登录。当用户主动退出的时候,会把它的session对象里的登录凭证清掉。所以在用户登录前或退出后或者session对象失效时,肯定都是拿不到需要的登录凭证的。

以上过程可简单使用流程图描述如下:
技术分享

主流的web开发平台(java,.net,php)都原生支持这种会话管理的方式,而且开发起来很简单,相信大部分后端开发人员在入门的时候都了解并使用过它。它还有一个比较大的优点就是安全性好,因为在浏览器端与服务器端保持会话状态的媒介始终只是一个sessionid串,只要这个串够随机,攻击者就不能轻易冒充他人的sessionid进行操作;除非通过CSRF或http劫持的方式,才有可能冒充别人进行操作;即使冒充成功,也必须被冒充的用户session里面包含有效的登录凭证才行。但是在真正决定用它管理会话之前,也得根据自己的应用情况考虑以下几个问题:

1)这种方式将会话信息存储在web服务器里面,所以在用户同时在线量比较多时,这些会话信息会占据比较多的内存;

2)当应用采用集群部署的时候,会遇到多台web服务器之间如何做session共享的问题。因为session是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建session的服务器,这样他就拿不到之前已经放入到session中的登录凭证之类的信息了;

3)多个应用要共享session时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好cookie跨域的处理。

针对问题1和问题2,我见过的解决方案是采用redis这种中间服务器来管理session的增删改查,一来减轻web服务器的负担,二来解决不同web服务器共享session的问题。针对问题3,由于服务端的session依赖cookie来传递sessionid,所以在实际项目中,只要解决各个项目里面如何实现sessionid的cookie跨域访问即可,这个是可以实现的,就是比较麻烦,前后端有可能都要做处理。

如果不考虑以上三个问题,这种管理方式比较值得使用,尤其是一些小型的web应用。但是一旦应用将来有扩展的必要,那就得谨慎对待前面的三个问题。如果真要在项目中使用这种方式,推荐结合单点登录框架如CAS一起用,这样会使应用的扩展性更强。

接下来是session的一些使用代码:

html代码:

技术分享
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Insert title here</title></head><body>    <form action="login.do" method="post">        帐号:<input type="text" name="uname" /><br><br>        密码:<input type="password" name="upass" /><br><br>        <input type="submit" value="http://www.mamicode.com/登录"/>    </form></body></html>
login.html

servlet:

技术分享
package cn.xdl.session;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;/** * Servlet implementation class LoginServlet */@WebServlet("/login.do")public class LoginServlet extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        response.setCharacterEncoding("utf-8");        //1.    获取请求的参数        String uname = request.getParameter("uname");        String upass = request.getParameter("upass");        if("wangpeng".equals(uname)&&"yanghai".equals(upass)){            //登录失败            response.getWriter().append("恭喜你,登录成功");        }else{            //登录成功            HttpSession session = request.getSession();//获得session对象            session.setAttribute("uname", uname);            response.getWriter().append("登录成功");        }    }    /**     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)     */    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // TODO Auto-generated method stub        doGet(request, response);    }}
LoginServlet.java
技术分享
package cn.xdl.session;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;/** * Servlet implementation class HomeServlet */@WebServlet("/home.do")public class HomeServlet extends HttpServlet {        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        //response.setCharacterEncoding("utf-8");        response.setContentType("text/html;charset=utf-8");        HttpSession session = request.getSession();                        String uname = (String)session.getAttribute("uname");        //null        //String uname2 = session.getAttribute("uname").toString();        //        //String uname3 = ""+session.getAttribute("uname");                if(uname == null){            //没有存储用户, 表示未登录过            response.getWriter().append("您尚未登录, 请登录");        }else{            //存储的有用户的名称            response.getWriter().append("欢迎您, "+uname);            response.getWriter().append("<a href=http://www.mamicode.com/‘ss2.do‘>点击注销用户");        }            }    /**     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)     */    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // TODO Auto-generated method stub        doGet(request, response);    }}
HomeServlet.java
技术分享
package cn.xdl.session;import java.io.IOException;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;/** */@WebServlet("/ss2.do")public class ServletSessionDemo2 extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        response.setContentType("text/html;charset=utf-8");        HttpSession session = request.getSession();        //移除一个键值对        //session.removeAttribute("uname");        //销毁session        session.invalidate();        response.getWriter().append("注销成功");                    }    /**     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)     */    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // TODO Auto-generated method stub        doGet(request, response);    }}
HomeServletRelease

2. cookie-based的管理方式

由于前一种方式会增加服务器的负担和架构的复杂性,所以后来就有人想出直接把用户的登录凭证直接存到客户端的方案,当用户登录成功之后,把登录凭证写到cookie里面,并给cookie设置有效期,后续请求直接验证存有登录凭证的cookie是否存在以及凭证是否有效,即可判断用户的登录状态。使用它来实现会话管理的整体流程如下:

1)用户发起登录请求,服务端根据传入的用户密码之类的身份信息,验证用户是否满足登录条件,如果满足,就根据用户信息创建一个登录凭证,这个登录凭证简单来说就是一个对象,最简单的形式可以只包含用户id,凭证创建时间和过期时间三个值。

2)服务端把上一步创建好的登录凭证,先对它做数字签名,然后再用对称加密算法做加密处理,将签名、加密后的字串,写入cookie。cookie的名字必须固定(如ticket),因为后面再获取的时候,还得根据这个名字来获取cookie值。这一步添加数字签名的目的是防止登录凭证里的信息被篡改,因为一旦信息被篡改,那么下一步做签名验证的时候肯定会失败。做加密的目的,是防止cookie被别人截取的时候,无法轻易读到其中的用户信息。

3)用户登录后发起后续请求,服务端根据上一步存登录凭证的cookie名字,获取到相关的cookie值。然后先做解密处理,再做数字签名的认证,如果这两步都失败,说明这个登录凭证非法;如果这两步成功,接着就可以拿到原始存入的登录凭证了。然后用这个凭证的过期时间和当前时间做对比,判断凭证是否过期,如果过期,就需要用户再重新登录;如果未过期,则允许请求继续。

技术分享

这种方式最大的优点就是实现了服务端的无状态化,彻底移除了服务端对会话的管理的逻辑,服务端只需要负责创建和验证登录cookie即可,无需保持用户的状态信息。对于第一种方式的第二个问题,用户会话信息共享的问题,它也能很好解决:因为如果只是同一个应用做集群部署,由于验证登录凭证的代码都是一样的,所以不管是哪个服务器处理用户请求,总能拿到cookie中的登录凭证来进行验证;如果是不同的应用,只要每个应用都包含相同的登录逻辑,那么他们也是能轻易实现会话共享的,不过这种情况下,登录逻辑里面数字签名以及加密解密要用到的密钥文件或者密钥串,需要在不同的应用里面共享,总而言之,就是需要算法完全保持一致。

这种方式由于把登录凭证直接存放客户端,并且需要cookie传来传去,所以它的缺点也比较明显:

1)cookie有大小限制,存储不了太多数据,所以要是登录凭证存的消息过多,导致加密签名后的串太长,就会引发别的问题,比如其它业务场景需要cookie的时候,就有可能没那么多空间可用了;所以用的时候得谨慎,得观察实际的登录cookie的大小;比如太长,就要考虑是非是数字签名的算法太严格,导致签名后的串太长,那就适当调整签名逻辑;比如如果一开始用4096位的RSA算法做数字签名,可以考虑换成1024、2048位;

2)每次传送cookie,增加了请求的数量,对访问性能也有影响;

3)也有跨域问题,毕竟还是要用cookie。

相比起第一种方式,cookie-based方案明显还是要好一些,目前好多web开发平台或框架都默认使用这种方式来做会话管理,比如php里面yii框架,这是我们团队后端目前用的,它用的就是这个方案,以上提到的那些登录逻辑,框架也都已经封装好了,实际用起来也很简单;asp.net里面forms身份认证,也是这个思路,这里有一篇好文章把它的实现细节都说的很清楚:

http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html

前面两种会话管理方式因为都用到cookie,不适合用在native app里面:native app不好管理cookie,毕竟它不是浏览器。这两种方案都不适合用来做纯api服务的登录认证。要实现api服务的登录认证,就要考虑下面要介绍的第三种会话管理方式。

接下来是Cookie使用的一些代码:

html文件:

技术分享
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>用户登录</title></head><body><form action="login.do" method="POST">  <span>用户名:</span><input type="text" name="uname"/><br>  <span>密码:</span><input type="password" name="upass"/><br>  <input type="submit" value="提交" /></form><p>为了后面测试成功,请在用户名中输入jame,在密码中输入123456abc</body></html>
login.html
技术分享
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>测试类</title></head><body><form action="home.do">  <p>点击测试</p>  <input type="submit" value="点击这里"/></form></body></html>
home.html

servlet文件:

技术分享
package com.stateManage.cn;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/** * Servlet implementation class UserServlet */@WebServlet("/login.do")public class UserLogin extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        //设置响应的编码格式        response.setContentType("text/html;charset=utf-8");                //解决请求乱码        //获得表单中提交的数据        String uname=new String(request.getParameter("uname").getBytes("iso-8859-1"),"utf-8");        String upass=new String(request.getParameter("upass").getBytes("iso-8859-1"),"utf-8");                //创建Cookie对象        Cookie cookieUname=new Cookie("uname",uname);        Cookie cookieUpass=new Cookie("upass",upass);                //设置cookie存活的最长时间        /*         *  如果为正数,表示在设置完成以后cookie存活的时间         *  如果为0,表示立即删除cookie,如果需要删除Cookie,那么可以把cookie的存活时间重置为0         *  如果为负数,表示在浏览器关闭以后删除cookie         */        cookieUname.setMaxAge(60*60*24*365);//设置一年以后        cookieUpass.setMaxAge(60*60*24*356);//设置一年以后                //设置cookie的存储位置        /*         * cookie存储的 相同路径 可以对cookie进行访问、修改、删除         * cookie存储的 子路径 可以对cookie进行访问,但是不可以修改、删除         *          * 一般情况下,都会将项目下的所有cookie路径设置为"/","/"表示相对于 服务器 的根路径         */        cookieUname.setPath("/");        cookieUpass.setPath("/");                /*        cookieUname.setMaxAge(expiry);//设置最大存活时间        cookieUname.setDomain(pattern);//设置有效域属性        cookieUname.setPath(uri);//设置cookie属性路径        cookieUname.setValue(newValue);//重写设置值        cookieUname.setVersion(v);//设置版本号        */                //将cookie添加到响应中        response.addCookie(cookieUname);        response.addCookie(cookieUpass);                //响应信息        response.getWriter().append("cookie添加成功");        response.getWriter().append("<a href=http://www.mamicode.com/‘home.html‘>点我跳转测试");    }    /**     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)     */    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        doGet(request, response);    }}
UserLogin.java
技术分享
package com.stateManage.cn;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/** * Servlet implementation class Home */@WebServlet("/home.do")public class Home extends HttpServlet {    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        //设置响应的编码格式        response.setContentType("text/html;charset=utf-8");        //获取Cookie        Cookie[] cookies=request.getCookies();                for(Cookie cookie:cookies){            if(cookie.getName().equals("upass") && cookie.getValue().equals("123456abc")){//比较Cookie的名称和Cookie的值                response.getWriter().append("我没有忘记你<br>");                response.getWriter().append("名称:"+cookie.getName() +"<br>");                response.getWriter().append("值:"+cookie.getValue() +"<br>");                response.getWriter().append("最大存活时间:"+cookie.getMaxAge() +"<br>");                response.getWriter().append("有效域:"+cookie.getDomain() +"<br>");                response.getWriter().append("版本:"+cookie.getVersion()+"<br><br>");            }            if(cookie.getName().equals("uname") && cookie.getValue().equals("jame")){//比较Cookie的名称和Cookie的值                response.getWriter().append("我没有忘记你<br>");                response.getWriter().append("名称:"+cookie.getName() +"<br>");                response.getWriter().append("值:"+cookie.getValue() +"<br>");                response.getWriter().append("最大存活时间:"+cookie.getMaxAge() +"<br>");                response.getWriter().append("有效域:"+cookie.getDomain() +"<br>");                response.getWriter().append("版本:"+cookie.getVersion()+"<br><br>");            }        }                response.getWriter().append("输出完毕");    }    /**     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)     */    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // TODO Auto-generated method stub        doGet(request, response);    }}
Home.java

3. token-based的管理方式

这种方式从流程和实现上来说,跟cookie-based的方式没有太多区别,只不过cookie-based里面写到cookie里面的ticket在这种方式下称为token,这个token在返回给客户端之后,后续请求都必须通过url参数或者是http header的形式,主动带上token,这样服务端接收到请求之后就能直接从http header或者url里面取到token进行验证:

技术分享

这种方式不通过cookie进行token的传递,而是每次请求的时候,主动把token加到http header里面或者url后面,所以即使在native app里面也能使用它来调用我们通过web发布的api接口。app里面还要做两件事情:

1)有效存储token,得保证每次调接口的时候都能从同一个位置拿到同一个token;

2)每次调接口的的代码里都得把token加到header或者接口地址里面。

看起来麻烦,其实也不麻烦,这两件事情,对于app来说,很容易做到,只要对接口调用的模块稍加封装即可。

这种方式同样适用于网页应用,token可以存于localStorage或者sessionStorage里面,然后每发ajax请求的时候,都把token拿出来放到ajax请求的header里即可。不过如果是非接口的请求,比如直接通过点击链接请求一个页面这种,是无法自动带上token的。所以这种方式也仅限于走纯接口的web应用。

这种方式用在web应用里也有跨域的问题,比如应用如果部署在a.com,api服务部署在b.com,从a.com里面发出ajax请求到b.com,默认情况下是会报跨域错误的,这种问题可以用CORS(跨域资源共享)的方式来快速解决,相关细节可去阅读前面给出的CORS文章详细了解。

这种方式跟cookie-based的方式同样都还有的一个问题就是ticket或者token刷新的问题。有的产品里面,你肯定不希望用户登录后,操作了半个小时,结果ticket或者token到了过期时间,然后用户又得去重新登录的情况出现。这个时候就得考虑ticket或token的自动刷新的问题,简单来说,可以在验证ticket或token有效之后,自动把ticket或token的失效时间延长,然后把它再返回给客户端;客户端如果检测到服务器有返回新的ticket或token,就替换原来的ticket或token。

4. 安全问题

在web应用里面,会话管理的安全性始终是最重要的安全问题,这个对用户的影响极大。

首先从会话管理凭证来说,第一种方式的会话凭证仅仅是一个session id,所以只要这个session id足够随机,而不是一个自增的数字id值,那么其它人就不可能轻易地冒充别人的session id进行操作;第二种方式的凭证(ticket)以及第三种方式的凭证(token)都是一个在服务端做了数字签名,和加密处理的串,所以只要密钥不泄露,别人也无法轻易地拿到这个串中的有效信息并对它进行篡改。总之,这三种会话管理方式的凭证本身是比较安全的。

然后从客户端和服务端的http过程来说,当别人截获到客户端请求中的会话凭证,就能拿这个凭证冒充原用户,做一些非法操作,而服务器也认不出来。这种安全问题,可以简单采用https来解决,虽然可能还有http劫持这种更高程度的威胁存在,但是我们从代码能做的防范,确实也就是这个层次了。

最后的安全问题就是CSRF(跨站请求伪造)。这个跟代码有很大关系,本质上它就是代码的漏洞,只不过一般情况下这些漏洞,作为开发人员都不容易发现,只有那些一门心思想搞些事情的人才会专门去找这些漏洞,所以这种问题的防范更多地还是依赖于开发人员对这种攻击方式的了解,包括常见的攻击形式和应对方法。不管凭证信息本身多么安全,别人利用CSRF,就能拿到别人的凭证,然后用它冒充别人进行非法操作,所以有时间还真得多去了解下它的相关资料才行。举例来说,假如我们把凭证直接放到url后面进行传递,就有可能成为一个CSRF的漏洞:当恶意用户在我们的应用内上传了1张引用了他自己网站的图片,当正常的用户登录之后访问的页面里面包含这个图片的时候,由于这个图片加载的时候会向恶意网站发送get请求;当恶意网站收到请求的时候,就会从这个请求的Reffer header里面看到包含这个图片的页面地址,而这个地址正好包含了正常用户的会话凭证;于是恶意用户就拿到了正常用户的凭证;只要这个凭证还没失效,他就能用它冒充用户进行非法操作。

【Servlet】3中web重定向的方式