目录
怎样向session中存取数据(session也是一个域对象)
cookie
会话,指用户登录网站后的一系列动作,比如浏览商品添加到购物车并购买。会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。
常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。
由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的
会话还是用户B的会话了。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端会把Cookie保存起来。
当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
Cookie是访问某些网站以后在本地存储的一些网站相关的信息,下次再访问的时候减少一些步骤。
Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器,是一种在客户端保持状态的方案。
cookie的实现
cookie的实现,是通过html里报文的首部来实现的
https://blog.csdn.net/xushiyu1996818/article/details/103352446
cookie的内容
cookie的内容主要包括:名字,值,过期时间,路径和域。路径与域一起构成cookie的作用范围。
1)Name 和 Value 属性由程序设定,默认值都是空引用。
2)Domain属性的默认值为当前URL的域名部分,不管发出这个cookie的页面在哪个目录下的。
3)Path属性的默认值是根目录,即 ”/” ,不管发出这个cookie的页面在哪个目录下的。可以由程序设置为一定的路径来进一步限制此cookie的作用范围。
4)Expires 属性,这个属性设置此Cookie 的过期日期和时间。
HttpCookie cookie = new HttpCookie("MyCook");//初使化并设置Cookie的名称
DateTime dt = DateTime.Now;
TimeSpan ts = new TimeSpan(0, 0, 1, 0, 0);//过期时间为1分钟
cookie.Expires = dt.Add(ts);//设置过期时间
cookie.Values.Add("userid", "value");
cookie.Values.Add("userid2", "value2");
Response.AppendCookie(cookie);
Path和Domain属性
path:
如果http://www.china.com/test/index.html 建立了一个cookie,那么在http://www.china.com/test/目录里的所有页面,以及该目录下面任何子目录里的页面都可以访问这个cookie。这就是说,在http://www.china.com/test/test2/test3 里的任何页面都可以访问http://www.china.com/test/index.html建立的cookie。
但是,如果http://www.china.com/test/ 需要访问http://www.china.com/test/index.html设置的cookies,该怎么办?
这时,我们要把cookies的path属性设置成“/”。在指定路径的时候,凡是来自同一服务器,URL里有相同路径的所有WEB页面都可以共享cookies。
Domain:
比如: http://www.baidu.com/xxx/login.aspx 页面中发出一个cookie,Domain属性缺省就是www.baidu.com ,可以由程序设置此属性为需要的值。
值是域名,比如www.china.com。这是对path路径属性的一个延伸。如果我们想让 www.china.com能够访问bbs.china.com设置的cookies,该怎么办? 我们可以把domain属性设置成“china.com”, 并把path属性设置成“/”。
会话Cookie和持久Cookie
若不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,关闭浏览器窗口,cookie就消失。这种生命期为浏览器会话期的cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。
若设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在浏览器的不同进程间共享。
这种称为持久Cookie。
服务器端向客户端发送一个Cookie
1)创建Cookie:
Cookie cookie = new Cookie(String cookieName,String cookieValue);
示例:
Cookie cookie = new Cookie("username","zhangsan");
那么该cookie会以响应头的形式发送给客户端:
注意:Cookie中不能存储中文
2)设置Cookie在客户端的持久化时间:
cookie.setMaxAge(int seconds); ---时间秒
注意:如果不设置持久化时间,cookie会存储在浏览器的内存中,浏览器关闭cookie信息销毁(会话级别的cookie),如果设置持久化时间,cookie信息会被持久化到浏览器的磁盘文件里
示例:
cookie.setMaxAge(10*60);
设置cookie信息在浏览器的磁盘文件中存储的时间是10分钟,过期浏览器 自动删除该cookie信息
3)设置Cookie的携带路径:
cookie.setPath(String path);
注意:如果不设置携带路径,那么该cookie信息会在访问产生该cookie的web资源所在的路径都携带cookie信息
示例:
cookie.setPath("/WEB16");
代表访问WEB16应用中的任何资源都携带cookie
cookie.setPath("/WEB16/cookieServlet");
代表访问WEB16中的cookieServlet时才携带cookie信息
4)向客户端发送cookie:
response.addCookie(Cookie cookie);
5)删除客户端的cookie:
如果想删除客户端的已经存储的cookie信息,那么就使用同名同路径的持久化时间为0的cookie进行覆盖即可
服务器端怎么接受客户端携带的Cookie
cookie信息是以请求头的方式发送到服务器端的:
1)通过request获得所有的Cookie:
Cookie[] cookies = request.getCookies();
2)遍历Cookie数组,通过Cookie的名称获得我们想要的Cookie
for(Cookie cookie : cookies){
if(cookie.getName().equal(cookieName)){
String cookieValue = cookie.getValue();
}
}
cookie跨域问题
Cookie本身具有不可跨域名性。就是说,浏览器访问百度不会带上谷歌的cookie。
在前端开发中经常会遇到跨域的问题,比如前后端分离中前后端部署在不同的端口上,或者在前端页面中需要向另外一个服务请求数据,这些都会被跨域所阻挡。
cookie 同源策略
cookie 的同源策略是通过Domain
和path
两个部分来共同确认一个 cookie 在哪些页面上可用。
Domain
确定这个 cookie 所属的域名,不能带端口或协议。因此 cookie 便可在不同端口/不同协议下共享,只要域名相同。有一个例外是父子域名间也能共享 cookie,只需将 Domain 设置为.父域名
。
path
就简单多了,通过 Domain 确定哪些域名可以共享 cookie,然后在通过path
来确定 cookie 在哪些路径下可用。使用/
表示所有路径都可共享。
具体如下:
- Domain :
example
,path :/a
可获取 cookie:http://example:8081/a,https://example:8081/a - Domain :
example
,path :/
可获取 cookie:http://example:8081/a,https://example:8081/a , http://example:12/abcd - Domain :
.example
,path :/a
可获取 cookie:http://example:8081/a , https://localhost:8081/a , http://test.example:889/a
注意:在跨域请求中,即时目标地址有 cookie 且发起请求的页面也能读取到该 cookie,浏览器也不会将 cookie 自动设置到该跨域请求中。
比如在http://localhost:8082/a页面中请求http://localhost:8081/abc,这两个地址下拥有共享cookie,http请求也不会携带cookie。
浏览器同源策略
相信很多人在 web 入门时,都被跨域问题折磨的死去活来。要想完全掌握跨域就得知道为什么会有跨域这个问题出现。
简单来说跨域问题是因为浏览器的同源策略导致的。那浏览器为什么要有同源策略呢?
当然是为了安全。没有同源策略限制的浏览器环境是非常危险的(即使有了同源策略也不是百分百安全),有兴趣的可以去了解了解CSRF和XSS攻击。
所谓的“同源”指的是“三个相同”:
- 协议相同。不能一个是 http 协议,一个是 https
- 域名相同
- 端口相同
如果非同源页面有以下限制:
- LocalStore 和 IndexDB 无法读取。这两个显然是不能读取的,但是 cookie 有点不一样,放在后面单独说明
- DOM 无法获取,比如如法在页面 A 中通过 iframe 获取异源页面 B 的 DOM
- AJAX 请求无法读取(可以发送请求,但是无法读取到请求结果。比如在页面 A 中请求异源接口 B,请求会正常发出处理,但是在页面 A 中无法获取请求结果,除非响应头 Access-Control-Allow-Headers 中允许了页面 A 的源,这样就能读取到结果)
但是这里有个例外,所有带“src”属性的标签都可以跨域加载资源,不受同源策略的限制,这样你应该可以想到一个比较古老的跨域解决方案(JSONP),同时这个特性也会被用作 CSRF 攻击。
http 请求跨域
在前端开发中经常会遇到跨域的问题,比如前后端分离中前后端部署在不同的端口上,或者在前端页面中需要向另外一个服务请求数据,这些都会被跨域所阻挡。
目前主要有以下几种办法解决跨域问题:
1、关闭浏览器同源检查
这个太暴力,也太不安全了,不用考虑。
2、jsonp 实现跨域请求
前面说过了浏览器对于带 src 属性的标签都可以跨域的。因此 jsonp 的实现流失利用了这个特性,在页面中动态插入一个<script>
标签,然后他的 src 属性就是接口调用地址,这样就能访问过去了,然后再讲返回内容特殊处理成立即执行的函数,这样就看起像进行了一次跨域请求。
之所以不推荐这种方式,主要有以下两个原因:
实现复杂,且需要前后台同时修改才能实现
只能进行 get 请求
3、服务器设置运行跨域
这种方法只需要后台做处理便能实现跨域,前面说的 http 跨域请求是能够发出去的,只是不能接收,那我们只要在响应头Access-Control-Allow-Headers
中加入允许请求的地址即可,以,
分隔,同时*
代表所有地址都允许。比如:
Access-Control-Allow-Headers:http://localhost:8081,http://localhost:8082
本方法是较为常用的一中跨域办法,只需简单修改服务端代码即可。
4、请求代理
这也是非常常用的一种跨域方法。跨域限制只是浏览器限制,服务端并没有这个概念,因此我们在前端还是请求同域地址,然后在服务端做一个代理,将请求转发到真正的 ip 和端口上。通常使用 nginx 实现端口转发,比如下面一段 nginx 配置:
server {
# /test1/abc 转发到 http://a.com:8011/abc
location /test1/ {
proxy_pass http://a.com:8011/;
}
# /test2/abc 转发到 http://b.com:8011/main/abc
location /test2/ {
proxy_pass http://b.com:8011/main/;
}
# /test3/abc 转发到 http://c.com:8011/test3/abc
location /test3/ {
proxy_pass http://c.com:8081;
}
}
Session
Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
Session技术是将数据存储在服务器端的技术,会为每个客户端都创建一块内存空间 存储客户的数据,但客户端需要每次都携带一个标识ID去服务器中寻找属于自己的内 存空间。所以说Session的实现是基于Cookie,Session需要借助于Cookie存储客 户的唯一性标识JSESSIONID
每个用户访问服务器都会建立一个session,那服务器是怎么标识用户的唯一身份呢?事实上,用户与服务器建立连接的同时,服务器会自动为其分配一个SessionId。
什么东西可以让你每次请求都把SessionId自动带到服务器呢?
显然就是cookie了,如果你想为用户建立一次会话,可以在用户授权成功时给他一个唯一的cookie。
当一个用户提交了表单时,浏览器会将用户的SessionId自动附加在HTTP头信息中,(这是浏览器的自动功能,用户不会察觉到),当服务器处理完这个表单后,将结果返回给SessionId所对应的用户。试想,如果没有 SessionId,当有两个用户同时进行注册时,服务器怎样才能知道到底是哪个用户提交了哪个表单呢。
储存需要的信息。
服务器通过SessionId作为key,读写到对应的value,这就达到了保持会话信息的目的。
session的实现
session的实现,就是在首次使用session时,在服务器内部开辟一个内存空间,内存空间是HashTable结构,存放数据,同时生成一个Session ID用来唯一标识这个HashTable。
客户端根据服务端发来的cookie里面的sessionid,再发给服务端,根据sessionid来确定内存空间是哪个,对这个hashtable进行增删改查字段。
当首次使用session时,服务器端要创建session,session是保存在服务器端,而给客户端的session的id(一个cookie中保存了sessionId)。客户端带走的是sessionId,而数据是保存在session中。
当客户端再次访问服务器时,在请求中会带上sessionId,而服务器会通过sessionId找到对应的session,而无需再创建新的session。
session的创建
当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了sessionId,如果已包含则说明以前已经为此客户端创建过session,服务器就按照sessionId把这个session检索出来使用(检索不到,会新建一个)。
如果客户端请求不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关联的sessionId,sessionId的值是一个既不会重复,又不容易被找到规律以仿造的字符串,这个sessionId将被在本次响应中返回给客户端保存。
禁用cookie
如果客户端禁用了cookie,通常有两种方法实现session而不依赖cookie。
1)URL重写,就是把sessionId直接附加在URL路径的后面。
2)表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。比如:
<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764">
<input type="text">
</form>
Session共享
对于多网站(同一父域不同子域)单服务器,我们需要解决的就是来自不同网站之间SessionId的共享。由于域名不同(aaa.test.com和bbb.test.com),而SessionId又分别储存在各自的cookie中,因此服务器会认为对于两个子站的访问,是来自不同的会话。解决的方法是通过修改cookies的域名为父域名达到cookie共享的目的,从而实现SessionId的共享。带来的弊端就是,子站间的cookie信息也同时被共享了。
获得Session对象
HttpSession session = request.getSession();
此方法会获得专属于当前会话的Session对象,如果服务器端没有该会话的Session 对象会创建一个新的Session返回,如果已经有了属于该会话的Session直接将已有 的Session返回(实质就是根据JSESSIONID判断该客户端是否在服务器上已经存在 session了)
怎样向session中存取数据(session也是一个域对象)
Session也是存储数据的区域对象,所以session对象也具有如下三个方法:
session.setAttribute(String name,Object obj);
session.getAttribute(String name);
session.removeAttribute(String name);
Session对象的生命周期
创建:
第一次执行request.getSession()时创建
销毁:
1)服务器(非正常)关闭时
2)session过期/失效(默认30分钟)
问题:时间的起算点 从何时开始计算30分钟?
从不操作服务器端的资源开始计时(例如:当你访问淘宝页面时,点开页面不动,第29分钟再动一下页面,就得重新计时30分钟;当过了30分钟,就失效了。)
可以在工程的web.xml中进行配置
<session-config>
<session-timeout>30</session-timeout>
</session-config>
3)手动销毁session
session.invalidate();
面试题:浏览器关闭,session就销毁了?
不对,浏览器关闭和服务器session销毁没有任何关系!
作用范围
默认在一次会话中,也就是说在,一次会话中任何资源公用一个session对象
session一致性
分布式情况下,如果每台服务器都session存在自己的内存中,不同服务器之间就会造成数据不一致问题,
这时候就需要session共享。单机情况下,不存在Session共享的情况。
分布式情况下,如果不进行Session共享会出现数据不一致,比如:会导致请求落到不同服务器要重复登录的情况。
session复制
应用服务器开启web容器的session复制功能,在集群中的几台服务器之间同步session对象,多个web-server之间相互同步session,这样每个web-server之间都包含全部的session。
不足:
-
session的同步需要数据传输,占内网带宽,有时延
-
有更多web-server时,容易造成网络风暴
客户端存储
将session存储到浏览器cookie中。每次请求服务器的时候,将session放在请求中发送给服务器,服务器处理完请求后再将修改后的session响应给客户端。
缺点:
-
数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患
-
session存储的数据大小受cookie限制
反向代理hash一致性
反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个web-server上
反向代理层让同一个用户的请求保证落在一台server上呢?
方法一:四层代理hash。反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个server上(更推荐,保证传输层不引入业务层的逻辑)
方法二:七层代理hash。反向代理使用http协议中的某些业务属性来做hash,例如sid,city_id,user_id等,能够更加灵活的实施hash策略,以保证同一个浏览器用户的请求落在同一个server上
优点:
只需要改nginx配置,不需要修改应用代码
可以支持server水平扩展
不足:
server水平扩展,rehash后session重新分布,会有一部分用户路由不到正确的session
即使hash散列均匀,也不能保证server的负载均匀
如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录
后端统一集中存储
将session存储在web-server后端的存储层,数据库或者缓存,一般用redis/memchache缓存。
优点:
没有安全隐患
可以水平扩展,支持缓存集群或横向拓展
不足:
增加了一次网络调用
需要修改应用代码
实现分布式session
场景:一台tomcat时,肯定是不会出现什么问题。如果是A,B两台服务器呢?请求1先访问到A,A中存储了1的信息,当请求1访问B时,B中并没有1的信息。如何让A,B共享同一session呢?
其实tomcat可用配置session复制,从而实现集群间session共享。但是当集群结点数很多,这种session复制会消耗大量带宽,形成网络风暴。
常用的解决方案是:将session信息存放到第三方,客户端和服务端在通信时,去第三方存取session信息。
redis常作为第三方,主要是由于:
1)redis读写更快。redis是直接操作内存数据,而之前的cookie、session都是以文件形式存储,读写较慢。
2)更好设置过期时间。文件形式保存信息,有时并不能及时删除,占用磁盘空间(session的垃圾回收)。redis不存在这种问题。
3)更好的分布式同步。设置redis主从同步,可快速同步session到各台web服务器,比文件存储更加快速。
实现redis中session的读取
思路:在用户登陆成功之后,服务端生成一个token,一个token标识一个用户的信息,将用户信息保存到redis。
关于redis中key的设计
Redis做缓存时,可能会设置很多key,来标识我们需要存取的数据,如何能够保证key的唯一呢?
我们可用考虑给key加一个前缀,例如:用户相关的缓存都以用户为前缀,公司所有的缓存都以公司为前缀......。我们在key前拼接上前缀,作为redis中真正读写的key,这样是不是就能使得key唯一且易区分呢?
这里,我们采用模板模式来封装key。
接口<---抽象类<---实现类
接口就是定义一些契约,抽象类来做一些共同的操作,实现类依照特定的要求来完成具体功能,这种设计也是非常常用的。
我们以电商为例来进行说明:
电商中有很多模块,用户模块、商品模块、订单模块......
如何保证每个模块的前缀不一样呢?
小技巧:通过类名,我们将类名作为前缀,这样就可以保证key的唯一性了。
第一步,接口:
public interface KeyPrefix {
//有效期
public int expireSeconds();
//前缀
public String getPrefix();
}
第二步,抽象类:
public abstract class BasePrefix implements KeyPrefix{
private int expireSeconds;
private String prefix;
public BasePrefix(String prefix) {
//0-永不过期
this(0, prefix);
}
public BasePrefix(int expireSeconds, String prefix) {
super();
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}
@Override
public int expireSeconds() {
return expireSeconds;
}
@Override
public String getPrefix() {
String className = getClass().getSimpleName();
return className + ":" + prefix;
}
}
第三步,实现类:
相当于getById这个UserKey的getPrefix方法,返回的是UserKey:id
public class UserKey extends BasePrefix{
private UserKey(String prefix) {
super(prefix);
}
//同一模块内,可自定义变量来区分
public static UserKey getById = new UserKey("id");
public static UserKey getByName = new UserKey("name");
}
java内部redis工具类
https://blog.csdn.net/tiankong_12345/article/details/86654938
代码修改
UserServiceImpl中添加对token的操作
public static final String COOKI_NAME_TOKEN = "token";
@Override
public User getByToken(String token) {
if(StringUtils.isEmpty(token)){
return null;
}
return redisService.get(UserKey.getById, token, User.class);
}
@Override
public void setToken(HttpServletResponse response, User user) {
String token = UUIDUtil.uuid();
redisService.set(UserKey.getById, token, user);
Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
cookie.setMaxAge(UserKey.getById.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}
LoginController中修改:
doLogin方法,登录后,调用userService的setToken方法
setToken方法在redis的key为UserKey.getById的HashTable下,key为新生成的uuid,即token,value为user对象,并把token放入cookie中。
getRedisUser方法,先从cookie中取token,取不到就从request中取,然后调用userService的getByToken(token)
getByToken方法,在redis的key为UserKey.getById的HashTable下,以token为key,得到value,转换为user的class类
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, User user) {
User userDb = userService.getById(user.getId());
if(userDb == null) {
return Result.error("用户不存在!");
}else if(user.getPassword().equals(userDb.getPassword())) {
//登陆成功以后,设置token
userService.setToken(response, user);
return Result.success(true);
}else {
return Result.error("密码有误!");
}
}
@RequestMapping("/get_user")
public String getRedisUser(Model model,
@CookieValue(value = UserServiceImpl.COOKI_NAME_TOKEN, required = false)String cookieToken,
@RequestParam(value = UserServiceImpl.COOKI_NAME_TOKEN, required = false)String paramToken){
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
//这里先从cookie中取token,取不到就从request中取
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
User user = userService.getByToken(token);
model.addAttribute("user", user);
return "hello";
}
总结图
cookie,session的区别
1、最明显的不同是一个在客户端一个在服务端。
因为Cookie存在客户端所以用户可以看见,所以也可以编辑伪造,不是十分安全。
2、Session过多的时候会消耗服务器资源,所以大型网站会有专门的Session服务器。
单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。Cookie存在客户端所以没什么问题。
3、域的支持范围不一样,比方说a.com的Cookie在a.com下都能用,而www.a.com的Session在api.a.com下都不能用,解决这个问题的办法是JSONP或者跨域资源共享