精华内容
下载资源
问答
  • 前后端分离架构概述

    万次阅读 多人点赞 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的结构图如下:

    640?wx_fmt=jpeg

          大致就是所有的请求都被发送给作为控制器的Servlet,它接受请求,并根据请求信息将它们分发给适当的JSP来响应。同时,Servlet还根据JSP的需求生成JavaBeans的实例并输出给JSP环境。JSP可以通过直接调用方法或使用UseBean的自定义标签得到JavaBeans中的数据。需要说明的是,这个View还可以采用 Velocity、Freemaker 等模板引擎。使用了这些模板引擎,可以使得开发过程中的人员分工更加明确,还能提高开发效率。

          那么,在这个时期,开发方式有如下两种:

    方式一 640?wx_fmt=png 方式二 640?wx_fmt=jpeg

          方式二已经逐渐淘汰。主要原因有两点:
          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应用(单页应用)结合的方式,其结构图如下:

    640?wx_fmt=jpeg

          步骤如下:
    (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密集、少量业务逻辑的场景。最重要的一点是,前端不用再学一门其他的语言了,对前端来说,上手度大大提高。

    640?wx_fmt=jpeg

          可以就把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层,此时架构图如下:

    640?wx_fmt=jpeg

          在该结构下,每种前端的界面展示逻辑由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的全栈开发变成一个稳定,让大家都能接受的东西还有很多路要走。创新之路不会止步,无论是前后端分离模式还是其他模式,都是为了更方便得解决需求,但它们都只是一个“中转站”。前端项目与后端项目是两个项目,放在两个不同的服务器,需要独立部署,两个不同的工程,两个不同的代码库,不同的开发人员。前端只需要关注页面的样式与动态数据的解析及渲染,而后端专注于具体业务逻辑。

          参考:淘宝前后端分离解决方案

          参考:从分布式之的角度告诉你前后端分离架构的必要性!

          参考:浅谈前后端分离技术

    展开全文
  • 前后端分离已经在慢慢走进各公司的技术栈,根据松哥了解到的消息,不少公司都已经切换到这个技术栈上面了。即使贵司目前没有切换到这个技术栈上面,松哥也非常建议大家学习一下前后端分离开发,以免在公司干了两三年...

    前后端分离已经在慢慢走进各公司的技术栈,根据松哥了解到的消息,不少公司都已经切换到这个技术栈上面了。即使贵司目前没有切换到这个技术栈上面,松哥也非常建议大家学习一下前后端分离开发,以免在公司干了两三年,SSH 框架用的滚瓜烂熟,出来却发现自己依然没有任何优势!

    其实前后端分离本身并不难,后段提供接口,前端做数据展示,关键是这种思想。很多人做惯了前后端不分的开发,在做前后端分离的时候,很容易带进来一些前后端不分时候的开发思路,结果做出来的产品不伦不类,因此松哥这里给大家整理了几个开源的前后端分离项目,帮助大家快速掌握前后端分离开发技术栈。

    美人鱼

    听名字就知道这是个不错的项目,事实上确实不赖。NiceFish(美人鱼) 是一个系列项目,目标是示范前后端分离的开发模式:前端浏览器、移动端、Electron 环境中的各种开发模式;后端有两个版本:SpringBoot 版本和 SpringCloud 版本,前端有 Angular 、React 以及 Electron 等版本。

    项目效果图:

    微人事

    微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot + Vue 开发。项目打通了前后端,并且提供了非常详尽的文档,从 Spring Boot 接口设计到前端 Vue 的开发思路,作者全部都记录在项目的 wiki 中,是不可多得的 Java 全栈学习资料。

    项目效果图:

    项目部分文档截图:

    bootshiro

    bootshiro 是基于 Spring Boot + Shiro + JWT 的真正 RESTful URL 资源无状态认证权限管理系统的后端,前端 usthe 。区别于一般项目,该项目提供页面可配置式的、动态的 RESTful api 安全管理支持,并且实现数据传输动态秘钥加密,jwt 过期刷新,用户操作监控等,加固应用安全。

    项目效果图:

    open-capacity-platform

    open-capacity-platform 微服务能力开放平台,简称 ocp ,是基于 layui + springcloud 的企业级微服务框架(用户权限管理,配置中心管理,应用管理,…),其核心的设计目标是分离前后端,快速开发部署,学习简单,功能强大,提供快速接入核心接口能力,其目标是帮助企业搭建一套类似百度能力开放平台的框架。

    项目效果图:

    V 部落

    V部落是一个多用户博客管理平台,采用 Vue + SpringBoot + ElementUI 开发。这个项目最大的优势是简单,属于功能完整但是又非常简单的那种,非常非常适合初学者。

    项目效果图:

    悟空 CRM

    悟空 CRM 是基于 jfinal + vue + ElementUI 的前后端分离 CRM 系统。

    老实说,jfinal 了解下就行了,没必要认真研究,Vue + ElementUI 的组合可以认真学习下、前后端交互的方式可以认真学习下。

    paascloud-master

    paascloud-master 核心技术为 SpringCloud + Vue 两个全家桶实现,采取了取自开源用于开源的目标,所以能用开源绝不用收费框架,整体技术栈只有阿里云短信服务是收费的,都是目前 java 前瞻性的框架,可以为中小企业解决微服务架构难题,可以帮助企业快速建站。由于服务器成本较高,尽量降低开发成本的原则,本项目由 10 个后端项目和 3 个前端项目共同组成。真正实现了基于 RBAC、jwt 和 oauth2 的无状态统一权限认证的解决方案,实现了异常和日志的统一管理,实现了 MQ 落地保证 100% 到达的解决方案。

    项目效果图:

    总结

    他山之石,可以攻玉。当我们学会了很多知识点之后,需要一个项目来将这些知识点融会贯通,这些开源项目就是很好的资料。现在前后端分离开发方式日渐火热,松哥也强烈建议大家有空学习下这种开发方式。虽然我们身为 Java 工程师,可是也不能固步自封,看看前端单页面应用怎么构建,看看前端工程化是怎么回事,这些都有助于我们开发出更加合理好用的后端接口。好了,七个开源项目,助力大家在全栈的路上更进一步!

    关注公众号【江南一点雨】,专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货!

    展开全文
  • 在前后端分离的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整合

    展开全文
  • 前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter....

    目录

    前言

    Let's do it!!

    第一步:新建工程

    第二步:准备好要用的包包和类类

    第三步:编写登陆入口

    第四步:编写ShiroService中的方法

    第五步:编写ShiroConfig类

    第六步:实现自定义的AuthenticationToken。

    第七步:编写自己的Realm

    第八步:实现自定义AuthenticatingFilter。

    第九步:详解校验流程

    看看效果

    总结


    前言

    前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter.setLoginUrl();也阻止不了讨人厌的login.jsp的出现。直到我看到了renren-fast的源码...废话不多说,让我们来看看如何实现吧!

     前后端分离
    要实现前后端分离,需要考虑以下2个问题: 1. 项目不再基于session了,如何知道访问者是谁? 2. 如何确认访问者的权限?


    前后端分离,一般都是通过token实现,本项目也是一样;用户登录时,生成token及 token过期时间,token与用户是一一对应关系,调用接口的时候,把token放到header或 请求参数中,服务端就知道是谁在调用接口。

    代码已上传到Git:

    后台代码:https://github.com/FENGZHIJIE1998/shiro-auth 

    前端代码:https://github.com/FENGZHIJIE1998/shiro-vue

    觉得好用的记得点个Star哦

    Let's do it!!


    介绍:这次我们使用Shiro快速搭建前后端分离的权限管理系统 利用JPA帮我们管理数据库,Swagger Knife4j 帮我搭建Web测试环境;

    后台基于 Springboot JPA Knife4j Shiro

    前端基于 VUE ElementUI

    注意:主要观察token的使用方法!

    第一步:新建工程

    pom文件application.yml巴拉巴拉这里省略,这里贴出需要用到的依赖:

            <!--starter-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
            <!--  test-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!--web-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--validation-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
            <!--JPA-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <!--JDBC-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
            <!-- shiro-->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.3.2</version>
            </dependency>
            <!--mysql-connector-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <!-- druid-spring-boot-starter -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.10</version>
            </dependency>
            <!-- swagger -->
            <dependency>
                <groupId>com.spring4all</groupId>
                <artifactId>swagger-spring-boot-starter</artifactId>
                <version>1.8.0.RELEASE</version>
            </dependency>
            <!-- knife4j -->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>2.0.2</version>
            </dependency>
            <!-- commons-lang -->
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>2.6</version>
            </dependency>

    第二步:准备好要用的包包和类类

    第三步:编写登陆入口

    为了方便这里不做密码加盐加密

    /**
     * @Author 大誌
     * @Date 2019/3/30 22:04
     * @Version 1.0
     */
    @RestController
    public class ShiroController {
    
        private final ShiroService shiroService;
    
        public ShiroController(ShiroService shiroService) {
            this.shiroService = shiroService;
        }
    
    
        /**
         * 登录
         */
        @ApiOperation(value = "登陆", notes = "参数:用户名 密码")
        @PostMapping("/sys/login")
        public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {
            Map<String, Object> result = new HashMap<>();
            if (bindingResult.hasErrors()) {
                result.put("status", 400);
                result.put("msg", bindingResult.getFieldError().getDefaultMessage());
                return result;
            }
    
            String username = loginDTO.getUsername();
            String password = loginDTO.getPassword();
            //用户信息
            User user = shiroService.findByUsername(username);
            //账号不存在、密码错误
            if (user == null || !user.getPassword().equals(password)) {
                result.put("status", 400);
                result.put("msg", "账号或密码有误");
            } else {
                //生成token,并保存到数据库
                result = shiroService.createToken(user.getUserId());
                result.put("status", 200);
                result.put("msg", "登陆成功");
            }
            return result;
        }
    
        /**
         * 退出
         */
        @ApiOperation(value = "登出", notes = "参数:token")
        @PostMapping("/sys/logout")
        public Map<String, Object> logout(@RequestHeader("token")String token) {
            Map<String, Object> result = new HashMap<>();
            shiroService.logout(token);
            result.put("status", 200);
            result.put("msg", "您已安全退出系统");
            return result;
        }
    }

    第四步:编写ShiroService中的方法

    主要是生成一个token返回给前端。

    /**
     * @Author 大誌
     * @Date 2019/3/30 22:18
     * @Version 1.0
     */
    @Service
    public class ShiroServiceImpl implements ShiroService {
    
    
        @Autowired
        private UserRepository userRepository;
        @Autowired
        private SysTokenRepository sysTokenRepository;
    
        /**
         * 根据username查找用户
         *
         * @param username
         * @return User
         */
        @Override
        public User findByUsername(String username) {
            User user = userRepository.findByUsername(username);
            return user;
        }
    
        //12小时后过期
        private final static int EXPIRE = 3600 * 12;
    
        @Override
        /**
         * 生成一个token
         *@param  [userId]
         *@return Result
         */
        public Map<String, Object> createToken(Integer userId) {
            Map<String, Object> result = new HashMap<>();
            //生成一个token
            String token = TokenGenerator.generateValue();
            //当前时间
            Date now = new Date();
            //过期时间
            Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
            //判断是否生成过token
            SysToken tokenEntity = sysTokenRepository.findByUserId(userId);
            if (tokenEntity == null) {
                tokenEntity = new SysToken();
                tokenEntity.setUserId(userId);
                tokenEntity.setToken(token);
                tokenEntity.setUpdateTime(now);
                tokenEntity.setExpireTime(expireTime);
                //保存token
                sysTokenRepository.save(tokenEntity);
            } else {
                tokenEntity.setToken(token);
                tokenEntity.setUpdateTime(now);
                tokenEntity.setExpireTime(expireTime);
                //更新token
                sysTokenRepository.save(tokenEntity);
            }
            result.put("token", token);
            result.put("expire", EXPIRE);
            return result;
        }
    
        @Override
        public void logout(String token) {
            SysToken byToken = findByToken(token);
            //生成一个token
            token = TokenGenerator.generateValue();
            //修改token
            SysToken tokenEntity = new SysToken();
            tokenEntity.setUserId(byToken.getUserId());
            tokenEntity.setToken(token);
            sysTokenRepository.save(tokenEntity);
        }
    
        @Override
        public SysToken findByToken(String accessToken) {
            return sysTokenRepository.findByToken(accessToken);
    
        }
    
        @Override
        public User findByUserId(Integer userId) {
            return userRepository.findByUserId(userId);
        }
    }
    

    第五步:编写ShiroConfig类

    /**
     * @Author 大誌
     * @Date 2019/3/30 21:50
     * @Version 1.0
     */
    @Configuration
    public class ShiroConfig {
    
        @Bean("securityManager")
        public SecurityManager securityManager(AuthRealm authRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(authRealm);
            securityManager.setRememberMeManager(null);
            return securityManager;
        }
    
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
            //oauth过滤
            Map<String, Filter> filters = new HashMap<>();
            filters.put("auth", new AuthFilter());
            shiroFilter.setFilters(filters);
            Map<String, String> filterMap = new LinkedHashMap<>();
            filterMap.put("/webjars/**", "anon");
            filterMap.put("/druid/**", "anon");
            filterMap.put("/sys/login", "anon");
            filterMap.put("/swagger/**", "anon");
            filterMap.put("/v2/api-docs", "anon");
            filterMap.put("/swagger-ui.html", "anon");
            filterMap.put("/swagger-resources/**", "anon");
            filterMap.put("/**", "auth");
            shiroFilter.setFilterChainDefinitionMap(filterMap);
    
            return shiroFilter;
        }
    
        @Bean("lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    }
    

    第六步:实现自定义的AuthenticationToken。

    阅读AuthenticatingFilter抽象类中executeLogin方法,我们发现调用 了subject.login(token),这是shiro的登录方法,且需要token参数,我们自定义 AuthToken类,只要实现AuthenticationToken接口,就可以了。

     //AuthenticatingFilter中的executeLogin()
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            AuthenticationToken token = createToken(request, response);
            if (token == null) {
                String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                        "must be created in order to execute a login attempt.";
                throw new IllegalStateException(msg);
            }
            try {
                Subject subject = getSubject(request, response);
                //重点!
                subject.login(token);
                return onLoginSuccess(token, subject, request, response);
            } catch (AuthenticationException e) {
                return onLoginFailure(token, e, request, response);
            }
        }
    /**
     * 自定义AuthenticationToken类
     * @Author 大誌
     * @Date 2019/3/31 10:58
     * @Version 1.0
     */
    public class AuthToken extends UsernamePasswordToken{
    
        private String token;
    
        public AuthToken(String token) {
            this.token = token;
        }
    
        @Override
        public Object getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    }

    这里我实现的时候出现了Token不匹配的Bug。DeBug下可以查到源头是代码是用UsernamePasswordToken.class和我自定义的AuthToken.class配对。按道理应该是true,却返回了false...于是我就把自定义的AuthToken不实现AuthenticationToken,转为继承UsernamePasswordToken,就可以了。(renren-fast中却可以,可能是版本的问题)

    2020/4/27修改: 为了避免误导,将上诉代码 AuthenticationToken 修改为 UsernamePasswordToken,并且走了一下源码,发现这个getAuthenticationTokenClass()实际上获取到的是UsernamePasswordToken.class

    再回头看看renren-fast中的源码,原来他重写了supports方法!

    第七步:编写自己的Realm

    发起请求时,接受传过来的token后,如何保证token有效及用户权限呢?调用接口时,接受传过来的token后,如何保证token有效及用户权限呢?其实,Shiro提供了AuthorizingRealm以及AuthenticatingFilter抽象类,继承AuthorizingRealm和AuthenticatingFilter抽象类重写方法即可。

    /**
     * @Author 大誌
     * @Date 2019/3/30 21:38
     * @Version 1.0
     */
    @Component
    public class AuthRealm extends AuthorizingRealm {
    
        @Autowired
        private ShiroService shiroService;
    
        @Override
        /**
         * 授权 获取用户的角色和权限
         *@param  [principals]
         *@return org.apache.shiro.authz.AuthorizationInfo
         */
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            //1. 从 PrincipalCollection 中来获取登录用户的信息
            User user = (User) principals.getPrimaryPrincipal();
            //Integer userId = user.getUserId();
            //2.添加角色和权限
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            for (Role role : user.getRoles()) {
                //2.1添加角色
                simpleAuthorizationInfo.addRole(role.getRoleName());
                for (Permission permission : role.getPermissions()) {
                    //2.1.1添加权限
                    simpleAuthorizationInfo.addStringPermission(permission.getPermission());
                }
            }
            return simpleAuthorizationInfo;
        }
    
        @Override
        /**
         * 认证 判断token的有效性
         *@param  [token]
         *@return org.apache.shiro.authc.AuthenticationInfo
         */
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            //获取token,既前端传入的token
            String accessToken = (String) token.getPrincipal();
            //1. 根据accessToken,查询用户信息
            SysToken tokenEntity = shiroService.findByToken(accessToken);
            //2. token失效
            if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
                throw new IncorrectCredentialsException("token失效,请重新登录");
            }
            //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
            User user = shiroService.findByUserId(tokenEntity.getUserId());
            //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
            if (user == null) {
                throw new UnknownAccountException("用户不存在!");
            }
            //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, this.getName());
            return info;
        }
    }
    

    第八步:实现自定义AuthenticatingFilter。

    /**
     * Shiro自定义auth过滤器
     *
     * @Author 大誌
     * @Date 2019/3/31 10:38
     * @Version 1.0
     */
    @Component
    public class AuthFilter extends AuthenticatingFilter {
    
    
        // 定义jackson对象
        private static final ObjectMapper MAPPER = new ObjectMapper();
    
        /**
         * 生成自定义token
         *
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @Override
        protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
            //获取请求token
            String token = TokenUtil.getRequestToken((HttpServletRequest) request);
    
            return new AuthToken(token);
        }
    
        /**
         * 步骤1.所有请求全部拒绝访问
         *
         * @param request
         * @param response
         * @param mappedValue
         * @return
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
                return true;
            }
            return false;
        }
    
        /**
         * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
         *
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            //获取请求token,如果token不存在,直接返回
            String token = TokenUtil.getRequestToken((HttpServletRequest) request);
            if (StringUtils.isBlank(token)) {
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
                httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
                httpResponse.setCharacterEncoding("UTF-8");
                Map<String, Object> result = new HashMap<>();
                result.put("status", 400);
                result.put("msg", "请先登录");
                String json = MAPPER.writeValueAsString(result);
                httpResponse.getWriter().print(json);
                return false;
            }
            return executeLogin(request, response);
        }
    
        /**
         * token失效时候调用
         */
        @Override
        protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
            httpResponse.setCharacterEncoding("UTF-8");
            try {
                //处理登录失败的异常
                Throwable throwable = e.getCause() == null ? e : e.getCause();
                Map<String, Object> result = new HashMap<>();
                result.put("status", 400);
                result.put("msg", "登录凭证已失效,请重新登录");
                String json = MAPPER.writeValueAsString(result);
                httpResponse.getWriter().print(json);
            } catch (IOException e1) {
            }
            return false;
        }
    
    }
    

    第九步:详解校验流程

    先给你们上一个超级详细的流程图。

     

    接着我们打上断点按照代码走走,可能会有点啰嗦。

    1. 前端发起请求首先会进入AuthFilter的 isAccessAllowed(),除了OPTION方法,其余都拦截。

    2. 拦截之后进入AuthFilter的onAccessDenied(),这里获取token后判断token是否isBlank。如果是,代表请求未携带token,直接默认返回400,未登录给前端,流程就结束了。如果携带了token则进入第三步,继续流程。

    3. 接着进入AuthFilter的createToken,这里生成我们自定义的AuthToken对象。

    4. 接着就会来到AuthRealm中的doGetAuthenticationInfo(),在这个方法中继续token的有效性校验,例如过期、和数据库的token对不上(用户已退出)的情况。如果校验失败,进入第5步,否则进入第6步。

    5. token失效后回到AuthFilter中的onLoginFailure(),返回400以及msg,流程结束。

    6. Token校验成功后进入AuthRealm的doGetAuthorizationInfo(),进行获取当前用户拥有的权限,之后底层代码会进行权限验证。如果用户有权限则会进入请求方法,否则抛出异常。到这一步校验过程就结束了。

    看看效果

    终于熬完上面的步骤了,这时候总体的架构已经确立好了,下面让我们来看看效果如何

    DTO

    /**
     * 登录传输类
     */
    @Data
    public class LoginDTO {
        @NotBlank(message = "用户名不能为空")
        private String username;
        @NotBlank(message = "密码不能为空")
        private String password;
    }
    

    实体类

    @Getter
    @Setter
    @Entity
    public class User {
        @Id
        private Integer userId;
     
        private String username;
        private String password;
     
        @ManyToMany(fetch = FetchType.EAGER)
        @JoinTable(name = "user_role",
                joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "userId")},
                inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")})
        private Set<Role> roles;
     
    }
     
    @Getter
    @Setter
    @Entity
    public class Role {
     
        @Id
        private Integer roleId;
        private String roleName;
     
        @ManyToMany(fetch = FetchType.EAGER)
        @JoinTable(name = "role_permission",
                joinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")},
                inverseJoinColumns = {@JoinColumn(name = "PERMISSION_ID", referencedColumnName = "permissionId")})
        private Set<Permission> permissions;
    }
     
    @Getter
    @Setter
    @Entity
    public class Permission {
     
        @Id
        private Integer permissionId;
        private String permissionName;
        private String permission;
    }
    
    @Getter
    @Setter
    @Entity
    public class SysToken{
     
        @Id
        private Integer userId;
        private String token;
        private Date expireTime;
        private Date updateTime
    }

    以及给实体类附上权限:

    我定义了三个用户 

    用户 角色 权限
    Jack SVIP select;save;delete;update
    Rose VIP select;save;update
    Paul P select
    /*
    Navicat MySQL Data Transfer
    Source Server         : localhost
    Source Server Version : 50549
    Source Host           : localhost:3306
    Source Database       : shiro
    Target Server Type    : MYSQL
    Target Server Version : 50549
    File Encoding         : 65001
    Date: 2019-04-07 17:06:36
    */
     
    SET FOREIGN_KEY_CHECKS=0;
     
    -- ----------------------------
    -- Table structure for permission
    -- ----------------------------
    DROP TABLE IF EXISTS `permission`;
    CREATE TABLE `permission` (
      `permission_id` int(11) NOT NULL,
      `permission` varchar(255) DEFAULT NULL,
      `permission_name` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`permission_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     
    -- ----------------------------
    -- Records of permission
    -- ----------------------------
    INSERT INTO `permission` VALUES ('1', 'select', '查看');
    INSERT INTO `permission` VALUES ('2', 'update', '更新');
    INSERT INTO `permission` VALUES ('3', 'delete', '删除');
    INSERT INTO `permission` VALUES ('4', 'save', '新增');
     
    -- ----------------------------
    -- Table structure for role
    -- ----------------------------
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role` (
      `role_id` int(11) NOT NULL,
      `role_name` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`role_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     
    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES ('1', 'svip');
    INSERT INTO `role` VALUES ('2', 'vip');
    INSERT INTO `role` VALUES ('3', 'p');
     
    -- ----------------------------
    -- Table structure for role_permission
    -- ----------------------------
    DROP TABLE IF EXISTS `role_permission`;
    CREATE TABLE `role_permission` (
      `role_id` int(11) NOT NULL,
      `permission_id` int(11) NOT NULL,
      PRIMARY KEY (`role_id`,`permission_id`),
      KEY `FKf8yllw1ecvwqy3ehyxawqa1qp` (`permission_id`),
      CONSTRAINT `FKa6jx8n8xkesmjmv6jqug6bg68` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`),
      CONSTRAINT `FKf8yllw1ecvwqy3ehyxawqa1qp` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`permission_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     
    -- ----------------------------
    -- Records of role_permission
    -- ----------------------------
    INSERT INTO `role_permission` VALUES ('1', '1');
    INSERT INTO `role_permission` VALUES ('2', '1');
    INSERT INTO `role_permission` VALUES ('3', '1');
    INSERT INTO `role_permission` VALUES ('1', '2');
    INSERT INTO `role_permission` VALUES ('2', '2');
    INSERT INTO `role_permission` VALUES ('1', '3');
    INSERT INTO `role_permission` VALUES ('1', '4');
    INSERT INTO `role_permission` VALUES ('2', '4');
     
    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
      `user_id` int(11) NOT NULL,
      `password` varchar(255) DEFAULT NULL,
      `username` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES ('1', '123', 'Jack');
    INSERT INTO `user` VALUES ('2', '123', 'Rose');
    INSERT INTO `user` VALUES ('3', '123', 'Paul');
     
    -- ----------------------------
    -- Table structure for user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role` (
      `user_id` int(11) NOT NULL,
      `role_id` int(11) NOT NULL,
      PRIMARY KEY (`user_id`,`role_id`),
      KEY `FKa68196081fvovjhkek5m97n3y` (`role_id`),
      CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),
      CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     
    -- ----------------------------
    -- Table structure for sys_token
    -- ----------------------------
    CREATE TABLE `sys_token` (
      `user_id` int(11) NOT NULL,
      `expire_time` datetime DEFAULT NULL,
      `token` varchar(255) DEFAULT NULL,
      `update_time` datetime DEFAULT NULL,
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    
    
    -- ----------------------------
    -- Records of user_role
    -- ----------------------------
    INSERT INTO `user_role` VALUES ('1', '1');
    INSERT INTO `user_role` VALUES ('2', '2');
    INSERT INTO `user_role` VALUES ('3', '3');

    测试类:因为我是用Swagger来测试,所以为了方便就直接传递token参数。具体开发时候可由前端把接收到的token放入Header。

    /**
     * @Author 大誌
     * @Date 2019/4/7 15:20
     * @Version 1.0
     */
    @RestController
    public class TestController {
    
        @RequiresPermissions({"save"}) //没有的话 AuthorizationException
        @PostMapping("/save")
        public Map<String, Object> save(String token) {
            System.out.println("save");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有save的权力");
            return map;
        }
    
        @RequiresPermissions({"delete"}) //没有的话 AuthorizationException
        @DeleteMapping("/delete")
        public Map<String, Object> delete(String token) {
            System.out.println("delete");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有delete的权力");
            return map;
        }
    
        @RequiresPermissions({"update"}) //没有的话 AuthorizationException
        @PutMapping("update")
        public Map<String, Object> update(String token) {
            System.out.println("update");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有update的权力");
            return map;
        }
    
        @RequiresPermissions({"select"}) //没有的话 AuthorizationException
        @GetMapping("select")
        public Map<String, Object> select(String token, HttpSession session) {
            System.out.println("select");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有select的权力");
            return map;
        }
    
        @RequiresRoles({"vip"}) //没有的话 AuthorizationException
        @GetMapping("/vip")
        public Map<String, Object> vip(String token) {
            System.out.println("vip");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有VIP角色");
            return map;
        }
        @RequiresRoles({"svip"}) //没有的话 AuthorizationException
        @GetMapping("/svip")
        public Map<String, Object> svip(String token) {
            System.out.println("svip");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有SVIP角色");
            return map;
        }
        @RequiresRoles({"p"}) //没有的话 AuthorizationException
        @GetMapping("/p")
        public Map<String, Object> p(String token) {
            System.out.println("p");
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("status", 200);
            map.put("msg", "当前用户有P角色");
            return map;
        }
    }
    

    ExceptionHandler 异常处理器,用于捕获无权限时候的异常。

    @ControllerAdvice
    public class MyExceptionHandler {
    
        @ExceptionHandler(value = AuthorizationException.class)
        @ResponseBody
        public Map<String, String> handleException(AuthorizationException e) {
            //e.printStackTrace();
            Map<String, String> result = new HashMap<String, String>();
            result.put("status", "400");
            //获取错误中中括号的内容
            String message = e.getMessage();
            String msg=message.substring(message.indexOf("[")+1,message.indexOf("]"));
            //判断是角色错误还是权限错误
            if (message.contains("role")) {
                result.put("msg", "对不起,您没有" + msg + "角色");
            } else if (message.contains("permission")) {
                result.put("msg", "对不起,您没有" + msg + "权限");
            } else {
                result.put("msg", "对不起,您的权限有误");
            }
            return result;
        }
    }

    启动项目来看看效果: 访问 localhost:9090/shiro/doc.html

    登陆失败:

    登陆成功:

    登录成功后会返回token,记得带上token访问以下接口

    有某个角色时候:

    没有某个角色的时候:

    有某个权力时候:

    没有某个权力的时候:

    退出系统

    原本的token就失效了,我们再访问原本可以访问的接口看看

    至此就已经进入尾声了

    2020/3/27 新编写了VUE+Element前端页面

    正常访问:

    非法访问:

    重点:当未登录时候访问项目内部页面,由前端控制路由返回登录页,并不会出现可恶的login.jsp,这里我们故意改变数据库token来展示效果。

    总结

    至于最后没有权利或角色返回的json字符串是因为他抛出AuthorizationException。可以自定义全局异常处理器进行处理。通过这种token达到即可达到前后端分离开发。各位客官,点个赞吧qaq。

    2019/11/26日修改:在后续开发中,发现shiro如果使用ShiroConfig中shiroFiltet的map进行权限或角色拦截,会出现只走登陆认证,不走授权认证的情况。这是个巨坑!后续再写一篇文章深究一下。解决方法:使用注解@RequiresRoles() 以及@RequiresPermissions()进行权限和角色拦截

    
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
            //自定义过滤(关键)
            Map<String, Filter> filters = new HashMap<>();
            filters.put("auth", new AuthFilter());
            shiroFilter.setFilters(filters);
            Map<String, String> filterMap = new LinkedHashMap<>();
            //主要是这部分: 不要用这种方法,最好用注解的方法
            filterMap.put("/add", "roles[admin]");
            filterMap.put("/list", "roles[admin,user]");
            filterMap.put("/delete", "perms[admin:delete]");
    
            filterMap.put("/**", "auth");
            shiroFilter.setFilterChainDefinitionMap(filterMap);
    
            return shiroFilter;
        }
    

    2020/3/25补充,修改了部分不符合规范的代码,添加了全局异常捕获器。同时补充了校验流程。同时提示两句,因为token频繁在客户端和服务器端传输,因此可能会造成token劫持攻击(既黑客捕获你的token之后就可以代替你为所欲为),如果对这方面有安全隐患的担忧,可以采取每访问一次接口,更新一次token。并且我这里处于方便的原因是采用了mysql存储token,具体开发中应该用redis缓存来存储。


    有什么问题可以评论或者私信我,每日在线解(LIAO)疑(SAO)。

    我是大誌,一位准备996的卑微码农🐶,觉得好用记得点赞收藏!!!

    展开全文
  • 前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦, 并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种...
  • 文章目录简介什么是数据库读写分离为什么要搞数据库读写分离配置主从同步的基本步骤实践阶段技术栈docker容器安装(若在不同服务器搭建数据库,此步可省略)Mysql主从同步配置Django项目搭建 简介 什么是数据库读写...
  • 搭建spring-boot+vue前后端分离框架并实现登录功能

    万次阅读 多人点赞 2018-08-12 12:17:01
    源码链接:https://pan.baidu.com/s/1E0e72K6P3_xtkFscL6iEtQ 提取码:t2te 一、环境、工具 jdk1.8 maven spring-boot idea VSVode vue 二、搭建后台spring-boot框架 ...2、创建项目文件结构以...
  • 先前发布springboot项目的时候,每次改动一点东西,就需要将整个项目重新打包部署,并且打包出来的jar包太庞大,每次更新项目的时候,需要上传的文件很大,十分不便,故把依赖lib里面的jar包从项目分离出来,每次...
  • 线程分离

    千次阅读 2018-08-29 11:44:50
    线程分离 什么是线程分离(WHAT) 简单来讲,线程分离就是当线程被设置为分离状态后,线程结束时,它的资源会被系统自动的回收,而不再需要在其它线程中对其进行 pthread_join() 操作。 为什么线程分离(WHY) ...
  • 随着技术的不断更新进步,业务需求的不断发展,进而系统也在不断的更新迭代,常用的前后混合开发模式已严重影响到开发人员的开发质量、开发效率,业务系统的推进,这时候就需要思考前后分离的开发的问题,让后台研发...
  • 动静分离

    千次阅读 2018-10-19 19:46:39
    动静分离的实现思路 动静分离是将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问。 1.静态资源部署在Nginx 动静分离的一种做法是将静态资源...
  • 前后端分离与不分离的区别

    万次阅读 多人点赞 2018-12-10 21:58:57
    前后端不分离 概念 在前后端不分离的应用模式中,前端页面看到的效果都是由后端控制,由后端渲染页面或重定向,也就是后端需要控制前端的展示,前端与后端的耦合度很高。 前后端分离 概念 前后...
  • 前后端分离时代,Java 程序员的变与不变!

    千次阅读 多人点赞 2019-07-01 10:30:38
    前后端分离的时代,Java后台程序员的技术建议? 松哥认真看了下这个问题,感觉对于初次接触前后端分离的小伙伴来说,可能都会存在这样的疑问,于是决定通过这篇文章和大家聊一聊这个话题。 我这里还是尽量从一个 ...
  • Redis主从配置读写分离

    万次阅读 2019-07-15 00:00:01
    1.主从概念 一个master可以拥有多个slave,一个slave又可以拥有多个slave,如此下去,... 通过主从配置可以实现读写分离 2.配置主(绑定本机ip) 修改/etc/redis/redis.conf文件 sudo vim redis.co...
  • 登录交互2.1 前后端分离的数据交互2.2 登录成功2.3 登录失败3. 未认证处理方案4. 注销登录 这是本系列的第四篇,有小伙伴找不到之前文章,松哥给大家列一个索引出来: 挖一个大坑,Spring Security 开搞! 松哥...
  • 前后端分离Cookie sameSite坑 跨域之坑

    万次阅读 热门讨论 2019-01-22 15:18:09
    在前后端分离解决跨域问题过程中,利用CORS解决跨域问题,前后端按照规范处理了,但不管怎样session都是不一致,所以前端无法登陆无法在本地测试。查了几天资料,中间反反复复,最后要放弃的时候无意中看到一个大神...
  • 空间可分离卷积:将一个卷积核分为两部分(降低计算复杂度,但并非所有的卷积核都可以分) 深度可分离卷积的过程:先深度卷积,再点态卷积 (对卷积中的通道数不了解的请参考:关于卷积中的通道数问题)...
  • 语言分离(噪声中分离

    千次阅读 2018-04-23 20:37:52
    第一篇论文:《基于深度学习的语音分离研究_张晖》第二篇论文:《基于卷积神经网络的语音分离方法研究与实现_杨冰晴》第三篇论文:《基于深层神经网络的语音增强方法研究_徐勇》(大神的论文,各种公式,看不太懂)...
  • nginx动静分离

    千次阅读 2020-12-25 18:42:00
    一、概述 1.1 动态页面与静态页面区别 静态资源: 当用户多次访问这个资源,资源的源...动静分离简单的概括是:动态文件与静态文件的分离。 伪静态:网站如果想被搜索引擎搜素到,动态页面静态技术freemarker等模版引擎
  • 前后端分离

    千次阅读 2021-01-14 15:28:18
    这里写自定义目录标题前后端分离 前后端分离 前后端分离就是将一个应用的前端代码和后端代码分开写,为什么要这样做? 如果不使用前后端分离的方式,会有哪些问题? 传统的Java Web开发中,前端使用JSP开发,JSP不是由...
  • 【mysql 读写分离】10分钟了解读写分离的作用

    万次阅读 多人点赞 2017-12-13 17:04:31
    1、what 读写分离 读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。2、...
  • 前后端分离架构:Web实现前后端分离,前后端解耦

    万次阅读 多人点赞 2018-04-16 13:55:40
    一、前言 ”前后端分离“已经成为互联网项目开发的业界标杆,通过Tomcat+Ngnix(也可以中间有个Node.js),有效地进行解耦。并且前后端分离会为以后的大型分布式架构、弹性计算架构、微服务架构、多端化服务(多种...
  • 前后端分离与前后端不分离

    千次阅读 2019-11-23 10:12:08
    前后端分离 开发模式介绍 前后端不分离 定义:以后端直接渲染模板完成响应为主的一种开发模式 特点 http请求次数少 只需要一个后台服务器 前后端开发耦合,责任不明确 单纯开发网站,效率非常高 响应的往往是html...
  • 前后端分离项目,如何解决跨域问题 跨域资源共享(CORS)是前后端分离项目很常见的问题,本文主要介绍当SpringBoot应用整合SpringSecurity以后如何解决该问题。 什么是跨域问题 CORS全称Cross-Origin Resource ...
  • 线程的分离与非分离状态

    千次阅读 2017-02-04 15:00:30
    线程的分离状态决定一个线程以什么样的方式来终止自己  非分离 【joinable】 分离 【detached】 非分离的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的【默认状态...
  • 额…看标题大家应该觉得很奇怪,为什么叫伪分离,因为嘛,正常前后端分离都是使用springboot+vue的,使用thymeleaf,那不是跟jsp一样么,对吧。那么先说起因吧,我上篇文章讲了。把第三方jar单独分离出来,这样可以...
  • 深度可分离卷积

    千次阅读 多人点赞 2019-03-22 09:06:25
    下面这个文章介绍了深度可分离卷积是...(我记得有一片文章证明了深度可分离卷积和正常卷积是等效的,但是暂时没找到链接。) 正常卷积 原始图像是12x12的,由于是RGB格式的,所以有三个通道。其输入图片格式是:12...
  • 数字分离问题

    千次阅读 2019-08-31 23:21:43
    数字分离问题 今天来讲一下数字分离(又叫数位分离)问题,那么什么是数字分离呢? 顾名思义,数字分离就是将一个正整数的每一个数字单独分离出来,并进行打印或者储存操作或者是进行其他运算。 数字分离在编程练习...
  • 当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异。笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方案,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 222,183
精华内容 88,873
关键字:

分离