1.1 单个程序部署
http请求本身是无状态的,session是解决这个问题的。
session是一次会话的过程。
过程:
1.浏览器第一次请求网站,服务端将用户信息保存至Session中;
2.服务端将Session_id返回的浏览器,客户端保存session_id在cookie中
3.浏览器请求时带着session_id,服务器端判断session中是否有此session_id,若有会话建立。
1.2 分布式系统部署
一处登录,多处使用。
前提:单点登录多使用在分布式系统中。
之前存储session方式部署多个程序会出现Session共享问题。
Session一致性解决方案
1.session复制(同步)Tomcat自带该功能
思路:多个web-server 之间相互同步session,这样每个web-server之间都包含全部的session
优点:web-server 支持的功能,应用程序不需要修改代码
不足:
- session 的同步需要数据传输,占内网带宽,有时延
- 所有web-server 都包含所有session数据,数据量受内存限制,无法水平扩展
-
2.客户端存储法
思路:服务端存储所有用户的session,内存占用较大,可以将session存储到浏览器cookie中,每个端只要存储一个用户的数据了
优点:服务端不需要存储
缺点: 每次http请求都携带session,占外网带宽
- 数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患
- session存储的数据大小受cookie限制
3.反向代理hash一致性
思路:web-server为了保证高可用,有多台冗余,反向代理层能不能做一些事情,让同一个用户的请求保证落在一台web-server 上呢?
使用Nginx的负载均衡算法其中的hash_ip算法将ip固定到某一台服务器上,这样就不会出现session共享问题,因为同一个ip访问下,永远是同一个服务器。
缺点:失去了Nginx负载均衡的初心。
优点:
- 只需要改nginx配置,不需要修改应用代码
- 负载均衡,只要hash 属性是均匀的,多台web-server的负载是均衡的
- 可以支持web-server水平扩展(session 同步法是不行的,受内存限制)
不足:
- 如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录
如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session。
4.后端统一集中存储
思路:将session存储在web-server后端的存储层,数据库或者缓存
优点:没有安全隐患
- 可以水平扩展,数据库/缓存水平切分即可
- web-server重启或者扩容都不会有session丢失
不足:增加了一次网络调用,并且需要修改应用代码
对于db存储还是cache,个人推荐后者:session读取的频率会很高,数据库压力会比较大。如果有session高可用需求,cache可以做高可用,但大部分情况下session可以丢失,一般也不需要考虑高可用。
方案:使用Spring Session框架,相当于将Session之缓存到Redis中。
问:在项目发布的时候,Session如何控制不会失效的?
答:使用缓存框架,缓存Session的值(这里可以使用Redis加上EhCache实现一级和危机缓存)
5.使用Token的方式代替Session功能
在移动端,是没有Session这个概念的,都是使用Token的方式来实现的。
token最终会存放到Redis中,redis-cluster分片集群中是默认支持分布式共享的。完美的解决的共享问题。
推荐使用 4、5方式。
最靠谱的分布式Session解决方案
基于令牌(Token)方式实现Session解决方案,因为Session本身就是分布式共享连接。
将生成的Token 存入到Redis中。
分布式系统部署总结
通常,我们的项目都是多实例部署的,生产环境通常一个模块可能都会部署四五台服务器。 我们知道用户登录后,需要存储 session 信息,session 信息通常是存储在服务器的内存中的,不能持久化(服务器重启失效),多台服务器也不能共存。为了解决这个问题,我们可以将 session 存到几个服务器共享的地方里去,比如 Redis,只要在一个内网中,几台服务器可以共享 Redis (Redis本质也是装在某台服务器中)。 具体怎么实现呢?这里简单描述下:
- 用户登录成功,通过UUID生成一个随机唯一字符串(token可以使用JWT),命名为 token,通过向 redis 中 set 一个值,key 为 token 字符串,value 为用户对象序列化后的字符串。
- 当用户访问其他页面,请求方法时,检验请求参数或cookie中是否有 token;
- 如果有,则从redis 查询token,验证token是否有效;
- 如果没有,则抛出异常 “用户未登录”。
1.3 单点登录
三种单点登录的三种实现方式
https://zhuanlan.zhihu.com/p/354205290前言
在 B/S 系统中,登录功能通常都是基于 Cookie 来实现的。当用户登录成功后,一般会将登录状态记录到 Session 中,或者是给用户签发一个 Token,无论哪一种方式,都需要在客户端保存一些信息(Session ID 或 Token ),并要求客户端在之后的每次请求中携带它们。在这样的场景下,使用 Cookie 无疑是最方便的,因此我们一般都会将 Session 的 ID 或 Token 保存到 Cookie 中,当服务端收到请求后,通过验证 Cookie 中的信息来判断用户是否登录 。
单点登录(Single Sign On, SSO)是指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的应用系统。举例来说,百度贴吧和百度地图是百度公司旗下的两个不同的应用系统,如果用户在百度贴吧登录过之后,当他访问百度地图时无需再次登录,那么就说明百度贴吧和百度地图之间实现了单点登录。
单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,比如可以将 Session 序列化到 Redis 中,让多个应用系统共享同一个 Redis,直接读取 Redis 来获取 Session。当然仅此是不够的,因为不同的应用系统有着不同的域名,尽管 Session 共享了,但是由于 Session ID 是往往保存在浏览器 Cookie 中的,因此存在作用域的限制,无法跨域名传递,也就是说当用户在 http://app1.com 中登录后,Session ID 仅在浏览器访问 http://app1.com 时才会自动在请求头中携带,而当浏览器访问 http://app2.com 时,Session ID 是不会被带过去的。实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。实现方式一:父域 Cookie
在将具体实现之前,我们先来聊一聊 Cookie 的作用域。
Cookie 的作用域由 domain 属性和 path 属性共同决定。domain 属性的有效值为当前域或其父域的域名/IP地址,在 Tomcat 中,domain 属性默认为当前域的域名/IP地址。path 属性的有效值是以“/”开头的路径,在 Tomcat 中,path 属性默认为当前 Web 应用的上下文路径。
如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的Cookie。
利用 Cookie 的这个特点,不难想到,将 Session ID(或 Token)保存到父域中不就行了。没错,我们只需要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。不过这要求应用系统的域名需建立在一个共同的主域名之下,如 http://tieba.baidu.com 和 http://map.baidu.com,它们都建立在 http://baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。
总结:此种实现方式比较简单,但不支持跨主域名。实现方式二:认证中心
我们可以部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的 Web 服务。
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的。)
应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。
应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的,其他应用系统是访问不到的。)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。
这里顺便介绍两款认证中心的开源实现:
- Apereo CAS 是一个企业级单点登录系统,其中 CAS 的意思是”Central Authentication Service“。它最初是耶鲁大学实验室的项目,后来转让给了 JASIG 组织,项目更名为 JASIG CAS,后来该组织并入了Apereo 基金会,项目也随之更名为 Apereo CAS。
- XXL-SSO 是一个简易的单点登录系统,由大众点评工程师许雪里个人开发,代码比较简单,没有做安全控制,因而不推荐直接应用在项目中,这里列出来仅供参考。
总结:此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。
实现方式三:LocalStorage 跨域
前面,我们说实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。
父域 Cookie 确实是一种不错的解决方案,但是不支持跨域。那么有没有什么奇淫技巧能够让 Cookie 跨域传递呢?
很遗憾,浏览器对 Cookie 的跨域限制越来越严格。Chrome 浏览器还给 Cookie 新增了一个 SameSite 属性,此举几乎禁止了一切跨域请求的 Cookie 传递(超链接除外),并且只有当使用 HTTPs 协议时,才有可能被允许在 AJAX 跨域请求中接受服务器传来的 Cookie。
不过,在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端。
在这样的场景下,单点登录完全可以在前端实现。前端拿到 Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。
关键代码如下:
// 获取 token
var token = result.data.token;
// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
iframe.remove();
}, 6000);
// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
localStorage.setItem('token', event.data)
}, false);
前端通过 iframe+postMessage() 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享。
总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。
1.4 过滤器、拦截器
过滤器(Filter):过滤请求的一些信息,比如编码格式;如:过滤低俗文字、危险字符等。
拦截器(Interceptor):检查请求是否合法,一般是URL;拦截器可以对静态资源的请求进行拦截处理。
两者的本质区别:
- 拦截器(Interceptor)它依赖于web框架是基于Java的反射机制,而过滤器(Filter)它依赖于servlet容器是基于函数回调。
- 从灵活性上说拦截器功能更强大些,Filter能做的事情,都能做,而且可以在请求前,请求后执行,比较灵活。
- Filter主要是针对URL地址做一个编码的事情、过滤掉没用的参数、安全校验(比较泛的,比如登录不登录之类),太细的话,还是建议用interceptor。不过还是根据不同情况选择合适的。
- Filter(过滤器)和拦截器执行顺序不同,Filter要先于拦截器执行。
AOP思想:面向切面思想,主要用于代码解耦。(过滤器和拦截器都是AOP思想的体现)
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任,例如事务处理、日志管理、权限控制等,封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
使用AOP的好处
- 降低模块的耦合度
- 使系统容易扩展
- 提高代码复用性
实现AOP的主要设计模式就是动态代理。Spring的动态代理有两种:一是JDK的动态代理;另一个是cglib动态代理。