-
前后端分离架构概述
2018-08-12 14:16:51前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种...1、背景
前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如:浏览器,车载终端,安卓,IOS等等)打下坚实的基础。这个步骤是系统架构从猿进化成人的必经之路。
核心思想是前端HTML页面通过AJAX调用后端的RESTFUL API接口并使用JSON数据进行交互。
Web服务器:一般指像Nginx,Apache这类的服务器,他们一般只能解析静态资源;
应用服务器:一般指像Tomcat,Jetty,Resin这类的服务器可以解析动态资源也可以解析静态资源,但解析静态资源的能力没有web服务器好;
一般都是只有web服务器才能被外网访问,应用服务器只能内网访问。
以前的Java Web项目大多数都是Java程序员又当爹又当妈,又搞前端,又搞后端。随着时代的发展,渐渐的许多大中小公司开始把前后端的界限分的越来越明确,前端工程师只管前端的事情,后端工程师只管后端的事情。正所谓术业有专攻,一个人如果什么都会,那么他毕竟什么都不精。大中型公司需要专业人才,小公司需要全才,但是对于个人职业发展来说,前后端需要分离。
2、未分离时代(各种耦合)
早期主要使用MVC框架,Jsp+Servlet的结构图如下:
大致就是所有的请求都被发送给作为控制器的Servlet,它接受请求,并根据请求信息将它们分发给适当的JSP来响应。同时,Servlet还根据JSP的需求生成JavaBeans的实例并输出给JSP环境。JSP可以通过直接调用方法或使用UseBean的自定义标签得到JavaBeans中的数据。需要说明的是,这个View还可以采用 Velocity、Freemaker 等模板引擎。使用了这些模板引擎,可以使得开发过程中的人员分工更加明确,还能提高开发效率。
那么,在这个时期,开发方式有如下两种:
方式一方式二
方式二已经逐渐淘汰。主要原因有两点:
1)前端在开发过程中严重依赖后端,在后端没有完成的情况下,前端根本无法干活;
2)由于趋势问题,会JSP,懂velocity,freemarker等模板引擎的前端越来越少;
因此,方式二逐渐不被采用。然而,不得不说一点,方式一,其实很多小型传统软件公司至今还在使用。那么,方式一和方式二具有哪些共同的缺点呢?
1、前端无法单独调试,开发效率低;
2、 前端不可避免会遇到后台代码,例如:<body> <% request.setCharacterEncoding("utf-8") String name=request.getParameter("username"); out.print(name); %> </body>
这种方式耦合性太强。那么,就算你用了freemarker等模板引擎,不能写Java代码。那前端也不可避免的要去重新学习该模板引擎的模板语法,无谓增加了前端的学习成本。正如我们后端开发不想写前端一样,你想想如果你的后台代码里嵌入前端代码,你是什么感受?因此,这种方式十分不妥。
3、JSP本身所导致的一些其他问题 比如,JSP第一次运行的时候比较缓慢,因为里头包含一个将JSP翻译为Servlet的步骤。再比如因为同步加载的原因,在JSP中有很多内容的情况下,页面响应会很慢。
3、半分离时代
前后端半分离,前端负责开发页面,通过接口(Ajax)获取数据,采用Dom操作对页面进行数据绑定,最终是由前端把页面渲染出来。这也就是Ajax与SPA应用(单页应用)结合的方式,其结构图如下:
步骤如下:
(1)浏览器请求,CDN返回HTML页面;
(2)HTML中的JS代码以Ajax方式请求后台的Restful接口;
(3)接口返回Json数据,页面解析Json数据,通过Dom操作渲染页面;
后端提供的都是以JSON为数据格式的API接口供Native端使用,同样提供给WEB的也是JSON格式的API接口。
那么意味着WEB工作流程是:
1、打开web,加载基本资源,如CSS,JS等;
2、发起一个Ajax请求再到服务端请求数据,同时展示loading;
3、得到json格式的数据后再根据逻辑选择模板渲染出DOM字符串;
4、将DOM字符串插入页面中web view渲染出DOM结构;
这些步骤都由用户所使用的设备中逐步执行,也就是说用户的设备性能与APP的运行速度联系的更紧换句话说就是如果用户的设备很低端,那么APP打开页面的速度会越慢。
为什么说是半分离的?因为不是所有页面都是单页面应用,在多页面应用的情况下,前端因为没有掌握controller层,前端需要跟后端讨论,我们这个页面是要同步输出呢,还是异步Json渲染呢?而且,即使在这一时期,通常也是一个工程师搞定前后端所有工作。因此,在这一阶段,只能算半分离。
首先,这种方式的优点是很明显的。前端不会嵌入任何后台代码,前端专注于HTML、CSS、JS的开发,不依赖于后端。自己还能够模拟Json数据来渲染页面。发现Bug,也能迅速定位出是谁的问题。
然而,在这种架构下,还是存在明显的弊端的。最明显的有如下几点:
1)JS存在大量冗余,在业务复杂的情况下,页面的渲染部分的代码,非常复杂;
2)在Json返回的数据量比较大的情况下,渲染的十分缓慢,会出现页面卡顿的情况;
3)SEO( Search Engine Optimization,即搜索引擎优化)非常不方便,由于搜索引擎的爬虫无法爬下JS异步渲染的数据,导致这样的页面,SEO会存在一定的问题;
4)资源消耗严重,在业务复杂的情况下,一个页面可能要发起多次HTTP请求才能将页面渲染完毕。可能有人不服,觉得PC端建立多次HTTP请求也没啥。那你考虑过移动端么,知道移动端建立一次HTTP请求需要消耗多少资源么?正是因为如上缺点,我们才亟需真正的前后端分离架构。
4、分离时代
大家一致认同的前后端分离的例子就是SPA(Single-page application),所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现。从某种意义上来说,SPA确实做到了前后端分离,但这种方式存在两个问题:
- WEB服务中,SPA类占的比例很少。很多场景下还有同步/同步+异步混合的模式,SPA不能作为一种通用的解决方案;
- 现阶段的SPA开发模式,接口通常是按照展现逻辑来提供的,而且为了提高效率我们也需要后端帮我们处理一些展现逻辑,这就意味着后端还是涉足了view层的工作,不是真正的前后端分离。
SPA式的前后端分离,从物理层做区分(认为只要是客户端的就是前端,服务器端就是后端)这种分法已经无法满足前后端分离的需求,我们认为从职责上划分才能满足目前的使用场景:
- 前端负责view和controller层
- 后端只负责model层,业务处理与数据持久化等
controller层与view层对于目前的后端开发来说,只是很边缘的一层,目前的java更适合做持久层、model层的业务。
在前后端彻底分离这一时期,前端的范围被扩展,controller层也被认为属于前端的一部分。在这一时期:
前端:负责View和Controller层。
后端:只负责Model层,业务/数据处理等。可是服务端人员对前端HTML结构不熟悉,前端也不懂后台代码呀,controller层如何实现呢?这就是node.js的妙用了,node.js适合运用在高并发、I/O密集、少量业务逻辑的场景。最重要的一点是,前端不用再学一门其他的语言了,对前端来说,上手度大大提高。
可以就把Nodejs当成跟前端交互的api。总得来说,NodeJs的作用在MVC中相当于C(控制器)。Nodejs路由的实现逻辑是把前端静态页面代码当成字符串发送到客户端(例如浏览器),简单理解可以理解为路由是提供给客户端的一组api接口,只不过返回的数据是页面代码的字符串而已。
用NodeJs来作为桥梁架接服务器端API输出的JSON。后端出于性能和别的原因,提供的接口所返回的数据格式也许不太适合前端直接使用,前端所需的排序功能、筛选功能,以及到了视图层的页面展现,也许都需要对接口所提供的数据进行二次处理。这些处理虽可以放在前端来进行,但也许数据量一大便会浪费浏览器性能。因而现今,增加Node中间层便是一种良好的解决方案。
浏览器(webview)不再直接请求JSP的API,而是:
1)浏览器请求服务器端的NodeJS;
2)NodeJS再发起HTTP去请求JSP;
3)JSP依然原样API输出JSON给NodeJS;
4)NodeJS收到JSON后再渲染出HTML页面;
5)NodeJS直接将HTML页面flush到浏览器;
这样,浏览器得到的就是普通的HTML页面,而不用再发Ajax去请求服务器了。
淘宝的前端团队提出的中途岛(Midway Framework)的架构如下图所示:
增加node.js作为中间层,具体有哪些好处呢?
(1)适配性提升;我们其实在开发过程中,经常会给PC端、mobile、app端各自研发一套前端。其实对于这三端来说,大部分端业务逻辑是一样的。唯一区别就是交互展现逻辑不同。如果controller层在后端手里,后端为了这些不同端页面展示逻辑,自己维护这些controller,模版无法重用,徒增和前端沟通端成本。 如果增加了node.js层,此时架构图如下:
在该结构下,每种前端的界面展示逻辑由node层自己维护。如果产品经理中途想要改动界面什么的,可以由前端自己专职维护,后端无需操心。前后端各司其职,后端专注自己的业务逻辑开发,前端专注产品效果开发。
(2)响应速度提升;我们有时候,会遇到后端返回给前端的数据太简单了,前端需要对这些数据进行逻辑运算。那么在数据量比较小的时候,对其做运算分组等操作,并无影响。但是当数据量大的时候,会有明显的卡顿效果。这时候,node中间层其实可以将很多这样的代码放入node层处理、也可以替后端分担一些简单的逻辑、又可以用模板引擎自己掌握前台的输出。这样做灵活度、响应度都大大提升。
举个例子,即使做了页面静态化之后,前端依然还是有不少需要实时从后端获取的信息,这些信息都在不同的业务系统中,所以需要前端发送5、6个异步请求来。有了NodeJs之后,前端可以在NodeJs中去代理这5个异步请求。还能很容易的做bigpipe,这块的优化能让整个渲染效率提升很多。在PC上你觉得发5、6个异步请求也没什么,但是在无线端,在客户手机上建立一个http请求开销很大。有了这个优化,性能一下提升好几倍。
(3)性能得到提升;大家应该都知道单一职责原则。从该角度来看,我们,请求一个页面,可能要响应很多个后端接口,请求变多了,自然速度就变慢了,这种现象在mobile端更加严重。采用node作为中间层,将页面所需要的多个后端数据,直接在内网阶段就拼装好,再统一返回给前端,会得到更好的性能。
(4)异步与模板统一;淘宝首页就是被几十个HTML片段(每个片段一个文件)拼装成,之前PHP同步include这几十个片段,一定是串行的,Node可以异步,读文件可以并行,一旦这些片段中也包含业务逻辑,异步的优势就很明显了,真正做到哪个文件先渲染完就先输出显示。前端机的文件系统越复杂,页面的组成片段越多,这种异步的提速效果就越明显。前后端模板统一在无线领域很有用,PC页面和WIFI场景下的页面适合前端渲染(后端数据Ajax到前端),2G、3G弱网络环境适合后端渲染(数据随页面吐给前端),所以同样的模板,在不同的条件下走不同的渲染渠道,模板只需一次开发。
增加NodeJS中间层后的前后端职责划分:
5、总结
从经典的JSP+Servlet+JavaBean的MVC时代,到SSM(Spring + SpringMVC + Mybatis)和SSH(Spring + Struts + Hibernate)的Java 框架时代,再到前端框架(KnockoutJS、AngularJS、vueJS、ReactJS)为主的MV*时代,然后是Nodejs引领的全栈时代,技术和架构一直都在进步。虽然“基于NodeJS的全栈式开发”模式很让人兴奋,但是把基于Node的全栈开发变成一个稳定,让大家都能接受的东西还有很多路要走。创新之路不会止步,无论是前后端分离模式还是其他模式,都是为了更方便得解决需求,但它们都只是一个“中转站”。前端项目与后端项目是两个项目,放在两个不同的服务器,需要独立部署,两个不同的工程,两个不同的代码库,不同的开发人员。前端只需要关注页面的样式与动态数据的解析及渲染,而后端专注于具体业务逻辑。
参考:淘宝前后端分离解决方案
参考:浅谈前后端分离技术
-
七个开源的 Spring Boot 前后端分离项目,一定要收藏!
2019-09-18 10:05:53前后端分离已经在慢慢走进各公司的技术栈,根据松哥了解到的消息,不少公司都已经切换到这个技术栈上面了。即使贵司目前没有切换到这个技术栈上面,松哥也非常建议大家学习一下前后端分离开发,以免在公司干了两三年...前后端分离已经在慢慢走进各公司的技术栈,根据松哥了解到的消息,不少公司都已经切换到这个技术栈上面了。即使贵司目前没有切换到这个技术栈上面,松哥也非常建议大家学习一下前后端分离开发,以免在公司干了两三年,SSH 框架用的滚瓜烂熟,出来却发现自己依然没有任何优势!
其实前后端分离本身并不难,后段提供接口,前端做数据展示,关键是这种思想。很多人做惯了前后端不分的开发,在做前后端分离的时候,很容易带进来一些前后端不分时候的开发思路,结果做出来的产品不伦不类,因此松哥这里给大家整理了几个开源的前后端分离项目,帮助大家快速掌握前后端分离开发技术栈。
美人鱼
- star 数 3499
- 项目地址: https://gitee.com/mumu-osc/NiceFish
听名字就知道这是个不错的项目,事实上确实不赖。NiceFish(美人鱼) 是一个系列项目,目标是示范前后端分离的开发模式:前端浏览器、移动端、Electron 环境中的各种开发模式;后端有两个版本:SpringBoot 版本和 SpringCloud 版本,前端有 Angular 、React 以及 Electron 等版本。
项目效果图:
微人事
- star 数 9313
- 项目地址:https://github.com/lenve/vhr
微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot + Vue 开发。项目打通了前后端,并且提供了非常详尽的文档,从 Spring Boot 接口设计到前端 Vue 的开发思路,作者全部都记录在项目的 wiki 中,是不可多得的 Java 全栈学习资料。
项目效果图:
项目部分文档截图:
bootshiro
- star 数 1370
- 项目地址: https://gitee.com/tomsun28/bootshiro
bootshiro 是基于 Spring Boot + Shiro + JWT 的真正 RESTful URL 资源无状态认证权限管理系统的后端,前端 usthe 。区别于一般项目,该项目提供页面可配置式的、动态的 RESTful api 安全管理支持,并且实现数据传输动态秘钥加密,jwt 过期刷新,用户操作监控等,加固应用安全。
项目效果图:
open-capacity-platform
- star 数 2643
- 项目地址:https://gitee.com/owenwangwen/open-capacity-platform
open-capacity-platform 微服务能力开放平台,简称 ocp ,是基于 layui + springcloud 的企业级微服务框架(用户权限管理,配置中心管理,应用管理,…),其核心的设计目标是分离前后端,快速开发部署,学习简单,功能强大,提供快速接入核心接口能力,其目标是帮助企业搭建一套类似百度能力开放平台的框架。
项目效果图:
V 部落
- star 数 2902
- 项目地址:https://github.com/lenve/VBlog
V部落是一个多用户博客管理平台,采用 Vue + SpringBoot + ElementUI 开发。这个项目最大的优势是简单,属于功能完整但是又非常简单的那种,非常非常适合初学者。
项目效果图:
悟空 CRM
- star 数 650
- 项目地址:https://gitee.com/wukongcrm/72crm-java
悟空 CRM 是基于 jfinal + vue + ElementUI 的前后端分离 CRM 系统。
老实说,jfinal 了解下就行了,没必要认真研究,Vue + ElementUI 的组合可以认真学习下、前后端交互的方式可以认真学习下。
paascloud-master
- star 数 5168
- 项目地址:https://github.com/paascloud/paascloud-master
paascloud-master 核心技术为 SpringCloud + Vue 两个全家桶实现,采取了取自开源用于开源的目标,所以能用开源绝不用收费框架,整体技术栈只有阿里云短信服务是收费的,都是目前 java 前瞻性的框架,可以为中小企业解决微服务架构难题,可以帮助企业快速建站。由于服务器成本较高,尽量降低开发成本的原则,本项目由 10 个后端项目和 3 个前端项目共同组成。真正实现了基于 RBAC、jwt 和 oauth2 的无状态统一权限认证的解决方案,实现了异常和日志的统一管理,实现了 MQ 落地保证 100% 到达的解决方案。
项目效果图:
总结
他山之石,可以攻玉。当我们学会了很多知识点之后,需要一个项目来将这些知识点融会贯通,这些开源项目就是很好的资料。现在前后端分离开发方式日渐火热,松哥也强烈建议大家有空学习下这种开发方式。虽然我们身为 Java 工程师,可是也不能固步自封,看看前端单页面应用怎么构建,看看前端工程化是怎么回事,这些都有助于我们开发出更加合理好用的后端接口。好了,七个开源项目,助力大家在全栈的路上更进一步!
关注公众号【江南一点雨】,专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货!
-
前后端分离
2019-01-14 19:15:34前后端分离。都说前后端分离,前后端分离有什么好处?为什么要前后端分离? BibPipe,将按步就班改成了流水线。前后端分离。现在都在说前后端分离。前后端分离的好处毋庸置疑,大咖们已经说的很清楚了,解耦啦,职责分离啦,并行开发啦,后端可以重用啦,安全啦,提高性能啦,好多。
我想说的是,什么是前端?
过去,你要问我的话,我会回答说,运行在浏览器端的东东就叫前端。前几天刚接触前后端分离这个概念,不禁心痒难搔,赶了一把时髦,将新项目搞成前后端分离,并着手开发前端。没办法,谁叫咱们是搞asp网站出身,对前端东西比较熟悉呢!结果那个痛苦!
痛苦的地方,主要在于模板重用和配置文件读取。
原本这些东西,用以前的前后不分模式,是非常容易解决的。因为页面输出到浏览器之间,是经过服务器处理的。服务器可以纵横捭阖,各种资源信手拈来,任意组合,再输出。但前后端分离后,这一切都要靠浏览器来完成!
浏览器,辣么弱智,辣么脑残的一个东东。切。一生只负责显示,让你组合资源,还不如吃屎靠谱。
所以,前端不应该仅仅是运行在浏览器端的页面(V,视图),还应该包括输出页面的这部分内容(C,控制器)。按照目前的流行前后端架构,这部分内容可以是node.js。node.js是运行在服务器端的JS,有服务器资源和功能加持,同时又有着前端的语法,我靠,舍我其谁!
浏览器很弱,它的职责就是展示和交互。前端<>浏览器,后端<>服务器。应该按职责划分:
前端 = V + C 后端 = M
-
在前后端分离的SpringBoot项目中集成Shiro权限框架
2017-12-12 14:13:47公司在几年前就采用了前后端分离的开发模式,前端所有请求都使用ajax。这样的项目结构在与CAS单点登录等权限管理框架集成时遇到了很多问题,使得权限部分的代码冗长丑陋,CAS的各种重定向也使得用户体验很差,在...项目背景
公司在几年前就采用了前后端分离的开发模式,前端所有请求都使用ajax。这样的项目结构在与CAS单点登录等权限管理框架集成时遇到了很多问题,使得权限部分的代码冗长丑陋,CAS的各种重定向也使得用户体验很差,在前端使用vue-router管理页面跳转时,问题更加尖锐。于是我就在寻找一个解决方案,这个方案应该对代码的侵入较少,开发速度快,实现优雅。最近无意中看到springboot与shiro框架集成的文章,在了解了springboot以及shiro的发展状况,并学习了使用方法后,开始在网上搜索前后端分离模式下这两个框架的适应性,在经过测试后发现可行,完全符合个人预期。
解决方案
本文中项目核心包为SpringBoot1.5.9.RELEASE以及shiro-spring 1.4.0,为了加快开发效率,持久化框架使用hibernate-JPA,为增加可靠性,sessionId的管理使用了shiro-redis开源插件,避免sessionId断电丢失,同时使得多端可共享session,项目结构为多模块项目,详见下图。
其中spring-boot-shiro模块为本文重点,该模块包含shiro核心配置,shiro数据源配置以及各种自定义实现,登录相关服务等。该模块在项目中使用时可直接在pom中引用,并在spring-boot-main入口模块中配置相应数据库连接信息即可,且该模块可以在多个项目中复用,避免重复开发。spring-boot-module1为模拟真实项目中的业务模块,可能会有多个。spring-boot-common中包含通用工具类,常量,异常等等。多模块项目的搭建在本文中不作赘述。
母模块pom.xml代码如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xxx</groupId> <artifactId>spring-boot-parent</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>spring-boot-main</module> <module>spring-boot-module1</module> <module>spring-boot-shiro</module> <module>spring-boot-common</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-boot.version>1.5.9.RELEASE</spring-boot.version> <shiro.version>1.4.0</shiro.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <!--在外部tomcat中发布故移除内置包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <version>${spring-boot.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.28</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.39</version> <scope>runtime</scope> </dependency> <!--<dependency>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-starter-thymeleaf</artifactId>--> <!--<version>${spring-boot.version}</version>--> <!--</dependency>--> <!--<dependency>--> <!--<groupId>net.sourceforge.nekohtml</groupId>--> <!--<artifactId>nekohtml</artifactId>--> <!--<version>1.9.22</version>--> <!--</dependency>--> </dependencies> </project>
spring-boot-shiro模块接口如下图
传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,代码如下
import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import org.springframework.util.StringUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; /** * Created by Administrator on 2017/12/11. * 自定义sessionId获取 */ public class MySessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public MySessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); //如果请求头中有 Authorization 则其值为sessionId if (!StringUtils.isEmpty(id)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } else { //否则按默认规则从cookie取sessionId return super.getSessionId(request, response); } } }
如何配置让shiro执行我们的自定义sessionManager呢?下面看ShiroConfig类。
package com.xxx.shiro.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerExceptionResolver; import java.util.LinkedHashMap; import java.util.Map; /** * Created by Administrator on 2017/12/11. */ @Configuration public class ShiroConfig { @Value("${spring.redis.shiro.host}") private String host; @Value("${spring.redis.shiro.port}") private int port; @Value("${spring.redis.shiro.timeout}") private int timeout; @Value("${spring.redis.shiro.password}") private String password; @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //注意过滤器配置顺序 不能颠倒 //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl filterChainDefinitionMap.put("/logout", "logout"); // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/ajaxLogin", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/**", "authc"); //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 shiroFilterFactoryBean.setLoginUrl("/unauth"); // 登录成功后要跳转的链接 // shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面; // shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * ) * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); // 自定义session管理 使用redis securityManager.setSessionManager(sessionManager()); // 自定义缓存实现 使用redis securityManager.setCacheManager(cacheManager()); return securityManager; } //自定义sessionManager @Bean public SessionManager sessionManager() { MySessionManager mySessionManager = new MySessionManager(); mySessionManager.setSessionDAO(redisSessionDAO()); return mySessionManager; } /** * 配置shiro redisManager * <p> * 使用的是shiro-redis开源插件 * * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setExpire(1800);// 配置缓存过期时间 redisManager.setTimeout(timeout); redisManager.setPassword(password); return redisManager; } /** * cacheManager 缓存 redis实现 * <p> * 使用的是shiro-redis开源插件 * * @return */ @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * <p> * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 注册全局异常处理 * @return */ @Bean(name = "exceptionHandler") public HandlerExceptionResolver handlerExceptionResolver() { return new MyExceptionHandler(); } }
在定义的SessionManager的Bean中返回我们的MySessionManager,然后在SecurityManager的Bean中调用setSessionManager(SessionManager sessionManager)方法加载我们的自定义SessionManager。
附上
MyShiroRealm的代码package com.xxx.shiro.config; import com.xxx.shiro.entity.SysPermission; import com.xxx.shiro.entity.SysRole; import com.xxx.shiro.entity.UserInfo; import com.xxx.shiro.service.UserInfoService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import javax.annotation.Resource; /** * Created by Administrator on 2017/12/11. * 自定义权限匹配和账号密码匹配 */ public class MyShiroRealm extends AuthorizingRealm { @Resource private UserInfoService userInfoService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal(); for (SysRole role : userInfo.getRoleList()) { authorizationInfo.addRole(role.getRole()); for (SysPermission p : role.getPermissions()) { authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // System.out.println("MyShiroRealm.doGetAuthenticationInfo()"); //获取用户的输入的账号. String username = (String) token.getPrincipal(); // System.out.println(token.getCredentials()); //通过username从数据库中查找 User对象,如果找到,没找到. //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 UserInfo userInfo = userInfoService.findByUsername(username); // System.out.println("----->>userInfo="+userInfo); if (userInfo == null) { return null; } if (userInfo.getState() == 1) { //账户冻结 throw new LockedAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userInfo, //用户名 userInfo.getPassword(), //密码 ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt getName() //realm name ); return authenticationInfo; } }
传统项目中,登录成功后应该重定向请求,但在前后端分离项目中,通过ajax登录后应该返回登录状态标志以及相关信息。Web层登录方法代码如下
/** * 登录方法 * @param userInfo * @return */ @RequestMapping(value = "/ajaxLogin", method = RequestMethod.POST) @ResponseBody public String ajaxLogin(UserInfo userInfo) { JSONObject jsonObject = new JSONObject(); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userInfo.getUsername(), userInfo.getPassword()); try { subject.login(token); jsonObject.put("token", subject.getSession().getId()); jsonObject.put("msg", "登录成功"); } catch (IncorrectCredentialsException e) { jsonObject.put("msg", "密码错误"); } catch (LockedAccountException e) { jsonObject.put("msg", "登录失败,该用户已被冻结"); } catch (AuthenticationException e) { jsonObject.put("msg", "该用户不存在"); } catch (Exception e) { e.printStackTrace(); } return jsonObject.toString(); }
本项目使用SpringMVC框架,可以自行修改使用其他MVC框架。登录成功则返回sessionId作为token给前端存储,前端请求时将该token放入请求头,以Authorization为key,以此来鉴权。如果出现账号或密码错误等异常则返回错误信息。
传统项目中,登出后应重定向请求,到登录界面或其他指定界面,在前后端分离的项目中,我们应该返回json信息。在上面提到的ShiroConfig中配置了默认登录路由
在Web层加入方法
/** * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面 * @return */ @RequestMapping(value = "/unauth") @ResponseBody public Object unauth() { Map<String, Object> map = new HashMap<String, Object>(); map.put("code", "1000000"); map.put("msg", "未登录"); return map; }
此处简单提示未登录返回状态码,也可自行定义信息。
在项目中,权限相关表可能不在业务库中,因此有必要单独配置权限相关表的数据源。详细配置可以参见《Spring Boot多数据源配置与使用》一文。
Shiro数据源配置代码
package com.xxx.shiro.datasource; import java.util.Map; import javax.persistence.EntityManager; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * Created by Administrator on 2017/12/11. */ @Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef="shiroEntityManagerFactory", transactionManagerRef="shiroTransactionManager", basePackages= { "com.xxx.shiro.dao" }) public class ShiroDataSourceConfig { @Autowired private JpaProperties jpaProperties; @Autowired @Qualifier("shiroDataSource") private DataSource shiroDataSource; @Bean(name = "shiroEntityManager") public EntityManager shiroEntityManager(EntityManagerFactoryBuilder builder) { return shiroEntityManagerFactory(builder).getObject().createEntityManager(); } @Bean(name = "shiroEntityManagerFactory") public LocalContainerEntityManagerFactoryBean shiroEntityManagerFactory (EntityManagerFactoryBuilder builder) { return builder .dataSource(shiroDataSource) .properties(getVendorProperties(shiroDataSource)) .packages("com.xxx.shiro.entity") .persistenceUnit("shiroPersistenceUnit") .build(); } private Map<String, String> getVendorProperties(DataSource dataSource) { return jpaProperties.getHibernateProperties(dataSource); } @Bean(name = "shiroTransactionManager") PlatformTransactionManager shiroTransactionManager(EntityManagerFactoryBuilder builder) { return new JpaTransactionManager(shiroEntityManagerFactory(builder).getObject()); } }
IDEA下JpaProperties可能会报错,可以忽略。
入口模块结构如下图
DataSourceConfig中配置了多个数据源的Bean,其中shiro数据源Bean代码
/** * shiro数据源 * @return */ @Bean(name = "shiroDataSource") @Qualifier("shiroDataSource") @ConfigurationProperties(prefix="spring.datasource.shiro") public DataSource shiroDataSource() { return DataSourceBuilder.create().build(); }
ServletInitializer和StartApp为SpringBoot在外部tomcat启动配置,不赘述。
SpringBoot的相关配置在application.yml中,shiro配置代码如下图
Primary为主库配置。当在某个项目中引入spring-boot-shiro模块时,只需要在配置文件中加入shiro数据源及redis的相关配置,并在DataSourceConfig加入shiro数据源Bean即可。
Shiro框架会根据用户登录及权限状态抛出异常,建议使用SpringMVC的全局异常捕获来处理异常,避免重复代码。该项目中代码如下
package com.xxx.shiro.config; import com.alibaba.fastjson.support.spring.FastJsonJsonView; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /** * Created by Administrator on 2017/12/11. * 全局异常处理 */ public class MyExceptionHandler implements HandlerExceptionResolver { public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception ex) { ModelAndView mv = new ModelAndView(); FastJsonJsonView view = new FastJsonJsonView(); Map<String, Object> attributes = new HashMap<String, Object>(); if (ex instanceof UnauthenticatedException) { attributes.put("code", "1000001"); attributes.put("msg", "token错误"); } else if (ex instanceof UnauthorizedException) { attributes.put("code", "1000002"); attributes.put("msg", "用户无权限"); } else { attributes.put("code", "1000003"); attributes.put("msg", ex.getMessage()); } view.setAttributesMap(attributes); mv.setView(view); return mv; } }
该Bean在ShiroConfig中已有注册代码。
至此,shiro框架的集成就结束了。至于shiro框架的使用细节,可以自行查阅相关资料。项目代码本人测试可正常工作,未应用到生产环境,仅供学习交流使用。
参考文章
1. 《在前后端分离的项目中,后台使用shiro框架时,怎样使用它的会话管理系统(session),从而实现权限控制》
2. 《Spring Boot多数据源配置与使用》
3. 《springboot整合shiro-登录认证和权限管理》
需要源码的朋友可以看看我的另一篇博文《SpringBoot+Shiro+MyBatisPlus搭建前后端分离的多模块项目》
更优雅的源代码,更新的springboot版本,热点问题解答,可以看看最新博文《SpringBoot2.0,Thymeleaf与Shiro整合》
-
前后端分离架构:Web实现前后端分离,前后端解耦
2018-04-16 13:55:40一、前言 ”前后端分离“已经成为互联网项目开发的业界标杆,通过Tomcat+Ngnix(也可以中间有个Node.js),有效地进行解耦。并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种... -
前后端分离:Web实现前后端分离,前后端解耦
2019-03-12 13:50:59前后端分离架构:Web实现前后端分离,前后端解耦 一、前言 ”前后端分离“已经成为互联网项目开发的业界标杆,通过Tomcat+Ngnix(也可以中间有个Node.js),有效地进行解耦。并且前后端分离会为以后的大型分布式架构、... -
JavaWeb项目为什么我们要放弃jsp?为什么要前后端解耦?为什么要前后端分离?2.0版,为分布式架构打基础。
2017-03-23 18:09:44前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦, 并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种... -
【架构】前后端分离架构:Web 实现前后端分离,前后端解耦
2020-04-18 19:30:28”前后端分离“已经成为互联网项目开发的业界标杆,通过Tomcat+Ngnix(也可以中间有个Node.js),有效地进行解耦。并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种客户端,例如... -
前后端分离与前后端不分离
2019-11-23 10:12:08前后端分离 开发模式介绍 前后端不分离 定义:以后端直接渲染模板完成响应为主的一种开发模式 特点 http请求次数少 只需要一个后台服务器 前后端开发耦合,责任不明确 单纯开发网站,效率非常高 响应的往往是html... -
前后端分离Cookie sameSite坑 跨域之坑
2019-01-22 15:18:09在前后端分离解决跨域问题过程中,利用CORS解决跨域问题,前后端按照规范处理了,但不管怎样session都是不一致,所以前端无法登陆无法在本地测试。查了几天资料,中间反反复复,最后要放弃的时候无意中看到一个大神... -
(自学/初学者普及)浅谈前后端与前后端分离(别再说你不懂什么是前后端分离)
2020-04-04 01:12:43程序员都在说前后端分离,开发岗位也被很明确的分成了前后端工程师,很多大学的刚进入计算机专业的小伙伴和打算进入计算机行业的朋友,通常会有这些问题: 究竟什么是前后端呢? 前后端分离又是什么呢? 为... -
一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统
2019-04-15 20:40:28前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter.... -
前后端分离(面试:说一说你理解的前后端分离?)
2020-02-20 02:36:38谈谈你对前后端分离这件事的理解?你觉得一个项目该如何实施前后端分离? -
前后端分离与前后端不分离的区别
2020-05-26 22:32:56前后端分离 在前后端分离的应用模式中,后端仅返回前端所需的数据,不再渲染HTML页面,不再控制前端的效果。至于前端用户看到什么效果,从后端请求的数据如何加载到前端中,都由前端自己决定,网页有网页的处理方式... -
前端通信, 前后端分离 、 前后端不分离
2019-06-21 19:00:32前端通信 基于后端的通信( 后端完成 )( pc端用 ) ...使用终端( shell )作为客户端 思维流程 ...基于H5的webSocket来完成( 应用于移动端 )...前后端分离 、 前后端不分离 市场流行: 前后端分离 前后端分... -
前后端分离和前后端不分离的区别
2018-11-20 19:28:38前后端分离 1 前后端不分离 在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高。 这种应用模式比较适合纯网页应用... -
前后端分离时代,Java 程序员的变与不变!
2019-07-01 10:30:38前后端分离的时代,Java后台程序员的技术建议? 松哥认真看了下这个问题,感觉对于初次接触前后端分离的小伙伴来说,可能都会存在这样的疑问,于是决定通过这篇文章和大家聊一聊这个话题。 我这里还是尽量从一个 ... -
什么是前后端分离与前后端不分离
2019-01-13 20:25:00我起初认为前后端分离是,在软件开发过程中前后端分工就叫做前后端分离,其实是前端所有用到的数据都是后端通过异步接口的方式提供的,前端只管页面的展示及效果。 前端和后端不分离的时候,前端的页面也是由... -
Springboot + Spring Security 实现前后端分离登录认证及权限控制
2019-09-05 16:53:37Springboot + Spring Security 实现前后端分离登录认证及权限控制前言本文主要的功能文章目录一、数据库表设计建表语句初始化表数据语句二、Spring Security核心配置:WebSecurityConfig三、用户登录认证逻辑:... -
谈谈前后端分离
2016-09-28 13:22:26前后端分离 -
前后端分离开发的理解以及和前后端不分离的区别
2020-03-21 22:03:51一、前后端分离的概念 前后端分离 前后端分离是一种架构模式,说通俗点就是后端项目里面看不到页面(JSP | HTML),后端给前端提供接口,前端调用后端提供的 REST 风格接口就行,前端专注写页面(html|jsp)和... -
前后端解耦 前后端分离
2017-07-11 14:48:10前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦, 并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种... -
前后端分离标准化应用的开发过程管理
2019-09-23 11:33:45前后端分离标准化应用的开发过程管理 背景 最近一段时间系统维护过程中暴露出很多问题,甚至是事故。集中复盘之后,认为是当前的开发过程过于“自由”,开发人员甚至可以本地编译后直接拷贝到生产服务器投产。 基于... -
自己动手——快速搭建前后端分离应用服务器
2017-05-08 11:33:17Motivation 我写这套帖子的目的,是在自己学会、实现并熟练掌握之后,想帮助下面将会提到的这样一群正在学习Android的新手(虽然我自己也是新手),通过自己的经验传递出去,让他们少走一点弯路,节省大量用在... -
都前后端分离了,咱就别做页面跳转了!统统 JSON 交互
2020-04-02 09:09:17登录交互2.1 前后端分离的数据交互2.2 登录成功2.3 登录失败3. 未认证处理方案4. 注销登录 这是本系列的第四篇,有小伙伴找不到之前文章,松哥给大家列一个索引出来: 挖一个大坑,Spring Security 开搞! 松哥... -
理解前后端分离
2018-08-08 17:27:48理解前后端分离 理解前后端分离 为什么要做前后端分离,它到底有什么好处? 实现的一些表现 RESTful风格的API 为什么要做前后端分离,它到底有什么好处? 前后端的分离也实现了前后端架构的分离,带来的...
-
前端性能优化
-
算法导论(基础知识)——编程大牛的必经之路
-
单例模式
-
BabelLanguagePack-rt.rap-zh_4.18.0.v20201226020001.zip Eclipse语言包
-
Unity与安卓丨AS报错:SSL peer shut down incorrectly
-
数据类型转换、运算符、方法入门
-
基于FPGA的光口通信开发案例.pdf
-
衡水中学高2数学试卷(解析版).pdf
-
数字图像处理lab1-3.zip
-
pyechart数据可视化
-
uni-app实战专题
-
SSL_1125【集合】
-
Java仿微博系统实战-架构1.0(Spring Boot2.X)
-
2021-01-21
-
【ssm项目源码】学生成绩管理系统前端.zip
-
衡水中学高一化学开学考试(解析版).pdf
-
工业品出厂价格(PPI)(1993.1-2019.10).csv
-
1A sum
-
单片机完全学习课程全五季套餐
-
数据结构之单向链表实现栈