目录
五、springboot整合spring-session+redis的解决方案
使用原HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分中两次请求是否由一个客户端发出。这样的设计严重阻碍的Web程序的设计。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。 Session管理器不管用什么数据结构和算法都要耗费大量内存和CPU时间;而用cookie,则根本不用检索和维护session数据,服务器可以做成无状态的,当然高效);
一、Cookie
1.1 Cookie是什么?
Cookie是由服务器端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie,一般情况下用户可以设置浏览器禁用cookie)
Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息。(Cookie 会随每次HTTP请求一起被传递服务器端,排除js,css,image等静态文件,这个过程可以从fiddler或者ie自带的网络监控里面分析到,考虑性能的化可以从尽量减少cookie着手)。
Cookie是HTTP协议制定的!先由服务器保存Cookie到浏览器,再下次浏览器请求服务器时把上一次请求得到Cookie再归还给服务器。
Cookie是HTTP协议的一部分,用于在客户端存储和传递信息,主要用于跟踪用户状态。Cookie规范是为了给HTTP增加状态跟踪用的(如果要精确把握,建议仔细阅读一下相关的RFC),但不是唯一的手段;
目前Cookie已经成为标准,所有的主流浏览器如IE、Netscape、Firefox、Opera等都支持Cookie。cookie默认根据域名自动判断是否带上cookie信息访问。
当用户访问一个网站时,该网站的服务器可能会向用户的浏览器发送一条信息请求存储一个或多个cookie。
如果用户接受了请求,浏览器通常会按照请求存储这些信息,并在用户下次访问同一网站时将这些信息发回服务器。这样,服务器可以识别并响应用户的个人偏好或之前的行为。
每个Cookie包含的信息可以是简单的设置偏好,也可能是复杂的登录凭据。当你输入用户名和密码时,网站可能会询问你是否希望保持登录状态。
如果你同意,网站将创建一个Cookie,其中存储了你的登录信息的加密版本。下次访问时,你无需重新输入登录凭据,网站会自动将你登录进去。
1.2 Cookie 用途
- 会话管理:Cookie最初也是最主要的作用就是用于会话管理。当用户登录一个网站时,服务器会生成一个包含会话ID的Cookie并发送给浏览器,浏览器将这个Cookie保存在本地。此后,每次用户发送请求时,浏览器都会自动将这个Cookie发送给服务器,服务器通过会话ID识别用户身份,从而保持用户的登录状态。
- 个性化设置:Cookie还可以用来保存用户的个性化设置,如主题、语言、字体大小等。这样,当用户再次访问网站时,网站可以根据Cookie中的信息为用户提供更加个性化的体验。
- 购物车功能:在电子商务网站中,Cookie经常被用来实现购物车功能。当用户将商品添加到购物车时,这些信息会被保存在Cookie中。这样,即使用户关闭了浏览器或换了一台电脑,只要Cookie还在,购物车中的商品信息就不会丢失。
- 跟踪用户行为:网站可以使用Cookie跟踪用户在网站上的行为,如访问了哪些页面、停留了多长时间、点击了哪些链接等。这些数据对于网站优化、广告投放等都非常有价值。
- 第三方Cookie与广告定向:除了网站自己设置的Cookie外,还有一些第三方Cookie,它们通常由广告商或数据分析公司设置。这些Cookie可以用来跟踪用户在多个网站上的行为,从而为用户提供更加精准的广告定向服务。然而,这种跨站跟踪的行为也引发了关于隐私保护的争议。
- 安全性和认证:在登录敏感网站(如银行或个人邮件)时,Cookie能够在你浏览网站时持续验证你的登录状态,确保是你本人在操作,防止未授权的访问。此外,一些网站使用Cookie来实现多因素认证,进一步增强账户安全。
1.3 Cookie的类型
- 会话Cookie(Session Cookies):这种类型的Cookie在浏览器关闭后就会被删除,主要用于保存用户的会话信息。由于它们不会在用户的计算机上长期保存,因此相对较为安全。
- 持久Cookie(Persistent Cookies):与会话Cookie不同,持久Cookie会在用户的计算机上长期保存,直到其过期时间到达或被用户手动删除。这种类型的Cookie常用于保存用户的登录状态、个性化设置等信息。
- 安全Cookie(Secure Cookies):安全Cookie只能通过HTTPS协议传输,不能通过未加密的HTTP协议传输。这增加了Cookie在传输过程中的安全性。
- HttpOnly Cookie:HttpOnly是一个标志属性,用于防止JavaScript代码访问特定的Cookie。当设置了HttpOnly属性的Cookie被创建后,它将无法通过客户端脚本(如JavaScript)进行访问。这有助于减少跨站脚本攻击(XSS)的风险。
1.4 Cookie的组成
一般情况下,cookie是以键值对进行表示的(key-value)的字符串。它通常由名称、值、域名、路径、过期时间等字段组成。
cookie常用属性说明:
- name:String cookie的名称,Cookie一旦创建,名称便不可更改。
- value:Object cooke的值
- path:String Cookie的使用路径, “/”
- expires/maxAge:int cookie的有效时间,单位秒。默认为–1,浏览器关闭即失效。
- domain:String 可访问该Cookie的域名,注意第一个字符必须为“.”。
- secure:Boolean Cookie是否仅被使用安全协议传输,安全协议。
- 注意:浏览器提交Cookie时只会提交name与value属性。
1.5 Cookie约束
Http协议规定:
1个Cookie最大4KB
1个服务器最多向1个浏览器保存20个Cookie
1个浏览器最多可以保存300个Cookie
注意:Cookie是不能跨浏览器的!
浏览器大战:因为浏览器竞争很激励,所以很多浏览器都会在一定范围内违反HTTP规定。
1.6 Cookie 生命周期
cookie有2种存储方式,一种是会话性,一种是持久性。
cookie我们是可以进行设置的,我们可以人为设置cookie的有效时间,什么时候创建,什么时候销毁。
Cookie默认有效性:当前会话有效(与浏览器有关,浏览器关闭或换一个浏览器,Cookie失效。)
1,持久化Cookie
如果cookie为持久性,那么cookie会保存在用户的硬盘中,直至生存期结束或者用户主动将其销毁。
2,会话cookie
如果cookie为会话性,那么cookie仅会保存在客户端的内存中,当我们关闭客服端时cookie也就失效了
cookie.setMaxAge(ss秒)
ss>0:在ss秒后失效
ss=0:立即失效
ss<0:默认情况(默认值是-1),表示浏览器一关,Cookie 就会被删除
注意:一旦设置持久化Cookie,Cookie默认(就是自带的cookie)有效性就不起作用了。
创建一个60分钟的Cooike
protected void life3600(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie = new Cookie("life3600_key", "life3600_value");
cookie.setMaxAge(60 * 60); // 设置Cookie 一小时之后被删除。无效
resp.addCookie(cookie);
resp.getWriter().write("已经创建了一个存活一小时的 Cookie");
}
马上删除一个Cookie
protected void deleteNow(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 先找到你要删除的Cookie 对象
Cookie cookie = CookieUtils.findCookie("life3600_key", req.getCookies());
if (cookie != null) {
// 调用setMaxAge(0);
cookie.setMaxAge(0); // 表示马上删除,都不需要等待浏览器关闭
// 调用response.addCookie(cookie);
resp.addCookie(cookie);
resp.getWriter().write("key4 的 Cookie 已经被删除");
}
}
补充一下:
cookie默认有效路径为:当前项目(/项目命)
设置cookie的有效路径
* cookie.setPath()
* 注意:一般设置有效路径在当前项目下的某个路径。 (cookie.setPath(request.getContextPath()+"/demopath");)
Cookie 的path属性可以有效的过滤哪些Cookie 可以发送给服务器,哪些不发。
path 属性是通过请求的地址来进行有效的过滤。
CookieA path=/工程路径
CookieB path=/工程路径/abc
请求地址如下:
1,请求地址:http://ip:port/工程路径/a.html
CookieA 发送
CookieB 不发送
2,http://ip:port/工程路径/abc/a.html
CookieA 发送
CookieB 发送
创建一个带有Path路径的cookie
protected void testPath(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie = new Cookie("path1", "path1");
// getContextPath() ===>>>> 得到工程路径
cookie.setPath( req.getContextPath() + "/abc" ); // ===>>>> /工程路径/abc
resp.addCookie(cookie);
resp.getWriter().write("创建了一个带有 Path 路径的 Cookie");
}
1.7 cooike创建
cookie写入浏览器的过程:我们可以使用如下代码写一个Cookie 并发送到客户端的浏览器(为了简单我没有设置其它属性)。
Cookie cookie = new Cookie("key", "value");
Response对象中的addCookie( )方法会将cookie对象保存到客户端
protected void creatCookie(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1 创建Cookie对象
Cookie cookie = new Cookie("username", "尚杰颖");
//2 通知客户端保存Cookie
resp.addCookie(cookie);
//1 创建Cookie 对象
Cookie cookie1 = new Cookie("password", "021211");
//2 通知客户端保存Cookie
resp.addCookie(cookie1);
resp.getWriter().write("Cookie 创建成功");
}
import javax.servlet.http.Cookie;
import java.util.UUID;
String sessionId = UUID.randomUUID().toString();
Cookie cookie = new Cookie("sessionId", sessionId);
response.addCookie(cookie);
1.8 Cooike的获取
request.getCookie()方法获取cookies返回的时一个Cookie[]数组。
protected void getCookie(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//使用request请求对象,来获取客户端传过来的cookie对象
Cookie[] cookies = req.getCookies(); //返回的是一个cookie数组
Cookie usernameCookie = CookieUtils.findCookie("username", cookies);
if (usernameCookie!=null){
System.out.println("usernameCookie = " + usernameCookie);
}
Cookie passwordCookie = CookieUtils.findCookie("password", cookies);
if (passwordCookie!=null){
System.out.println("passwordCookie = " + passwordCookie);
}
}
Cooike的工具类
public class CookieUtils {
/**
*查找指定名称的Cookie 对象
*@param name
*@param cookies
*@return
*/
public static Cookie findCookie(String name , Cookie[] cookies) {
if (name == null || cookies == null || cookies.length == 0) {
return null;
}
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie;
}
}
return null;
}
//创建cookie对象并设置cookie对象有效期
public static void setCookies(HttpServletResponse resp, String key, String value, int i) {
if (key != null && value!=null){
//创建cookie对象
Cookie cookie = new Cookie(key,value);
//设置cookie对象有效期
cookie.setMaxAge(i);
//将cookie添加到响应对象中,保存到浏览器上
resp.addCookie(cookie);
}
}
}
1.9 Cookie的HTTP传输
其一:HTTP Request 客户端把cookie发送到服务器
注意:游览器只会携带在当前请求的url中包含了该cookie中path值的cookie。并且以key:value的形式进行表示。多个cookie用 ;进行隔开。
其二:HTTP Response 服务器发送cookie给客户端
HTTP响应中, cookie的表示形式是,Set-Cookie:cookie的名字,cookie的值。如果有多个cookie,那么在HTTP响应中就使用多个Set-Cookie进行表示。
注意Unicode编码:保存中文
中文与英文字符不同,中文属于Unicode字符,在内存中占4个字符,而英文属于ASCII字符,内存中只占2个字节。Cookie中使用Unicode字符时需要对Unicode字符进行编码,否则会乱码。
提示:Cookie中保存中文只能编码。一般使用UTF-8编码即可。不推荐使用GBK等中文编码,因为浏览器不一定支持,而且JavaScript也不支持GBK编码。
1.10 cookie缺陷
1,Cookie的value只能是String类型,不灵活。
2,Cookie存放到客户端浏览器中,不安全。因为Cookie是以明文传送。
3,Cookie过多,Cookie是为请求或响应报文发送,无形中增加了网络流量。
4,各个浏览器对Cookie有限制,使用上有局限
1.11 Cookie的安全性问题
虽然Cookie在许多方面都非常有用,但它们也存在一些潜在的安全风险:
- XSS攻击:跨站脚本攻击(XSS)是一种常见的网络攻击方式,攻击者通过在网页中注入恶意脚本,窃取用户的Cookie信息,从而获取用户的登录凭证或其他敏感数据。为了防止XSS攻击,网站可以采取设置HttpOnly属性、对输出进行编码等措施。
- CSRF攻击:跨站请求伪造(CSRF)是另一种常见的网络攻击方式。攻击者通过诱导用户点击恶意链接或执行恶意操作,使用户的浏览器在不知情的情况下向服务器发送伪造的请求,从而执行非授权的操作。为了防止CSRF攻击,网站可以使用Token验证、检查请求的来源等措施。
- Cookie劫持:Cookie劫持是指攻击者通过各种手段获取用户的Cookie信息,并冒充用户进行恶意操作。为了防止Cookie劫持,网站可以使用HTTPS协议对Cookie进行加密传输、设置SameSite属性等措施。
- 隐私泄露:由于Cookie可以保存用户的个人信息和行为数据,如果这些数据被恶意获取或滥用,可能会导致用户的隐私泄露。为了保护用户的隐私,网站应该遵循最小必要原则收集和使用用户数据,并采取加密、匿名化等安全措施。
二、Session
2.1 Session是什么?
HTTP协议是无状态的,对于一个浏览器发出的多次请求,WEB服务器无法区分是不是来源于同一个浏览器。所以服务器为了区分这个过程会通过一个sessionid来区分请求,而这个sessionid是怎么发送给服务端的呢?
补充一下:
为何使用SessionId作为标识,而不是使用IP作为标识呐?
是因为很多机器是通过代理服务器方式上网,
没法区分每一台机器(Nginx等代理服务器)。
Session是浏览器和服务器之间的一次会话,包含多个请求。
(会话结束:关闭浏览器、请求超时)
Session是服务器为每个客户在服务器端开辟的一块空间。
2.2 Session请求流程
当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。
2.3 SessionId存储方式
对于session标识号(sessionID),有两种方式实现:cookies和URL重写。
方式一:cookie方式
在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器,一般这个cookie的名字都是类似于SEEESIONID。比如weblogic对于web应用程序生成的cookie,JSESSIONID= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是 JSESSIONID。
前面说了cookie会随每次请求发送到服务端,并且cookie相对用户是不可见的,用来保存这个sessionid是最好不过了,我们通过下面过程来验证一下。
通过上图再次验证了session和cookie的关系,服务器产生了一次设置cookie的操作,这里的sessionid就是用来区分浏览器的。为了实验是区分浏览器的,可以实验在IE下进行登录,然后在用chrome打开相同页面,你会发现在chrome还是需要你登录的,原因是chrome这时没有sessionid。httpOnly是表示这个cookie是不会在浏览器端通过js进行操作的,防止人为串改sessionid。
方式二:URL重写方式
由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时,仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种:
附加形式一:作为URL路径的附加信息
http://...../xxx;jsessionid= ByOK3vjFD75aPnrF7C2HmdnV6Q
附加形式二:作为URL的参数
http://...../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6Q
这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。
方式三:http请求head头部增加Tocken方式。
2.4 Session应用场景
- 网上商城中的购物车
- 保存登录用户的信息
- 将数据放入到 Session 中,供用户在访问不同页面时,实现跨页面访问数据
- 防止用户非法登录到某个页面
2.5 SessionId实现原理
HTTP协议本身是“连接-请求-应答-关闭连接”模式的,是一种无状态协议(HTTP只是一个传输协议);
所谓Session,指的是客户端和服务端之间的一段交互过程的状态信息(数据);这个状态如何界定,生命期有多长,这是应用本身的事情。HttpSession是JavaWeb提供的会话跟踪类,保存在服务器端,依赖于Cookie或URL重写实现。
由于HTTP本身的无状态性,服务端无法知道客户端相继发来的请求是来自一个客户的,所以,当使用服务端HttpSession存储会话数据的时候客户端的每个请求都应该包含一个session的标识(sid, jsessionid 等等)来告诉服务端;Cookie保存SessionId,浏览器与服务器交互时候负责把SessionId传输给服务器。
2.6 session消失原因
在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。
其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个 session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。
恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。
2.7 跨应用程序session共享
常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(single sign on),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。
2.8 多窗口导致session混乱
如何防止用户打开两个浏览器窗口操作导致的session混乱?
这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascript window.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。
2.9 Session与Cooike存储场景
使用服务端还是客户端session存储要看应用的实际情况的。
一般来说不要求用户注册登录的公共服务系统(如google)采用cookie做客户端session存储(如google的用户偏好设置),而需要用户登录注册的管理系统则使用服务端存储。
原因很显然:无需用户登录的系统唯一能够标识用户的就是用户的电脑,换一台机器就不知道谁是谁了,服务端session存储根本不管用;
而有用户管理的系统则可以通过用户id来管理用户个人数据,从而提供任意复杂的个性化服务;
小结:session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。
三、httpSession
HttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体到每个web应用服务器的提供商,除了对规范支持之外,仍然会有一些规范里没有规定的细微差异。HttpSession是Java Servlet API中用于管理用户会话的核心组件,它允许Web应用程序在服务器端存储和检索与单个用户相关的状态信息。
一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用, 它支持的持久性方式包括文件、数据库、客户端cookie保存。
3.1 作用与目的
HttpSession主要用于解决HTTP协议的无状态特性问题,即服务器无法区分两次不同请求是否来自于同一用户。通过引入HttpSession,服务器可以在用户的一系列请求之间维持状态信息。
3.2 创建与标识
当一个新用户首次访问Web应用时,如果需要启动会话,则服务器会在接收到请求后创建一个新的HttpSession实例,并为其生成一个唯一的会话ID(SessionID)。SessionID通常以Cookie的形式发送回客户端,客户端后续的请求会携带这个SessionID,服务器依据SessionID识别出属于同一个用户的多个请求之间的关联性。servlet容器就创建了一个HttpSession对象,其中存储了和本session相关的信息。所以,在一个servlet中有多少个不同用户连接,就会有多少个HttpSession对象。
3.3 存储与管理:
HttpSession是域对象,一个会话创建一个HttpSession对象,同一会话中的多个请求中可以共享session中的数据。HttpSession对象存储在服务器端,可以存放任意类型的对象属性,开发者可以通过setAttribute()方法将数据放入会话,然后通过getAttribute()方法从会话中获取数据。
Web容器(如Tomcat、Jetty等)内部会维护一个“session列表”,即session池,用于管理所有活跃的HttpSession实例。
补充:Servlet中可以使用的域对象
HttpServletRequest、ServletContext、HttpSession,它们三个是Servlet中可以使用的域对象,
HttpServletRequest、ServletContext、HttpSession三者的区别:
(1) HttpServletRequest:
一个请求创建一个request对象,
所以在同一个请求中可以共享request,
例如一个请求从AServlet转发到BServlet,
那么AServlet和BServlet可以共享request域中的数据;
(2)ServletContext:
一个应用只创建一个ServletContext对象,
所以在ServletContext中的数据可以在整个应用中共享,
只要不启动服务器,那么ServletContext中的数据就可以共享;
(3)HttpSession:
一个会话创建一个HttpSession对象,
同一会话中的多个请求中可以共享session中的数据;
作用域: ServletContext > HttpSession > HttpServletRequest
3.4 生命周期
会话的生命周期始于创建并分配SessionID之时,结束于以下几种情况之一:
- 开发者明确调用invalidate()方法销毁会话。
- 用户关闭浏览器(实际取决于浏览器的行为,有些情况下不关闭也会因为超时而失效)。
- 服务器配置的会话超时时间到达,会自动清除未活动的会话。
- 服务器关闭或者重启(对于非持久化的会话数据来说,这些数据将会丢失)。
3.5 获取会话
在Servlet中,可以通过HttpServletRequest对象的方法获取HttpSession:
- HttpSession session = request.getSession(); // 如果不存在则创建新的会话
- HttpSession session = request.getSession(true); // 同上,布尔参数true表示如果没有就创建
- HttpSession session = request.getSession(false); // 如果不存在则返回null
3.6 安全性
SessionID的安全性很重要,因此许多Web容器会对SessionID进行加密处理,以防被篡改或恶意利用。为了提高安全性,还可以配置HttpSession的超时时间以及使用HTTPS来传输SessionID,防止在网络中被窃听。
3.7 跨域共享
默认情况下,HttpSession是在单一Web应用程序的上下文中有效的。但在集群环境中,为了使多个服务器节点间能够共享会话,需要采取分布式缓存或其他方式同步Session信息。
3.8 Session的存和取数据代码示例
相关代码如下:
public class RegistServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
//获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("用户名:"+username);
System.out.println("密码:"+password);
//获得session对象
HttpSession session = request.getSession();
//添加session属性,在session中的属性在整次会话(浏览器不关闭)都有作用
session.setAttribute("username", username);
session.setAttribute("password", password);
request.getRequestDispatcher("/regist1.html").forward(request, response);
}
}
public class Regist1Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获得性别
String gender = request.getParameter("gender");
//获得职位
String job = request.getParameter("job");
//根据request获得session, 如果session已存在就直接使用当前会话的session,如果不存在就创建新的session对象
HttpSession session = request.getSession();
//从session中获得用户和密码,返回的是Object需要强转为String类型
String username = (String)session.getAttribute("username");
String password = (String) session.getAttribute("password");
//获得session的id,session利用id和浏览器的cookie进行绑定用来区分会话
String sessionID = session.getId();
System.out.println(sessionID);
switch(gender){
case "1" :
gender="男";
break;
case "2" :
gender="女";
break;
}
switch(job){
case "1" :
job="讲师";
break;
case "2" :
job="构架师";
break;
}
//设置response的类型和编码
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//打印到页面上
response.getWriter().println("<h1>注册成功</h1>");
response.getWriter().println("<hr>");
response.getWriter().println("用户名:"+username+"<br>");
response.getWriter().println("密码:"+password+"<br>");
response.getWriter().println("性别:"+gender+"<br>");
response.getWriter().println("职位:"+job+"<br>");
}
}
四、Spring-Session
4.1 SpringSession介绍
Spring-Session是通过过滤器实现的session共享。
在传统单机web应用中,一般使用tomcat/jetty等web容器时,用户的session都是由容器管理。浏览器使用cookie中记录sessionId,容器根据sessionId判断用户是否存在会话session。这里的限制是,session存储在web容器中,被单台服务器容器管理。
但是网站主键演变,分布式应用和集群是趋势(提高性能)。此时用户的请求可能被负载分发至不同的服务器,此时传统的web容器管理用户会话session的方式即行不通。除非集群或者分布式web应用能够共享session,尽管tomcat等支持这样做。但是这样存在以下两点问题:
需要侵入web容器,提高问题的复杂
web容器之间共享session,集群机器之间势必要交互耦合
基于这些,必须提供新的可靠的集群分布式/集群session的解决方案,突破web容器session单机限制,spring-session应用而生。
spring-session的核心思想在于此:将session从web容器中剥离,存储在独立的存储服务器中。目前支持多种形式的session存储器:Redis、Database、MogonDB等。session的管理责任委托给spring-session承担。当request进入web容器,根据request获取session时,由spring-session负责存存储器中获取session,如果存在则返回,如果不存在则创建并持久化至存储器中。
4.2 SpringSession原理
Spring-Session的实现就是设计一个过滤器Filter,当Web服务器接收到http请求后,当请求进入对应的Filter进行过滤,利用HttpServletRequestWrapper,实现自己的 getSession()方法,接管创建和管理Session数据的工作。将原本需要由web服务器创建会话的过程转交给Spring-Session进行创建,本来创建的会话保存在Web服务器内存中,通过Spring-Session创建的会话信息可以保存第三方的服务中,如:redis,mysql等。Web服务器之间通过连接第三方服务来共享数据,实现Session共享!
SpringSession主要通过SessionRepositoryFilter和SessionRepository接口来实现其功能。
拦截请求:SessionRepositoryFilter是Servlet规范中Filter的实现,用于切换HttpSession至Spring Session,并重新定义包装HttpServletRequest和HttpServletResponse。
保存session:SessionRepository接口定义了会话存储的基本操作,如保存、获取、删除会话等。
4.3 SpringSession流程
步骤 描述
1 客户端发送请求到Web服务器
2 Web服务器根据请求信息,生成一个全局唯一的Session ID,并创建一个空的Session对象
3 将Session ID添加到响应的Cookie中,并返回给客户端
4 客户端将Cookie保存起来,在后续的请求中带上该Cookie
5 Web服务器接收到请求后,从Cookie中获取Session ID
6 根据Session ID,从Redis中获取对应的Session对象
7 如果Session对象不存在,则创建一个新的Session对象
8 Web服务器将处理结果保存到Session对象中
9 将更新后的Session对象保存到Redis中
10 Web服务器将响应结果返回给客户端
4.4 spring-session与JSR340规范
JSR340是Java Servlet 3.1的规范提案,其中定义了大量的api,包括:
servlet、servletRequest/HttpServletRequest/HttpServletRequestWrapper、servletResponse/HttpServletResponse/HttpServletResponseWrapper、
Filter、Session等,
是标准的web容器需要遵循的规约,如tomcat/jetty/weblogic等等。
在日常的应用开发中,开发者也在频繁的使用servlet-api,比如:以下的方式获取请求的session:
HttpServletRequest request = ...
HttpSession session = request.getSession(false);
其中HttpServletRequest和HttpSession都是servlet规范中定义的接口,web容器实现的标准。
那如果引入spring-session,要如何获取session?
遵循servlet规范,spring-session同样方式获取session,对应用代码无侵入且对于开发者透明化。
接口适配:仍然使用HttpServletRequest获取session,仍然是HttpSession类型——适配器模式
类型包装增强:Session不能存储在web容器内,要外化存储——装饰模式
让人兴奋的是,以上的需求在Servlet规范中的扩展性都是予以支持!Servlet规范中定义一系列的接口都是支持扩展,同时提供Filter支撑扩展点,建议阅读《JavaTM Servlet Specification》。
Spring Session特性
下面是来自官网的特性介绍:Spring Session提供了一套创建和管理Servlet HttpSession的方案。Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。
4.5 Spring Session特性
- API和用于管理用户会话的实现;
- 允许以应用程序容器(即Tomcat)中性的方式替换HttpSession;
- Spring Session 让支持集群会话变得不那么繁琐,并且不和应用程序容器金习性绑定到。
- Spring 会话支持在单个浏览器实例中管理多个用户的会话。
- Spring Session 允许在headers 中提供会话ID以使用RESTful API。
五、springboot整合spring-session+redis的解决方案
5.1 分布式服务介绍
可以通过nginx的iphash实现,该方法非常简单,原理就是同一个ip的所有请求都会被nginx进行iphash进行计算,将结果绑定到指定服务器,之后这个请求都会访问到该服务器中。但是这样就有一些问题,首先就算负载均衡就没有太大意义了,如果绑定的服务器挂了,那么iphash也就失效了;又或者你的请求被其他服务分发而未走nginx服务,那么iphash同样不生效;所以谨慎使用;
通过redis实现Session共享就会显示的非常有意义。
5.2 本地服务添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
5.3 修改本地服务配置文件
配置连接Redis和springsession配置信息
store-type: redis:必须要添加这条信息,告诉程序session信息要存入到redis中。
server:
port: 8081
spring:
data:
redis:
database: 0
host: 192.168.133.145
port: 6379
timeout: 5000
password: 123456
session:
store-type: redis
timeout: 3600
redis:
namespace: logininfo
5.4 开启SpringSession
核心添加注解:@EnableRedisHttpSession
@SpringBootApplication
@EnableRedisHttpSession
public class DistributedSessionApplication {
public static void main(String[] args) {
SpringApplication.run(DistributedSessionApplication.class, args);
}
}
5.5 子域名共享session
需要创建一个config目录,新建SessionConfig配置类,修改domain作用域(非必须)。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
@Bean
public RedisSerializer<Object> redisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
5.5 编写控制器
@RestController
public class UserController {
@RequestMapping("/login")
public String login(@RequestParam String username,
HttpSession session){
System.out.println("==========login=============");
session.setAttribute("username",username);
return "登录成功";
}
@RequestMapping("/getUserInfo")
public String getUserInfo(HttpSession session){
return "当前登录用户=>"+session.getAttribute("username");
}
}
5.6 Ngin的负载均衡
upstream ngixServers{
server localhost:8081;
server localhost:8082;
}
server {
listen 8888;
server_name localhost;
location / {
proxy_pass http://ngixServers;
}
}
六,Redis+Session实现短信验证码登录
先说一下整个redis 实现共享session 的业务流程:
·在发送验证码的时候将手机号和对应验证码以key value 形式存储到redis中
·在对比验证码是否一致时,需要从redis里面取出手机号对应的code
·会使用UUID 创建一个登录令牌token
·将User对象转为 HashMap,并与token令牌一起以hash 键值对形式存储
·设置一个token的有效期并返回给前端
设置一个新的拦截器,用于刷新token,由于LoginInterceptor没有交给Spring进行管理,因此 StringRedis Template 不能通过@Resource自动注入。需要在配置文件中进行构造器注入。
public Result sendCode(String phone,HttpSession session) {
//校验手机号
//如果不符合就返回错误信息
if RegexUtils.isPhoneInvalid(phone)) {
return Result.failC"手机号格式错误");
}
//符合就发送验证码//胡图工具类的使用
String code = RandomUtil.randomNumbers(6);
//保存验证码到redis设置一个验证码有效时长key-手机号value-验证码stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY
+phone, code, LOGIN_CODE_TTL,TimeUnit.MINUTES);
//发送验证码,返回ok(这里假装发一下验证码,实际上要不调用云平台的服务log.debug("短信验证码为:"+ code);
return Result.ok();
}
public Result login(LoginFormDTO loginForm, HttpSession session) {
//校验⼿机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("⼿机号格式错误");
}
//从redis⾥获取并校验验证码
String cashCode =
stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);//找到key对
应的value
String code = loginForm.getCode();
if (cashCode == null || !cashCode.equals(code)) {
return Result.fail("验证码错误");
}
User user = query().eq("phone", phone).one();//⽤mybatis-plus的框
架查询
if (user == null) {//⽤户不存在就创建出来
user = createUserWithPhone(phone);
}
//保存在redis中
//随机⽣成⼀个token作为登录的令牌
String token= UUID.randomUUID().toString(true);
//将User对象转为HashMap存储
UserDTO userDTO =BeanUtil.copyProperties(user,UserDTO.class);
//⽤户脱敏
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new
HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) ->
fieldValue.toString()));
String tokenKey=LOGIN_USER_KEY+token;
//⽤哈希结构存储 多个字段有多个属性 可以存好⼏个键值对
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//设置token的有效期
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//30分钟
//返回token
return Result.ok(token);
}
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN获取redis中的⽤户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap =
stringRedisTemplate.opsForHash().entries(key);
// 3.判断⽤户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存⽤户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL,TimeUnit.MINUTES);
// 8.放⾏
return true;
}
使用redis实现session告一段落,下面留个问题讨论一下方案。微信开发提供了很多接口,参考下面截图,可以看到获取access_token接口每日最多调用2000次,现在大公司提供的很多接口针对不对级别的用户接口访问次数限制都是不一样的,至于做这个限制的原因应该是防止恶意攻击和流量限制之类的。那么我的问题是怎么实现这个接口调用次数限制功能。大家可以发挥想象力参与讨论哦,或许你也会碰到这个问题。
先说下我知道的两种方案:
1.使用流量整形中的令牌桶算法,大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
说浅显点:比如上面的获取access_token接口,一天2000次的频率,即1次/分钟。我们令牌桶容量为2000,可以使用redis 最简单的key/value来存储 ,key为用户id,value为整形存储还可使用次数,然后使用一个定时器1分钟调用client.Incr(key) 实现次数自增;用户每访问一次该接口,相应的client.Decr(key)来减少使用次数。
但是这里存在一个性能问题,这仅仅是针对一个用户来说,假设有10万个用户,怎么使用定时器来实现这个自增操作呢,难道是循环10万次分别调用client.Incr(key)吗?这一点没有考虑清楚。
2.直接用户访问一次 先进行总次数判断,符合条件再就进行一次自增
两种方案优缺点比较 | ||
---|---|---|
优点 | 缺点 | |
令牌桶算法 | 流量控制精确 | 实现复杂,并且由于控制精确反而在实际应用中有麻烦,很可能用户在晚上到凌晨期间访问接口次数不多,白天访问次数多些。 |
简单算法 | 实现简单可行,效率高 | 流量控制不精确 |