一、Cookie机制
背景:
在程序中,会话跟踪是很重要的事情。理论上,一个用户的所有请求操作都应该属于同一个会话,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆。例如,用户A在超市购买的任何商品都应该放在A的购物车内,不论是用户A什么时间购买的,这都是属于同一个会话的,不能放入用户B或用户C的购物车内,这不属于同一个会话。
而Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。即用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。要跟踪该会话,必须引入一种机制。
Cookie就是这样的一种机制。它可以弥补HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。
Cookie的工作原理:
由于HTTP协议是一种无状态的协议,为了让服务端能够辨认客户端的身份,在客户端第一次请求服务器的时候,服务器返回一个Cookie(通行证),客户端保存Cookie,之后再次访问服务器的时候在请求头部带上Cookie(Cookie字段),服务器检查该Cookie,以此来辨认客户端身份,服务器还可以根据需要来修改Cookie的内容。这就是Cookie的工作原理。
查看网站颁发的Cookie可以通过javascript:alert (document. cookie)。
注意:
1、Cookie功能需要浏览器的支持,如果浏览器不支持Cookie(如大部分手机中的浏览器)或者把Cookie禁用了),Cookie功能就会失效。
2、不同的浏览器采用不同的方式保存Cookie。
3、不同浏览器保存Cookie的路径也不一样。
4、Cookie具有不可跨域名性。根据Cookie规范,浏览器访问Google只会携带Google的Cookie,而不会携带Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。
5、由于cookie信息以明文方式保存在文本文件中,对一些敏感信息如口令、银行帐号如果要保存在本地cookie文件中,最好采用加密形式。
6、cookie其实是服务器返回一些东西(例如token)保存在客户端,在客户端把这些东西叫做cookie,之后每次请求的时候都会在请求头部里的cookie字段里带上,以此来进行身份的验证。
7、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。(Session对象没有对存储的数据量的限制,其中可以保存更为复杂的数据类型)
8、客户端一般是读取cookie里的内容,修改cookie是在服务端进行操作的。
Cookie属性:
属性 | 数据类型 | 描述 |
---|---|---|
name | String | 该Cookie的名称,Cookie一旦创建,名称便不可更改。 |
value | Object | 该Cookie的值,如果值为Unicode字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64编码。 |
maxAge | Int | 该Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒之后失效。如果为负数,该Cookie为临时Cookie,仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该Cookie即失效。如果为0,表示删除该Cookie。默认为–1。 |
secure | Boolean | 该Cookie是否仅被使用安全协议传输,默认为false。 |
path | String | 该Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下的contextPath都可以访问该Cookie。注意最后一个字符必须为“/” 。 |
domain | String | 可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.” 。 |
comment | String | 该Cookie的用处说明,浏览器显示Cookie信息的时候显示该说明。 |
version | Int | 该Cookie使用的版本号,0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范。 |
注意:每个属性对应一个getter方法与一个setter方法。
服务端设置Cookie的有效期:
Cookie的maxAge决定着Cookie的有效期,单位为秒(Second)。Cookie中通过getMaxAge()方法与setMaxAge()方法来读写maxAge属性。
如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。浏览器会将maxAge为正数的Cookie持久化,即写到对应的Cookie文件中。无论客户关闭了浏览器还是电脑,只要还在maxAge秒之前,登录网站时该Cookie仍然有效。下面代码中的Cookie信息将永远有效。
如果maxAge为负数,则表示该Cookie仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该Cookie即失效。不会被写到Cookie文件中,Cookie默认的maxAge值为–1。
Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建Cookie
cookie.setMaxAge(Integer.MAX_VALUE); // 设置生命周期为MAX_VALUE
response.addCookie(cookie); // 输出到客户端
如果maxAge为0,则表示删除该Cookie。Cookie机制没有提供删除Cookie的方法,因此通过设置该Cookie即时失效实现删除Cookie的效果。失效的Cookie会被浏览器从Cookie文件或者内存中删除。
Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建Cookie
cookie.setMaxAge(0); // 设置生命周期为0,不能为负数
response.addCookie(cookie); // 必须执行这一句
注意:
1、要想修改Cookie只能使用一个同名的Cookie来覆盖原来的Cookie,达到修改的目的。删除Cookie时只需要把maxAge属性修改为0即可。
2、从客户端读取Cookie时,包括maxAge在内的其他属性都是不可写的,也不会被提交。浏览器提交Cookie时只会提交name与value属性。maxAge属性只被浏览器用来判断Cookie是否过期。
思考:
如果cookie过期失效了,此时会怎样,要如何进行身份的验证?
答案:cookie失效之后,请求头中不会带上cookie,页面跳转到登陆页面,让用户重新登陆,服务端返回新的cookie。
服务端设置Cookie的其他属性:
Cookie cookie = new Cookie("time","20080808"); // 新建Cookie
cookie.setDomain(".helloweenvsfei.com"); // 设置域名
cookie.setPath("/"); // 设置路径
cookie.setMaxAge(Integer.MAX_VALUE); // 设置有效期
cookie.setSecure(true); // 设置安全属性,浏览器只会在HTTPS和SSL等安全协议中传输此类Cookie
response.addCookie(cookie); // 输出到客户端
注意:secure属性并不能对Cookie内容加密,因而不能保证绝对的安全性。如果需要高安全性,需要在程序中对Cookie内容加密、解密,以防泄密。
二、Session机制
Session工作原理:
Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
为了获得更高的存取速度,服务器一般把Session放在内存里。每个用户都会有一个独立的Session。如果Session内容过于复杂,当大量客户访问服务器时可能会导致内存溢出。因此,Session里的信息应该尽量精简。
注意:
客户端没办法修改服务端的Session,只有服务端才可以修改Session!
服务端实现用户登陆:
Session对应的类为javax.servlet.http.HttpSession类。每个来访者对应一个Session对象,所有该客户的状态信息都保存在这个Session对象里。Session对象是在客户端第一次请求服务器的时候创建的。Session也是一个key-value属性对的对象,通过getAttribute(Stringkey)和setAttribute(Stringkey,Objectvalue)方法读写客户状态信息。Servlet里通过request.getSession()方法获取该客户的Session。
HttpSession session = request.getSession(true); // 获取Session对象,如果不存在Session则会创建并返回
session.setAttribute("loginTime", new Date()); // 设置Session中的属性
out.println("登录时间为:" +(Date)session.getAttribute("loginTime")); // 获取Session属性
注意:request除了getSession(boolean)方法外,还可以使用getSession()方法来获取Session。区别是如果该客户的Session不存在,request.getSession()方法会返回null,而getSession(true)会先创建Session再将Session返回。
Session的生命周期
Session在用户第一次访问服务器的时候自动创建。需要注意只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session。如果尚未生成Session,也可以使用request.getSession(true)强制生成Session。
Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。用户每访问服务器一次,无论是否读写Session,服务器都认为该用户的Session“活跃(active)”了一次。
Session的有效期
由于会有越来越多的用户访问服务器,因此Session也会越来越多。为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。
Session的超时时间为maxInactiveInterval属性,可以通过对应的getMaxInactiveInterval()方法获取,通过setMaxInactiveInterval(longinterval)修改。
Session的超时时间也可以在web.xml中修改。另外,通过调用Session的invalidate()方法可以使Session失效。
Session的常用方法
方法 | 描述 |
---|---|
setAttribute(String attribute, Object value) | 设置Session属性。value参数可以为任何Java Object,通常为Java Bean,value信息不宜过大。 |
getAttribute(String attribute) | 返回Session属性。 |
getAttributeNames() | 返回Session中存在的所有属性名。 |
removeAttribute(String attribute) | 删除Session属性。 |
getId() | 返回Session的ID。该ID由服务器自动创建,不会重复。 |
getCreationTime() | 返回Session的创建日期。返回类型为long,常被转化为Date类型,例如:DateCreateTime = new Date(session.get CreationTime()) |
getLastAccessedTime() | 返回Session的最后活跃时间。返回类型为long。 |
getMaxInactiveInterval() | 返回Session的超时时间,单位为秒。超过该时间没有访问,服务器认为该Session失效。 |
setMaxInactiveInterval(int second) | 设置Session的超时时间,单位为秒。 |
isNew() | 返回该Session是否是新创建的,返回Boolean类型。 |
invalidate() | 使该Session失效。 |
注意:
1、Tomcat中Session的默认超时时间为20分钟。通过setMaxInactiveInterval(int seconds)修改超时时间。
2、客户端没办法修改服务端的Session,只有服务端才可以修改Session!
Session对浏览器的要求
虽然Session保存在服务器,对客户端是透明的,它的正常运行仍然需要客户端浏览器的支持,这是因为Session需要使用Cookie作为识别标志。HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为JSESSIONID的Cookie,它的值为该Session的id(也就是HttpSession.getId()的返回值)。Session依据该Cookie来识别是否为同一用户。
该Cookie为服务器自动生成的,如果它的maxAge属性为–1(默认属性),表示仅在当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。这种情况下,同一机器的两个浏览器窗口访问服务器时,会生成两个不同的Session。但如果是由浏览器窗口内的链接、脚本等打开的新窗口(也就是说不是双击桌面浏览器图标等打开的窗口),这类子窗口会共享父窗口的Cookie,因此会共享同一个Session。
如果客户端浏览器将Cookie功能禁用,或者不支持Cookie,例如,绝大多数的手机浏览器都不支持Cookie。Java Web提供了另一种解决方案:URL地址重写。
URL地址重写
URL地址重写是对客户端不支持Cookie的解决方案。URL地址重写的原理是将该用户Session的id信息重写到URL地址中。服务器能够解析重写后的URL获取Session的id。这样即使客户端不支持Cookie,也可以使用Session来记录用户状态。HttpServletResponse类提供了encodeURL(stringUrl)实现URL地址重写。
<td>
<a href="<%=response.encodeURL("index.jsp?c=1&wd=Java") %>">
Homepage</a>
</td>
该方法会自动判断客户端是否支持Cookie。如果客户端支持Cookie,会将URL原封不动地输出来;如果客户端不支持Cookie,则会将用户Session的id重写到URL中。重写后的输出可能是这样的:
<td>
<ahref="index.jsp;jsessionid=0CCD096E7F8D97B0BE608AFDC3E1931E?c=
1&wd=Java">Homepage</a>
</td>
即在文件名的后面,在URL参数的前面添加了字符串“;jsessionid=XXX”。其中XXX为Session的id。分析一下可以知道,增添的jsessionid字符串既不会影响请求的文件名,也不会影响提交的地址栏参数。用户单击这个链接的时候会把Session的id通过URL提交到服务器上,服务器通过解析URL地址获得Session的id。
如果是页面重定向(Redirection),URL地址重写可以这样写:
<%
if(“administrator”.equals(userName))
{
response.sendRedirect(response.encodeRedirectURL(“administrator.jsp”));
return;
}
%>
实现效果跟response.encodeURL(String url)是一样的:如果客户端支持Cookie,生成原URL地址,如果不支持Cookie,传回重写后的带有jsessionid字符串的地址。对于WAP程序,由于大部分的手机浏览器都不支持Cookie,WAP程序都会采用URL地址重写来跟踪用户会话。
三、Token机制
背景:
由于会话标识(session id)是一个随机的字符串,每个人的session id都不一样,对于客户端来说只需要保存自己的session id,而对于服务器来说,要保存所有人的session id,这对于服务器来说是一个巨大的消耗。为了不保存这些session id,并且同时验证客户端所发送的session id是由服务器生成的,我们需要进行验证。因此,令牌(Token)便应运而生了。我们可以为session id制作一个签名,例如使用HMAC-SHA256算法,加上一个只有服务器才知道的密钥,为该session id制作一个签名,然后把这个签名与session id一起作为token返回给客户端,客户端将token保存在cookie中。由于密钥是别人不知道的,因此就无法伪装token了。
之后客户端访问服务器的时候,在请求头部的Authorization字段中或Cookie字段中带上本地的token(具体放在哪里需要前端与后端统一协商确定)。服务器拿到该token之后,运用同样的HMAC-SHA256算法和自己的密钥,对session id再计算一次签名,最后和token里的签名进行比较,如果相同,则通过认证,如果不相同,则没有通过认证。
这样一来,服务器便不需要保存session id了,只是生成token,然后验证token。使用Token的目的是为了减轻服务器的压力,并且减少频繁的查询数据库,使服务器更加健壮。但Token中的数据是明文保存的,因此不能保存像密码这样的敏感信息。当然,如果一个人的token被偷走,该小偷也会被认为是合法用户。
Token的验证原理:
基于Token的身份验证过程:
1、用户通过用户名和密码发送请求;
2、服务器验证;
3、服务器返回一个token给客户端;
4、客户端储存token,并且每次用于每次的发送请求。
5、服务端验证token(例如服务端可以采用filter过滤器进行校验)并返回数据。
注意:
1、每一次请求都需要token。token放在HTTP请求头部发送保证了HTTP请求的无状态性。我们还可以通过设置服务器属性 Access-Control-Allow-Origin:* ;( 通过服务器端返回带有Access-Control-Allow-Origin标识的Response header,用来解决资源的跨域权限问题 ) 让服务器能接受来自所有域的请求。
2、扩展:
Filter也称之为过滤器,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp, Servlet, 静态图片文件或静态 html 文件等进行拦截,从而实现一些特殊的功能。例如实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。过滤器处于浏览器与servlet之间,客户端发送的请求、服务器发送的资源,需要通过过滤器,才可以继续流转。
Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。通过Filter技术,开发人员可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截。
3、Filter是如何实现拦截的:
Filter接口中有一个doFilter()方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service()方法之前,都会先调用一下filter的doFilter()方法,因此,在该方法内编写代码可达到如下目的:
调用目标资源之前,让一段代码执行。
是否调用目标资源(即是否让用户访问web资源)。
调用目标资源之后,让一段代码执行。
web服务器在调用doFilter()方法时,会传递一个filterChain 对象进来,filterChain 对象是filter 接口中最重要的一个对象,它也提供了一个doFilter()方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service()方法,即web资源就会被访问,否则web资源不会被访问。
Token的两种使用方式:
Token的优势:
一、无状态、可扩展
在客户端存储的Tokens是无状态的,并且能够被扩展。
无状态体现为:token放在HTTP请求头部发送保证了HTTP请求的无状态性。
可扩展体现为:服务器不存储session信息,只需要验证token。
基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。
如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性)。用户量大时,可能会造成一些拥堵。使用token可以优化这个问题,因为token本身就包含了用户的验证信息。
二、安全性
token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocation可以使一个特定的token或是一组有相同认证的token无效。
请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。
Token安全性的体现:
1、防止表单重复提交
2、anti csrf攻击(跨站点请求伪造)
1、防止表单重复提交:服务器端第一次验证通过后,会将session中的Token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的Token没变,但服务器端session中Token已经改变了。(注意:更新的token会发给客户端,用于下次登陆验证,但不会用于第二次提交的表单中)
2、预防CRSF攻击:服务器端对Token值进行验证,判断是否和session中的Token值相等,若相等,则可以证明请求有效,不是伪造的。
CSRF攻击思想:
总结:实际上CSRF攻击就是利用用户的Cookie来获取其他服务器的权限。CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!
Anti-CRSF扩展:
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
服务端进行CSRF防御:
服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。
(1).Cookie Hashing(所有表单都包含同一个伪随机值):
这可能是最简单的解决方案了,因为攻击者不能获得第三方的Cookie(理论上),所以虚假表单中的数据也就无法通过验证了。
<?php
//构造加密的Cookie信息
$value = “DefenseSCRF”;
setcookie(”cookie”, $value, time()+3600);
?>
在表单里增加Hash值,以认证这确实是用户发送的请求。
<?php
$hash = md5($_COOKIE['cookie']);
?>
<form method=”POST” action=”transfer.php”>
<input type=”text” name=”toBankId”>
<input type=”text” name=”money”>
<input type=”hidden” name=”hash” value=”<?=$hash;?>”>
<input type=”submit” name=”submit” value=”Submit”>
</form>
然后在服务器端进行Hash值验证。
<?php
if(isset($_POST['check'])) {
$hash = md5($_COOKIE['cookie']);
if($_POST['check'] == $hash) {
doJob();
} else {
//...
}
?>
这个方法个人觉得已经可以杜绝99%的CSRF攻击了,那还有1%呢?由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,这就是另外的1%。如果需要100%的杜绝,这个不是最好的方法。
(2).验证码
这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,这个方案可以完全解决CSRF,但个人觉得在易用性方面似乎不是太好,还有听闻是验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。
(3).One-Time Tokens(不同的表单包含一个不同的伪随机值)
在实现One-Time Tokens时,需要注意一点:就是“并行会话的兼容”。如果用户在一个站点上同时打开了两个不同的表单,CSRF保护措施不应该影响到他对任何表单的提交。考虑一下如果每次表单都生成一个不同的伪随机值来覆盖以前的伪随机值,用户只能成功地提交他最后打开的表单,因为所有其他的表单都含有非法的伪随机值。
实现方式:
先是令牌生成函数( gen_token() ):
<?php
function gen_token() {
$token = md5(uniqid(rand(), true));
//实际上单使用Rand()得出的随机数作为令牌,也是不安全的。
return $token;
}
?>
然后是Session令牌生成函数( gen_stoken() ):
<?php
function gen_stoken() {
$pToken = "";
if($_SESSION[STOKEN_NAME] == $pToken){
//没有值,赋新值
$_SESSION[STOKEN_NAME] = gen_token();
} else{
//继续使用旧的值
}
}
?>
WEB表单生成隐藏输入域的函数:
<?php
function gen_input() {
gen_stoken();
echo “<input type=\”hidden\” name=\”" . FTOKEN_NAME . “\”
value=\”" . $_SESSION[STOKEN_NAME] . “\”> “;
}
?>
WEB表单结构:
<?php
session_start();
include(”functions.php”);
?>
<FORM method=”POST” action=”transfer.php”>
<input type=”text” name=”toBankId”>
<input type=”text” name=”money”>
<? gen_input(); ?>
<input type=”submit” name=”submit” value=”Submit”>
</FORM>
服务端核对令牌:
这个很简单,这里就不再啰嗦了。
上面这个其实不完全符合“并行会话的兼容”的规则,大家可以在此基础上修改。
三、可扩展性
Tokens能够创建与其它程序共享权限的程序。使用tokens时,可以提供可选的权限给第三方应用程序。当用户想让第三方程序访问主程序的数据时,我们可以通过在主程序上建立自己的对外API,将AK和SK提供给第三方程序(涉及公有云API的认证方式:AK/SK认证),得出特殊权限的tokens。第三方程序访问主程序的数据时,带上特殊权限的tokens,验证通过后可以拿到数据。
四、多平台跨域
CORS(跨域资源共享):对应用程序和服务进行扩展的时候,需要介入各种各种的设备和应用程序。当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。
可以通过设置服务器属性 Access-Control-Allow-Origin:* 。( 通过服务器端返回带有Access-Control-Allow-Origin标识的Response header,用来解决资源的跨域权限问题 )