概述磁盘阵列（Redundant Arrays of Independent Drives，RAID），简单地说，就是讲若干块独立磁盘构成具有冗余能力的阵列。 他将很多块磁盘组合到一起构成一个磁盘组，来提升整个磁盘系统的读写性能及安全性。 利用同位检查（Parity Check）的观念，通过数据冗余实现磁盘系统中任何一个磁盘故障时整个磁盘系统仍然可以继续工作。 对于服务器开发和运维人员，RAID 是必须了解和使用的磁盘系统管理方式，随着时代的进步，越来越多的人在家庭、日常工作中使用简单的磁盘阵列来增加磁盘读写性能或提高数据安全性，甚至一些主板都已经提供了支持 RAID 的功能。 然而，RAID 概念很多，有时候会引起混淆，本文我们来详细介绍一下 RAID 技术的概念特征、基本原理、关键技术、各种等级和发展现状等方面的内容。RAIDRAID 是 1988 年美国加州大学伯克利分校的 D. A. Patterson 教授等首次在论文中提出的，起初，RAID 指的是廉价磁盘阵列，主要目的是为了节省购置大容量磁盘的成本，此后，RAID 咨询委员会将 Inexpensive 替换为 Independent 就是我们今日的 RAID，但其实质内容并没有改变。 RAID 主要利用数据条带、镜像和数据校验技术来获取高性能、可靠性、容错能力和扩展性，根据运用或组合运用这三种技术的策略和架构，拥有 RAID0 ~ RAID6 等的等级，近年来，存储厂商不断推出诸如 RAID7 、 RAID10/01 、 RAID50 、 RAID53 、 RAID100 等 RAID 等级，但这些并无统一的标准。 目前业界公认的标准是 RAID0 ~ RAID5 ，除 RAID2 外的四个等级被定为工业标准，而在实际应用领域中使用最多的 RAID 等级是 RAID0 、 RAID1 、 RAID3 、 RAID5 、 RAID6 和 RAID10。 从实现角度看， RAID 主要分为软 RAID、硬 RAID 以及软硬混合 RAID 三种。软 RAID 所有功能均有操作系统和 CPU 来完成，没有独立的 RAID 控制 / 处理芯片和 I/O 处理芯片，效率自然最低。硬 RAID 配备了专门的 RAID 控制 / 处理芯片和 I/O 处理芯片以及阵列缓冲，不占用 CPU 资源，但成本很高。软硬混合 RAID 具备 RAID 控制 / 处理芯片，但缺乏 I/O 处理芯片，需要 CPU 和驱动程序来完成，性能和成本 在软 RAID 和硬 RAID 之间。 目前来看，广泛采用的是以硬 RAID 为主的方式来实现 RAID，但各操作系统也都在不同程度上支持某些级别的软 RAID。关键技术RAID 主要有三个关键技术： 1. 镜像（Mirroring） 2. 数据条带（Data Stripping） 3. 校验（Data Parity） 不同的 RAID 级别会采用上述一到多个技术来获得不同的数据可靠性、可用性或IO性能。镜像镜像技术指的是将数据复制到多个磁盘，这样，在实际的使用中，只要有一个磁盘没有发生故障，都不影响整个磁盘系统的工作。 同时，并发地在多个磁盘上读取也可以提高读取性能，但是，相对的，镜像技术的写性能相对较低，每次写入都必须确保多个磁盘写成功。数据条带将数据分片保存在多个不同的磁盘，多个数据分片共同组成一个完整数据副本。 这与镜像的多个副本是不同的，它通常用于性能考虑。 数据条带具有更高的并发粒度，当访问数据时，可以同时对位于不同磁盘上数据进行读写操作， 从而获得非常可观的 I/O 性能提升。数据校验利用冗余数据进行数据错误检测和修复的技术。 冗余数据通常采用海明码、异或操作等算法来计算获得。 利用校验功能，可以很大程度上提高磁盘阵列的可靠性、鲁棒性和容错能力。 比起镜像 50% 的冗余开销 ，数据校验要小很多，它利用校验冗余信息对数据进行校验和纠错。 不过，数据校验需要从多处读取数据并进行计算和对比，会影响系统性能。优势大容量、高性能、可靠性、可管理性JBODJBOD 即 Just a Bunch Of Disks，他并不是标准的 RAID 等级。 它通常用来表示一个没有控制软件提供协调控制的磁盘集合，将多个物理磁盘串联起来，提供一个巨大的逻辑磁盘。 JBOD 的存放机制是由第一块磁盘开始按顺序往后存储，当前磁盘存储空间用完后，再依次往后面的磁盘存储数据。 它的存储性能完全等同于单块磁盘，而且也不提供数据安全保护，只是简单提供一种扩展存储空间的机制， JBOD 可用存储容量等于所有成员磁盘的存储空间之和。RAID0RAID 是一种简单、无数据冗余的数据带化技术，实际上它并不算是一种真正的 RAID，因为他不提供任何形式的冗余策略。 RAID0 将所在磁盘条带化后组成大容量存储空间，将数据分散存储在所有磁盘中，以独立访问方式实现多块磁盘的并发访问。 由于可以并发执行 I/O 操作，总线带宽得到充分利用。再加上不需要进行数据校验，RAID0 的性能在所有 RAID 等级中是最高的。 理论上讲，一个由 n 块磁盘组成的 RAID0 ，它的读写性能是单个磁盘性能的 n 倍，但由于总线带宽等多种因素的限制，实际的性能提升低于理论值。 RAID0 具有低成本、高读写性能、 100% 的高存储空间利用率等优点，但是它不提供数据冗余保护，一旦数据损坏，将无法恢复。 因此， RAID0 一般适用于对性能要求严格但对数据安全性和可靠性不高的应用，如视频、音频存储、临时数据缓存空间等。RAID1RAID1 称为镜像，它将数据完全一致地分别写到工作磁盘和镜像 磁盘，它的磁盘空间利用率为 50% RAID1 在数据写入时，响应时间会有所影响，但是读数据的时候没有影响。 RAID1 提供了最佳的数据保护，一旦工作磁盘发生故障，系统自动从镜像磁盘读取数据，不会影响用户工作。 RAID1 与 RAID0 刚好相反，是为了增强数据安全性使两块 磁盘数据呈现完全镜像，从而达到安全性好、技术简单、管理方便。 RAID1 拥有完全容错的能力，但实现成本高。 RAID1 应用于对顺序读写性能要求高以及对数据保护极为重视的应用，如对邮件系统的数据保护。RAID5至少使用3块硬盘（也可以更多）组建RAID5，一个磁盘发生损坏后，不会影响数据的完整性，从而保证了数据安全。常用在服务器数据存储中。当损坏的磁盘被替换后，RAID还会自动利用剩下奇偶校验信息去重建此磁盘上的数据，来保持RAID5的高可靠性。
功能描述代码删除文件夹find 文件夹名 -delete删除文件夹rm -rf 文件夹名删除文件rn -r 文件名退出文件编辑按ESC，然后：输入wq或者q查找a开头的文件ls a*删除所有a开头的文件rm -rf a*创建文件touch 文件名查看防火墙状态systemctl status firewalld/iptables开启防火墙systemctl start firewalld/iptables关闭防火墙systemctl stop firewalld/iptables删除当前目录下所有文件rm *上传文件scp test.png root@192.168.1.1:/home/查看端口监听状态netstat -aptu查看系统版本信息cat /etc/os-release文件重命名mv old_filename new_filename
情况描述用一台电脑做tracker负载均衡服务器，用两台电脑做storage做存储节点，两个storage归属于同group，数据自动进行备份。端口占用tracker占用端口为22122storage占用端口为23000storage的nginx映射为8888一个包既可以是tracker，也可以是storage，同时放出4个端口，8080，8888，22122，23000docker安装查看可用镜像docker search fastdfs拉取镜像docker pull delron/fastdfs在负载均衡服务器上，创建tracker服务docker run -d --name tracker --network=host -v /mydata/fastdfs/tracker:/var/fdfs delron/fastdfs trackernetwork=host意思是docker容器直接采用本机网络-v 后面是进行磁盘映射，将本机的电脑内存和docker容器内的内存映射到一起/mydata/fastdfs/tracker 为宿主机的物理内存地址，要实际存在在存储节点服务器上，创建storage服务docker run -d --name storage --network=host -e TRACKER_SERVER=192.168.32.128:22122 -v /mydata/fastdfs/storage:/var/fdfs -e GROUP_NAME=group1 delron/fastdfs storage/mydata/fastdfs/storage 为宿主机的物理内存地址，要实际存在更改storage的nginx监听端口（可选）本操作均在storage服务器内执行进/etc/fdfs/目录下，编辑storage.conf，在最后一行，更改http端口号进/ust/local/nginx/conf ，编辑nginx.conf，将listen的监听端口也同样进行修改原生上传文件功能先将文件上传到宿主机与storage容器映射的文件夹内docker exec命令进入storage容器再storage容器下，找到该映射路径，进入该目录。输入命令/usr/bin/fdfs_upload_file /etc/fdfs/client.conf 文件名 系统返回group名和路径名即成功通过http://storage容器IP地址:8888/系统上传后返回的信息。即可查看文件样例请求链接http://192.168.32.128:8888/group1/M00/00/00/wKgggGSD2feAXsv4AER92Sd_dzk072.jpg
基于过滤拦截技术实现的身份认证方案
基于过滤拦截技术实现的身份认证方案系统基本流程用户发送请求过滤器过滤掉，访问需要保持登录状态的资源但是又没有携带token的请求拦截器拦截掉，访问需要具备相应权限的资源但是又没有对应权限的请求进入业务逻辑代码层级configMyFilterConfig：自定义过滤器的基础配置，对所有资源都进行过滤MyInterceptorConfig：自定义拦截器的基础配置，设置所需拦截的放行的URLcontrollerUserController：接口层entityUser：用户的实体对象。目前为了简化操作，不必新建权限表，所以在用户的实体对象里存放了list<String>集合，即用户的属性里就带着用户的所有权限信息filter：MyFilter：自定义过滤器，这块主要是判断用户请求有没有携带token，如果没有携带token，而且还访问的是需要携带token(即保持登录)的接口，那么就过滤掉用户的这次请求。但是，有的资源是属于开放资源，即不需要用户保持登录状态，因此，在这个模块，做了白名单列表，过滤器不对白名单列表进行过滤。interceptorMyInterceptor：自定义拦截器，这块主要是拦截需要权限的接口，获取用户的token，并且解析出用户权限信息，拿用户权限信息和接口所需权限进行比对，失败则拒绝，成功则让请求进行接口层。serviceUserService：接口所对应的具体业务逻辑，这块目前主要是有判断用户名密码，进而生成token，返回前端。utilJWTUtil：JWT工具类，可以用于生成JWT，解密JWT
微机基本结构基本构成一般计算机包括五大部件，运算器、控制器、存储器、输入设备、输出设备。通过将运算器和控制器两大部件集成在一个集成电路芯片上，称为中央处理器，简称CPU，也叫微处理器。总线结构数据总线：CPU与内存或其他器件之间进行数据传送的通道地址总线：通过地址总线来指定存储单元控制总线：CPU通过控制总线对外部期间进行控制指令执行串行方式特点1：当CPU在指令执行阶段，不需要占用系统总线，但此时总线也不工作，因此系统总线的空闲时间比较多。 特点2：从存储器取指令、取数据或存数据时，总线处于忙状态，其所占用的时间也较长。而CPU却只需要花很短的时间去处理，因此大部分时间处于闲置状态。 指令流水线方式采用指令流水线可以显著提高工作效率。CPU内部采用了一种先进的指令六十线结构，这种结构可以有效而充分的利用各主要硬件资源。要实现指令流水线方式，从CPU组成结构上要划分成多个单元。 EUEU的主要任务是分析与执行指令，其具体包括1、从指令队列中取出指令代码，由控制器译码后产生相应的控制信号，继而控制各部件完成指令规定的操作。2、对操作数执行各种指定的算数或逻辑运算。3、向总线接口单元BIU发送访问主存或I/O的命令，并提供响应的地址和传送的数据。ALU取指令、存取数据
详解HTTPWeb及网络基础为了更好的理解什么是HTTP协议，我们需要先了解TCP/IP协议族。目前通常使用的网络都是在基于TCP/IP协议族的情况下进行信息传输，而HTTP也实际上是属于TCP/IP协议模型。要知道，不同的计算机之间要通过网络进行通信，那么必须遵循统一的规定，才能进行交流。这也就是协议的作用，通过制定规约，让不同的计算机可以进行信息互换。TCP/IP协议最重要的一点是对于信息加工传输进行了分层处理，这种分层结构清晰明了。TCP/IP协议族按层次划分为应用层、网络层、传输层、数据链路层。发送端数据依次经过应用层、网络层、传输层、数据链路层；接收端依次从数据链路层、传输层、网络层、应用层接收数据。而我们的HTTP协议就应用在应用层。HTTP报文用于HTTP协议交互的信息被称为是HTTP报文。客户端的HTTP报文叫做请求报文，响应端的HTTP报文称为响应报文。HTTP的报文是由多行数据构成的字符串文本。其结构大致可以分为报文首部和报文主体两块，两者通过空行进行划分，通常，报文是没有主体的。HTTP结构HTTP请求报文分为报文首部、空行、报文主体，其中报文首部内存放了请求行、请求首部字段、通用首部字段、实体首部字段和其他。HTTP响应报文分为报文首部、空行、报文主体，其中报文首部分为状态行、响应首部字段、通用首部字段、实体首部字段以及其他。HTTP状态码1XX：通知1XX系列响应代码仅在与HTTP服务器沟通时使用。100("Continue") 重要程度：中等，但（写操作时）很少用。这是对HTTP LBYL（look-before-you-leap）请求的一个可能的响应。该响应代码表明：客户端应重新发送初始请求，并在请求中附上第一次请求时未提供的（可能很大或者包含敏感信息的）表示。客户端这次发送的请求不会被拒绝。对LBYL请求的另一个可能的响应是417("Expectation Failed")。请求报头：要做一个LBYL请求，客户端必须把Expect请求报头设为字符串"100-continue"。除此以外，客户端还需要设置其他一些报头，服务器将根据这些报头决定是响应100还是417。101("Switching Protocols") 重要程度：非常低。当客户端通过在请求里使用Upgrade报头，以通知服务器它想改用除HTTP协议之外的其他协议时，客户端将获得此响应代码。101响应代码表示“行，我现在改用另一个协议了”。通常HTTP客户端会在收到服务器发来的101响应后关闭与服务器的TCP连接。101响应代码意味着，该客户端不再是一个HTTP客户端，而将成为另一种客户端。尽管可以通过Upgrade报头从HTTP切换到HTTPS，或者从HTTP1.1切换到某个未来的版本，但实际使用Upgrade报头的情况比较少。Upgrade报头也可用于HTTP切换到一个完全不同的协议（如IRC）上，但那需要在Web服务器切换为一个IRC服务器的同时，Web客户端切换为一个IRC的客户端，因为服务器将立刻在同一个TCP连接上开始使用新的协议。请求报头：客户端把Upgrade报头设置为一组希望使用的协议。响应报头：如果服务器同意切换协议，它就返回一个Upgrade报头，说明它将切换到那个协议，并附上一个空白行。服务器不用关闭TCP链接，而是直接在该TCP连接上开始使用新的协议。2XX: 成功2XX系列响应代码表明操作成功了。200("OK") 重要程度：非常高。一般来说，这是客户端希望看到的响应代码。它表示服务器成功执行了客户端所请求的动作，并且在2XX系列里没有其他更适合的响应代码了。实体主体：对于GET请求，服务器应返回客户端所请求资源的一个表示。对于其他请求，服务器应返回当前所选资源的一个表示，或者刚刚执行的动作的一个描述。-201("Created")重要程度：高。当服务器依照客户端的请求创建了一个新资源时，发送此响应代码。响应报头：Location报头应包含指向新创建资源的规范URI。实体主体：应该给出新创建资源的描述与链接。若已经在Location报头里给出了新资源的URI，那么可以用新资源的一个表示作为实体主体。-202("Accepted")重要程度：中等。客户端的请求无法或将不被实时处理。请求稍后会被处理。请求看上去是合法的，但在实际处理它时有出现问题的可能。若一个请求触发了一个异步操作，或者一个需要现实世界参与的动作，或者一个需要很长时间才能完成且没必要让Web客户端一直等待的动作时，这个相应代码是一个合适的选择。响应报头：应该把未处理完的请求暴露为一个资源，以便客户端稍后查询其状态。Location报头可以包含指向该资源的URI。实体主体：若无法让客户端稍后查询请求的状态，那么至少应该提供一个关于何时能处理该请求的估计。203("Non-Authoritative Information") 重要程度：非常低。这个响应代码跟200一样，只不过服务器想让客户端知道，有些响应报头并非来自该服务器--他们可能是从客户端先前发送的一个请求里复制的，或者从第三方得到的。响应报头：客户端应明白某些报头可能是不准确的，某些响应报头可能不是服务器自己生成的，所以服务器也不知道其含义。204("No Content") 重要程度：高。若服务器拒绝对PUT、POST或者DELETE请求返回任何状态信息或表示，那么通常采用此响应代码。服务器也可以对GET请求返回此响应代码，这表明“客户端请求的资源存在，但其表示是空的”。注意与304("Not Modified")的区别。204常常用在Ajax应用里。服务器通过这个响应代码告诉客户端：客户端的输入已被接受，但客户端不应该改变任何UI元素。实体主体：不允许。205("Reset Content") 重要程度：低。它与204类似，但与204不同的是，它表明客户端应重置数据源的视图或数据结构。假如你在浏览器里提交一个HTML表单，并得到响应代码204，那么表单里的各个字段值不变，可以继续修改它们；但假如得到的响应代码205，那么表单里的各个字段将被重置为它们的初始值。从数据录入方面讲：204适合对单条记录做一系列编辑，而205适于连续输入一组记录。206("Partial Content") 重要程度：对于支持部分GET（partial GET）的服务而言“非常高”，其他情况下“低”。它跟200类似，但它用于对部分GET请求（即使用Range请求报头的GET请求）的响应。部分GET请求常用于大型二进制文件的断点续传。请求报头：客户端为Range请求报头设置一个值。响应报头：需要提供Date报头。ETag报头与Content-Location报头的值应该跟正常GET请求相同。若实体主体是单个字节范围（byte range），那么HTTP响应里必须包含一个Content-Range报头，以说明本响应返回的是表示的哪个部分，若实体主体是一个多部分实体（multipart entity）（即该实体主体由多个字节范围构成），那么每一个部分都要有自己的Content-Range报头。实体主体：不是整个表示，而是一个或者多个字节范围。3XX 重定向3XX系列响应代码表明：客户端需要做些额外工作才能得到所需要的资源。它们通常用于GET请求。他们通常告诉客户端需要向另一个URI发送GET请求，才能得到所需的表示。那个URI就包含在Location响应报头里。300("Multiple Choices") 重要程度：低。若被请求的资源在服务器端存在多个表示，而服务器不知道客户端想要的是哪一个表示时，发送这个响应代码。或者当客户端没有使用Accept-*报头来指定一个表示，或者客户端所请求的表示不存在时，也发送这个响应代码。在这种情况下，一种选择是，服务器返回一个首选表示，并把响应代码设置为200，不过它也可以返回一个包含该资源各个表示的URI列表，并把响应代码设为300。响应报头：如果服务器有首选表示，那么它可以在Location响应报头中给出这个首选表示的URI。跟其他3XX响应代码一样，客户端可以自动跟随Location中的URI。实体主体：一个包含该资源各个表示的URI的列表。可以在表示中提供一些信息，以便用户作出选择。301("Moved Permanently") 重要程度：中等。服务器知道客户端试图访问的是哪个资源，但它不喜欢客户端用当前URI来请求该资源。它希望客户端记住另一个URI，并在今后的请求中使用那个新的URI。你可以通过这个响应代码来防止由于URI变更而导致老URI失效。响应报头：服务器应当把规范URI放在Location响应报头里。实体主体：服务器可以发送一个包含新URI的信息，不过这不是必需的。302("Found") 重要程度：应该了解，特别市编写客户端时。但我不推荐使用它。这个响应代码市造成大多数重定向方面的混乱的最根本原因。它应该是像307那样被处理。实际上，在HTTP 1.0中，响应代码302的名称是”Moved Temporarily”，不幸的是，在实际生活中，绝大多数客户端拿它像303一样处理。它的不同之处在于当服务器为客户端的PUT，POST或者DELETE请求返回302响应代码时，客户端要怎么做。为了消除这一混淆，在HTTP 1.1中，该响应代码被重命名为"Found"，并新加了一个响应代码307。这个响应代码目前仍在广泛使用，但它的含义市混淆的，所以我建议你的服务发送307或者303，而不要发送302.除非你知道正在与一个不能理解303或307的HTTP 1.0客户端交互。响应报头：把客户端应重新请求的那个URI放在Location报头里。实体主体：一个包含指向新URI的链接的超文本文档（就像301一样）。303("See Other") 重要程度：高。请求已经被处理，但服务器不是直接返回一个响应文档，而是返回一个响应文档的URI。该响应文档可能是一个静态的状态信息，也可能是一个更有趣的资源。对于后一种情况，303是一种令服务器可以“发送一个资源的表示，而不强迫客户端下载其所有数据”的方式。客户端可以向Location报头里的URI发送GET请求，但它不是必须这么做。303响应代码是一种规范化资源URI的好办法。一个资源可以有多个URIs，但每个资源的规范URI只有一个，该资源的所有其他URIs都通过303指向该资源的规范URI，例如：303可以把一个对http://www.example.com/software/current.tar.gz的请求重定向到http://www.example.com/software/1.0.2.tar.gz。响应报头：Location报头里包含资源的URI。实体主体：一个包含指向新URI的链接的超文本文档。304("Not Modified") 重要程度：高。这个响应代码跟204("No Content")类似：响应实体主体都必须为空。但204用于没有主体数据的情况，而304用于有主体数据，但客户端已拥有该数据，没必要重复发送的情况。这个响应代码可用于条件HTTP请求（conditional HTTP request).如果客户端在发送GET请求时附上了一个值为Sunday的If-Modified-Since报头，而客户端所请求的表示在服务器端自星期日（Sunday）以来一直没有改变过，那么服务器可以返回一个304响应。服务器也可以返回一个200响应，但由于客户端已拥有该表示，因此重复发送该表示只会白白浪费宽带。响应报头：需要提供Date报头。Etag与Content-Location报头的值，应该跟返回200响应时的一样。若Expires, Cache-Control及Vary报头的值自上次发送以来已经改变，那么就要提供这些报头。实体主体：不允许。305("Use Proxy") 重要程度：低。这个响应代码用于告诉客户端它需要再发一次请求，但这次要通过一个HTTP代理发送，而不是直接发送给服务器。这个响应代码使用的不多，因为服务器很少在意客户端是否使用某一特定代理。这个代码主要用于基于代理的镜像站点。现在，镜像站点（如http://www.example.com.mysite.com/）包含跟原始站点（如 http://www.example.com/）一样的内容，但具有不同的URI，原始站点可以通过307把客户端重新定向到镜像站点上。假如有基于代理的镜像站点，那么你可以通过把 http://proxy.mysite.com/设为代理，使用跟原始URI（http://www.example.com/）一样的URI来访问镜像站点。这里，原始站点example.com可以通过305把客户端路由到一个地理上接近客户端的镜像代理。web浏览器一般不能正确处理这个响应代码，这是导致305响应代码用的不多的另一个原因。响应报头：Location报头里包含代理的URI。306 未使用 重要程度：无306 响应代码没有在HTTP标准中定义过。307("Temporary Redirect") 重要程度：高。请求还没有被处理，因为所请求的资源不在本地：它在另一个URI处。客户端应该向那个URI重新发送请求。就GET请求来说，它只是请求得到一个表示，该响应代码跟303没有区别。当服务器希望把客户端重新定向到一个镜像站点时，可以用307来响应GET请求。但对于POST，PUT及DELETE请求，它们希望服务器执行一些操作，307和303有显著区别。对POST，PUT或者DELETE请求响应303表明：操作已经成功执行，但响应实体将不随本响应一起返回，若客户端想要获取响应实体主体，它需要向另一个URI发送GET请求。而307表明：服务器尚未执行操作，客户端需要向Location报头里的那个URI重新提交整个请求。响应报头： 把客户端应重新请求的那个URI放在Location报头里。实体主体：一个包含指向新URI的链接的超文本文档。4XX：客户端错误这些响应代码表明客户端出现错误。不是认证信息有问题，就是表示格式或HTTP库本身有问题。客户端需要自行改正。400("Bad Request") 重要程度：高。这是一个通用的客户端错误状态，当其他4XX响应代码不适用时，就采用400。此响应代码通常用于“服务器收到客户端通过PUT或者POST请求提交的表示，表示的格式正确，但服务器不懂它什么意思”的情况。实体主体：可以包含一个错误的描述文档。401("Unauthorized") 重要程度：高。客户端试图对一个受保护的资源进行操作，却又没有提供正确的认证证书。客户端提供了错误的证书，或者根本没有提供证书。这里的证书（credential）可以是一个用户名/密码，也可以市一个API key，或者一个认证令牌。客户端常常通过向一个URI发送请求，并查看收到401响应，以获知应该发送哪种证书，以及证书的格式。如果服务器不想让未授权的用户获知某个资源的存在，那么它可以谎报一个404而不是401。这样做的缺点是：客户端需要事先知道服务器接受哪种认证--这将导致HTTP摘要认证无法工作。响应报头：WWW-Authenticate报头描述服务器将接受哪种认证。实体主体：一个错误的描述文档。假如最终用户可通过“在网站上注册”的方式得到证书，那么应提供一个指向该注册页面的链接。402("Payment Required") 重要程度：无。除了它的名字外，HTTP标准没有对该响应的其他方面作任何定义。因为目前还没有用于HTTP的微支付系统，所以它被留作将来使用。尽管如此，若存在一个用于HTTP的微支付系统，那么这些系统将首先出现在web服务领域。如果想按请求向用户收费，而且你与用户之间的关系允许这么做的话，那么或许用得上这个响应代码。注：该书印于2008年403("Forbidden") 重要程度：中等。客户端请求的结构正确，但是服务器不想处理它。这跟证书不正确的情况不同--若证书不正确，应该发送响应代码401。该响应代码常用于一个资源只允许在特定时间段内访问，或者允许特定IP地址的用户访问的情况。403暗示了所请求的资源确实存在。跟401一样，若服务器不想透露此信息，它可以谎报一个404。既然客户端请求的结构正确，那为什么还要把本响应代码放在4XX系列（客户端错误），而不是5XX系列（服务端错误）呢？因为服务器不是根据请求的结构，而是根据请求的其他方面（比如说发出请求的时间）作出的决定的。实体主体：一个描述拒绝原因的文档（可选）。404("Not Found") 重要程度：高。这也许是最广为人知的HTTP响应代码了。404表明服务器无法把客户端请求的URI转换为一个资源。相比之下，410更有用一些。web服务可以通过404响应告诉客户端所请求的URI是空的，然后客户端就可以通过向该URI发送PUT请求来创建一个新资源了。但是404也有可能是用来掩饰403或者401.405("Method Not Allowd") 重要程度：中等。客户端试图使用一个本资源不支持的HTTP方法。例如：一个资源只支持GET方法，但是客户端使用PUT方法访问。响应报头：Allow报头列出本资源支持哪些HTTP方法，例如：Allow：GET，POST406("Not Acceptable") 重要程度：中等。当客户端对表示有太多要求，以至于服务器无法提供满足要求的表示，服务器可以发送这个响应代码。例如：客户端通过Accept头指定媒体类型为application/json+hic，但是服务器只支持application/json。服务器的另一个选择是：忽略客户端挑剔的要求，返回首选表示，并把响应代码设为200。实体主体：一个可选表示的链接列表。407("Proxy Authentication Required") 重要程度：低。只有HTTP代理会发送这个响应代码。它跟401类似，唯一区别在于：这里不是无权访问web服务，而是无权访问代理。跟401一样，可能是因为客户端没有提供证书，也可能是客户端提供的证书不正确或不充分。请求报头：客户端通过使用Proxy-Authorization报头（而不是Authorization）把证书提供给代理。格式跟Authrization一样。响应报头：代理通过Proxy-Authenticate报头（而不是WWW-Authenticate）告诉客户端它接受哪种认证。格式跟WWW-Authenticate一样。408("Reqeust Timeout") 重要程度：低。假如HTTP客户端与服务器建立链接后，却不发送任何请求（或从不发送表明请求结束的空白行），那么服务器最终应该发送一个408响应代码，并关闭此连接。409("Conflict") 重要程度：高。此响应代码表明：你请求的操作会导致服务器的资源处于一种不可能或不一致的状态。例如你试图修改某个用户的用户名，而修改后的用户名与其他存在的用户名冲突了。响应报头：若冲突是因为某个其他资源的存在而引起的，那么应该在Location报头里给出那个资源的URI。实体主体：一个描述冲突的文档，以便客户端可以解决冲突。410("Gone") 重要程度：中等。这个响应代码跟404类似，但它提供的有用信息更多一些。这个响应代码用于服务器知道被请求的URI过去曾指向一个资源，但该资源现在不存在了的情况。服务器不知道该资源的新URI，服务器要是知道该URI的话，它就发送响应代码301.410和310一样，都有暗示客户端不应该再请求该URI的意思，不同之处在于：410只是指出该资源不存在，但没有给出该资源的新URI。RFC2616建议“为短期的推广服务，以及属于个人但不继续在服务端运行的资源”采用410.411("Length Required") 重要程度：低到中等。若HTTP请求包含表示，它应该把Content-Length请求报头的值设为该表示的长度（以字节为单位）。对客户端而言，有时这不太方便（例如，当表示是来自其他来源的字节流时）。所以HTTP并不要求客户端在每个请求中都提供Content-Length报头。但HTTP服务器可以要求客户端必须设置该报头。服务器可以中断任何没有提供Content-Length报头的请求，并要求客户端重新提交包含Content-Length报头的请求。这个响应代码就是用于中断未提供Content-Lenght报头的请求的。假如客户端提供错误的长度，或发送超过长度的表示，服务器可以中断请求并关闭链接，并返回响应代码413。412("Precondition Failed") 重要程度：中等。客户端在请求报头里指定一些前提条件，并要求服务器只有在满足一定条件的情况下才能处理本请求。若服务器不满足这些条件，就返回此响应代码。If-Unmodified-Since是一个常见的前提条件。客户端可以通过PUT请求来修改一个资源，但它要求，仅在自客户端最后一次获取该资源后该资源未被别人修改过才能执行修改操作。若没有这一前提条件，客户端可能会无意识地覆盖别人做的修改，或者导致409的产生。请求报头：若客户但设置了If-Match，If-None-Match或If-Unmodified-Since报头，那就有可能得到这个响应代码。If-None-Match稍微特别一些。若客户端在发送GET或HEAD请求时指定了If-None-Match，并且服务器不满足该前提条件的话，那么响应代码不是412而是304，这是实现条件HTTP GET的基础。若客户端在发送PUT，POST或DELETE请求时指定了If-None-Match,并且服务器不满足该前提条件的话，那么响应代码是412.另外，若客户端指定了If-Match或If-Unmodified-Since(无论采用什么HTTP方法)，而服务器不满足该前提条件的话，响应代码也是412。413("Request Entity Too Large") 重要程度：低到中等。这个响应代码跟411类似，服务器可以用它来中断客户端的请求并关闭连接，而不需要等待请求完成。411用于客户端未指定长度的情况，而413用于客户端发送的表示太大，以至于服务器无法处理。客户端可以先做一个LBYL（look-before-you-leap）请求，以免请求被413中断。若LBYL请求获得响应代码为100，客户端再提交完整的表示。响应报头：如果因为服务器方面临时遇到问题（比如资源不足），而不是因为客户端方面的问题而导致中断请求的话，服务器可以把Retry-After报头的值设为一个日期或一个间隔时间，以秒为单位，以便客户端可以过段时间重试。414("Request-URI Too Long") 重要程度：低。HTTP标准并没有对URI长度作出官方限制，但大部分现有的web服务器都对URI长度有一个上限，而web服务可能也一样。导致URI超长的最常见的原因是：表示数据明明是该放在实体主体里的，但客户端却把它放在了URI里。深度嵌套的数据结构也有可能引起URI过长。415("Unsupported Media Type") 重要程度：中等。当客户端在发送表示时采用了一种服务器无法理解的媒体类型，服务器发送此响应代码。比如说，服务器期望的是XML格式，而客户端发送的确实JSON格式。如果客户端采用的媒体类型正确，但格式有问题，这时最好返回更通用的400。416("Requestd Range Not Satisfiable") 重要程度：低。当客户端所请求的字节范围超出表示的实际大小时，服务器发送此响应代码。例如：你请求一个表示的1-100字节，但该表示总共只用99字节大小。请求报头：仅当原始请求里包含Range报头时，才有可能收到此响应代码。若原始请求提供的是If-Range报头，则不会收到此响应代码。响应报头：服务器应当通过Content-Range报头告诉客户端表示的实际大小。417("Expectation Failed") 重要程度：中等。此响应代码跟100正好相反。当你用LBYL请求来考察服务器是否会接受你的表示时，如果服务器确认会接受你的表示，那么你将获得响应代码100，否则你将获得417。5XX 服务端错误这些响应代码表明服务器端出现错误。一般来说，这些代码意味着服务器处于不能执行客户端请求的状态，此时客户端应稍后重试。有时，服务器能够估计客户端应在多久之后重试。并把该信息放在Retry-After响应报头里。5XX系列响应代码在数量上不如4XX系列多，这不是因为服务器错误的几率小，而是因为没有必要如此详细--对于服务器方面的问题，客户端是无能为力的。500("Internal Server Error") 重要程度：高。这是一个通用的服务器错误响应。对于大多数web框架，如果在执行请求处理代码时遇到了异常，它们就发送此响应代码。501("Not Implemented") 重要程度：低。客户端试图使用一个服务器不支持的HTTP特性。最常见的例子是：客户端试图做一个采用了拓展HTTP方法的请求，而普通web服务器不支持此请求。它跟响应代码405比较相似，405表明客户端所用的方法是一个可识别的方法，但该资源不支持，而501表明服务器根本不能识别该方法。502("Bad Gateway") 重要程度：低。只有HTTP代理会发送这个响应代码。它表明代理方面出现问题，或者代理与上行服务器之间出现问题，而不是上行服务器本身有问题。若代理根本无法访问上行服务器，响应代码将是504。503("Service Unavailable") 重要程度：中等到高。此响应代码表明HTTP服务器正常，只是下层web服务服务不能正常工作。最可能的原因是资源不足：服务器突然收到太多请求，以至于无法全部处理。由于此问题多半由客户端反复发送请求造成，因此HTTP服务器可以选择拒绝接受客户端请求而不是接受它，并发送503响应代码。响应报头：服务器可以通过Retry-After报头告知客户端何时可以重试。504("Gateway Timeout") 重要程度：低。跟502类似，只有HTTP代理会发送此响应代码。此响应代码表明代理无法连接上行服务器。505("HTTP Version Not Supported") 重要程度： 非常低。当服务器不支持客户端试图使用的HTTP版本时发送此响应代码。实体主体：一个描述服务器支持哪些协议的文档。Web服务器HTTP进行通信时，实际上会经过很多的转发程序，例如通过代理、网关和隧道。代理：代理时一种具有转发功能的应用程序，接收由客户端发送的请求并转发至服务器，同时也接收服务器返回的响应给客户端隧道：隧道是在相隔距离较远的客户端和服务器之间进行中转，保证双方通信连接完整网关：严格来说，网关和代理的功能较为相似，区别在于代理连接的是两个或多个使用相同协议的应用程序，而网关连接的则是两个或多个使用不同协议的应用程序。HTTP首部请求首部字段首部字段名 说明 Accept-Ranges 是否接受字节范围请求 Age 推算资源创建经过时间 ETag 资源的匹配信息 Location 令客户端重定向至指定URI Proxy-Authenticate 代理服务器对客户端的认证信息 Retry-After 对再次发起请求的时机要求 Server HTTP 服务器的安装信息 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息响应首部字段首部字段名 说明 Accept-Ranges 是否接受字节范围请求 Age 推算资源创建经过时间 ETag 资源的匹配信息 Location 令客户端重定向至指定URI Proxy-Authenticate 代理服务器对客户端的认证信息 Retry-After 对再次发起请求的时机要求 Server HTTP 服务器的安装信息 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息实体首部字段首部字段名 说明 Allow 资源可支持的HTTP方法 Content-Encoding 实体主体适用的编码方式 Content-Language 实体主体的自然语言 Content-Length 实体主体的大小（单位：字节） Content-Location 替代对应资源的URI Content-MD5 实体主体的报文摘要 Content-Range 实体主体的位置范围 Content-Type 实体主体的媒体类型 Expires 实体主体过期的日期时间 Last-Modified 资源的最后修改日期时间HTTP安全性HTTP使用明文进行传输，内容可能会被窃听HTTP不会验证通信方的身份，因此有可能遭遇伪装HTTP无法证明报文的完整性，所以有可能在请求传输途中遭遇纂改通过在HTTP协议的基础上，在通信接口使用SSL和TLS协议进行代替，通过使用SSL+TLS可以实现加密、认证以及完整性保护的功能。通常，HTTP直接和TCP进行通信，但当使用SSL是，则演变成先和SSL通信，继而由SSL和TCP进行通信。HTTP功能追加协议WebSocket：即Web浏览器和Web服务器之间的全双工通信标准，一旦Web服务器和客户端之间建立起WebSocket协议的通信连接， 之后所有的通信都将依靠这个协议进行通信，并且通信过程中可互相发送JSON、XML、HTML或图片等任意的数据。通过使用WebSocket，可以实现推送功能，支持由服务器向客户端推送数据的推送功能；可以减少通信量，只要建立了WebSocket连接，就希望一直保持着连接状态（HTTP需要多次的连接），而且WebSocket的首部信息量很小，通信量也相应的减少了。
彻底搞懂Session、Cookie、token的区别今天纠正了最大的一个误区，session、cookie、token到底是些什么东西，是有什么作用，值得记录一下。前言HTTP协议是无状态的，什么是无状态呢，就是你的下一次请求和上一次请求是没有任何关系的，服务器也不知道是一个人请求了两遍接口，还是两个人分别请求了一次接口。这样的好处肯定是服务器不需要作额外的操作来分辨，降低了服务器负载，但是但是但是，如果要做一个系统，这个系统操作很多，第一步是用户登录，后续的操作都是基于用户登录的情况下进行的，那么服务器就没办法识别了。所以这种情况行不通，必须有其他解决办法来实现服务器分辨用户的能力。Session会话模式session字面意思就是会话，所谓会话就是要保持你和另一个人沟通的这种状态。服务器为了区分用户，会给用户一个session_id，这个session_id一般存在客户端（浏览器）的cookie中，每次请求的时候都把session_id传给后端，后端很具session_id从session里面进行寻找，找到了，就相当于知道用户了。服务器会把session存放在服务器上，但是如果在高并发的情况下，服务器的session会存很多信息，而且session一般都在内存里，很影响服务器性能，所以在高并发状况下，不建议使用session。另外，它也不支持分布式，因为分布式相当于分成多个后端，那这样的话，我的session只能保存在一个后端，用户访问其他节点接口时候，就会出现认证失败的情况。token模式token模式相比于session模式，会更容易支持高并发。用户一开始请求服务器进行登录认证，认证成功之后，服务器根据用户的id等不敏感信息生成一个加密后的字符串序列，也就是token，把token放在响应头里，返回给前端用户，以后每次的请求，用户都需要带着token。后端接收到token，进行解析，解析成功，就认为用户已经登录，而且也可以根据当时加密所加的那些字段，识别用户身份。cookiecookie一般称为cookies，类型为小型的文本文件，相当于一个浏览器的小型数据库，用于存储简单不敏感信息，内存很小，一般存放session_id。如果系统采用session会话模式，那么前端从cookie中取得session_id，再去访问服务器，这是正常流程。由于cookie内存非常小，所以后续浏览器衍生出了SessionStorage、localstorage等浏览器的小型数据库。SessionStorage里的数据关闭浏览器自动清除。
授权，也叫访问控制，即在应用中控制谁能访问哪些资源（如访问页面/编辑数据/页面操作等）。在授权中需了解的几个关键对象：主体（Subject）、资源（Resource）、权限（Permission）、角色（Role） 主体主体，即访问应用的用户，在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。资源在应用中用户可以访问的任何东西，比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。权限安全策略中的原子授权单位，通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源，如：访问用户列表页面查看/新增/修改/删除用户数据（即很多时候都是CRUD（增查改删）式权限控制）打印文档等等。。。如上可以看出，权限代表了用户有没有操作某个资源的权利，即反映在某个资源上的操作允不允许，不反映谁去执行这个操作。所以后续还需要把权限赋予给用户，即定义哪个用户允许在某个资源上做什么操作（权限），Shiro不会去做这件事情，而是由实现人员提供。Shiro支持粗粒度权限（如用户模块的所有权限）和细粒度权限（操作某个用户的权限，即实例级别的），后续部分介绍。角色角色代表了操作集合，可以理解为权限的集合，一般情况下我们会赋予用户角色而不是权限，即这样用户可以拥有一组权限，赋予权限时比较方便。典型的如：项目经理、技术总监、CTO、开发工程师等都是角色，不同的角色拥有一组不同的权限。隐式角色：即直接通过角色来验证用户有没有操作权限，如在应用中CTO、技术总监、开发工程师可以使用打印机，假设某天不允许开发工程师使用打印机，此时需要从应用中删除相应代码；再如在应用中CTO、技术总监可以查看用户、查看权限；突然有一天不允许技术总监查看用户、查看权限了，需要在相关代码中把技术总监角色从判断逻辑中删除掉；即粒度是以角色为单位进行访问控制的，粒度较粗；如果进行修改可能造成多处代码修改。显示角色：在程序中通过权限控制谁能访问某个资源，角色聚合一组权限集合；这样假设哪个角色不能访问某个资源，只需要从角色代表的权限集合中移除即可；无须修改多处代码；即粒度是以资源/实例为单位的；粒度较细。请google搜索“RBAC”和“RBAC新解”分别了解“基于角色的访问控制”“基于资源的访问控制(Resource-Based Access Control)”。授权方式Shiro支持三种方式的授权：编程式：通过写if/else授权代码块完成：Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }注解式：通过在执行的Java方法上放置相应的注解完成：@RequiresRoles("admin") public void hello() { //有权限 } 没有权限将抛出相应的异常；JSP/GSP标签：在JSP/GSP页面通过相应的标签完成：<shiro:hasRole name="admin"> <!— 有权限 —></shiro:hasRole> 后续部分将详细介绍如何使用。 授权基于角色的访问控制（隐式角色）1、在ini配置文件配置用户拥有的角色（shiro-role.ini）[users] zhang=123,role1,role2 wang=123,role1规则即：“用户名=密码,角色1，角色2”，如果需要在应用中判断用户是否有相应角色，就需要在相应的Realm中返回角色信息，也就是说Shiro不负责维护用户-角色信息，需要应用提供，Shiro只是提供相应的接口方便验证，后续会介绍如何动态的获取用户角色。2、测试用例（com.github.zhangkaitao.shiro.chapter3.RoleTest）@Test public void testHasRole() { login("classpath:shiro-role.ini", "zhang", "123"); //判断拥有角色：role1 Assert.assertTrue(subject().hasRole("role1")); //判断拥有角色：role1 and role2 Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1", "role2"))); //判断拥有角色：role1 and role2 and !role3 boolean[] result = subject().hasRoles(Arrays.asList("role1", "role2", "role3")); Assert.assertEquals(true, result[0]); Assert.assertEquals(true, result[1]); Assert.assertEquals(false, result[2]); } Shiro提供了hasRole/hasRole用于判断用户是否拥有某个角色/某些权限；但是没有提供如hashAnyRole用于判断是否有某些权限中的某一个。@Test(expected = UnauthorizedException.class) public void testCheckRole() { login("classpath:shiro-role.ini", "zhang", "123"); //断言拥有角色：role1 subject().checkRole("role1"); //断言拥有角色：role1 and role3 失败抛出异常 subject().checkRoles("role1", "role3"); }Shiro提供的checkRole/checkRoles和hasRole/hasAllRoles不同的地方是它在判断为假的情况下会抛出UnauthorizedException异常。到此基于角色的访问控制（即隐式角色）就完成了，这种方式的缺点就是如果很多地方进行了角色判断，但是有一天不需要了那么就需要修改相应代码把所有相关的地方进行删除；这就是粗粒度造成的问题。基于资源的访问控制（显示角色）1、在ini配置文件配置用户拥有的角色及角色-权限关系（shiro-permission.ini）[users] zhang=123,role1,role2 wang=123,role1 [roles] role1=user:create,user:update role2=user:create,user:delete 规则：“用户名=密码，角色1，角色2”“角色=权限1，权限2”，即首先根据用户名找到角色，然后根据角色再找到权限；即角色是权限集合；Shiro同样不进行权限的维护，需要我们通过Realm返回相应的权限信息。只需要维护“用户——角色”之间的关系即可。2、测试用例（com.github.zhangkaitao.shiro.chapter3.PermissionTest）@Test public void testIsPermitted() { login("classpath:shiro-permission.ini", "zhang", "123"); //判断拥有权限：user:create Assert.assertTrue(subject().isPermitted("user:create")); //判断拥有权限：user:update and user:delete Assert.assertTrue(subject().isPermittedAll("user:update", "user:delete")); //判断没有权限：user:view Assert.assertFalse(subject().isPermitted("user:view")); } Shiro提供了isPermitted和isPermittedAll用于判断用户是否拥有某个权限或所有权限，也没有提供如isPermittedAny用于判断拥有某一个权限的接口。@Test(expected = UnauthorizedExceptionclass) public void testCheckPermission () { login("classpath:shiro-permissionini", "zhang", "123"); //断言拥有权限：user:create subject().checkPermission("user:create"); //断言拥有权限：user:delete and user:update subject().checkPermissions("user:delete", "user:update"); //断言拥有权限：user:view 失败抛出异常 subject().checkPermissions("user:view"); } 但是失败的情况下会抛出UnauthorizedException异常。到此基于资源的访问控制（显示角色）就完成了，也可以叫基于权限的访问控制，这种方式的一般规则是“资源标识符：操作”，即是资源级别的粒度；这种方式的好处就是如果要修改基本都是一个资源级别的修改，不会对其他模块代码产生影响，粒度小。但是实现起来可能稍微复杂点，需要维护“用户——角色，角色——权限（资源：操作）”之间的关系。 Permission字符串通配符权限规则：“资源标识符：操作：对象实例ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串，“:”表示资源/操作/实例的分割；“,”表示操作的分割；“*”表示任意资源/操作/实例。1、单个资源单个权限subject().checkPermissions("system:user:update");用户拥有资源“system:user”的“update”权限。2、单个资源多个权限ini配置文件role41=system:user:update,system:user:delete然后通过如下代码判断subject().checkPermissions("system:user:update", "system:user:delete");用户拥有资源“system:user”的“update”和“delete”权限。如上可以简写成：ini配置（表示角色4拥有system:user资源的update和delete权限）role42="system:user:update,delete"接着可以通过如下代码判断subject().checkPermissions("system:user:update,delete");通过“system:user:update,delete”验证"system:user:update, system:user:delete"是没问题的，但是反过来是规则不成立。3、单个资源全部权限ini配置role51="system:user:create,update,delete,view"然后通过如下代码判断subject().checkPermissions("system:user:create,delete,update:view");用户拥有资源“system:user”的“create”、“update”、“delete”和“view”所有权限。如上可以简写成：ini配置文件（表示角色5拥有system:user的所有权限）role52=system:user:*也可以简写为（推荐上边的写法）：role53=system:user然后通过如下代码判断subject().checkPermissions("system:user:*"); subject().checkPermissions("system:user"); 通过“system:user:*”验证“system:user:create,delete,update:view”可以，但是反过来是不成立的。4、所有资源全部权限ini配置role61=*:view然后通过如下代码判断subject().checkPermissions("user:view"); 用户拥有所有资源的“view”所有权限。假设判断的权限是“"system:user:view”，那么需要“role5=::view”这样写才行。5、实例级别的权限5.1、单个实例单个权限ini配置role71=user:view:1对资源user的1实例拥有view权限。然后通过如下代码判断 subject().checkPermissions("user:view:1");5.2、单个实例多个权限ini配置 role72="user:update,delete:1"对资源user的1实例拥有update、delete权限。然后通过如下代码判断subject().checkPermissions("user:delete,update:1"); subject().checkPermissions("user:update:1", "user:delete:1");5.3、单个实例所有权限ini配置 role73=user:*:1对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");5.4、所有实例单个权限ini配置 role74=user:auth:*对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:auth:1", "user:auth:2");5.5、所有实例所有权限ini配置 role75=user::对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:view:1", "user:auth:2");6、Shiro对权限字符串缺失部分的处理如“user:view”等价于“user:view:”；而“organization”等价于“organization:”或者“organization::”。可以这么理解，这种方式实现了前缀匹配。另外如“user:”可以匹配如“user:delete”、“user:delete”可以匹配如“user:delete:1”、“user::1”可以匹配如“user:view:1”、“user”可以匹配“user:view”或“user:view:1”等。即可以匹配所有，不加可以进行前缀匹配；但是如“:view”不能匹配“system:user:view”，需要使用“::view”，即后缀匹配必须指定前缀（多个冒号就需要多个来匹配）。7、WildcardPermission如下两种方式是等价的： subject().checkPermission("menu:view:1"); subject().checkPermission(new WildcardPermission("menu:view:1"));因此没什么必要的话使用字符串更方便。8、性能问题通配符匹配方式比字符串相等匹配来说是更复杂的，因此需要花费更长时间，但是一般系统的权限不会太多，且可以配合缓存来提供其性能，如果这样性能还达不到要求我们可以实现位操作算法实现性能更好的权限匹配。另外实例级别的权限验证如果数据量太大也不建议使用，可能造成查询权限及匹配变慢。可以考虑比如在sql查询时加上权限字符串之类的方式在查询时就完成了权限匹配。 授权流程流程如下：1、首先调用Subject.isPermitted/hasRole接口，其会委托给SecurityManager，而SecurityManager接着会委托给Authorizer；2、Authorizer是真正的授权者，如果我们调用如isPermitted(“user:view”)，其首先会通过PermissionResolver把字符串转换成相应的Permission实例；3、在进行授权之前，其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限；4、Authorizer会判断Realm的角色/权限是否和传入的匹配，如果有多个Realm，会委托给ModularRealmAuthorizer进行循环判断，如果匹配如isPermitted/hasRole会返回true，否则返回false表示授权失败。ModularRealmAuthorizer进行多Realm匹配流程：1、首先检查相应的Realm是否实现了实现了Authorizer；2、如果实现了Authorizer，那么接着调用其相应的isPermitted/hasRole接口进行匹配；3、如果有一个Realm匹配那么将返回true，否则返回false。如果Realm进行授权的话，应该继承AuthorizingRealm，其流程是：1.1、如果调用hasRole*，则直接获取AuthorizationInfo.getRoles()与传入的角色比较即可；1.2、首先如果调用如isPermitted(“user:view”)，首先通过PermissionResolver将权限字符串转换成相应的Permission实例，默认使用WildcardPermissionResolver，即转换为通配符的WildcardPermission；2、通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合；通过AuthorizationInfo. getStringPermissions()得到字符串集合并通过PermissionResolver解析为Permission实例；然后获取用户的角色，并通过RolePermissionResolver解析角色对应的权限集合（默认没有实现，可以自己提供）；3、接着调用Permission. implies(Permission p)逐个与传入的权限比较，如果有匹配的则返回true，否则false。 Authorizer、PermissionResolver及RolePermissionResolverAuthorizer的职责是进行授权（访问控制），是Shiro API中授权核心的入口点，其提供了相应的角色/权限判断接口，具体请参考其Javadoc。SecurityManager继承了Authorizer接口，且提供了ModularRealmAuthorizer用于多Realm时的授权匹配。PermissionResolver用于解析权限字符串到Permission实例，而RolePermissionResolver用于根据角色解析相应的权限集合。我们可以通过如下ini配置更改Authorizer实现：authorizer=org.apache.shiro.authz.ModularRealmAuthorizer securityManager.authorizer=$authorizer对于ModularRealmAuthorizer，相应的AuthorizingSecurityManager会在初始化完成后自动将相应的realm设置进去，我们也可以通过调用其setRealms()方法进行设置。对于实现自己的authorizer可以参考ModularRealmAuthorizer实现即可，在此就不提供示例了。设置ModularRealmAuthorizer的permissionResolver，其会自动设置到相应的Realm上（其实现了PermissionResolverAware接口），如：permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver authorizer.permissionResolver=$permissionResolver设置ModularRealmAuthorizer的rolePermissionResolver，其会自动设置到相应的Realm上（其实现了RolePermissionResolverAware接口），如：rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver authorizer.rolePermissionResolver=$rolePermissionResolver示例1、ini配置（shiro-authorizer.ini） [main] 自定义authorizer authorizer=org.apache.shiro.authz.ModularRealmAuthorizer 自定义permissionResolverpermissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver permissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.BitAndWildPermissionResolver authorizer.permissionResolver=$permissionResolver 自定义rolePermissionResolverrolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver 1authorizer.rolePermissionResolver=$rolePermissionResolver securityManager.authorizer=$authorizer自定义realm 一定要放在securityManager.authorizer赋值之后（因为调用setRealms会将realms设置给authorizer，并给各个Realm设置permissionResolver和rolePermissionResolver） realm=com.github.zhangkaitao.shiro.chapter3.realm.MyRealm securityManager.realms=$realm 设置securityManager 的realms一定要放到最后，因为在调用SecurityManager.setRealms时会将realms设置给authorizer，并为各个Realm设置permissionResolver和rolePermissionResolver。另外，不能使用IniSecurityManagerFactory创建的IniRealm，因为其初始化顺序的问题可能造成后续的初始化Permission造成影响。2、定义BitAndWildPermissionResolver及BitPermissionBitPermission用于实现位移方式的权限，如规则是：权限字符串格式：+资源字符串+权限位+实例ID；以+开头中间通过+分割；权限：0 表示所有权限；1 新增（二进制：0001）、2 修改（二进制：0010）、4 删除（二进制：0100）、8 查看（二进制：1000）；如 +user+10 表示对资源user拥有修改/查看权限。public class BitPermission implements Permission { private String resourceIdentify; private int permissionBit; private String instanceId; public BitPermission(String permissionString) { String[] array = permissionString.split("\\+"); if(array.length > 1) { resourceIdentify = array[1]; } if(StringUtils.isEmpty(resourceIdentify)) { resourceIdentify = "*"; } if(array.length > 2) { permissionBit = Integer.valueOf(array[2]); } if(array.length > 3) { instanceId = array[3]; } if(StringUtils.isEmpty(instanceId)) { instanceId = "*"; } } @Override public boolean implies(Permission p) { if(!(p instanceof BitPermission)) { return false; } BitPermission other = (BitPermission) p; if(!("*".equals(this.resourceIdentify) || this.resourceIdentify.equals(other.resourceIdentify))) { return false; } if(!(this.permissionBit ==0 || (this.permissionBit & other.permissionBit) != 0)) { return false; } if(!("*".equals(this.instanceId) || this.instanceId.equals(other.instanceId))) { return false; } return true; } } Permission接口提供了boolean implies(Permission p)方法用于判断权限匹配的； public class BitAndWildPermissionResolver implements PermissionResolver { @Override public Permission resolvePermission(String permissionString) { if(permissionString.startsWith("+")) { return new BitPermission(permissionString); } return new WildcardPermission(permissionString); } } BitAndWildPermissionResolver实现了PermissionResolver接口，并根据权限字符串是否以“+”开头来解析权限字符串为BitPermission或WildcardPermission。 3、定义MyRolePermissionResolver RolePermissionResolver用于根据角色字符串来解析得到权限集合。 public class MyRolePermissionResolver implements RolePermissionResolver { @Override public Collection<Permission> resolvePermissionsInRole(String roleString) { if("role1".equals(roleString)) { return Arrays.asList((Permission)new WildcardPermission("menu:*")); } return null; } } 此处的实现很简单，如果用户拥有role1，那么就返回一个“menu:*”的权限。4、自定义Realmpublic class MyRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRole("role1"); authorizationInfo.addRole("role2"); authorizationInfo.addObjectPermission(new BitPermission("+user1+10")); authorizationInfo.addObjectPermission(new WildcardPermission("user1:*")); authorizationInfo.addStringPermission("+user2+10"); authorizationInfo.addStringPermission("user2:*"); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //和com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1. getAuthenticationInfo代码一样，省略 } } 此时我们继承AuthorizingRealm而不是实现Realm接口；推荐使用AuthorizingRealm，因为：AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)：表示获取身份验证信息；AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)：表示根据用户身份获取授权信息。这种方式的好处是当只需要身份验证时只需要获取身份验证信息而不需要获取授权信息。对于AuthenticationInfo和AuthorizationInfo请参考其Javadoc获取相关接口信息。另外我们可以使用JdbcRealm，需要做的操作如下：1、执行sql/ shiro-init-data.sql 插入相关的权限数据；2、使用shiro-jdbc-authorizer.ini配置文件，需要设置jdbcRealm.permissionsLookupEnabled为true来开启权限查询。此次还要注意就是不能把我们自定义的如“+user1+10”配置到INI配置文件，即使有IniRealm完成，因为IniRealm在new完成后就会解析这些权限字符串，默认使用了WildcardPermissionResolver完成，即此处是一个设计权限，如果采用生命周期（如使用初始化方法）的方式进行加载就可以解决我们自定义permissionResolver的问题。5、测试用例public class AuthorizerTest extends BaseTest { @Test public void testIsPermitted() { login("classpath:shiro-authorizer.ini", "zhang", "123"); //判断拥有权限：user:create Assert.assertTrue(subject().isPermitted("user1:update")); Assert.assertTrue(subject().isPermitted("user2:update")); //通过二进制位的方式表示权限 Assert.assertTrue(subject().isPermitted("+user1+2"));//新增权限 Assert.assertTrue(subject().isPermitted("+user1+8"));//查看权限 Assert.assertTrue(subject().isPermitted("+user2+10"));//新增及查看 Assert.assertFalse(subject().isPermitted("+user1+4"));//没有删除权限 Assert.assertTrue(subject().isPermitted("menu:view"));//通过MyRolePermissionResolver解析得到的权限 } }
一 shiro的简介shiro是apache的一个开源框架，是一个权限管理的框架，实现 用户认证、用户授权。二 shiro的架构图authenticator:认证器，主体进行认证最终通过authenticator进行authorizer:授权器，主体进行授权最终通过authorizer进行sessionManager:web应用中一般是用web容器对session进行管理，shiro也提供了一套session管理方式SessionDao:通过SessionDao管理session数据，以下是SessionDao的方法public interface SessionDAO {/*如DefaultSessionManager在创建完session后会调用该方法； 如保存到关系数据库/文件系统/NoSQL数据库；即可以实现会话的持久化； 返回会话ID；主要此处返回的ID.equals(session.getId())； */ Serializable create(Session session); //根据会话ID获取会话 Session readSession(Serializable sessionId) throws UnknownSessionException; //更新会话；如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用 void update(Session session) throws UnknownSessionException; //删除会话；当会话过期/会话停止（如用户退出时）会调用 void delete(Session session); //获取当前所有活跃用户，如果用户量多此方法影响性能 Collection<Session> getActiveSessions();cache Manager：缓存管理器，主要对session和授权数据进行缓存，比如将授权数据通过cacheManager进行缓存管理，和ehcache整合对缓存数据进行管理。三个核心组件:Subject, SecurityManager 和 Realms.Subject:即"当前操作用户"。但是，在Shiro中，Subject这一概念并不仅仅指人，也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着"当前跟软件交互的东西"。但考虑到大多数目的和用途，你可以把它认为是Shiro的"用户"概念。Subject代表了当前用户的安全操作，SecurityManager则管理所有用户的安全操作。SecurityManager:它是Shiro框架的核心，典型的Facade模式，Shiro通过SecurityManager来管理内部组件实例，并通过它来提供安全管理的各种服务。Realm: Realm充当了Shiro与应用安全数据间的"桥梁"或者"连接器"。也就是说，当对用户执行认证(登录)和授权(访问控制)验证时，Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲，Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节，并在需要时将相关数据提供给Shiro。当配置Shiro时，你必须至少指定一个Realm，用于认证和(或)授权。配置多个Realm是可以的，但是至少需要一个。Shiro内置了可以连接大量安全数据源(又名目录)的Realm，如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求，你还可以插入代表自定义数据源的自己的Realm实现。三 shiro提供的功能Authentication：用户认证（登录）Authorization：权限控制Session Management：会话管理Cryptography：数据加密Web Support：支持web的APICaching：缓存Concurrency：支持多线程应用程序Testing：测试的支持“Run As”：假设一个用户为另一个用户的身份“Remember Me”：在Session中保存用户身份
一、单系统登录机制1、http无状态协议 web应用采用browser/server架构，http作为通信协议。http是无状态协议，浏览器的每一次请求，服务器会独立处理，不与之前或之后的请求产生关联，这个过程用下图说明，三次请求/响应对之间没有任何联系3c91a3bf-25d8-4b1f-8e4a-68535c51aaa8 但这也同时意味着，任何用户都能通过浏览器访问服务器资源，如果想保护服务器的某些资源，必须限制浏览器请求；要限制浏览器请求，必须鉴别浏览器请求，响应合法请求，忽略非法请求；要鉴别浏览器请求，必须清楚浏览器请求状态。既然http协议无状态，那就让服务器和浏览器共同维护一个状态吧！这就是会话机制2、会话机制 浏览器第一次请求服务器，服务器创建一个会话，并将会话的id作为响应的一部分发送给浏览器，浏览器存储会话id，并在后续第二次和第三次请求中带上会话id，服务器取得请求中的会话id就知道是不是同一个用户了，这个过程用下图说明，后续请求与第一次请求产生了关联8a9fb230-d506-4b19-b821-4001c68c4588 服务器在内存中保存会话对象，浏览器怎么保存会话id呢？你可能会想到两种方式请求参数cookie 将会话id作为每一个请求的参数，服务器接收请求自然能解析参数获得会话id，并借此判断是否来自同一会话，很明显，这种方式不靠谱。那就浏览器自己来维护这个会话id吧，每次发送http请求时浏览器自动发送会话id，cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制，数据以”key/value“形式存储，浏览器发送http请求时自动附带cookie信息 tomcat会话机制当然也实现了cookie，访问tomcat服务器时，浏览器中可以看到一个名为“JSESSIONID”的cookie，这就是tomcat会话机制维护的会话id，使用了cookie的请求响应过程如下图518293d9-64b2-459c-9d45-9f353c757d1f3、登录状态 有了会话机制，登录状态就好明白了，我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份，服务器拿到用户名密码去数据库比对，正确的话说明当前持有这个会话的用户是合法用户，应该将这个会话标记为“已授权”或者“已登录”等等之类的状态，既然是会话的状态，自然要保存在会话对象中，tomcat在会话对象中设置登录状态如下用户再次访问时，tomcat在会话对象中查看登录状态HttpSession session = request.getSession();session.setAttribute("isLogin", true);HttpSession session = request.getSession();session.getAttribute("isLogin"); 实现了登录状态的浏览器请求服务器模型如下图描述 每次请求受保护资源时都会检查会话对象中的登录状态，只有 isLogin=true 的会话才能访问，登录机制因此而实现。二、多系统的复杂性 web系统早已从久远的单系统发展成为如今由多系统组成的应用群，面对如此众多的系统，用户难道要一个一个登录、然后一个一个注销吗？就像下图描述的这样6dfbb0b1-46c0-4945-a3bf-5f060fa80710 web系统由单系统发展成多系统组成的应用群，复杂性应该由系统内部承担，而不是用户。无论web系统内部多么复杂，对用户而言，都是一个统一的整体，也就是说，用户访问web系统的整个应用群与访问单个系统一样，登录/注销只要一次就够了9fe14ab3-4254-447b-b850-0436e628c254 虽然单系统的登录解决方案很完美，但对于多系统应用群已经不再适用了，为什么呢？ 单系统登录解决方案的核心是cookie，cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的，这个限制就是cookie的域（通常对应网站的域名），浏览器发送http请求时会自动携带与该域匹配的cookie，而不是所有cookie4d58ccfa-0114-486d-bec2-c28f2f9eb513 既然这样，为什么不将web应用群中所有子系统的域名统一在一个顶级域名下，例如“*.baidu.com”，然后将它们的cookie域设置为“baidu.com”，这种做法理论上是可以的，甚至早期很多多系统登录就采用这种同域名共享cookie的方式。 然而，可行并不代表好，共享cookie的方式存在众多局限。首先，应用群域名得统一；其次，应用群各系统使用的技术（至少是web服务器）要相同，不然cookie的key值（tomcat为JSESSIONID）不同，无法维持会话，共享cookie的方式是无法实现跨语言技术平台登录的，比如java、php、.net系统之间；第三，cookie本身不安全。 因此，我们需要一种全新的登录方式来实现多系统应用群的登录，这就是单点登录三、单点登录 什么是单点登录？单点登录全称Single Sign On（以下简称SSO），是指在多系统应用群中登录一个系统，便可在其他所有系统中得到授权而无需再次登录，包括单点登录与单点注销两部分1、登录 相比于单系统登录，sso需要一个独立的认证中心，只有认证中心能接受用户的用户名密码等安全信息，其他系统不提供登录入口，只接受认证中心的间接授权。间接授权通过令牌实现，sso认证中心验证用户的用户名密码没问题，创建授权令牌，在接下来的跳转过程中，授权令牌作为参数发送给各个子系统，子系统拿到令牌，即得到了授权，可以借此创建局部会话，局部会话登录方式与单系统的登录方式相同。这个过程，也就是单点登录的原理，用下图说明 下面对上图简要描述用户访问系统1的受保护资源，系统1发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数sso认证中心发现用户未登录，将用户引导至登录页面用户输入用户名密码提交登录申请sso认证中心校验用户信息，创建用户与sso认证中心之间的会话，称为全局会话，同时创建授权令牌sso认证中心带着令牌跳转会最初的请求地址（系统1）系统1拿到令牌，去sso认证中心校验令牌是否有效sso认证中心校验令牌，返回有效，注册系统1系统1使用该令牌创建与用户的会话，称为局部会话，返回受保护资源用户访问系统2的受保护资源系统2发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数sso认证中心发现用户已登录，跳转回系统2的地址，并附上令牌系统2拿到令牌，去sso认证中心校验令牌是否有效sso认证中心校验令牌，返回有效，注册系统2系统2使用该令牌创建与用户的局部会话，返回受保护资源 用户登录成功之后，会与sso认证中心及各个子系统建立会话，用户与sso认证中心建立的会话称为全局会话，用户与各个子系统建立的会话称为局部会话，局部会话建立之后，用户访问子系统受保护资源将不再通过sso认证中心，全局会话与局部会话有如下约束关系局部会话存在，全局会话一定存在全局会话存在，局部会话不一定存在全局会话销毁，局部会话必须销毁 你可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解，注意观察登录过程中的跳转url与参数2、注销 单点登录自然也要单点注销，在一个子系统中注销，所有子系统的会话都将被销毁，用下面的图来说明3b139d2e-0b83-4a69-b4f2-316adb8997ce sso认证中心一直监听全局会话的状态，一旦全局会话销毁，监听器将通知所有注册系统执行注销操作 下面对上图简要说明用户向系统1发起注销请求系统1根据用户与系统1建立的会话id拿到令牌，向sso认证中心发起注销请求sso认证中心校验令牌有效，销毁全局会话，同时取出所有用此令牌注册的系统地址sso认证中心向所有注册系统发起注销请求各注册系统接收sso认证中心的注销请求，销毁局部会话sso认证中心引导用户至登录页面四、部署图 单点登录涉及sso认证中心与众子系统，子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求，因而子系统必须集成sso的客户端，sso认证中心则是sso服务端，整个单点登录过程实质是sso客户端与服务端通信的过程，用下图描述fb29685c-487c-42b9-9ceb-6c7ee29e98c9 sso认证中心与sso客户端通信方式有多种，这里以简单好用的httpClient为例，web service、rpc、restful api都可以五、实现 只是简要介绍下基于java的实现过程，不提供完整源码，明白了原理，我相信你们可以自己实现。sso采用客户端/服务端架构，我们先看sso-client与sso-server要实现的功能（下面：sso认证中心=sso-server） sso-client拦截子系统未登录用户请求，跳转至sso认证中心接收并存储sso认证中心发送的令牌与sso-server通信，校验令牌的有效性建立局部会话拦截用户注销请求，向sso认证中心发送注销请求接收sso认证中心发出的注销请求，销毁局部会话 sso-server验证用户的登录信息创建全局会话创建授权令牌与sso-client通信发送令牌校验sso-client令牌有效性系统注册接收sso-client注销请求，注销所有会话 接下来，我们按照原理来一步步实现sso吧！1、sso-client拦截未登录请求 java拦截请求的方式有servlet、filter、listener三种方式，我们采用filter。在sso-client中新建LoginFilter.java类并实现Filter接口，在doFilter()方法中加入对未登录用户的拦截public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession(); if (session.getAttribute("isLogin")) { chain.doFilter(request, response); return; } //跳转至sso认证中心 res.sendRedirect("sso-server-url-with-system-url"); }2、sso-server拦截未登录请求 拦截从sso-client跳转至sso认证中心的未登录请求，跳转至登录页面，这个过程与sso-client完全一样3、sso-server验证用户登录信息 用户在登录页面输入用户名密码，请求登录，sso认证中心校验用户信息，校验成功，将会话状态标记为“已登录”@RequestMapping("/login") public String login(String username, String password, HttpServletRequest req) { this.checkLoginInfo(username, password); req.getSession().setAttribute("isLogin", true); return "success"; }4、sso-server创建授权令牌 授权令牌是一串随机字符，以什么样的方式生成都没有关系，只要不重复、不易伪造即可，下面是一个例子String token = UUID.randomUUID().toString();5、sso-client取得令牌并校验 sso认证中心登录后，跳转回子系统并附上令牌，子系统（sso-client）取得令牌，然后去sso认证中心校验，在LoginFilter.java的doFilter()中添加几行 // 请求附带token参数 String token = req.getParameter("token"); if (token != null) { // 去sso认证中心校验token boolean verifyResult = this.verify("sso-server-verify-url", token); if (!verifyResult) { res.sendRedirect("sso-server-url"); return; } chain.doFilter(request, response); } verify()方法使用httpClient实现，这里仅简略介绍，httpClient详细使用方法请参考官方文档HttpPost httpPost = new HttpPost("sso-server-verify-url-with-token"); HttpResponse httpResponse = httpClient.execute(httpPost);6、sso-server接收并处理校验令牌请求 用户在sso认证中心登录成功后，sso-server创建授权令牌并存储该令牌，所以，sso-server对令牌的校验就是去查找这个令牌是否存在以及是否过期，令牌校验成功后sso-server将发送校验请求的系统注册到sso认证中心（就是存储起来的意思） 令牌与注册系统地址通常存储在key-value数据库（如redis）中，redis可以为key设置有效时间也就是令牌的有效期。redis运行在内存中，速度非常快，正好sso-server不需要持久化任何数据。 令牌与注册系统地址可以用下图描述的结构存储在redis中，可能你会问，为什么要存储这些系统的地址？如果不存储，注销的时候就麻烦了，用户向sso认证中心提交注销请求，sso认证中心注销全局会话，但不知道哪些系统用此全局会话建立了自己的局部会话，也不知道要向哪些子系统发送注销请求注销局部会话3b221593-f9c4-45af-a567-4937786993e87、sso-client校验令牌成功创建局部会话 令牌校验成功后，sso-client将当前局部会话标记为“已登录”，修改LoginFilter.java，添加几行if (verifyResult) { session.setAttribute("isLogin", true); } sso-client还需将当前会话id与令牌绑定，表示这个会话的登录状态与令牌相关，此关系可以用java的hashmap保存，保存的数据用来处理sso认证中心发来的注销请求8、注销过程 用户向子系统发送带有“logout”参数的请求（注销请求），sso-client拦截器拦截该请求，向sso认证中心发起注销请求String logout = req.getParameter("logout"); if (logout != null) { this.ssoServer.logout(token); } sso认证中心也用同样的方式识别出sso-client的请求是注销请求（带有“logout”参数），sso认证中心注销全局会话@RequestMapping("/logout") public String logout(HttpServletRequest req) { HttpSession session = req.getSession(); if (session != null) { session.invalidate();//触发LogoutListener } return "redirect:/"; } sso认证中心有一个全局会话的监听器，一旦全局会话注销，将通知所有注册系统注销public class LogoutListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent event) {} @Override public void sessionDestroyed(HttpSessionEvent event) { //通过httpClient向所有注册系统发送注销请求 } }