应用的状态

在天码营上发表经验文章时,除了标题和内容,天码营网站的实现还需要知道是谁创建的这篇文章。如何知道是谁在创建文章呢? 我们不可能在在表单中添加一个输入框让用户输入自己是谁——因为用户的身份很有可能被伪造。

一个办法是在每一个页面被访问之前,我们要求用户输入用户名/密码进行验证,只有验证通过才可以访问对应的页面。HTTP基本认证正好可以完成这样的功能。可是这样实在是太麻烦了,每访问一个页面都需要用户进行输入对于用户体验来说是不可接受的。

HTTP协议本身是无状态的,也就是说多个HTTP请求之间是没有任何关系的,处理第二个请求时并不知道曾经处理过第一个请求。但是实际的应用中却往往不是这个情况,比如第一个是登陆请求,第二个是创建博客请求,在处理完登陆请求的前提下,才能处理创建博客请求,因为没有登陆的情况下是不允许创建博客的。这就需要我们在HTTP请求中自己来定义状态了,回到这个例子中,就是通过某种机制记住用户已经登陆的状态。

Cookie

Cookie正是实现保存HTTP请求状态的一种手段,它是由客户端保存的小型文本文件,其内容为一系列的键值对(即Key-Value形式),在浏览器访问同一个域名的不同页面时,会在HTTP请求中附上Cookie。

Cookie可以保存在内存中(关闭浏览器即消失),也可以保存在硬盘中(到达过期时间后消失)。Cookie存放的时候是以明文方式存放的,因此安全性比较低,实际项目中一般会通过加密后保存。另外,Cookie也是一个比较古老的东西,与JavaScript同样由网景公司发明,现已被标准化为RFC2109。

Cookie是由服务器端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的Key/Value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器。

比如每当用户访问一个网站时,服务器程序会分配给它一个唯一的id,这个id就是设置在Cookie中的。对于JSP和Servlet应用,这个id是命名为JSESSIONID的键。下一次用户继续访问这个网站的页面时,就会带上Cookie中的JSESSIONID,这样服务器可以知道该用户是否是合法用户以及是否需要重新登录等。服务器可以设置或读取Cookies中包含信息,从而维护用户跟服务器会话中的状态。

Cookie在生成时就会被指定一个Expire值,这就是Cookie的生存周期,在这个周期内Cookie有效,超出周期Cookie就会被清除。有些页面将Cookie的生存周期设置为“0”或负值,这样在关闭浏览器时,就马上清除Cookie,不会记录用户信息,更加安全。

以打开浏览器访问[http://tianmaying.com](http://tianmaying.com)为例,打开Chrome或者Firefox的开发者工具,我们可以看到发送给天码营的Web请求中附带了很多Cookie的信息,正是这些信息可以帮助天码营的服务器端来维护每一个用户的状态。

alter-text

那么在服务器端如何来操作Cookie呢? 这就需要用到Servlet提供的Cookie API了。

操作Cookie

Cookie在Servlet规范中对应的类是javax.servlet.http.Cookie,它的构造函数的签名为Cookie(java.lang.String name, java.lang.String value)

比如我们创一个键为foo,值为bar的Cookie,则可以通过new Cookie("foo", "bar");来完成。

增加Cookie和获取Cookie是使用Cookie的两个最常见的场景,可以通过HttpServletResponse在HTTP返回中增加Cookie,通过HttpServletRequest获取一个HTTP请求的Cookie:

  • 把cookie发送给客户端调用方法HttpServletResponse.addCookie(javax.servlet.http.Cookie)

  • 取得客户浏览器的Cookie:HttpServletRequest.getCookies(),注意这里这个方法返回的是Cookie数组。

此外,下面几个方法是javax.servlet.http.Cookie类最常用的方法,进行Cookie操作一般都会用到:

  • getMaxAge/setMaxAge:获取/设置Cookie过期之前的时间,以秒计。如果不设置该值,则Cookie只在当前会话内有效,即在用户关闭浏览器之前有效,而且这些Cookie不会保存到磁盘上。若生存时间为负值,代表浏览器关闭Cookie即消失。生存时间为0,代表删除Cookie,生存时间为正数,代表Cookie存在多少秒。

  • getName/setName:获取/设置Cookie的名字,由于HttpServletRequest的getCookies方法返回的是一个Cookie对象的数组,因此通常要用循环来访问这个数组查找特定名字,然后用getValue检查它的值。  

  • getValue/setValue:获取/设置Cookie的值。如前所述,名字和值实际上是我们始终关心的两个方面。不过也有一些例外情况,比如把名字作为逻辑标记(也就是说,如果名字存在,则表示true)。

完整的CookieAPI接口可以查看Tomcat的Cookie API 文档

Remember Me的实现思路

了解了Cookie以及Servlet规范中Cookie的API,接下来我们基于Cookie来实现登陆中的Remember Me功能:即如果用户在登陆表单中勾选了Remember Me选项,则下一次方法用户登陆页面就直接跳转到博客首页,不需要再进行输入和点击的操作。

这里我们先来通过MVC来重新组织一下流程:

  • 创建一个LoginServlet类,处理/login的URL。如果是GET请求,则显示登陆表单页面login.jsp;如果是POST请求,则对登陆表单发出的POST请求进行处理,登陆成功则进入homepage.jsp页面。

  • login.jsp页面中表单的action属性设置为/login,method设置为POST,这样LoginServlet就能处理登陆的POST请求。

基于这个流程,要实现Remember Me功能:

  1. 首先应该在LoginServletdoPost方法中判断用户是否勾选了Remember Me选项,如果勾选,则在返回中设置一个Cookie值来标识用户;
  2. 然后在LoginServletdoGet方法中(即用户在浏览器中访问/loginURL时),看用户请求中是否存在标识用户的Cookie,如果存在,则直接跳转到homepage.jsp页面,这样就跳过了登陆流程

LoginServlet的实现

doGet方法的实现

Cookie[] cookies = request.getCookies(); 

for(int i = 0; i < cookies.length; i++) { 
    Cookie c = cookies[i];
    if (c.getName().equals("user")) {
        String user = c.getValue();
        // 说明存在cookie,应该跳过登陆过程,显示homepage.jsp
        return;
    }
}

// 执行到这个地方说明cookie中没有用户信息,即首次访问或者登陆时没有勾选Remember Me,此时应该显示login.jsp页面

这里我们获取Cookie、遍历Cookie、获取Cookie的名称和获取Cookie的值使用了上一小节介绍的四个API,注释处的代码你来补齐吧。

doPost方法的实现

Remember Me相关的处理代码如下:

String[] values = request.getParameterValues("remember-me");

if (values != null && !values[0].isEmpty()) {// 这里表示用户勾选了Remember Me

    Cookie c = new Cookie("user", username);
    c.setMaxAge(7 * 24 * 60 * 60); 
    response.addCookie(c); 
}

上面的代码中,创建了Cookie,设置了Cookie的过期时间,并且最后添加到response中。

JSP页面

login.jsp页面只需在原来的HTML页面上做简单的修改即可:

  <form class="form-signin" action="/login" method="POST">
    <h2 class="form-signin-heading">用户登录</h2>
    <input type="text" id="username" name="username" class="form-control" placeholder="电子邮件或用户名" required="" autofocus="">
    <input type="password" id="password" name="password" class="form-control" placeholder="密码" required="">
    <div class="checkbox">
      <label>
        <input type="checkbox" name="remember-me" value="remember-me"> 记住我
      </label>
    </div>
    <button class="btn btn-primary btn-block" type="submit">确定</button>
  </form>

登录发表评论 注册

QINYICHAO__

学习了

反馈意见