利用Cookie跟踪用户状态
博客系统由多个功能(页面)组成:
- 首页——包含博客功能简介、用户列表
- 用户博客列表——包含某一用户的文章列表
- 文章详细页面——包含某一篇文章的标题、详细内容、创建时间等信息
- 创建文章页面——包含文章标题、内容的表单
在创建文章时,除了标题和内容,还需要知道是谁创建的这篇文章,当然我们不能够在表单中添加一个输入框让用户输入自己是谁——因为用户的身份很有可能被伪造。一个办法是在每一个页面被访问之前,我们要求用户输入在网站注册时输入的用户名/密码进行验证,如果验证通过才可以访问对应的页面。HTTP基本认证正好可以完成这样的功能,可是这样实在是太麻烦了,没访问一个页面都需要用户进行输入。
我们知道HTTP协议本身是无状态的,也就是说任何完全一致的请求都将会得到完全相同的返回。但是我们可以认为的在HTTP请求中定义状态。
Cookie是由客户端保存的小型文本文件,其内容为一系列的键值对,在浏览器访问同一个域名的不同页面时,会在HTTP请求中附上Cookie。
Cookie可以保存在内存中(关闭浏览器即消失),也可以保存在硬盘中(到达过期时间后消失)。另外,Cookie也是一个比较古老的东西,与JavaScript同样由网景公司发明,现已被标准化为RFC2109。
我们正是通过Cookie的这些特性来实现用户状态的保存的,每当用户访问一个网站时,服务器程序会分配给它一个唯一的id,这个id就是设置在Cookie中的,以打开浏览器第一次访问http://tianmaying.com
为例,因为是打开浏览器后第一次访问,所以请求头中不会有任何网站相关的Cookie存在,这时服务器会随机分配的一个字符串作为用户的id并放在Cookie中:
Set-Cookie:JSESSIONID=9074327855952DA4A296B67685523812; Path=/; HttpOnly
这里没有指定Cookie的过期时间(Expire),那么当浏览器关闭后,Cookie自动失效,从获取网站的Cookie到浏览器关闭的这个周期,被称为一个会话——Session。那么在这个会话中,在同一浏览器中任何一次对tianmaying.com
的访问,都会带上这个名字为JSESSIONID
(Servlet默认的名字)的Cookie值(在服务器上随机生成的字符串,服务器并会将其保存下来)。
可以简单地这样理解,服务端建立了一个Map<String, Object>
,对于每一个请求都可以拿到Cookie中保存的JSESSIONID
值,那么服务器程序可以往这个Map
里写入任何对象:
map.put(JSESSIONID, <obj>);
那么在这一次会话中,不管是怎样的请求,都可以访问到这个Map
中的对象。而服务器保存的这个Map
,也被称为Session)。Session是一种服务器端保存用户状态的技术,它最常见的实现方式是在Cookie中加入一个字段存储Session ID;但同时Cookie并不是实现Session的唯一手段,在URL中通过参数来保存Session ID同样也是可行的。
用户登录
在注册用户后,用户需要登录网站来确认自己的身份,登录的方式就是在HTML表单中填入用户名和密码并提交给服务器进行验证。在浏览器的一次Session会话中,Cookie中的JSESSIONID
保持不变,所以登录成功(服务器端通过验证)后,我们可以在服务端Session中放入当前的用户对象,这样以后访问任何URL,在对应的JSP/Servlet中都能轻易的拿到它。以下是一个多用户访问的Web应用示意图:
可以看到,每一个用户都有一个自己的Session。而在JSP/Servlet中,通过API可以很容易的获取到当前用户的Session并将状态信息放入其中。
在Servlet中设置Session属性值
以用户登录为例:
@WebServlet("/account/login")
public class LoginController extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = Data.getByUsername(username);
if (user == null || !user.getPassword().equals(password)) {
//登录不成功,返回错误信息
} else {
req.getSession().setAttribute("user", user);
//返回登录成功信息
}
}
}
获取Session的API是req.getSession()
,接下来通过getAttribute()
和setAttribute()
方法就可以像一个Map
一样获取/设置Session的属性值。这里我们将User
对象放入了Session,名字为user
。
在JSP中获取Session属性值
在渲染页面时,很多时候需要获取当前用户的信息,例如导航栏的显示:
- 根据用户是否登录显示不同的信息
- 如果用户已经登录,需要根据当前用户信息显示某些内容——例如“我的首页”的链接
在JSP Scriptlet中可以通过session
对象来获取Session的属性值,如果使用EL表达式,那么需要使用${sessionScope}
得到Session对象,下面以导航栏为例:
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<c:choose>
<c:when test="${sessionScope.user != null}">
<li><a href="userPosts?username=${sessionScope.user.username}">我的首页</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">管理
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">博客</li>
<li><a href="#">博客信息</a></li>
<li><a href="createPost">创建博文</a></li>
<li><a href="admin/posts">博客文章</a></li>
<li class="divider"></li>
<li class="dropdown-header">账号</li>
<li><a href="#">个人信息</a></li>
<li><a href="#">更改密码</a></li>
<li><a href="account/logout">退出</a></li>
</ul>
</li>
<li><a href="account/logout">退出登录</a></li>
</c:when>
<c:otherwise>
<li><a href="account/login">登录</a>
<li><a href="jsp/register.jsp">注册</a>
</c:otherwise>
</c:choose>
</ul>
</div>
${sessionScope.user != null}
作为判断条件表示当前用户已经登录;href="userPosts?username=${sessionScope.user.username}"
则是利用当前用户的属性值渲染链接。