前言去年写了一篇阿里云 FC 对接 OpenAI 的教程，没想到浏览量暴增，相比之前的技术文章，大家貌似对人工智能这块蛋糕比较感兴趣。随着 猿码盒 的开发以及一部分内测用户的体验，我也接到了几个委托，比如接入 Whisper 相关的语音系统、针对顾博士的研究课题进行数据计算等等，但是今天要讲的是另一位朋友的委托——将钉钉机器人接入 ChatGPT。前提阿里云账号 （本次需要用到 FC 和 OSS）OpenAI 账号 （需要绑定银行卡，不需要开会员）如果你没梯子，先找网上免费的用一下，然后到 这里 去买稳定套餐，我用五六年了，没出过任何问题。如果你没账号，暂不提供注册方式，自行研究一下。如果你没美区银行卡，到 这里 申请一张虚拟卡，会提供美国家庭环境和教程教你如何绑卡，费率不高，稳定性未知，才用了一年。钉钉账号开整Github可以先去 这里 学习一下本次需要用到的主要操作，或者直接跟我来。首先把这两份文件下载到本地 直接请求脚本 异步回复脚本两份脚本写的都不是很完美，咱们要修改一下逻辑，使用你的文本编辑器打开。第一份 Dingtalk_Conversation.py 脚本，阿里云 FC 执行的时候可能会获取不到环境变量而报错，由于我们是自己做的机器人，验签操作不影响结果，把框出来的代码删除。第二份 Dingtalk_ChatGPT_Reply.py 脚本，逻辑很怪，回复次数变量解析为整形，所以 HISTORY_LENGTH 配 1 时，会取最后两次的问答结果，配 0 时，Python 中数组切片 [0:]会取全部回答的记录，然而在某些特定的场景，我们不需要机器人有任何记忆的时候，通过脚本的环境变量就配不出来。因此，如果你不要机器人的记忆功能，那么把下面我框出来的部分删除。代码这块就算处理完了，其他按你的需求自行调整。记住把这两个代码放到两个文件夹里，或者压缩到两个压缩包里，因为后续阿里 FC 导入的时候不支持文件直接导入。阿里云OSS先去工作台创建一个 OSS，收费但可以忽略不计。FC这部分是重中之重，比较容易搞错，仔细看。先去工作台创建服务，地点必须选美国，除非你用三方代理的 api，否则官网的 api 无法调用，且有封号风险。取名为 ChatGTP_Services（取其他名字也行，但是记得改后面的环境变量），如果你懂技术，建议把日志开起来，配错的时候根据日志定位问题。然后要挂载一下刚才创建的 oss，否则无法异步处理消息。检查一下该服务有没有绑定角色，如果没有记得绑一下 AliyunFcDefaultRole，或其他有权限的角色。接下来绑定上面我们在 oss 里创建的 bucket，目录填 /mnt/oss。接下来我们要针对两份代码分别创建两个函数，首先是第一个Dingtalk_Conversation。高级参数推荐按我截图的配。环境变量把下面的复制过去后修改，Endpoint地址详见文档 https://help.aliyun.com/document_detail/52984.html，比如我选的弗吉尼亚，那就是 <主账号ID>.us-east-1-internal.fc.aliyuncs.com，如果第一步里的服务名称改了，那这里的SERVICE_NAME也记得修改，同理如果接下来要添加的回复函数名称不叫Dingtalk_ChatGPT_Reply这里也要改一下CHATGPT_FUNCTION。{ "CHATGPT_FUNCTION":"Dingtalk_ChatGPT_Reply", "DINGTALK_APP_SECRET":"修改为你的钉钉应用的appSecret", "ENDPOINT":"修改为你的阿里云函数计算Endpoint地址", "SERVICE_NAME":"ChatGTP_Services", "VERBOSE":"25" }触发器用默认的即可，点击创建后，再创建第二个函数Dingtalk_ChatGPT_Reply，这次要选择处理事件请求。再提醒一下，handler改成脚本的文件名。这次的环境变量用下面的，但是配置的修改有两种情况：你用的是官方接口，那么只要修改CHATGPT_API_KEY你用的是三方接口，那么要修改ENDPOINT为三方接入的 url，并且修改USER_API_KEY为三方提供的 key，上面的CHATGPT_API_KEY就不用改了，随便填什么HISTORY_LENGTH：代表会取多少次历史问答记录，比如配置1，那么会把上一次提问+回答+这一次提问，合并一起发送。如果你不需要历史记录，且已经按照上面我说的修改过代码，那么这里不用管了。OSS_MOUNT_POINT：这里配置成服务挂载的 oss 路径即可，建议多个不同的机器人绑定不同的路径，否则消息会被多个机器人同时共享。{ "CHATGPT_API_KEY":"修改为你的ChatGPT API Key", "HISTORY_LENGTH":"5", "OSS_MOUNT_POINT":"/mnt/oss", "TIMEOUT":"55", "VERBOSE":"25", "ENDPOINT":"https://api.openai.com", "USER_API_KEY":"" }最后一步，复制 Dingtalk_Conversation函数中触发器管理里的公网访问地址，后续要填入钉钉里。钉钉省略注册账号的步骤，随便创建一个组织后进入，然后创建一个机器人应用。将上面的地址配置到机器人里面，然后直接发布，不要使用测试（测试名称在发送消息时会解析失败）发布一下应用。发布完成之后，去钉钉群就可以使用啦。参考资料对于阿里 FC 接入不太熟悉的可以看看我上一篇发的文章： 国内使用阿里云 FC 对接 OpenAI API 方案脚本仓库：https://github.com/Sunny-Law/Dingtalk-ChatGPT-ConnectorChatGPT API 调用文档：https://platform.openai.com/docs/api-reference/making-requests钉钉机器人接收消息文档：https://open.dingtalk.com/document/orgapp/receive-message阿里云函数计算概述文档：https://help.aliyun.com/document_detail/61009.html阿里云对象存储文档: https://help.aliyun.com/product/31815.html阿里云AccessKey获取方式: https://help.aliyun.com/document_detail/116401.htm函数计算挂载OSS文档：https://help.aliyun.com/document_detail/454199.html
开篇 为什么写这篇文章作为一名合格的全栈开发工程师（外加运维、UI设计……），在开发软件的时候，需要处理很多的事务，有些操作是频繁的、重复的、可程序化的，如果能通过脚本或工具来自动实现这些步骤，那开发者所操心的就只有如何写出高效可靠的代码了。 多人/多端开发环境诟病倘若是个单人小型项目，使用我之前文章中的实践方针已经基本可以达到目的。但是多人开发或者是大型的项目需要考虑到联调环境以及其他不同的情况如下：后端本地测试：考虑到电脑性能以及网络限制问题，不打算在本地跑数据库、缓存和消息队列等，需要远程云服务器。但是阿里云服务器限制性较多，经过测试发现 Mysql8 只能通过 SSH 连，否则不给本地回传数据。Redis 可以直连，但是可能会被服务器中断连接。前端本地测试：前端若要安装后端项目的环境，会比较麻烦，如果能直接在阿里云服务器上部署后端项目，前端再启动 node 访问服务器的接口，对于前端来说就是无感联调了。生产环境部署：当开发完一个 Release 版本后，需要发布到服务器部署，这个时候再使用前后端本地测试时的环境配置就不适合了，部分配置需要修改为 localhost 或者直接上域名。所以前后端都得准备至少两套配置。 抛砖引玉基于上面的情况，使用 Jenkins 实现自动化的持续集成就最好不过了，有了这个工具，后端写完项目只要提交到 git ，就会被监听触发构建，根据配置的命令上传到服务器网站目录下并实现自动部署，方便前端联调。若是生产环境也只需要修改打包的环境变量即可。所以写一篇文章来记录一下整个流程。 前提条件一个存储在 Gitee 的前后端分离项目一台配置还可以的云服务器（不够 4GB 内存构建前端可能会溢出） 实战 Jenkins 配置关于容器方面本篇不做教学，博主使用的是 1Panel面板，自带容器环境。默认端口8777进入，一路安装默认插件注册管理员账号。然后在插件页面根据实际需求下载所需的插件。Maven IntegrationGiteeNodeJSPublish Over SSH 403 问题处理如果容器部署的 Jenkins 遇到了 403 的问题，可以进容器内部，先装个 vim，再编辑 /usr/local/bin/jenkins.sh 文件，在 exec java这一行加上一个参数-Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true，解除跨站请求改造的拦截。（高版本引入的这个问题） 引入 Gitee 凭证直接进【系统管理】-【系统配置】，拉到下面填写 Gitee 相关信息和凭证。添加凭证点击后，获取方式如下图中的链接，转到码云里面生成一个 token。 Maven 安装由于是容器部署的，还要额外在容器里安装maven，或者直接用 Jenkins 自动安装。 NodeJs 安装为了加快国内依赖包下载速度，建议使用 cnpm 并添加淘宝镜像源，不过此镜像不是完全稳定，如果打包遇到问题，也可以考虑使用 yarn、pnpm，或其他镜像。cnpm --registry=https://registry.npm.taobao.org SSH 配置如果非容器化本地部署的 Jenkins，无需考虑这一步的操作，可以直接在后续的构建后置操作中写脚本来完成。但是对于容器部署的环境，以及源码、流水线、成果物三者不在同一个环境，则需要使用到SSH工具来远程连接并进行操作。我这个 Jenkins 版本不支持 pem 格式的密钥，所以需要使用 rsa 在容器内生成密钥，并将密钥填写至系统管理中。ssh-keygen -m PEM -t rsa -f id_rsa别忘了把公钥放到你要连接的服务器上。最后添加一个 SSH 服务并测试连接即可。 创建任务并配置返回首页点击新建任务后，选择构建一个maven项目由于上面配置了 Gitee 的凭证，这里就无需选择了，而且本来也选不到。只需填写代码仓库路径和打包分支。勾选 webhook 触发构建，并用这里的 url 在码云中创建。构建环境中需要选上 node，这样后续才能用之前配置的 cnpm 打包。同理配置 maven 打包命令。此处跳过 test，并且使用 prod 环境变量来匹配我的项目中的 application-prod.yml，请根据实际项目对应生产环境的打包命令填写。在构建后置操作中添加 shell 脚本来打包前端文件，具体脚本请根据实际来填写。例如你的 Jenkins 和部署的成果物在同一个环境，那么可以直接在这里打包完 dist 之后，将文件 cp 到网站运行目录下，并且通过 sh 脚本启动 Java 项目。由于我的 Jenkins 在容器中，所以后续通过 SSH 来操作主机。# 第一行和最后一行是为了防止构建过程被 Jenkins 终止 BUILD_ID=dontKillMe cd ctb-ui rm -rf dist # 有时候遇到网络等问题，可以关闭 ssl 或者在这里切换镜像 cnpm config set strict-ssl false cnpm install # 根据实际生产环境打包命令填写 cnpm run build:prod export BUILD_ID=dontKillMe最后根据需要创建 SSH 步骤，由于传送文件过于缓慢，实测一个 100 MB 的 jar 包要传半小时，而容器部署的 Jenkins 原本就会持久化构建后的成果物目录，因此我写了个脚本，直接将挂载目录下的文件 cp 到项目路径下。特别注意：此处的脚本和文件传送的三个参数，至少有一种被填写，也可以都填写。都填写的情况下是先执行脚本再传文件的，一般来说我们的操作都是先传完文件再执行某些脚本，所以如果希望脚本后执行，第一个步骤可以不填，然后再添加一个 SSH 步骤，里面不填传文件的参数，只填脚本。 Gitee 配置 凭证获取生成令牌后填写至 Jenkins 中。 WebHooks此处的密码是在 Jenkins 中生成的，点击生成按钮即可，Url 也填写 Jenkins 配置页面上提示的即可。如果我们点击测试失败，不要紧，这是码云的问题。你也可以把请求 copy 到 postman 里，将 header 里的 X-Gitee-Event和X-GIT-OSCHINA-EVENT修改为Push Hook后再测试调用，或者直接触发推送来测试。结论通过如上的步骤，我们就可以实现自动化的生产环境持续集成。程序员提交代码（可指定分支，评论或 PR）Jenkins 拉取 Gitee 代码构建前后端成果物将成果物拷贝到网站目录下（或使用 dockerfile 制作成镜像）自动覆盖前端静态文件，启动后端程序（或启动容器部署）整个过程从程序员提交代码的那一刻起，到生产环境环境的更新，只需短短五分钟时间。配合其他优雅的操作，可以使整个流程更加简单高效。相比之前纯手工的本地打包，不再受限于本地环境的限制，也无需再考虑文件传输的费时与流量，更不用手动编写命令或执行脚本重启服务。
国内使用阿里云 FC 对接 OpenAI API 方案前言早在 ChatGPT 22 年 11 月底首次发布时，我就在第一时间体验了一把如今热的快要爆炸的人工智能。体验感就像老鼠舔了牛皮——属实牛批。之前元宇宙爆火的时候，我并没有太大的兴趣去参与，从程序员的角度上来看，这并不能成为一个突破性的技术，并且很难被推广和收到大众的喜爱。尽管在国外类似 Roblox 这类的元宇宙游戏，已经斩获接近 1kw 以上的用户的青睐，但在我看来这似乎只是一个 MMO + The Sim 的升级版。然而 ChatGPT 非同凡响，只是看了一言，就有一种：人类文明不存在了的感觉。尽管我在第一时间为此开发了软件，并且接入了人工智能，但是一个人的力量还是比不过团队，可以看到非常多原先的微信公众号，都开始卖课、卖会员、也有包装的非常好的换皮 GPT，最早一批变现的现在可能已经赚得盆满钵满了。GPT 并不 Open，而是逐渐 Close，限制登陆 IP，封禁批量注册用户……这也让我的脚步逐渐变缓。无论如何，我还是打算出一篇攻略，讲解一下国内的开发者们，如何以最稳健的方式在应用中对接 ChatGpt。前提你有阿里云账号你有 open ai 账号，且绑定了银行卡，能够正常调用 api下载代理包点击下载代理函数首先去 github 找到 openai-scf-prox 这是一个简易的代理程序，我们将使用他配合美区的云函数服务器，对国内请求进行反向代理，达到正常的调用目的。进入控制台进入阿里云函数计算FC控制台进入首页之后，一开始还不能选择地区，你会看到一些账号下的基础信息。创建服务进入【服务及函数】菜单，这里注意选择美区的地理位置，随便选一个地区即可，然后创建服务，随便填写名称。再点开高级选项，这里要创建一个阿里云的角色，并且分配 FC 的默认权限。其他的配置按照默认的即可（开日志功能和链路追踪要额外花钱）创建函数服务创建完成之后，就可以创建函数。选择【使用自定义运行时函数创建】->【处理HTTP请求】，运行环境推荐选比较稳定的 16，然后上传第一步里下载的代理包。在高级配置处，CPU 和内存全部选最小的，越小的免费调用额度越大 （好像是每月 1W 次？）并发度阿里云推荐压测后填写，代理包作者推荐填 2，我填的是 10，这块目前就自己用，没测试过。然后就可以创建了。集成到应用最后在触发器管理中，可以看到函数反向代理的 url。以 plexpt 的 java sdk 为例，填写触发器中的内网访问地址：以 openai 的 node sdk 为例：测试使用至此，一个简单的代理就完成了，国内网络均可快速调用 openai api。over！
开端这次也并不是为了写些什么，因为最近发生的事儿有些多，还都挺重要的，就想找个空闲时间记录一下。其实主要原因是我正在迁移服务器数据，总不能白等，无聊就随便写写咳咳，既然如此，先从数据迁移开始。数据迁移先来说说服务器的数据迁移。悲惨世界放心，今天没有看不懂的教程，没有天书。事情的经过是这样的：去年6月参加了阿里云的云翼计划，买了台学生价服务器，大概一百多吧，毛毛细雨，小钱小钱。But，最近提示我快到期了阿里爸爸要来收租了，点开续费一看。小了，格局小了。续费一年只要一千四，真便宜（bushi）。带着满脸黑人问号的我点开了云翼计划，里面写着，老活动失效了，现在可以通过新活动比原本更便宜的购买服务器并且按优惠价可续费3次。也就是说98元*4=392元可以买一台四年期限的服务器，好家伙，简直好到我可以提前准备好我女儿的诞辰礼物了。But，这和我又有什么关系呢，关键问题是，我原本的服务器到期了，而我想用优惠价续费，现在阿里告诉我只能花四年的优惠价给我的服务器续费四个月，这谁顶得住啊。再加上最近阿里的面试，这家公司在我心目当中的地位直线下降（后面再讲面试）。我花了一个下午的时间，研究如何在时间和空间上寻找突破点，后来，并没有找到。下面的解释会很绕：第一种方法，为原服务器创建快照后做成自定义镜像，然后使用自定义镜像创建服务器。听起来很好，我也试了，结果发现创建服务器的时候还是高达上千的费用，原因是没有通过云翼活动中购买，无法享受优惠。OK，既然知道必须通过云翼活动才能优惠，那么第二种方法，easy。那就是在云翼计划中购买服务器，但是选择系统镜像的时候，切换到同地区级别的自定义镜像。我也试了，此处忍住不爆粗口。因为云翼计划购买服务器的时候压根就没有自定义镜像的选择，也就是说我只能买一个空的服务器。也就是说，现在只能以优惠价买一台空壳，然后使用数据迁移技术将网站内容迁移至新服务器。所以我马上就买了一台机子，按照步骤往账户上充了一张毛爷爷，又开通了OSS服务，开通了RAM权限，巴拉巴拉等等等等。最后还进了一个数据迁移的钉钉群，当我看到迁移费用的时候，打扰了，出口在哪儿。如果仔细看完这三段，您一定能理解我有多么气愤，俺还是个孩子，至于这么绕弯子坑蒙拐骗吗。也就是说我研究了半天最后徒劳无获，最后我只能尝试，手动迁移！！！现在正在迁移过程中，文件传输和比较慢，如果成功我再把经验分享在此。如果失败……有谁可以995。路漫漫经过一整天的奋斗，总算是完成了两台阿里云服务器的数据迁移（纯手动，所以接下来给大家讲一下迁移的过程，顺带帮助大家远离大坑）。首先还是购买云服务器，这里就不说了，因为我现在算是老用户，不能购买ECS，所以买了台轻量应用服务器，之前那台也是轻量应用服务器。如果大家买的是ECS应该直接通过重置系统中的重置为自定义镜像就可以了。打开轻量应用服务器的管理页面后选择磁盘列表，选择右方的创建快照。创建快照的过程比较缓慢，请耐心等待。创建完成之后，进入快照列表，选择我们刚刚创建好的快照，然后创建自定义镜像。名称和描述自己写即可。记住镜像所在的地区必须和所要迁移到的服务器所在地区一致，可以到服务器列表中查看自己的服务器是买在哪个区的。最后我们就得到了一份自定义镜像。如果大家用的是ECS，先选择共享到ECS，然后就可以创建ECS，如果用的是轻量应用服务器，那就选最后的选项。没有学生优惠的朋友们，教程到此就已经结束了。有学生优惠购买了一台新服务器需要迁移的朋友们请看接下来的内容。复制粘贴大家记得开两个网页，同时进入两台服务器的管理页面，方便进行对照，快速配置。域名我们最后再解析，先点开两个防火墙。对照第一台服务器，同步第二台服务器的防火墙配置，ECS好像可以导出安全组什么的，没研究过轻量行不行，手动配也不是很累。像这样，尤其是宝塔面板需要的端口一定注意开启，其他根据原先服务器的项目来配。接下来直接安装宝塔面板，首先远程连接到你的服务器，使用sudo su root进入管理员模式，然后输入如下命令：centos脚本： yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh ubuntu脚本： wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh debian脚本： wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && bash install.sh一路yes之后，就静待安装。并不需要很长的时间，这里特别要注意。最后宝塔会给一个登陆的网址，用户名和密码，复制网址后打开并收藏，以免忘记。然后使用它给的账号登录面板。第一次初始化完成之后，会提示安装软件，推荐选第一个，差别好像也就第一个是是Nginx第二个是Apache。时间充裕的小朋友们可以选择编译安装，估计要等四十分钟左右。而快速安装只需十几分钟，为了我们服务器的稳定，建议还是选择稳健一些的编译安装。反正也是要等安装，不如乘机做一些有意义的事。如果你的服务器需要tomcat的话（我有一个项目就需要），选择软件商店里的tomcat进行安装，推荐版本8。他会自动排列到编译安装任务之后。然后我们打开原服务器的宝塔面板，选择安全，这里的操作就和刚刚开通服务器防火墙一样。说明里写了默认端口等等的不需要配置，装完所有的软件就会自动配。我们只需要配自己项目所需的端口，按照原服务器上的来即可。这里不能像服务器管理器里面一样群体配置，只能一个一个配，而且有些卡。大家耐心等待。放行完端口，软件应该还没装完，那么继续。看一看任务进度，如果MySQL装好了，就可以开始备份数据库，但是估计也没那么快，所以还是先备份网站吧。我们打开原服务器的网站。找到下面这个东东，然后把每个网站都备份一遍。备份完记得全部下载到本地，应该可以通过两台服务器的ftp文件传输，我觉得还是纯手动既快又方便。同样的我们打开数据库，也进行备份后下载到本地。现在我们的数据库应该已经安装好了，我们创建和原服务器上的同名同用户同密码的数据库即可，然后使用刚刚下载到本地的压缩包进行恢复。由于数据库的文件比较小，基本不需要等待。这里有一个非常坑的地方，昨晚也花了我一个多小时去研究。就是老版的宝塔面板中数据库的名字是可以大写可以小写的，而在新版中，即使你设置了一个大写的数据库名，仍旧会被自动改写成小写。并且当你同时创建一个大写一个小写的两个同名数据库之后，会提示你数据库已存在。那么问题就来了，我原项目中需要用的数据库名字是大写的。无论我怎么改，项目跑起来都不能连接数据库，并且我使用终端连接服务器后访问MySQL，也提示不存在这个大写的数据库。因此，我最后只能重新打开我不想去动的项目，重新修改文件内的配置为服务器的配置，修改连接的数据库名为小写字母，最后再打包、上传、部署、启动，期间还出现了ssl证书的问题以及数据库密码的错误，来来回回部署了半个多小时。其实大多数的时间全部浪费在了文件传输上，项目用的是jar包，内置了tomcat，以200kb的上传速度，龟速上传。期间并没有遇到特别大的问题。而关于需要使用内置tomcat部署war包的项目，相信宝塔基础软件的安装已经完成了，直接到文件，进入www/server/tomcat/wabapps目录中，将war包丢进去重启tomcat即可，这个项目的迁移也没有遇到问题。吃了个饭，讲到哪儿了。原来数据库已经装完了，那接下来恢复网站即可。首先按照原服务器的网站在新服务器上创建同名网站，如果数据库之前配过不需要再创建，否则打上勾。完成之后进到网站根目录，里面应该只有两个配置文件，不管它直接上传之前备份过的网站压缩包，这里需要等很长的时间，因为网站的数据包真的挺大的，完成之后解压到当前目录即可。我们按照同样的步骤恢复所有的网站数据，花不了很长的时间。差不多了，还要注意的是，如果是打包成jar的java项目，需要开启终端，进入目标目录后启动，启动方式见之前写过的文章，前端vue、后端ssm、前后端分离项目服务器部署实战。（因为我就是忘记启动了结果测试项目是否迁移成功的时候发现前端一直都不能给后端发送请求）。最后亿步，打开域名控制台，进入域名解析列表，然后点开你的每一个域名，修改所有的记录，将记录值改成你迁移服务器的公网地址。等待一段时间完成解析。解析完域名后在浏览器中访问你的网站，你的博客，你的项目，发现全都正常的跑起来了，服务器的数据迁移也就到此结束。后续咳咳，很久之后，我无意间点开了续费的页面，发现去年的服务器又可以按原价续费了，不清楚什么原因，不过既然都喷完了，那我也没办法。总结关于面试的部分，很多言语不太妥当，所以全部删掉了。本来还想多写一点，但是工作实在是占用了太多的时间，早上八点多起床，晚上回家洗衣服洗澡收拾完也接近九十点了，而且经常还要处理学校的事务，实在忙不过来，这篇博文也是写了快一个月，一直没有发布。以后不一定还会有时间学习新的技术，可能需要巩固一下我的基础知识，准备接下来的面试和工作，所以博客的更新到此告一段落。
开端目前主流的负载方案一般为两种：一种是集中式的负载均衡，在消费者和服务提供方中间使用独立的代理方式进行负载，有硬件（F5），也有软件（Nginx）。另一种则是客户端自己做负载均衡。我觉得使用Nginx也是一个不错的方式，但是这样我们还要令开一块专题去学习，既然在学Spring Cloud，我们就来试试使用Netflix开源的Ribbon做负载。另外ribbon的内容不是特别多，我们在此还会学习另一个知识点，声明式REST客户端Feign。Ribbon开始学习Ribbon。了解首先还是来了解一下Ribbon的基本模块：ribbon-loadbalancer：负载均衡模块，内置的负载均衡算法都实现在其中。ribbon-eureka：基于Eureka封装的模块，能够快速集成。ribbon-transport：基于Netty实现多协议的支持，比如HTTP、TCP、UDP等。ribbon-httpclient：基于Apache HttpClient封装的REST客户端，集成了负载均衡模块，可以直接在项目中调用接口。ribbon-example：代码示例，供学习。ribbon-core：核心且通用的代码，客户端API的一些配置和其他API的定义。使用浅尝结束初步的了解，咱们直接开始上手。关于我们之前Eureka的项目文件，大家可以放在一起，右键每一个pom文件后使用add as maven project功能构建。如此：这样一来就方便大家管理项目，以及统一启动和终止。我们首先复制一份eureka-client项目，创建一个消费者集群。启动类、控制层、配置文件、Maven文件的修改这里就不写了，都是些小细节。我将这个项目的端口设置为8084，然后启动，回到注册中心就可以看到集群成功。现在我们需要右键外面的大项目文件夹，重新创建一个ribbon-native-demo项目，创建方式同client。为pom添加依赖：<dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon-loadbalancer</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>io.reactivex</groupId> <artifactId>rxjava</artifactId> </dependency>复制一份client的yml，修改应用名为ribbon-native-demo，端口为8083。然后写一个测试类调用hello接口。public static void main(String[] args) { List<Server> serverList = Lists.newArrayList( new Server("localhost", 8081), new Server("localhost", 8084) ); ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder() .buildFixedServerListLoadBalancer(serverList); for (int i = 0; i < 6; i++) { String result = LoadBalancerCommand.<String>builder() .withLoadBalancer(loadBalancer) .build() .submit(new ServerOperation<String>() { @Override public Observable<String> call(Server server) { try { String addr = "http://" + server.getHost() + ":" + server.getPort() + "/test/hello"; System.out.println("调用地址: " + addr); URL url = new URL(addr); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setRequestMethod("GET"); conn.connect(); InputStream in = conn.getInputStream(); byte[] data = new byte[in.available()]; in.read(data); return Observable.just(new String(data)); } catch (Exception e) { return Observable.error(e); } } }).toBlocking().first(); System.out.println("调用结果: " + result); } }跑起来之后我们看到结果：循环六次，三次分配给了8081端口，另外三次分配给了8084端口，实现了负载均衡。这时候，各位同学有没有想起来在上一篇中我们添加了服务消费者，使用了RestTemplate的那个，还记不记得我们在调用接口时并没有指明端口号，而是直接使用服务的名字来调用，这时候负载的作用就体现出来了。仔细领悟，RestTemplate的具体用法我就不展开了。值得一提的是，如果想要脱离Eureka使用Ribbon，请加入下列pom，由于我们测试的时候是使用了Eureka的，所以没有配置：<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-atarter-netflix-ribbon</artifactId> </dependency>辄止说到底，我们在使用RestTemplate调用接口时，最关键的步骤在于@LoadBalanced这一个注解。多亏Spring Cloud为我们做了大量的底层工作，才得以如此方便的使用。其实它的原理莫过于拦截请求-替换地址-选择均衡策略-调用，这几步。由于源码像裹脚布一样又臭又长，难以理解，这里就不做分析，大体知道这个逻辑即可。拓展接下来从我们方便接触的角度尝试一下Ribbon的一些拓展功能。Ribbon API有时候面对一些特殊的需求，可能需要通过Ribbon获取对应的服务信息，可以使用LoadBalancerClient来获取，比如你想获取一个ribbon-eureka-demo服务的服务地址，可以通过LoadBalancerClient的choose方法来实现。@Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/choose") public Object chooseUrl() { return loadBalancerClient.choose("eureka-client"); }这样我们就可以获取到eureka-client这一个服务的信息，我们使用Postman测试结果如下：{ "metadata": { "management.port": "8081" }, "secure": false, "scheme": "http", "host": "xxx.xxx.xx.xx", "port": 8081, "uri": "http://xxx.xxx.xx.xx:8081", "serviceId": "EUREKA-CLIENT", "instanceInfo": { "instanceId": "eureka-client:xxx.xxx.xx.xx:8081", "app": "EUREKA-CLIENT", "appGroupName": null, "ipAddr": "192.168.43.87", "sid": "na", "homePageUrl": "http://xxx.xxx.xx.xx:8081/", "statusPageUrl": "http://xxx.xxx.xx.xx:8081/actuator/info", "healthCheckUrl": "http://xxx.xxx.xx.xx:8081/actuator/health", "secureHealthCheckUrl": null, "vipAddress": "eureka-client", "secureVipAddress": "eureka-client", "countryId": 1, "dataCenterInfo": { "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", "name": "MyOwn" }, "hostName": "xxx.xxx.xx.xx", "status": "UP", "overriddenStatus": "UNKNOWN", "leaseInfo": { "renewalIntervalInSecs": 5, "durationInSecs": 5, "registrationTimestamp": 1616034670752, "lastRenewalTimestamp": 1616035593582, "evictionTimestamp": 0, "serviceUpTimestamp": 1616034670754 }, "isCoordinatingDiscoveryServer": false, "metadata": { "management.port": "8081" }, "lastUpdatedTimestamp": 1616034670755, "lastDirtyTimestamp": 1616034670568, "actionType": "ADDED", "asgName": null }, "instanceId": "eureka-client:xxx.xxx.xx.xx:8081" }Eager Load加入网络不好，往往会令Ribbon的第一次调用超时，我们可以将超时时间改长，也可以禁用超时，但是最新版的Spring Cloud中提供了饥饿加载的策略解决这一问题。要开启饥饿加载，往yml中加入如下配置：ribbon: eager-load: enabled: true clients: ribbon-native-demo自定义差不多也算是Ribbon入门了，我们接下来了解一下他最核心的负载均衡策略。Ribbon默认的负载策略是轮询，也就是和我们之前测试中的结果一样，每次调用都依次分配给集群中的服务，谁都能吃到这一口瓜。但是实际的线上高并发状态，并不是这么简单就能解决的，因此Ribbon还提供了如下的几个策略：BestAvailable：选择一个最小并发请求的Server，逐个考察，若被标记为错误则跳过。AvailabilityFilteringRule：检查Status里记录的各个Server的运行状态，过滤掉高并发的后端Server。ZoneAvoidanceRule：判断Zone的运行性能是否可用，剔除掉不可用的并且过滤掉连接数过多的。RandomRule：顾名思义，随机选择，可能有惊喜，可能。RoundRobinRule：即默认的轮询方式。RetryRule：重试机制策略，若选择Server不成功则一直尝试使用subRule的方式选择一个可用Server。WeightedResponseTimeRule：根据响应时间分配一个权重，时间越长权重越小，被选中的概率也越小。ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule如此设置策略为随机。重试集群环境中，用多个节点提供服务，难免会遇到故障。由于Eureka是基于AP原则构建的，牺牲了数据的一致性，每个Eureka服务都会保存注册的服务信息，当心跳无法接受到时，仍会保存信息，这个时候Ribbon就可能拿到已经挂掉的服务信息，导致请求失败。我们可以为其配置重试机制。首先在pom中添加依赖：<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>总结总的来说，Ribbon是一款非常优秀的客户端负载均衡组件，但是在Spring Cloud里结合RestTemplate使用Ribbon还是相对麻烦的，所以接下来我们看看如何优雅的使用Feign去调用服务中的接口。Feign开始学习Feign。了解Feign是一个声明式的REST客户端，让接口的调用更加简单，它提供了HTTP请求的模板，通过编写简单的接口和插入注解，就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求，只需要像调用方法一样调用它就可以完成服务请求及相关处理。集成首先依旧是添加依赖： <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>在启动来加上注解@EnableFeignClients。如果接口定义和启动类不再同一个包名下，还需要指定扫描的包名@EnableFeignClients(basePackages = "com.ky.kevin")。然后定义一个Feign的客户端，以接口的形式存在。package com.ky.kevin.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @FeignClient(value = "eureka-client") public interface UserRemoteClient { @GetMapping("/test/hello") String hello(); }主要就是一个注解，value填写对应的eureka服务提供者。将提供的所有服务都单独抽出来，使用注解标记它的url即可。接下来回到控制层，之前写的大段大段测试语句可以删除了，创建的RestTemplate也可以删除了，写上下面的内容。package com.ky.kevin.controller; import com.ky.kevin.feign.UserRemoteClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/article") public class HelloController { @Autowired private UserRemoteClient userRemoteClient; @GetMapping("/callHello") private String callHello() { return userRemoteClient.hello(); } }接下来启动项目，多次访问路径，如果添加了输出信息可以发现访问成功而且自动完成了负载均衡。如果初步看这些操作，可能会觉得多次一局，多嵌套了一层，到时候修改接口会复杂不少。但是这样调用接口确实非常优雅，相当于我们之前每次调用都需要写上服务的名称（一开始甚至要写主机号和端口号），现在我们把这一块内容抽出来作为接口的注解，极大的简化了调用，并且这样一来也更加方便我们的视觉上的审阅。自定义讲讲自定义的配置方式。日志好像都是老生常谈的内容了，还是同一个问题，遇到Bug怎么办，那就定义一个配置类，把请求信息输出。package com.ky.kevin.configuration; import feign.Logger; import org.springframework.context.annotation.Bean; public class FeignConfiguration { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }接着回到FeignClient，将注解修改为@FeignClient(value = "eureka-client", configuration = FeignConfiguration.class)，表示使用指定的配置类。最后在yml中写上日志级别的显示如下：logging: level: com.ky.kevin.feign.UserRemoteClient: DEBUG再次启动程序，调用接口我们可以看到DEBUG信息：超时修改连接超时时间和读取超时时间。@Bean public Request.Options options() { return new Request.Options(5000, 10000); }压缩开启压缩以有效节约网络资源，提升接口性能，我们可以配置GZIP来压缩数据，在yml中添加一下配置：feign: compression: request: enabled: true mime-types: - text/xml - application/xml - application/json min-request-size: 2048 response: enabled: true总结通过本节，我们已经对Feign有了一个初步的了解，通过Feign简化调用接口的方式，还可以和Eureka何Ribbon轻松集成，所以还是非常值得学习的，相信接下来的项目实战中我们还有很多的机会可以接触这一块内容。大总结本文一共学习了Ribbon和Feign两块知识。我们现在已经可以自己创建注册中心集群，创建服务提供者和消费者集群。使用Ribbon实现有效的负载均衡策略，最后再利用Feign优化接口调用。从一开始的迷茫，无从下手，到现在雾霾的驱散，前方的道路越来越清晰。虽然这部分的内容在实战当中肯定还是经不起考验的，但是毕竟毕竟我们也只是初步的一个了解，最终肯定是由实践来检验这些真理。本来想试试Hystrix来保护高并发下的服务安全，但是目前我们手上的小项目好像并不会涉及到这方面的内容，所以放到最后有时间我们再回来，接下来的内容又是一块比较重要比较难的--Zuul网关，敬请期待。
开端抛开别的不说，在这大学三年的时间里，我总是有意无意的回避开了一门技术，那就是Spring Cloud。因为它复杂繁多的模块而让我望而生畏，因为它永远排在我目标技术栈的最后一位。即使前继的技术栈基本已经粗略撸了个遍，我还是迟迟不敢在分布式服务上动刀子。想想我的大学生活也不久了，早晚要整活微服务，然后写我的毕业设计。So,Hi there,I'm TremblingBoy.了解在初步了解了微服务框架之后，我总结出了几点微服务的优劣之处：优点拆分项目，独立部署，耦合度低。也就是说不同服务之间不需要高度的依赖。快速启动，拆分的项目依赖库减少，代码量减少，启动速度必然加快。敏捷开发，发布新版本只需发布对应服务。高校代码复用，所有底层实现都可以以接口方式提供。缺陷更多的网络问题、容错问题、调用关系等。独立的数据库面临更多的挑战，尤其是事务的问题。测试难度提升，API的修改涉及到所有调用它的服务。运维难度提升，部署和监控变得非常复杂。总而言之，能量守恒，有得必有失，不可能存在完美无缺的代码，我相信微服务的少量缺陷，都能被它的大量优势所弥补。常用模块Eureka：服务注册中心，用于服务管理。Ribbon：基于客户端的负载均衡组件。Hystrix：容错框架，能够防止服务的雪崩效应。Feign：Web服务客户端，能够简化HTTP接口的调用。Zuul：API网关，提供路由转发、请求过滤等功能。Config：分布式配置管理。Sleuth：服务跟踪。Stream：构建消息驱动的微服务应用程序的框架。Bus：消息代理的集群消息总线。Boot复习要想使用Spring Cloud，有必要使用Spring Boot来简化操作。之前没有写过Spring Boot的文章，今天在这先简单创建一个Spring Boot项目吧，有机会咱们再整装待发杀入Spring Boot。准备首先准备好以下几个开发环境：JDK1.8（说实话还是8好用，我之前的开发环境用的11部署的时候好多问题）Maven（版本不规定了，用新的就好）IDEA（如果使用Eclipse请安装Spring Tools 4 for Eclipse和Lombok插件）创建项目咱们首先打开IDEA，创建一个Maven项目。先在pom里加个阿里的仓库。<repositories> <repository> <id>alimaven</id> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>设置JDK为1.8，加入spring-boot-maven-plugin插件，此插件可以将项目打包成jar文件直接启动。<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> 1.8 <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>添加启动包。<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>编写启动类，并且使用@SpringBootApplication注解标记启动类。package com.ky.kevin; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class); } }编写REST接口，@GetMapping相当于@RequestMapping(method=RequestMethod.GET)，并且写上映射的URL/hello。package com.ky.kevin.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } }简单几步之后，启动我们的项目。默认的Tomcat端口号是8080，我们在浏览器访问localhost:8080/hello，成功获取到了hello的返回。简单的回顾到此为止。热部署开发过程中经常要修改代码，虽然后端方面的修改相比前端不那么需要立竿见影的看到，但是后台的启动时间明显慢很多，如果启动热部署，一次可能节省10秒，随着重启的次数增加，好处就明显了很多，你一定会对热部署的功能赞不绝口。那么接下来，往pom里添加热部署的依赖。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency>如果使用的是IDEA，请前往设置开启自动构建-Build project automatically。接下来同时按下Shift+Ctrl+Alt+/，Mac用户按下Command+Shift+Option+/，然后选择Registry，把热编译选项勾上后重启IDEA。我们再来启动项目，访问接口，然后修改接口返回的hello为world后保存，但是不重启项目。我们发现，项目自动进行了悄无声息的重启。虽然这样的操作和人为的点击暂停和点击开始并无差别，但是我们可以按下Command+S保存项目后继续编写代码，直到需要测试项目时再点开网页，这时候已经可以无缝衔接进行测试了。Eureka注册中心OriginEureka是Netflix微服务套件的一部分，做了二次封装，主要负责实现微服务架构中的服务治理功能。这是一套基于REST的服务，提供了基于Java的客户端组件，能够非常方便地将服务注册到Spring Cloud Eureka中进行统一管理。除了Eureka，还有Zookeeper（阿里Dubbo专用）、Consul、Etcd等。当然Spring还是首选Eureka。根据分布式系统领域著名的CAP定理，C代表数据一致性，A代表服务可用性，P代表服务对网络分区故障的容错性。这三个特性在如何分布式系统中都不能同时满足，最多同时满足两个，而Eureka符合AP原则，Zookeeper符合CP原则，具体想要实现什么样的条件还是根据具体项目来定。Server重新创建一个Maven项目，取名为eureka-server，然后修改pom.xml为如下配置，请根据实际情况作修改。根据我三个多月小时的尝试，自行搭建注册中心失败，原因可能是Spring Boot的版本不兼容，那么既然我不能从底层构建，可以选择使用IDEA提供的自动初始化功能，又便宜不占王八蛋。所以接下来给大家展示一下如何只通过点击鼠标初始化一个Eureka注册中心。首先打开IDEA，选择创建新项目。点击其中的Spring Initialize，选择Java1.8后一路next。填写好名称等信息，也可以直接用默认的，还要再选择一次Java的版本（我这默认11，不太好用），打包方式不用改。接下来选择Spring Cloud Discovery，勾上其中的Eureka Server即可，接下来选择好项目路径就可以完成构建啦。如果是第一次创建，需要花大概5分钟的时间下载所需的jar包等内容。构建完成之后，还没有满足全部的启动要求，首先打开自动生成的启动类，加上一个注解@EnableEurekaServer，代表启用Eureka注册中心服务。然后我们修改Resource中的application.properties后缀名为yml（不修改也行，我这里推荐使用yml文件作为Spring的配置文件）。文件内还没有任何信息，所以我们要加上以下的代码：spring: application: # 应用的名字 name: eureka-server server: # Eureka注册中心端口号 port: 14001 eureka: instance: hostname: localhost client: # 由于该应用为注册中心，所以设置为false，代表不向注册中心注册自己 register-with-eureka: false # 由于注册中心的职责就是维护服务实例，它并不需要去检索服务，所以也设置为false fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/最后再瞟一眼pom，应该没什么问题，那就直接启动程序吧。打开浏览器访问`localhost:14001就可以成功进入注册中心了。这样一来其实也省去了很多步骤，免得大家面对不同的环境和更新换代的pom文件不知所措。Client搭建好注册中心，接下来我们注册一个服务试试能不能正常调用接口。重新创建一个叫eureka-client的项目，按照Server项目的流程来，在选择依赖库这一步取消选择Eureka Server，选上Eureka Discovery Client，其他一致。创建完成之后，依旧是修改启动类，这一次我们加的注解是@EnableDiscoveryClient，注意不要写错。接下来也是配置文件，和之前的略有不同，尤其是一个实例id中的端口号是当前的client端口，而客户端默认服务url则是之前设置的server端口（一个是14001，一个是8081，仔细甄别，选择空闲的端口就行）。spring: application: # 应用的名字 name: eureka-client server: # Eureka服务提供者端口号 port: 8081 eureka: instance: hostname: localhost # 采用IP注册 prefer-ip-address: true # 实例id格式 instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port} client: service-url: defaultZone: http://${eureka.instance.hostname}:14001/eureka/现在在启动Server的情况下再启动Client，打开之前的`localhost:14001，就可以看到实例已经创建成功。大家可以试试定义一个hello接口然后访问，注意使用之前Boot复习这一小节里的知识点哦，这是可以实现的，就不多赘述了。Client Article Service最后我们再试试消费者模块，消费刚刚编写的hello模块。同理和Client的流程一样，创建第三个项目名为eureka-client-article-service。此时yml文件中的文件名也同步修改，端口咱们设置为8763。接下来创建一个RestTemplate，这是Spring提供用于访问Rest服务的客户端，提供多种远程访问Http服务的方法，并且我们在这里使用@LoadBalanced自动构造LoadBalancerClient接口的实现类并注册到Spring容器中。package com.ky.kevin.configuration; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class BeanConfiguration { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } 接下来我们创建一个控制器，直接使用Eureka中注册的服务名称来访问hello接口，但是在实际的浏览器中还是需要使用具体的ip而不是名字。package com.ky.kevin.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController @RequestMapping("/article") public class ArticleController { @Autowired private RestTemplate restTemplate; @GetMapping("/callHello") public String callHello() { return restTemplate.getForObject("http://eureka-client/test/hello", String.class); } }如此，当我们访问localhost:8082/article/callHello时，就会自动调用localhost:8081/test/hello的接口。Security抛开本地测试的环境，当我们把项目部署到服务器上时，公网IP必然直接暴露，任何用户都可以直接访问到Eureka管理的信息，所以我们再加入一个安全验证，使用Spring-Security进行认证。现在我们回到注册中心，修改pom并加入下列内容。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>打开yml，添加下列内容：spring: security: user: name: xxx password: 123456创建一个配置类：package com.ky.kevin.configuration; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 关闭csrf http.csrf().disable(); // 支持httpBasic http.authorizeRequests() .anyRequest() .authenticated() .and() .httpBasic(); } }大功告成，现在重启项目打开Eureka管理页面，发现需要输入密码才可以查看信息。 TIPS：记得修改每一个服务的DefaultZone中的地址，加上用户名和密码，比如：defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:14001/eureka/在实际测试时我的端口号进行了修改，请以文字为准，图片仅供参考。Cluster安全问题解决之后，还要考虑一个高可用的性能问题，这就要涉及到集群的搭建了。Eureka的集群搭建比较简单：每一台Eureka只需要在配置中指定另外多个Eureka的地址就可以实现一个集群的搭建了。打个比方，有ABC三台机器，那么我们要实现：将A注册到B和C上将B注册到A和C上将C注册到A和B上对于集群的实现，需要修改我们之前server的配置。把yml文件中关于port和defaultZone的内容注释掉，这两块我们要单独写在其他配置文件中。首先我们要想好集群中每个注册中心的名字叫什么，我这里就演示一个双机集群，一个叫master，一个叫slaveone，所以我们修改本机的host设置以下内容（关于如何修改host自行百度，mac用户的修改方法为在terminal中输入sudo vi /etc/hosts）：127.0.0.1 eurekamaster 127.0.0.1 eurekaslaveone保存好后（先按Esc再输入wq后回车），我们回到server项目的yml文件。将hostname修改为eurekamaster，表示这是一台机器。然后添加一下内容：spring: profiles: active: master代表我们启用master配置，但是master配置还没有写，所以在application.yml同级创建一个叫application-master.yml的配置文件，在里面输入我们之前注释掉的内容：server: port: 14001 eureka: client: service-url: defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eurekaslaveone:14002/eureka/ 需要注意的是，这里defaultZone中的host我们填写的是另一个机器的名字：eurekaslaveone，并且端口也是另一台机器的端口。接下来再创建一个叫application-slaveone.yml的配置文件，输入这些内容：server: port: 14002 eureka: client: service-url: defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eurekamaster:14001/eureka/第一个配置代表第一台机器，第二个配置代表第二台机器，第二个配置中的host为第一台机器的hosteurekamaster。因此当我们设置active为master时，就会自动调用application-master中的配置。接下来的操作步骤大家应该已经猜到了，没错，就是和创建第一个注册中心一样的步骤再创建一个eureka-server-cluster。同样的为启动类添加注释，拷贝第一个注册中心的三个yml文件，将host修改为eurekaslaveone，将active修改为slaveone，其他不变。此时我们再分别启动两个项目，就可以访问到这一个小集群了。（但是这只是本地测试的方法，至于服务器上如何部署，还有待学习，等学完Spring Cloud上手项目之后应该可以给大家一个解决方案）下面我们来看看现在访问两个不同端口显示的内容。上图为14001端口的Eureka显示内容，可以发现复制品这一行多了一个eurekaslaveone，代表14002端口运行了一个它的复制品注册中心。上图为14002端口的Eureka显示内容，同样的显示的是14001端口的eurekamaster，也就是说，现在完成了A注册到B，B也同时注册到A的集群，至于更多数量的集群也是同理。现在我们转到Client，想一想如何将服务注册到两个注册中心去。很简单，我们只需要修改defaultZone，同时写上两个url即可。defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:14001/eureka/,/ http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:14002/eureka/这里的host我们就仍旧使用localhost即可，不需要修改特定的名称，端口号对应集群中的两个注册中心。然后我们打开任意一个注册中心，都可以看到下面的显示。集群的其中一个作用就是防止某一个注册中心因为一些问题而无法使用时服务的中断，所以我们现在随意中断其中一个注册中心的程序，就比如说master。这时候访问14001端口的网页，已经无法显示，但是访问14002端口的网页仍旧存在。IDEA中也会报错，提示找不到服务，但是没关系，我们访问localhost:8082/article/callHello试试，访问8081端口的接口操作成功！此时我们重新开启master注册中心并且访问14001端口，发现两个模块重新注册到了master上，在此期间整个程序没有出现任何的问题。这样一来建立集群的目的也就达到了。Protection关于Eureka还有一个自我保护的功能，当你在控制台看到一串红色字体时，表明Eureka Server进入保护模式了。微服务每隔一段时间就会向注册中心发送一个心跳包，如果在15分钟内没有收到85%的心跳包，就会触发这个模式，被认定为网络故障。在该模式下，Eureka会保护服务注册表中的信息，不再删除服务注册表中的数据。由于它既然开发出这一种模式，肯定是有利有弊的，所以我只提一下，只推荐在开发环境下关闭这个功能。（发送心跳包速率，检测时间等都可以修改）如果想要停用自动保护，想要在微服务失效时立即销毁实例的同学，可以为eureka-server的yml文件中添加以下代码：eureka: server: enableSelfPreservation: falseExtend最后来讲一些拓展应用，讲完之后我们的Eureka部分就正式结束啦。REST APIEureka作为注册中心，其本质是存储了每个客户端的注册信息，Ribbon在转发的时候会获取注册中心的服务列表，然后根据对应的路由规则来选择一个服务给Feign来进行调用。微服务是没有依赖的，可以独立部署，端口也可以随机分配，但是API网关的部署则不能这样，大部分使用Nginx作为负载，就必须知道API网关有几个节点。我们可以利用Eureka提供的API来获取某个服务的实例信息，做到自动部署和扩容。具体的请查看官方文档。比如，我们可以使用localhost:14001/eureka/apps/eureka-client来获取名为eureka-client的应用的信息，也可以使用Postman进行测试。由于我们开启了Spring Security的安全认证，所以需要在Authorization中开启Basic Auth，如下图：这样就可以正常获取到信息了。元数据我们使用Eureka自带API获取到的都叫标准元数据，还可以自定义元数据，格式如下：eureka: instance: metadataMap: key: value如此，就会在原本的信息中添加一对自定义的元数据。使用元数据可以做一些扩展信息，比如灰度发布之类的功能。EurekaClient其实除了使用API，我们还可以用EurekaClient来获取到我们想要的信息。package com.ky.kevin.controller; import com.netflix.discovery.EurekaClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/test") public class TestController { @Qualifier("eurekaClient") @Autowired private EurekaClient eurekaClient; @GetMapping("/infos") public Object serviceUrl() { return eurekaClient.getInstancesByVipAddress("eureka-client", false); } }我们也可以使用Spring Cloud重新封装过的DiscoveryClient来获取信息，大同小异。健康检查在某些情况下，比如MongoDB出现了异常，但是应用进程还是存在，这就意味着应用可以继续上报心跳，保持自己的信息不被剔除。我们可以开启健康检查，及时将应用的实例信息下线，隔离正常请求，防止出错。eureka: client: healthcheck: enabled: true服务上下线监控在某些特定的需求下，我们需要对服务的上下线进行监控，目前支持的事件：EurekaInstanceCanceledEvent 服务下线事件EurekaInstanceRegisteredEvent 服务注册事件EurekaInstanceRenewedEvent 服务续约事件EurekaRegistryAvailableEvent 注册中心启动事件EurekaServerStartedEvent 服务启动事件我们可以为注册中心添加下面这个类来进行监控：package com.ky.kevin.component; import com.netflix.appinfo.InstanceInfo; import org.springframework.cloud.netflix.eureka.server.event.*; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class EurekaStateChangeListener { @EventListener public void listen(EurekaInstanceCanceledEvent event) { System.err.println(event.getServerId() + "\t" + event.getAppName() + "服务下线 "); } @EventListener public void listen(EurekaInstanceRegisteredEvent event) { InstanceInfo instanceInfo = event.getInstanceInfo(); System.err.println(instanceInfo.getAppName() + " 进行注册 "); } @EventListener public void listen(EurekaInstanceRenewedEvent event) { System.err.println(event.getServerId() + "\t" + event.getAppName() + " 接受到心跳包 "); } @EventListener public void listen(EurekaRegistryAvailableEvent event) { System.err.println("中心启动 "); } @EventListener public void listen(EurekaServerStartedEvent event) { System.err.println("服务启动 "); } }总结在本文中，我们首先了解了一下Spring Cloud的一些基础模块，然后复习了一下必须要使用到的Spring Boot。准备好基础之后，从Eureka注册中心开始，了解了注册中心的起源。构建了一个注册中心，又写了一个客户端服务注册到了中心。尝试使用Article Service调用Service中的服务。为项目添加了安全认证后，建立了一个小型的注册中心集群。尝试关闭了保护模式，然后测试了几个小型的拓展功能。可以说，这篇文章非常详细的带领大家了解了Eureka的知识点，所有必要的代码和截图也都已经提供。那么接下来我们就要开始学习客户端负载均衡Ribbon了。本文到此结束，有问题可以评论问我。
开端挺多技术都还是会过时的，但是Spring作为一套功能十分强大的全家桶框架，地位很难被撼动，甚至我已经后悔之前的项目使用了Shiro作为权限控制框架而不是Spring Security。原本是打算进军Spring Cloud，然后直接上手毕业设计，但是回顾我曾经的学习路线，挺多知识都只是囫囵吞枣式的学习，并没有一个系统、全面、深度的掌握。比如我是先学的Spring Boot，再去学的Spring，是不是挺奇葩的。这就和先学了三年级的方程求解，再去学一年级的加减法算术一样。我确实已经会用方程求解了，但是直接将两个数字放在等号同一边的这种正向思维式的解题方式还是没有学会，我只会反向思维的方程求解。如果要完全掌握计算，必须不管正反都理解它的原理才行。所以我才打算，重新来过，从Spring开始，吃透源码。所以这一方面我肯定会写很多篇博文，今天的全文都只算是一篇开端。The importance of 源码如果只是使用一个框架，那叫会看。但是如果能读懂源码，那叫会读。如果还能结合相关算法和设计模式，那叫会写。作为一位合格的Java后端架构师，需要具备有写框架这方面的能力。然而阅读源码费时费力，且普及程度非常差，许多简单的应用其实是环环相扣，层层嵌套得来的，并且还会遇到过于老旧而完全不清楚为什么要这么写的情况。无论如何，放弃阅读源码还是不明智的。俗话说，读万卷书，行万里路。其实各种框架之间也有很多共同之处，当你认真读完Spring的源码，或许理解其他框架会快很多。或许我写的内容并不会十分全面，毕竟写下这些最主要的还是为了做一个学习笔记。但是也希望读到这些博文的你会有所收获，因为写在这里的都是精华。核心实现整体架构Spring是一个分层框架，大约分为20个模块，总体可以归纳为以下几个部分。Core Container核心容器包含Core、Beans、Context、Expression Language模块，前两者为基础部分，提供IOC和依赖注入特性。Core：核心工具类，其他组件都需要用到。Bean：包含访问配置文件、创建管理Bean的类。Context：提供类似于JNDI注册器的框架式对象访问方法，继承了Beans的特性，支持J2EE，关键接口是ApplicationContext。Data Access/Integration该层包含JDBC、ORM、OXM、JMS和Transaction模块。Web该模块建立在应用程序上下文模块之上。包含以下几个模块：Web模块：使用servlet listener初始化IOC容器。Web-Servlet模块：包含MVC实现，使代码和web forms之间能够清楚地分离开。Web-Struct模块：提供了用于Portlet环境和Web-Servlet模块的MVC的实现。AOPAspects模块：提供对AspectJ的集成支持。Instrumentation模块：提供对class instrumentation和classloader的实现。容器基本用法如果说Spring是一个水桶，Beans就是水桶中的水，在正式开始源码分析之前，有必要了解一下其核心的两个类。DefaultListableBeanFactory这是加载Bean的核心部分，是Spring注册及加载Bean的默认实现，XmlBeanFactory继承自它并且使用了自定义的XML读取器XmlBeanDefinitionReader实现个性化读取。这个类的层次结构较为复杂，分为以下几个：AliasRegistry：定义对alias的简单增删改等操作。SimpleAliasRegistry：使用map缓存，实现AliasRegistry接口。SingletonBeanRegistry：定义对单例的注册及获取。DefaultSingletonBeanRegistry：对前一个类函数的实现。FactoryBeanRegistrySupport：在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能。BeanFactory：定义获取Bean及Bean的各种属性。HierarchicalBeanFactory：继承前一个类，增加了对parentFactory的支持。BeanDefinitionRegistry：定义BeanDefinition的增删改操作。ConfigurableBeanFactory：提供配置Factory的各种方法。ListableBeanFactory：根据各种条件获取Bean 的配置清单。AbstractBeanFactory：综合FactoryBeanRegistrySupport和ConfigurableBeanFactory的功能。AutowireCapableBeanFactory：提供创建Bean、自动注入、初始化以及应用Bean的后处理器。AbstractAutowireCapableBeanFactory：综合人AbstractBeanFactory并对接口AutowireCapableBeanFactory实现。ConfigurableListableBeanFactory：BeanFactory配置清单，指定忽略类型及接口等。DefaultListableBeanFactory：综合上面所有功能，主要是对Bean注册后的处理。XmlBeanDefinitionReaderXML配置文件的读取是Spring中重要的功能，因为Spring的大部分功能都是以配置作为切入点的，我们也来看看各个类的功能。ResourceLoader：定义资源加载器，主要应用于根据给定的资源文件地址返回对应的Resource。BeanDefinitionReader：主要定义资源文件读取并转换为BeanDefinition的各个功能。EnvironmentCapable：定义获取Environment方法。DocumentLoader：定义从资源文件加载到转换为Document的功能。AbstractBeanDefinitionReader：对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现。BeanDefinitionDocumentReader：定义读取Document并注册BeanDefinition功能。BeanDefinitionParserDelegate：定义解析Element的各种方法。结果分析，我们可以得出XmlBeanDefinitionReader中主要包含以下几步的处理：使用ResourceLoader将资源文件路径转换为对应的Resource文件。通过DocumentLoader对Resource文件进行转换，将Resource文件转换成Document文件。通过实现接口BeanDefinitionDocumentReader对Document进行解析，并使用BeanDefinitionParserDelegate对Element进行解析。弃更写到这，我感觉Spring的源码实在是太过于复杂了。关于前几篇文章，都是我深刻理解之后写下的总结性博文，而学习Spring源码明显吃力很多，只能照办书中的文字，代码量也非常多。我认为单单把重要的内容摘抄下来并不能帮助到大家，所以Spring源码方面的博文不出意外地话就停止更新了。但我依旧会去学一遍，可能等我全部看完，才能再写一篇总结性的文章。
开端Everybody，好久不见，因为开学的原因，已经拖更了半个多月。周末总算是闲下来写写博文，主要还是不想学习学校里教的那些东西。我们都知道，API 接口是一个非常神圣的内容。你做的好，前后端开发人员情同手足，和和美美；你做的不好，说不定他们每天一见面就要吵。在现在这个阶段，大家更倾向于使用一些已有的框架来进行API文档的开发，这样更加方便管理。尤其是动态更新 API 文档，更加省去了后端程序员一大部分的工作。在《阿里巴巴 Java 开发手册》中写到，API 文档和 Javadoc 就是为了方便他人阅读和理解程序而设计的，后端程序员若是更改了源代码而没有对应修改文档或注释，那文档和注释就没有了他们存在的意义。所以今天，我就来讲一个快速生成 API 文档的框架——Swagger。没错，他就是那么 Swag！以及如何整合到项目中去，包含了后端的注解生成器与前端嵌入式的页面显示方案。话不多说，那我们就开始吧。补充：实践过程中遇到特殊问题，请前往下方链接下载 dist 文件夹的内容并导入至项目文件中->Github Swagger-UI介绍为了解决前面讲到的问题，人们就提出了各种解决方案，方案用的人一多，就自成了一套体系，成了一套规范，这就是 Swagger 的由来。按照新的开发模式，只要每次更新 Swagger 的描述文件，就可以自动生成接口文档。但即便如此，仍旧需要开发人员每次修改 yml 或 json 文件。人都是有惰性的，久而久之就再一次忽略了描述文件的修改，导致更新不同步。因此急需更加简便的方案。所以作为Java届服务端的大一统框架Spring，迅速将Swagger规范纳入自身的标准，建立了 Spring-swagger 项目，后面改成了现在的 Springfox。在项目中引入 Springfox 之后，可以扫描代码并生成描述文件，同步接口文档，实现自动化管理。就目前来说已经是最重要且高效的几个生成接口文档的方式之一了。->点击链接了解Swagger整合介绍不多说，直接开始整合。关于阿里云仓库的使用我就粘贴我的另一篇文章中的内容，免得大家频繁跳转页面。使用阿里云仓库众所周知，如果你从 github clone 一个项目，如果没有梯子下载 jar 包可能就要花几个小时的时间。还好阿里爸爸给我们提供了镜像仓库，几分钟就能解决 jar 包的问题。关于引用仓库可以在 maven 配置中修改：<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>不过我还是推荐大家在自己的项目中加入仓库，灵活性更高：<repositories> <repository> <id>alimaven</id> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>这里我就直接用 public 仓库，这个仓库整合了两个小仓库，东西更丰富些。releases 是线上版本，snapshots 是开发版本，都填 true 问题不大。搜索并导入 jar 包->阿里云仓库大家可以在上边这个网址内搜索自己要找的 jar 包，中心 central 仓库够大了，输入文件名，点击搜索到的蓝色文件名后把 Maven 依赖这些代码复制进项目里即可。像下面的一样，我们引入最新版本的 Swagger 相关的 jar 包，如需拓展可以引用其他包。 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>io.springfox</groupId>--> <artifactId>springfox-swagger-ui</artifactId>--> <version>3.0.0</version>--> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency>由于默认的 UI 界面并不那么舒适，也不适合用于前端展示，所以推荐使用最后一个包，两者选其一导入。这里提一点，项目使用的 Java 版本最好是高于或等于 11，如果太低下面讲到的一步中可能会报错。创建SpringFoxConfig配置类我们先来写配置，再使用注解优化显示。在后端项目中创建一个配置类如下：@Configuration @EnableSwagger2 public class SpringFoxConfig { @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .host("localhost:9988") .select() .apis(RequestHandlerSelectors.basePackage("cn.kevin.ims.controller")) .paths(Predicate.not(PathSelectors.regex("/error.*"))) .paths(PathSelectors.regex("/.*")) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder().title("接口文档") .contact(new Contact("Kevin", "http://test.ims.cool", "jiazekai1003@gmail.com")) .description("Swagger动态接口文档") .license("The Apache License, Version 2.0") .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html") .version("Demo") .build(); } }特别提醒，类名前一定要写两个注解。@Configuration表示这是受 Spring 控制的配置组件，@EnableSwagger2表示启用 Swagger 2。首先构建一个 docket。host 填写后端项目的 url。apis 填写需要扫描的包。paths中可以选择扫描路径以及不扫描的路径，推荐将 error 排除。然后构建一个 apiInfo，里面的信息自行填写，按我的模板改一下即可，不影响实际使用。通过注解实现文档优化如果看成果，可以跳过这一部分，但是优化之后的文档更加美观写易于阅读。先从底层的 Entity 开始写：@Data @ApiModel(description = "基本实体") public abstract class BaseEntity implements Serializable { @ApiModelProperty(value = "创建时间", required = true) protected long utcCreate; @ApiModelProperty(value = "修改时间", required = true) protected long utcModify; @ApiModelProperty(value = "修改人", required = true) protected String modifyBy; @ApiModelProperty(value = "是否有效", required = true) protected Boolean valid; @ApiModelProperty(value = "备注", required = true) protected String remark; }使用@ApiModel注释实体类的别名，使用@APIModelProperty注释实体类属性的别名和是否必要等信息。不要忘了把 VO 类也注解一遍。然后注解 Controller 层的类：@Api(tags = "用户模块") @CrossOrigin @RestController() @RequestMapping("/user/") public class UserController extends BaseController { @Resource(name = "userServiceImpl") private UserService userService; @Resource(name = "roleServiceImpl") private RoleService roleService; @ApiOperation(value = "默认Get请求", notes = "请求不存在的路径时调用此默认接口") @ApiImplicitParam(name = "name", value = "默认字段", required = true, dataTypeClass = String.class) @GetMapping(value = "/{name}") @ResponseBody public String helloWorld(@PathVariable(name = "name") String name) { return "Hello " + name; } @ApiOperation(value = "添加用户", notes = "插入一条用户记录") @ApiImplicitParam(name = "usrType", value = "用户角色", required = true, dataTypeClass = String.class) @RequiresRoles("admin") @RequiresPermissions("user:insert") @PostMapping(value = "/insert") @ResponseBody public Response<User> saveUserInfo(@NotNull @RequestBody User sysUser, @RequestParam String usrType) { User result = userService.saveUserInfo(sysUser); if (result != null) { roleService.createUserRole(usrType, result.getUsrName()); return getSuccessResult(result); } return getFailResult(405, "Message already exist!"); } }使用@APi标记类，并取别名。使用@ApiOperation备注接口，可以加上 note 写更多描述信息。使用@ApiImplicitParam备注每一个接口中所需要的参数，注意如果参数为先前已经注解过的实体则不需要再次备注，如果参数为多个可以在@ApiImplicitParams中内嵌@ApiImplicitParam。具体的填写格式如下：name：参数名value：参数的汉字说明、解释required：参数是否必须传paramType：参数放在哪个地方· header --> 请求参数的获取：@RequestHeader· query --> 请求参数的获取：@RequestParam· path（用于restful接口）--> 请求参数的获取：@PathVariable· body（不常用）· form（不常用）dataType：参数类型，默认String，其它值dataType="Integer"defaultValue：参数的默认值前端内嵌网页访问文档至此简单的优化已经完成了，如果项目工程比较大，还可以将不同的 Controller 放入不同的 Docket 中。那么前端我们的目的就是访问先前配置的路径下的接口文档，先前 docket 中我们设置的 host 为 localhost:9988，那么访问 localhost:9988/doc.html 就可以看到接口文档了，如果没有使用推荐的 UI 包，则需要访问 swagger-ui.html。可以看到已经有那味儿了，为了能让文档更符合我们项目的结构，更方便查阅，可以采用嵌入式的方法。在前端页面合适的组件内，使用 标签内嵌显示文档。 接下来我们在项目页面中直接打开文档，就可以看到内嵌效果了（我事先已经做好了一个新组件并注册在了菜单中）。可以发现，通过优化注解，中文版的 API 文档更加方便理解，让人一目了然。总结本文简单介绍了一下 Swagger 的由来，从后端导包、配置、注解，到前端内嵌网页显示文档，教大家如何快速整合这一框架。在实际测试中，在访问 UI 页面时遇到过问题，采用下载官方 GitHub 的 Dist 文件夹方法解决了，如果有更好的方法比如采用前端导入的方式，请大佬们留言教教我。另外，跨域问题相信也会有一部分朋友遇到，请查阅我之前写过的文章一并解决。希望本文能帮助大家对 Swagger2 和如何整合有一个初步的概念。篇幅有限，深入的知识就只能自行学习了。
权限框架Shiro快速整合前后端分离项目
开端好的，起因是我把最近做好的项目给一部分人进行了测试，发现大部分朋友都提出了同一个问题，你的系统权限管理是如何实现的。我只能尴尬的说一句，不好意思这部分还没开发。然而我也知道，其实对于一个项目来说，权限可以说是最主要的一部分。后端除了对权限进行处理，其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去，顺便把我的学习笔记分享给大家。而对于权限控制的框架呢，听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的，不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架，可以用在各种场合，甚至非 Web 项目中，由于它的会话独立于容器，后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。由于这部分内容比较多，我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服，所以我打算把这个内容分成几个部分：初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于如何利用我们之前学习的知识快速进行 Shiro 与前后端分离项目的整合。虽然说是快速，我还是会一步步都告诉大家，并没有想象中的那么快。整合那么之前结果两篇长文的讲解，大家应该已经初步了解了一些关于 Shiro 的知识，接下来我们开始快速整合 Shiro 和 SSM ，我只讲最关键的步骤，至于更多额外功能的拓展留给大家自行发掘。下文的内容可能略微与我前面写的两篇文章有所不同，不过不影响食用。毕竟一边做一边写，总会发现之前的一些问题并找到更好的解决方法。使用 Maven 管理 jar 包Maven 是一个后端的包管理工具，可以使用写 XML 的形式快速从仓库寻找 jar 包。使用阿里云仓库众所周知，如果你从 github clone 一个项目，如果没有梯子下载 jar 包可能就要花几个小时的时间。还好阿里爸爸给我们提供了镜像仓库，几分钟就能解决 jar 包的问题。关于引用仓库可以在 maven 配置中修改：<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>不过我还是推荐大家在自己的项目中加入仓库，灵活性更高：<repositories> <repository> <id>alimaven</id> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>这里我就直接用 public 仓库，这个仓库整合了两个小仓库，东西更丰富些。releases 是线上版本，snapshots 是开发版本，都填 true 问题不大。搜索并导入 jar 包大家可以在上边这个网址内搜索自己要找的 jar 包，中心 central 仓库够大了，组织的话就是 Shiro 的网址，按结构写不要反了，然后输入文件名，点击搜索到的蓝色文件名后把 Maven 依赖这些代码复制进项目里即可。像下面的一样，我们引入最新版本的 Shiro 相关的 jar 包。具体使用哪些包的话请大家去官网上查找资料，比如说官网里提示说 core 包需要 slf4j 的支持，再比如说你用了谷歌的 IOC 容器，那么需要使用 guice 包等等。 <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.9</version> </dependency>日志包和 Spring Boot 包都不是必要的，主要还是 shiro-all 和 shiro-core 这两个核心包。权限五表的建立请转到下文，翻到最后，把权限五表的 SQL 代码运行一下，完成用户、角色。权限、用户角色、角色权限五表的建立。我在这里贴出我的五张表，与下文中的略微不同，为了方便整合作出了一些调整。MySQL建表DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` INT AUTO_INCREMENT NOT NULL, `usrName` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrPassword` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrNick` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrPhone` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrEmail` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `remark` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `lastLogin` BIGINT DEFAULT 0 NOT NULL, `utcCreate` BIGINT DEFAULT 0 NOT NULL, `utcModify` BIGINT DEFAULT 0 NOT NULL, `modifyBy` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `valid` BIT (1) DEFAULT 1 NOT NULL, PRIMARY KEY ( `id` ) ) ENGINE=INNODB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` int(64) NOT NULL AUTO_INCREMENT, `description` varchar(32) DEFAULT NULL, `role` varchar(32) DEFAULT NULL, `remark` varchar( 255 ) DEFAULT NULL, `utcCreate` BIGINT DEFAULT 0, `utcModify` BIGINT DEFAULT 0, `modifyBy` VARCHAR ( 255 ) DEFAULT NULL, `valid` BIT (1) DEFAULT 1, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `id` int(64) NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NULL, `permission` varchar(64) DEFAULT NULL, `url` varchar(64) DEFAULT NULL, `remark` varchar( 255 ) DEFAULT NULL, `utcCreate` BIGINT DEFAULT 0, `utcModify` BIGINT DEFAULT 0, `modifyBy` VARCHAR ( 255 ) DEFAULT NULL, `valid` BIT (1) DEFAULT 1, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `role` VARCHAR(255) NOT NULL, `user_name` VARCHAR(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_role_permission`; CREATE TABLE `sys_role_permission` ( `role` VARCHAR(255) NOT NULL COMMENT '角色名', `permission` VARCHAR(255) NOT NULL COMMENT '权限名' ) ENGINE=InnoDB DEFAULT CHARSET=utf8;主要的不同在于两张关联表中我都是假定某个字段不能修改，然后把它当成关联键进行操作。如果直接用用户、角色、权限表的 id 主键的话确实更方便更合理，但是我还没正式学过 SQL ，只会简单的增删改查。前面讲到的正统方法涉及 SQL 中 LEFT JOIN 等操作，比较复杂，各位要是学过 SQL 推荐用主键关联。后端五表一条龙后端对于这五张表的操作主要在 POJO 层、Mapper 层、Mybatis 层、Service 层和 Controller 层。就和你的项目系统中任何一个实体类一样，方方面面都需要涉及。那么我相信大家既然看到这里，已经会写接口会写 Mybatis，所以怎么写就不讲了，贴一些重点的内容。POJOPOJO 层的实体类属性需要严格按照之前 MySQL 中的情况来，具体见下表：就举一个 Permission 的权限实体作参考，使用了 Lombok。@Getter @Setter @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor public class Permission extends BaseEntity { private static final long serialVersionUID = 8482551052982019660L; private Integer id; private String name; private String permission; private String url; }创建时间、修改时间等公共属性都放在 BaseEntity 中。就只要这么写一个实体类就完成了，Lombok 注解实现非常简单。同样对用户和角色作如上操作即可。Mapper、Mybatis这块关联性大一些，就放在一起写了。主要就是在 Mapper 中写接口，在 XML 的配置文件里写对应方法的 Mybatis SQL 语句。增删改查不多说，由于我们的 Shiro 在认证时需要查询用户、查询用户角色和查询角色权限这三个数据库操作，因此我只写这三个，其他自行完成。@Repository public interface UserMapper { User selectByName(@Param("usrName") String usrName); } @Repository public interface RoleMapper { Set<String> findRoleByUserName(@Param("usrName") String usrName); } @Repository public interface PermissionMapper { Set<String> findPermissionByRole(@Param("role") String role); }注意一下根据官方给出的案例，后边两个操作需要返回 Set 集合，我没试过 List 可不可行，既然例子是 Set 咱们也用 Set。接下来写 XML（我写在一起了，实际需要分三个 XML 写）： SELECT * FROM sys_user WHERE usrName = #{usrName} SELECT role FROM sys_user_role WHERE user_name = #{usrName} SELECT permission FROM sys_role_permission WHERE role = #{role} 需要注意的是，如果在 Spring 配置中没有为 Mybatis 对应的 POJO 配置 type-aliases，需要写全包名。而像 String 这种基本包装类简写会报错但不影响程序运行，这是 IDEA 的一个小 bug。千万别忘了写增删改查等等其他操作，我们接着往下。ServiceService 需要对数据库的原子操作开始组合，由于涉及接口和实现类，我这里只举一个例子：public interface PermissionService { Set<String> findPermissionByRole(String paramRole); } @Service @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED) public class PermissionServiceImpl implements PermissionService { @Resource private PermissionMapper permissionMapper; @Override public Set<String> findPermissionByRole(String paramRole) { return permissionMapper.findPermissionByRole(paramRole); } }注解主要是告诉 Spring 这是一个 Service ，交给它管理，然后设置一下事物传播属性、报错回滚类型、事物隔离级别等。其他基本业务需求也请自行组装。Controller终于到了最后一层，这一层就和前面的不太一样了。角色和权限的 Service 接口只在 Shiro 配置的时候用到，所以不需要写其他的。除非你的系统中涉及权限和角色的增删改查和分配才需要写，当然一般来说都是有这个需求的。我依旧是只举一个例子：@CrossOrigin @RestController() @RequestMapping("/permission/") public class PermissionController extends BaseController { @Resource(name = "permissionServiceImpl") private PermissionServiceImpl permissionServiceImpl; @GetMapping(value = "/findPermissionByRole") @ResponseBody public Response<Set<String>> findPermissionByRole(@RequestParam String role) { Set<String> results = permissionServiceImpl.findPermissionByRole(role); if (results.size() != 0) { return getSuccessResult(results); } return getFailResult(404, "MEssage not found!"); }这里的跨域注解只适用于项目初期，结合了 Vue 特别是整合了 Shiro 之后，跨域问题已经不是这个注解能够解决的了，我会在后面讲到。对于三个 Controller 的功能请自行完成。至此，对于我们来讲项目基本所需的新内容已经完成，接下来就是要进行 Shiro 的配置来实现权限的自动管理。配置Shiro开始配置 Shiro，我要先提跨域问题。在项目初期，跨域是不存在的，因为只有后端的程序在跑，直接用 Postman 测试不会出问题。在我们整合了 Vue 之后，开始出现跨域问题。由于本地测试时 Vue 其实是跑在 Node.js 的，前后端是两个不同的端口号。但是这是的跨域问题也不是特别难解决，设置一个全局跨域处理即可。说的简单其实我第一次碰到的时候捉摸了好几个周才解决。但是当我们整合 Shiro，就会多出好多问题。讲个容易理解的问题，在访问网页的时候并不是一次连通的，又是会遇到多次“握手”，浏览器先发送一个 OPTIONS 类型的请求（PreFlight）给后端，这种叫做预检请求，以检测实际请求是否可以被服务器所接收。那么问题就来了，既然我们设置了权限管理，用户在登录系统前肯定是不被允许有其他权限的，那么这个所谓的预检请求，就会被 Shiro 拦截。在 Shiro 中我们只能对指定接口指定 URL 的内容进行拦截或放行，但是对于预检请求就无能为力。这只是一个小例子，实际开发中还会遇到其他各种问题。所以我们先从跨域处理开始，以应对之后遇到的问题。创建工具类解决跨域对跨域忍无可忍，终于集合各路奇技，写了一个工具类解决一切跨域问题。public class CorsUtil { private static final String OPTIONS_FOR_REQUEST = "OPTIONS"; public static void setResponseHeader(HttpServletResponse response, HttpServletRequest request) { // 设置编码格式 response.setContentType("text/html;charset=UTF-8"); // 允许哪些Origin发起跨域请求,nginx下正常 response.setHeader( "Access-Control-Allow-Origin", request.getHeader("Origin")); // 允许请求的方法 response.setHeader( "Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE" ); // 多少秒内，不需要再发送预检验请求，可以缓存该结果 response.setHeader( "Access-Control-Max-Age", "86400" ); // 表明它允许跨域请求包含xxx头 response.setHeader( "Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With," + "If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type," + "X-E4M-With,userId,token, Accept, Authorization" ); // 允许浏览器携带用户身份信息（cookie） response.setHeader( "Access-Control-Allow-Credentials", "true" ); response.setHeader("XDomainRequestAllowed","1"); // 允许OPTIONS请求直接通过 if (OPTIONS_FOR_REQUEST.equals((request).getMethod().toUpperCase())) { response.setStatus(HttpServletResponse.SC_OK); } } }可以看到此类中，编码设置了；对源头地址直接采用 get 的方法不需要每次都重新修改了；请求方法接受 POST、GET、OPTIONS 和 DELETE；请求中也允许了各种类型奇奇怪怪的请求头，应有尽有；最后还设置了允许携带 Cookie 信息，允许同步 Session 信息等。方法的参数是 HttpServlet 中的 Response 和 Request，对 Response 进行处理后返回。基本上以后遇到什么需要跨域的处理，直接在需要的地方调用这个方法即可，比我之前百度到的什么配置全局拦截器好用多了。创建登录拦截器既然写好的跨域根据类，接下来我们重写一下 Shiro 的跨域过滤器。@Component @WebFilter(urlPatterns = "/*",filterName = "shiroCrossFilter") public class ShiroLoginFilter implements Filter { private FilterConfig config = null; @Override public void init(FilterConfig config) throws ServletException { this.config = config; } @Override public void destroy() { this.config = null; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; HttpServletRequest request = (HttpServletRequest) servletRequest; CorsUtil.setResponseHeader(response, request); filterChain.doFilter( servletRequest, response ); } }最主要的是 @WebFilter注解，名字千万不能错，要完全一致。然后重写一下三个方法，在 doFilter( ) 方法中使用我们刚刚写的跨域工具类处理。创建全局异常处理器针对登陆账户和操作权限时因某种因素操作失败，Shiro 会直接在控制台抛出异常，我们设置一个全局处理器找到这些异常并返回一些容易辨认的中文提示，也方便前端进行提示。@RestControllerAdvice public class GlobalExceptionHandle { @ExceptionHandler(ShiroException.class) public String doHandleShiroException(ShiroException se, Model model) { se.printStackTrace(); if(se instanceof UnknownAccountException) { return "该账户不存在"; } else if (se instanceof LockedAccountException) { return "该账户已锁定"; } else if (se instanceof IncorrectCredentialsException) { return "密码错误请重试"; } else if (se instanceof AuthorizationException) { return "没有相应权限"; } else { return "操作失败请重试"; } } }创建自定义会话管理器public class CustomSessionManager extends DefaultWebSessionManager { private static final Logger logger = LoggerFactory.getLogger(CustomSessionManager.class); private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public CustomSessionManager() { super(); setGlobalSessionTimeout(DEFAULT_GLOBAL_SESSION_TIMEOUT * 48); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION); if (!StringUtils.isEmpty(sessionId)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; } else { return super.getSessionId(request, response); } } }这部分照抄就可以了，没什么好讲的。创建授权拦截器对于访问某个资源，会出现两种情况，一种就是你有权限可以访问，一种就是你没有权限被 Shiro 拦截，在这里也会涉及跨域问题，所以我们继承 UserFilter （根据你的默认网页拦截规则选择继承的类），重写通过和拦截两个方法，首先给大家 Shiro 默认拦截器类对应的名称，再给代码。名称类authcFormAuthenticationFilterauthcBasicBasicHttpAuthenticationFilterlogoutLogoutFilteruserUserFilteranonAnonymousFilterrolesRolesAuthorizationFilterpermsPermissionsAuthorizationFilterportPortFilterrestHttpMethodPermissionFiltersslSslFilternoSessionCreationNoSessionCreationFilter每个拦截器的作用我在前文都讲过，不赘述了。public class CorsAuthenticationFilter extends UserFilter { private static final String OPTIONS_FOR_REQUEST = "OPTIONS"; public CorsAuthenticationFilter() { super(); } @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (OPTIONS_FOR_REQUEST.equals(((HttpServletRequest) request).getMethod().toUpperCase())) { return true; } return super.isAccessAllowed(request, response, mappedValue); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse res = (HttpServletResponse)response; HttpServletRequest req = (HttpServletRequest)request; CorsUtil.setResponseHeader(res, req); res.setStatus(HttpServletResponse.SC_OK); PrintWriter writer = res.getWriter(); Map<String, Object> map= new HashMap<>(16); map.put("success", false); map.put("code", 702); map.put("message", "用户未登录"); map.put("timestamp", DateUtil.currentSecond()); writer.write(JSON.toJSONString(map)); writer.close(); return false; } }当用户认证成功，只需要判断请求是否是 OPTIONS 类型，如果是直接放行。当用户认证失败，用户就不具有权限访问，需要调用我们的跨域工具进行处理，并且告诉控制台用户未登录，返回 false 进行拦截。创建安全实体数据源接下来的两块是最重要的，在之前的文章中也多次讲到过，这里再次演示一遍。public class MyShiroRealm extends AuthorizingRealm { private static final Logger LOGGER = LoggerFactory.getLogger(MyShiroRealm.class); @Resource(name = "roleServiceImpl") private RoleService roleService; @Resource(name = "permissionServiceImpl") private PermissionService permissionService; @Resource(name = "userServiceImpl") private UserService userService; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String userName = token.getUsername(); User user = userService.selectByName(userName); if (user == null) { throw new UnknownAccountException("Message not found"); } else if (!user.getValid()) { throw new LockedAccountException("Account locked"); } return new SimpleAuthenticationInfo( user.getUsrName(), user.getUsrPassword(), ByteSource.Util.bytes(userName), getName() ); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); String userName = (String)principalCollection.getPrimaryPrincipal(); if (userName == null) { LOGGER.error("授权失败，用户信息为空!"); return null; } try { Set<String> roles = roleService.findRoleByUserName(userName); simpleAuthorizationInfo.addRoles(roles); for (String role : roles) { Set<String> permissions = permissionService.findPermissionByRole(role); simpleAuthorizationInfo.addStringPermissions(permissions); } return simpleAuthorizationInfo; } catch (Exception e) { LOGGER.error("授权失败，系统内部错误!"); } return simpleAuthorizationInfo; } }需要注意的是按照原则，应当注入接口，使用@Resource注解和实现类的名字进行注入。用户验证使用 UserService 查询数据库中的用户信息并验证；用户授权使用 PermissionService 和 RoleService，首先从数据库读取用户角色交给 Shiro 管理。对于每个用户所对应的角色都从数据库再读取角色对应权限，由于 Set 集合有去重性，会得到唯一的 permission 集合，将其交给 Shiro 管理。具体的就不多说啦，见前两篇文章吧。创建配置之前写了这么多杂七杂八的，其实都是打地基，接下来要把他组合在 Shiro 配置中让其生效。@Configuration public class ShiroConfiguration { private final Log logger = LogFactory.getLog(ShiroConfiguration.class); @Bean public MyShiroRealm myShiroRealm(){ // 生成之前配置的数据源 return new MyShiroRealm(); } @Bean public CacheManager cacheManager() { // 生成缓存管理器 return new MemoryConstrainedCacheManager(); } @Bean public SessionManager sessionManager() { // 生成之前配置的会话管理器 return new CustomSessionManager(); } @Bean public CorsAuthenticationFilter corsAuthenticationFilter() { // 生成之前配置的授权拦截器 return new CorsAuthenticationFilter(); } @Bean public DefaultWebSecurityManager securityManager(){ // 生成默认安全管理器后将上文中配置全部加入其中 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); securityManager.setCacheManager(cacheManager()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setSessionManager(sessionManager()); return securityManager; } // 这部分的含义见之前的文章 @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/user/login"); Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/favicon.ico","anon"); filterChainDefinitionMap.put("/user/login","anon"); filterChainDefinitionMap.put("/**", "corsAuthenticationFilter"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter()); shiroFilterFactoryBean.setFilters(filterMap); return shiroFilterFactoryBean; } /** * SpringShiroFilter首先注册到spring容器 * 然后被包装成FilterRegistrationBean * 最后通过FilterRegistrationBean注册到servlet容器 */ @Bean public FilterRegistrationBean delegatingFilterProxy(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); DelegatingFilterProxy proxy = new DelegatingFilterProxy(); proxy.setTargetFilterLifecycle(true); proxy.setTargetBeanName("shiroFilter"); filterRegistrationBean.setFilter(proxy); return filterRegistrationBean; } @Bean public SimpleCookie rememberMeCookie(){ // 配置rememberMe Cookie SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); simpleCookie.setMaxAge(24 * 60 * 60); simpleCookie.setHttpOnly(false); return simpleCookie; } @Bean public CookieRememberMeManager rememberMeManager(){ // 将Cookie交给管理器并加密 CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag==")); return cookieRememberMeManager; } // 后面这仨方法直接照抄即可，配置生命周期和代理，否则Shiro无法生效 @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } @Bean @ConditionalOnMissingBean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } } 至此配置就算是全部完成了，接下来只需要在 Controller 层设置所需权限。注解标记权限@RestController() @CrossOrigin @RequestMapping("/user/") public class UserController extends BaseController { @Resource(name = "userServiceImpl") private UserService userService; @RequiresRoles("admin") @RequiresPermissions("user:selectAll") @GetMapping(value = "/selectAll") @ResponseBody public Response<List<User>> selectAll() { List<User> result = userService.selectAll(); if (result.size() != 0) { return getSuccessResult(result); } return getFailResult(404, "Message not found!"); } }比如我的用户 Controller 中的查询方法，同时使用@RequiresRoles和@RequiresPermissions表示用户不仅需要 admin 角色，而且还需要 user:selectAll 权限才能够使用此 api 接口，缺一不可。换句话来说，如果我给其他角色也分配了这个权限，他们依旧不能使用此方法，admin 用户被删除该权限后也将无法使用此方法。除此之外我再贴一个登陆方法和一个退出登录方法，其他都和查询方法类似。@PostMapping(value = "/login") @ResponseBody public Response<User> userLogin(@RequestBody User sysUser) { User result; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsrName(), sysUser.getUsrPassword()); token.setRememberMe(true); try { subject.login(token); result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword()); session.setAttribute("user", subject); } catch (UnknownAccountException e) { return getFailResult(404, "Message not found"); } catch (IncorrectCredentialsException e) { return getFailResult(412, "Incorrect message"); } catch (LockedAccountException e) { return getFailResult(401, "Account locked"); } catch (Exception e) { e.printStackTrace(); return getFailResult(408, "Unknown error"); } return getSuccessResult(result); } @GetMapping(value = "/logout") @ResponseBody public Response<String> userLogout() { CorsUtil.setResponseHeader(response, request); Subject subject = SecurityUtils.getSubject(); try { subject.logout(); } catch (SessionException e) { e.printStackTrace(); return getFailResult(408, e.toString()); } return getSuccessResult("logout"); }这部分之前也讲过，这里不说了，由于整合 Shiro 是在涉及了大量的内容，如果全部解释一遍今天这篇又得拆成很多篇来讲，我之所以分三篇写就是为了让这一篇的内容分散到其他两篇中去，有需要的同学可以回去巩固一下基础知识。最终测试差不多可以收工了，我们最后再使用 Postman 测试一下接口看看是什么效果。直接调查询所有用户的接口，发现提示“用户未登录”。接下来调用登录接口并传数据库中已经有的用户参数，发现登录成功。在登录之后重新调用查询接口，发现现在已经拥有看权限，可以点开 Cookie 查看，也是我们在后端设置过的 rememberMe，Headers 中也有我们设置的跨域处理内容。那么至此，Shiro 整合 SSM 项目就大功告成了，你可以在后端拓展一些关于权限方面的业务，在前端也做一些权限控制的内容，使项目更加完善。总结这篇文章解释性的话并不多，主要都是代码。从头到尾，贴出了配置 Shiro 并整合到项目中的过程。准确的来说，如果想要权限的功能完完全全的配置成功，这些代码都是必不可少的。因为其中还涉及跨域问题的处理等在实际操作中会遇到的问题，许多问题都是我在操作中遇到并解决的，其他一些博客中则并没有讲到。希望这篇文章对你有所帮助，尤其是也在开发前后端分离项目并且遇到很多问题的同学。接下来我打算写一篇前后端项目整合 Swagger 的文章，毕竟 api 接口对于前后端分离的项目可以说是两个人沟通的桥梁，非常容易在这一块出问题，学会使用 Swagger 不仅能减少问题的产生，也能优化我们的后台管理系统，丰富其内容。如果有什么建议，欢迎评论交流。
开端好的，起因是我把最近做好的项目给一部分人进行了测试，发现大部分朋友都提出了同一个问题，你的系统权限管理是如何实现的。我只能尴尬的说一句，不好意思这部分还没开发。然而我也知道，其实对于一个项目来说，权限可以说是最主要的一部分。后端除了对权限进行处理，其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去，顺便把我的学习笔记分享给大家。而对于权限控制的框架呢，听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的，不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架，可以用在各种场合，甚至非 Web 项目中，由于它的会话独立于容器，后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。由于这部分内容比较多，我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服，所以我打算把这个内容分成几个部分：初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于 Shiro 该如何配置的，面向 Spring Boot，讲一些比较基础的内容。深入Shiro之前我们了解了一下 Shiro 的基本功能和架构，现在我们来看看它的工作流程。工作流程拦截Shiro 特别重要的一块就是拦截，它通过过滤器拦截所有请求，根据配置决定哪些可以认证通过，哪些不用被拦截，还有哪些没有通过认证强制跳转。我们看一段配置代码，如何通过过滤器决定谁去谁留。@Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/user/login"); Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/user/login","anon"); filterChainDefinitionMap.put("/**", "corsAuthenticationFilter"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter()); shiroFilterFactoryBean.setFilters(filterMap); return shiroFilterFactoryBean; }代码非常简单，首先使用 @Bean 注释将这个方法交给 Spring 来管理，名字必须是 shiroFilter，这样才能和默认配置中的名字匹配，否则 Spring 会找不到这个过滤器。在这个方法中首先 new 一个过滤器工厂对象，将获取到的 securityManager 注入。之后设置一个登陆 url，告诉 Shiro，这个页面/接口是我的登陆页面，用户角色权限的获取也都在这里。接着就是刚刚图中的 filterChainDefinition，我们定义一个 map 来存储不被拦截的页面。每一个键值对中 key 存储 url，value 存储拦截器的名字，拦截器主要是以下几个：拦截器名称拦截器功能anno不需要授权和登录，可以匿名访问authc需要登录授权才能访问logout退出成功之后可以自定义重定向user需要登录授权或者登录后开启了记住我（remember me）就能访问ssl通过https协议才能通过roles角色拦截restrest风格拦截port端口拦截perms权限拦截noSessionCreation不创建会话连接器authcBasic基本http身份验证拦截比较常用的四个拦截器我使用加粗表示出来了，anno 一般是最先使用的，为什么说最先使用，因为拦截器拦截的顺序是按照你 map 中加入的键值对的顺序来的。按照第一次匹配优先原则，如果一开始就被拦截，那么之后如果符合条件也不会放行。有一些公开资源，尤其是登录接口，肯定是要设置 anno 匿名的。假如说连入口的登录功能都拦截，那用户还这么使用这个系统的其他功能呢？authc 是指需要登录或授权才能访问，这个值的 key 一般都是 "/*" ，而且写在最后。表示除了上述设置的拦截规则，对于其他所有 url 全部拦截并且需要授权才能访问。而我这里没有这么写，而是自定义了一个 corsAuthenticationFilter。从后面的代码也可以看到我又定义了一个 filterMap* ，使用我自定义的一个过滤方式拦截其他所有请求。本来我也是采用 authc 的方式，但是由于前后端跨域的需要，必须要加上跨域请求头的一些参数，所以直接另写了一个过滤器类，并且在其中处理了验证通过和验证失败的处理方式。具体可见下一篇，SSM 整合 Shiro，这里不多赘述。认证那么拦截之后我们怎么样判断用户是什么角色，拥有哪些权限呢？这块其实我在上一篇中有所讲到，已经把源码拉出来解析过了。有需要的朋友可以再去回顾一下，我这里只讲它整个认证的流程。获取当前的 Subject，调用 SecurityUtils.getSubject( )测试当前的用户是否已经被认证，即是否已登陆，调用 Subject 的 isAuthenticated( )若没有被认证，则把用户名和密码封装为 UsernamePasswordToken 对象执行登陆：调用 Subject 的 login(AuthenticationToken) 方法自定义 Realm 方法，从数据库中获取相应的记录，返回给 Shiro由 Shiro 完成对密码的比对回顾第一篇我们讲到的，第一步是从数据库中找到所有的用户。第三步是通过我们前端获取到的用户名和密码生成 token。第四步是利用生成的 token 参数传给 login 方法进行登录。登录时对用户角色和权限的验证采用了自定义 Reaml 中重写的两个方法 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( )，还记得吧，挺容易搞混的。机制在 Shiro 中有许多我们曾经经常使用到的内容，比如缓存，还有一些我们不太了解的东西，比如盐值加密。我这里简要讲一讲这些机制。MD5盐值加密盐值加密是我以前没有听说过的知识，所以在我第一次整合 Shiro 的时候也没有加入这一部分的内容。众所周知，用户名和密码是被保存在数据库中。可是一旦数据库发生了泄露，用户名和密码就都遭到了泄露。攻击者可以轻松的获取用户名和密码，进行操作。更大的危害是，由于现在需要注册的网站、app越来越多。用户名和密码很多时候都是相同的。一旦某处发生了泄露，则后果会慢慢的扩散。这些危害大家可以查询下近些年发生的一些安全事故，如Sony数据库泄露、网易数据库泄露、CSDN数据库泄露等，因此我们要对密码进行加密。为何取名为盐，此盐非彼颜。盐是许许多多微小的颗粒组成的，他就像一个随机数，你猜不到有多少颗。当你给密码加了盐，再结合 MD5，就不那么容易被破解了。盐值加密主要就是如何生成盐，我们一般使用唯一的数据来产生。比如说我的数据库中用户名是不可重复的，那么用户名就是唯一的（昵称不唯一），所以可以使用用户名生成盐。ByteSource.Util.bytes(userName)ByteSource 是 Shiro 提供的一个工具类，可以转换进制。其中还有一个静态子类 Util，里面的 bytes( ) 方法返回了一个 SimpleByteSource 对象，而这个对象又使用 CodecSupport 中的 toBytes( ) 方法对接收到的不同参数采取不同的加密方式。以下为源码：protected byte[] toBytes(Object o) { if (o == null) { String msg = "Argument for byte conversion cannot be null."; throw new IllegalArgumentException(msg); } else if (o instanceof byte[]) { return (byte[])((byte[])o); } else if (o instanceof ByteSource) { return ((ByteSource)o).getBytes(); } else if (o instanceof char[]) { return toBytes((char[])((char[])o)); } else if (o instanceof String) { return toBytes((String)o); } else if (o instanceof File) { return this.toBytes((File)o); } else { return o instanceof InputStream ? this.toBytes((InputStream)o) : this.objectToBytes(o); } }一般而言我们生成了盐值之后，会使用 MD5 将原密码与盐值拼接在一起，然后再次进行加密，之后的加密次数是可以设置的，理论上加密的次数越多越安全。因为我没搞过安全，不太了解这一方面。我觉得咱们要是做个项目练练手的话没有必要加这个内容。如果需要，加密两次也足够了。因为我的项目数据表中没有写盐值，所以没有尝试过存在数据库中，只单独测试了几个例子，也就不展开讲了。多Realm验证通常来讲一个项目可能会用到多个数据库，比如说用户的验证需要 MySQL 和 Oracle 两个数据库中的数据，那这里就涉及到了多 Realm 验证和验证策略的问题。对于每一个 Realm 都要配置到 ModularRealmAuthenticator 类中。还记得我们在认证和上一篇的 Shiro 基本功能中讲到了两个 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( ) 方法吗。这两个方法就不多说了，我们接着要复写 ModularRealmAuthenticator 类，主要是要重写两个 isPermitted( ) 方法和一个 hasRole( ) 方法。假设我们已经拥有了一个 AdminRealm 和一个 UserReal，其中分别重写了两个 doGet 方法。那么我们就要在 ModularRealm 中通过匹配判断应该实现哪一个验证器的策略。public class CustomizedModularRealmAuthorizer extends ModularRealmAuthorizer { @Override public boolean isPermitted(PrincipalCollection principals, String permission) { assertRealmsConfigured(); Set<String> realmNames = principals.getRealmNames(); //获取realm的名字 String realmName = realmNames.iterator().next(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; //匹配名字 if(realmName.equals("admin"))) { if (realm instanceof AdminRealm) { return ((AdminRealm) realm).isPermitted(principals, permission); } } if(realmName.equals("user")) { if (realm instanceof UserRealm) { return ((UserRealm) realm).isPermitted(principals, permission); } } } return false; } @Override public boolean isPermitted(PrincipalCollection principals, Permission permission) { // 省略，和上一个方法一样 } @Override public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { // 省略，和上一个方法一样 } }接着在 ShiroConfiguration 中的 securityManager( ) 中加入你的多 Realm，就像这样：@Bean public DefaultWebSecurityManager securityManager(UserRealm customRealm, AdminRealm adminRealm, DefaultWebSessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //======================设置多个Realm====================== List<Realm> realms = new ArrayList<>(); realms.add(customRealm); realms.add(adminRealm); securityManager.setRealms(realms); securityManager.setSessionManager(sessionManager); securityManager.setCacheManager(new RedisCacheManager()); //====================多realm授权核心代码=================== CustomizedModularRealmAuthorizer authorizer = new CustomizedModularRealmAuthorizer(); authorizer.setRealms(realms); securityManager.setAuthorizer(authorizer); return securityManager; }注意咯，不仅要把 Realm 放进 SecurityManager 中，还要将其放入 CustomizedModularRealmAuthorizer 中。这样相当于开启了多 Realm 模式，程序会在运行到 doAuthenticate( ) 方法时判断有多个 Realm，然后返回多 Realm 认证策略。// 判断Realm个数 protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); } // 施行多Realm策略 protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } else { AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } else { return info; } } }认证策略ModularRealmAuthorizer 有三种认证策略：名称策略FirstSuccessfulStrategy只要有一个Ream验证成功即可，返回第一个验证成功的认证信息而忽略其他AtLeastOneSuccessfulStrategy只要有一个Ream验证成功即可，返回所有认证成功的验证信息AllSuccessfulStrategy所有Realm都认证成功才算成功，且返回所有认证成功的验证信息之前没有讲这个，因为 ModularRealmAuthorizer 默认实现第二种认证策略，所以只要有一个认证成功即可。这块的配置我只能查到使用 ini 或 xml 的形式，百度根本没有 Spring Boot 的配置方式，如果有小伙伴会直接在 ShiroConfig 类中修改的话请评论告诉我谢谢。下面是我找到的源码，不过还是不太能懂他具体怎么使用这个配置的。public class AllSuccessfulStrategyTest { private AllSuccessfulStrategy strategy; @Before public void setUp() { strategy = new AllSuccessfulStrategy(); } @Test public void beforeAllAttempts() { AuthenticationInfo info = strategy.beforeAllAttempts(null, null); assertNotNull(info); } @Test public void beforeAttemptSupportingToken() { new SimpleAccountRealm(); } @Test(expected = UnsupportedTokenException.class) public void beforeAttemptRealmDoesntSupportToken() { Realm notSupportingRealm = new AuthorizingRealm() { public boolean supports(AuthenticationToken token) { return false; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { return null; } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { return null; } }; strategy.beforeAttempt(notSupportingRealm, null, null); } }权限注解Shiro 中权限的配置有多种方式，其中编程式不太使用，因为需要写很多代码。老式写法一般都是 xml，我的话采用注解来完成。 <property name="filterChainDefinitions"> <value> /login = anno /logout = logout /user = roles[user] /admin = permissions[admin:check] </value> </property>// 注解形式 @RequiresRoles("user") @RequiresPermissions("admin:check") @GetMapping(value = "/selectAll") @ResponseBody public Response<List<User>> selectAll() { List<User> result = userService.selectAll(); if (result.size() != 0) { return getSuccessResult(result); } return getFailResult(404, "Message not found!"); }显然注解更加方便，xml 的话能够统一配置。特别注意一下，如果一个接口同时注解了 role 和 permission，必须要同时满足才可以访问。除此之外还有 @RequireUser和 @RequireAuthentication 和 @RequireGuest 三种注解，分别表示需要登录或者记住我、必须登录、没有验证或记住我登录这三个条件，前两个相当于之前认证中讲到的 user、 authc。标签这部分内容是 jsp 中的标签，可以根据 shiro 的授权为不同角色显示不同信息或者输出权限信息等等，由于是涉及 jsp 的，我这里不展开讲了，今后的趋势是前后端分离，这些老旧的技术咱不讨论。虽然说这个机制确实挺方便的，但是熟悉前端的同学分分钟就能自己写 js 实现这些功能。会话管理在 Shiro 中有和 Java Web 想类似的会话管理。Shiro 中的会话是不需要基于容器的，而且可以在 Service 层跑，这是两个比较方便的特性。记住我你是否有印象，当访问一下网站登录时，下方会有一个记住我的选项框，如果勾选上之后并登录，第二次打开（一段时间之内）就不需要进行登录了。但是访问一些敏感网页，比如订单，还是需要重新登录以免他人借用你的电脑。事实上由于浏览器提供的密码保存功能，重新登录确实没有必要。所以我们需要记住一点，我们登录了一个系统，那我们要么就是通过身份验证登录的，要么就是通过记住我登录的，两者只取其一。对于记住我的设置也非常简单，只需要在用户的 Subject 采用 login 方法前设置即可，上代码。@PostMapping(value = "/login") @ResponseBody public Response<User> userLogin(@RequestBody User sysUser) { User result; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsrName(), sysUser.getUsrPassword()); token.setRememberMe(true); try { subject.login(token); result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword()); } catch (UnknownAccountException e) { return getFailResult(404, "Message not found"); } catch (IncorrectCredentialsException e) { return getFailResult(412, "Incorrect message"); } catch (LockedAccountException e) { return getFailResult(401, "Account locked"); } catch (Exception e) { e.printStackTrace(); return getFailResult(408, "Unknown error"); } return getSuccessResult(result); }注意一下我是直接设置为 true，如果小伙伴们希望通过用户在登录时勾选记住我才实现记住我的功能，需要在接口接受参数然后设置。除此之外还要在 ShiroConfiguration 中设置一下一些参数：@Bean public SimpleCookie rememberMeCookie(){ // 取名为rememberMe SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); simpleCookie.setMaxAge(36000); simpleCookie.setHttpOnly(false); return simpleCookie; } @Bean public CookieRememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); // 管理Cookie cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag==")); return cookieRememberMeManager; } @Bean public DefaultWebSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); securityManager.setCacheManager(cacheManager()); // 设置记住我 securityManager.setRememberMeManager(rememberMeManager()); securityManager.setSessionManager(sessionManager()); return securityManager; }设置 Cookie 的寿命为 36000 秒（10小时），设置 HTTPOnly 为 false，这一步很重要，否则前端无法读取 Cookie 数据。然后将其将入 rememberMeManager 里，顺带加个密。最后的最后将其注入 SecurityManager 中就完成了配置。总结经过前一篇的 Shiro 入门，大家已经初步了解了 Shiro 这个框架的基本功能和架构。在这篇进阶版教程中，更加详细的讲了拦截器和认证规则，以及许多我们项目中需要手动进行配置的内容，比如密码加密、权限注解、记住我功能等等。在下一篇 Shiro 系列的文章中，我会利用之前讲过的知识，教大家如何快速整合 Shiro、SSM 和 Vue 项目。如果在阅读的过程中大家觉得我有哪些地方描述的不正确，或者你能够解决文章中我提到的问题，欢迎评论交流！全部原创，真情实感，更新不易，需要动力 ：）