对于cookie和session,作为程序员的我们都听说过或者接触过,比如购物车就会用到,算是老生常谈了。不过不细谈还真不好把他们俩搞清楚。标题把cookie放在前面,是因为它出生的早,没有cookie,就没有session。但是有了session后,大家都不喜欢用cookie了。为啥呢?接下来会聊到。
首先我们需要搞清楚什么是cookie,什么是session,他们有什么共性和特性。cookie和session都是客户端和服务端之间的沟通,而且都是服务端为了克服http协议的无状态特性而利用了客户端。当服务端需要区分不同的客户端或者想往客户端里寄存点东西时,在响应的消息头Set-Cookie里设置相关信息(注意这里是响应消息头),通过一个字符串来存放一些键值对。客户端收到后根据域名、路径地址等信息分门别类存起来,这个存起来的本地文件就是cookie。下一次客户端再次请求服务端时,会把cookie通过消息头Cookie(注意这里是请求消息头)带过去,服务端取到cookie就能识别客户端了,也能取到之前寄存在客户端的货物了。
session的原理跟cookie是一样的,也是寄存货物在客户端,唯一不同的是寄存的是一个叫JSESSIONID的字符串,而真正的货物实际上还是在服务端的。拿登陆举例,登陆校验通过后通过响应的Set-Cookie消息头把JSESSIONID和一个服务端随机生成的字符串带给客户端寄存(当然服务端自己也有存底的)。我们可以把这个随机串看做寄存的凭条,客户端是小储物柜,只存了这张小纸片。而服务端是大储物柜,存放着真正有价值的大货物——客户信息。当客户端再次请求时通过Cookie消息头把这张凭条带给服务端,服务端照着凭条到自己的客户明细表里一查,就能找到对应的包裹——客户信息对象,这时就能拿到用户名和密码了。
说完联系,再说它们的不同点。对服务端而言,它关心的不是寄存的凭条,而是真正的包裹——客户信息,也就是session的值。这些真实货物存放的地方不在客户端,而是服务端。即使cookie失效了,session不一定也失效。cookie的失效时间分临时性和持久性两种,默认是临时性的,也就是随着客户端的打开而生、关闭而死的。想要延长寿命,就得启用持久性方式,写入本地文件中。临时性方式中,JSESSIONID作为一个cookie,存在于浏览器的内存里,每次新打开浏览器窗口,就是一个cookie新生命的诞生,都会实例化一个新session出来。自然的,服务端寄存的老货也就没用了。所以需要注意,用不到的session要设置失效,好让服务端清理这些无主之物,不然可能会把服务端内存撑爆的。
回来开篇的问题,有了session,大家都不用cookie了,这是为啥?两个方面,一个是cookie在每次请求时都会传送,这会加大网络传输的压力;另一个是cookie是放在消息头的,而且本地cookie文件的大小不能超过4KB,这就限制了cookie能存放的信息量。而这两个问题session都不存在,因为人家是存在服务端的,消息头中只要放一个JSESSIONID字符串就够了。话说回来,session还是离不开cookie的帮忙,那么最致命的问题来了,cookie被禁用了怎么办?还好有后门——URL重写,把JSESSIONID作为参数拼接到url里面去传给客端户,再由客户端传回服务端。
说了半天,看个例子吧,还是拿登陆说事:
登陆的action:
/** * 执行登陆行为 * * @author wulinfeng * @param request * @param user * @return * @throws ServletException * @throws IOException */ @RequestMapping(value = "/loginAction.html", method = RequestMethod.POST) public @ResponseBody void loginAction(HttpServletRequest request, HttpServletResponse response, @RequestBody UserBean user) throws ServletException, IOException { response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); // 验证码校验 String validateCode = (String)request.getSession().getAttribute("randomString"); if (StringUtils.isEmpty(validateCode) || !validateCode.equals(user.getValidate().toUpperCase())) { out.print(PropertiesConfigUtil.getProperty("verify_code_error")); out.flush(); return; } // 用户名密码校验 String result = testPillingService.login(user.getUsername(), user.getPassword()); // 校验通过,创建token并放入session中;校验失败,返回错误描述 if ("success".equals(result)) { String tokenId = UUID.randomUUID().toString(); // 登陆成功后是使用cookie还是session来存放tokenId if (IS_COOKIE.equals("1")) { Cookie cookie = new Cookie("tokenId", tokenId); cookie.setMaxAge(3 * 24 * 60 * 60); // 3天过期 response.addCookie(cookie); } else { request.getSession(true).setAttribute("tokenId", tokenId); } if (user.getUsername().toUpperCase().equals("ADMIN")) { response.getWriter().print("register"); return; } } out.print(result); out.flush(); }
拦截器:
import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** * 登陆拦截 * * @author wulinfeng * @version C10 2017年10月11日 * @since SDP V300R003C10 */ public class InterceptorUtil implements HandlerInterceptor { /** 日志对象 */ private static Logger logger = LogManager.getLogger(InterceptorUtil.class.getName()); /** 是否启用cookie */ private static final String IS_COOKIE = PropertiesConfigUtil.getProperty("iscookie"); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.debug("InterceptorUtil.doFilter requesturl: " + request.getRequestURL()); String tokenId = null; if (IS_COOKIE.equals("1")) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie == null) { continue; } if (cookie.getName().equals("tokenId")) { tokenId = cookie.getValue(); break; } } } } else { if (request.getSession() != null) { tokenId = (String)request.getSession().getAttribute("tokenId"); } } if (StringUtils.isEmpty(tokenId)) { response.sendRedirect("/login.html"); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // TODO Auto-generated method stub } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // TODO Auto-generated method stub } }
这里登陆校验通过时我们通过cookie或session把tokenId放入回话,再次请求时通过拦截器判断tokenId是否已经在会话里,是则说明该用户已登陆过,否则是未登录用户,给出页面提示,并跳转到登陆页面。