springMVC

Posted on 2022-03-19 14:42  夜雨初凉  阅读(83)  评论(0编辑  收藏  举报

准备工作

  • 学习前提
    • 了解单例模式
    • 了解代理模式

一、spring

1、 spring是什么?

简单说spring就是一个容器;也是一个框架提,供了一系列的功能组件(控制反转[依赖注入]、事件、资源、i18n、验证、数据绑定、SpEl表达式、类型转换、aop面向切面等)。

spring提供的各种功能帮助我们将复杂的问题简单化。

高内聚、低耦合,能够让你的代码读起来更加的简洁、优雅

2、spring的两个核心

2.1、IoC--控制反转(DI 依赖注入)

Inversion of Control (简称 Ioc)是编程的一种设计原则

IoC(控制反转)的主要实现方式有两种

  • 依赖注入
  • 依赖查找

其中,最常见的实现该原则的方式是 依赖注入

控制反转简单理解:原本是我要干的事情,我现在不干了,交给别人去干。

例:
大家之前写程序经常要创建对象,是吧?java程序创建对象其实是一件比较消耗资源的事情,而且要写一个严谨的单例也是一件很麻烦的事情,这么麻烦的事情我才不干呢?交给spring去做。何况spring帮你干还不要钱,以后我们程序所有实例化对象这么好的事情就都交给它了。

​ spring实现控制反转这个编程原则的 模式是依赖注入的模式。我们知道,程序在做某一件事情的时候,对象与对象之间可能存在关系,就像大家的爸妈一样,因为大家的爸妈有那层关系,才能完成某件事情,把你给造出来了吧?

​ spring 就是通过依赖注入 创建对象,并将对象与对象之间的关系给关联起来。

2.2.1、spring ioc使用

注:为什么说spring 是个容器,其实这个容器指的就是 它的ioc容器

  • pom.xml 加入spring容器的依赖(可以理解为创造一个spring运行环境)
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.0.3.RELEASE</version>
</dependency>

  • 我打算让spring来帮我造一辆爱车,我先来告诉spring我爱车的模型
    • 定义Car类
/**
 * 汽车类
 */
public class Car {
    //我爱车的引擎
    private Engine engine;
    //轮胎
    private List<String> lunTaiList;
    //map集合注入示例
    private Map<String,String> map;

    public Engine getEngine() {
        return engine;
    }
    public void setEngine(Engine engine) {
        this.engine = engine;
    }
    public List<String> getLunTaiList() {
        return lunTaiList;
    }

    public void setLunTaiList(List<String> lunTaiList) {
        this.lunTaiList = lunTaiList;
    }

    public Map<String, String> getMap() {
        return map;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }

    public Car(){
        System.out.println("spring示例化了Car类");
    }

    public void init(){
        System.out.println("----产出了我的爱车");
    }

    public void destroy(){
        System.out.println("----我的爱车报废了");
    }

    public void show(){
        System.out.println("=======================");
        System.out.println("大家好,这是我的三轮爱车。");
        System.out.println("爱车配置如下:");
        System.out.println("引擎:"+this.engine.toString());
        System.out.println("轮胎:"+this.lunTaiList);
        System.out.println(this.map);
        System.out.println("=======================");
    }
}

    • 定义Engine类
/**
 * 引擎
 */
public class Engine {
    private String name;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Engin{" +
                "name='" + name + '\'' +
                '}';
    }
}
  • 然后告诉spring,我的爱车具体内容的配置。创建beans.xml配置文件,该配置文件内容如下
    • <bean>标签属性
      • id: 该bean的唯一标识,spring通过该标识找到bean
      • class: 完整类路径,原理是 根据该完整类路径 利用java反射 实例化对象
      • scope:
        • singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值
        • prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行new XxxBean()
        • request 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境
        • session 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean,仅适用于WebApplicationContext环境
        • global-session 一般用于Portlet(一种容器,就像tomcat的servlet容器一样)应用环境,该运用域仅适用于WebApplicationContext环境
      • init-method:指定bean被创建后调用的初始化方法,另一种方式是实现 InitializingBean 接口中的 afterPropertiesSet方法
      • destroy-method: 指定bean被销毁后调用的方法,另一种方式是实现 DisposableBean 接口中的 destroy方法
<?xml version="1.0" encoding="UTF-8"?>


<beans
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation=
                "http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <!--
    告诉spring,实例化一个Car类
    spring实例化出来的类对象都是单例

    init-method:在spring实例化对象完成之后立马调用init-method指定的方法

    -->
    <bean id="car" class="com.zd.beans.Car"
          init-method="init"
          destroy-method="destroy"
    >
        <!--将引擎对象注入到 Car中,特别需要注意的是:
        spring中的注入是调用setXxx方法,所以你们对要注入的属性一定要记得生成他们的get/set方法
        -->
        <property name="engine" ref="engineId"/>
        <!--list 类型注入-->
        <property name="lunTaiList">
            <list>
                <value>左前轮</value>
                <value>右前轮</value>
                <value>左后轮</value>
                <value>右后轮</value>
            </list>
        </property>
        <!--map集合注入示例-->
        <property name="map" >
            <map>
                <entry key="key1" value="value1" />
                <entry key="key2" value="value2" />
            </map>
        </property>
    </bean>
    <!--spring实例化爱车引擎-->
    <bean id="engineId" class="com.zd.beans.Engine">
        <!--给字符串类型注入值-->
        <property name="name" value="地球梦" />
    </bean>

</beans>
  • 该告诉的都告诉了,接下来要spring这个大工厂要开工了。创建jave main方法,初始化 spring容器。main方法中核心代码如下
public static void main(String[] args) {
	//根据类路径去读取 xml配置文件,来初始化spring容器
    ApplicationContext context = new ClassPathXmlApplicationContext("conf/beans.xml");
    //根据ID获取spring容器中的 bean(spring帮我们实例化的一个类对象)
    Car car = (Car)context.getBean("car");
    car.show();


    AbstractApplicationContext c = (AbstractApplicationContext)context;
    //为了保证spring正常退出,能够正常的销毁bean,需要加入下面的代码
    c.registerShutdownHook();
}

2.2、AOP --面向切面编程

分离业务的主逻辑和次逻辑的一种思想(日志记录、请求拦截器、事物控制)。

就这么理解吧,假如你和女友约会,

女友出门之前需要 洗澡,洗头,化妆,约会内容(吃饭、喝饮料、去游乐园),卸妆,洗澡,睡觉

你能接触的,就是和女友吃喝玩乐。你着重关心的是和女朋友一起活动的内容。面向切面能够把

你和你女朋友一起活动这块内容给抽取出来,让你着重的去设计它。

2.2.1、aop 使用

这里就用上面那个例子

  • pom.xml 依赖如下
<!--spring 环境支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.18.RELEASE</version>
</dependency>
<!--spring aop支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.3.18.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.18.RELEASE</version>
</dependency>
  • 我现在有一个专门为女友服务的类,此类专门实现我与女友一起玩耍

GrilService.java

public class GrilService {
    /**
     * 约会
     */
    public void yueHui(String content){
        System.out.println("今天和女朋友去做了["+content+"]");
    }
    /**
     * 和女朋友看电影
     */
    public void watchMovie(){
        System.out.println("今天和女朋友去看了一场电影");
    }
    /**
     * 和女朋友一起玩耍
     */
    void play(){
        System.out.println("今天和女朋友一起玩耍");
    }

}
  • 但是每次找女友约会,女友都要早上起来刷牙、漱口、化妆、 和我约会完了之后她回家后也要 刷牙、漱口、卸妆、睡觉 等,这些事情 我是不关心的,因为这不是我的主要业务。而且这些次要的事情都是重复的,我们可以将其抽取出来,定义到一个切面类中

GrilExecutor.java

/**
 * 切入 女朋友约会 中做的事情具体实现类
 */
public class GrilExecutor {

    /**
     * 切入业务后的实现方法
     * @param pjp
     * @return
     */
    public Object handler(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("女朋友起床");
        System.out.println("女朋友早上洗漱");
        System.out.println("女朋友化妆");
        //真正主体业务(对女朋友的服务)
        Object ret = pjp.proceed();

        System.out.println("女朋友晚上洗漱");
        System.out.println("女朋友卸妆");
        System.out.println("女朋友睡觉");

        return ret;
    }
}
  • 然后配置 切面与切入点的关系,xml 配置文件如下
    • <aop:config ref=""></aop:config> 所有切面的配置必须都放在该元素之内
    • <aop:aspect></aop:aspect> 一个切面的配置
    • <aop:pointcut expression="" id=""/> 切入点配置
    • <aop:around method="" pointcut-ref=""/> 包裹的切入模式,以及指定包裹的方法
<?xml version="1.0" encoding="UTF-8"?>

<beans
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"

        xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/aop
    https://www.springframework.org/schema/aop/spring-aop-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <!--aop配置start-->
    <!--切面的具体实现类-->
    <bean id="grilExecutor" class="com.zd.service.GrilExecutor" />

    <aop:config>
        <!--配置一个切面
        ref:切面的具体实现类
        -->
        <aop:aspect ref="grilExecutor">
            <!--
            切入点
            expression :execution(切入的位置表达式)
            -->
            <aop:pointcut id="pointCutGril"
                          expression="execution(* com.zd.service.GrilService.*(..))"/>
            <!--
            around标签将被切入的方法包裹起来
            method:指定切面的具体实现类中的哪一个方法为次要业务实现
            -->
            <aop:around method="handler" pointcut-ref="pointCutGril"/>
        </aop:aspect>
    </aop:config>
    <!--aop配置end-->

    <!--为女朋友服务类-->
    <bean id="grilService" class="com.zd.service.GrilService"/>
</beans>
  • 然后运行,看看效果
public static void main(String[] args) {
	//根据类路径去读取 xml配置文件,来初始化spring容器
    ApplicationContext context = new ClassPathXmlApplicationContext("conf/beans.xml");
    //根据ID获取spring容器中的 bean(spring帮我们实例化的一个类对象)
    GrilService service = (GrilService)context.getBean("grilService");
    //周一和女朋友做的事情
    //        service.yueHui("吃饭");
    //周二和女朋友做的事情
    //        service.watchMovie();
    service.play();

    AbstractApplicationContext c = (AbstractApplicationContext)context;
    //为了保证spring正常退出,能够正常的销毁bean,需要加入下面的代码
    c.registerShutdownHook();
}

2.2.2、aop 注解使用

二、spring mvc

1、什么是mvc(model view controller)?

mvc是一种设计模式。是英文model(模型)、view(视图)、controller(请求控制)缩写。
(画图说明mvc流程)

image

2、为什么要用mvc?

大家在此之前都了解过三层架构,其实都一样。
为了符合 高内聚、低耦合的思想。为了让程序容易 移植、维护。增强扩展性。职责单一。逻辑复用等等。

3、编写一个示例

在编写示例之前,先向同学们介绍一下spring mvc的父子容器

  • 前面我们了解过spring的ioc容器,但如果我们使用spring mvc的框架,它有2个容器,且这两个容器具备父子关系,父容器访问不到子容器对象,子容器能访问父容器中的对象
    • 子容器:同学们可以将其理解为 就是在配置 DispatcherServlet (后面会介绍到DispatcherServlet)时指定的配置文件,此文件中可以配置 Controller(控制器)、ViewResolver(视图解析器)、HandlerMapping(映射处理器)
    • 父容器:就是spring的ioc容器,同学们可以将其理解为配置ContextLoaderListener时指定的配置文件,里面可以配置我们的service、dao、数据源...等

image

3.1、spring mvc maven依赖jar

<!--Spring mvc支持-->

   <dependency>

       <groupId>org.springframework</groupId>

       <artifactId>spring-webmvc</artifactId>

       <version>5.1.9.RELEASE</version>

   </dependency>

   <!--servlet的支持-->

   <dependency>

       <groupId>javax.servlet</groupId>

       <artifactId>servlet-api</artifactId>

       <version>2.5</version>

   </dependency>

   <!--jsp的支持-->

   <dependency>

       <groupId>javax.servlet.jsp</groupId>

       <artifactId>jsp-api</artifactId>

       <version>2.2</version>

   </dependency>

   <!--jsp的jstl标签库支持-->

   <dependency>

       <groupId>javax.servlet</groupId>

       <artifactId>jstl</artifactId>

       <version>1.2</version>

   </dependency>

3.2、核心调度的servlet

核心类:

org.springframework.web.servlet.DispatcherServlet

初始参数:

  • contextConfigLocation //指定controller配置文件

web.xml中配置示例:

<!--spring web mvc 的核心servlet-->
<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--spring mvc配置文件-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!--此配置文件中实例化的对象在子容器中-->
        <param-value>classpath:applicationContext.xml</param-value>
    </init-param>
    <!--tomcat启动就创建servlet-->
    <load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <!--
        注:spring的 DispatcherServlet 对url-pattern做过特殊处理。
        /  匹配任意路径,但是.jsp文件不会被拦截
        /* 能匹配任意路径,包括 .jsp
        -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

applicationContext.xml配置示例:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:mvc="http://www.springframework.org/schema/mvc"

       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.3.xsd
		http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
		http://www.springframework.org/schema/aop
		http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--启动spring web mvc 的注解-->
    <mvc:annotation-driven/>
    <!--扫描包-->
    <context:component-scan base-package="com.zd.controller"/>

    <!--配置一个视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>

    <!--解决静态资源被拦截的问题
    mapping : url路径
    location: 映射给指定的本地路径
    -->
    <mvc:resources mapping="/js/**" location="/js/" />
    <mvc:resources mapping="/css/**" location="/css/" />
    <mvc:resources mapping="/img/**" location="/img/" />

    <!--文件上传解析器
    特别要注意: spring 将 CommonsMultipartResolver类注入到相应属性中是根据 id匹配的,
    所以这个bean的id必须是 multipartResolver
    -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!--文件的编码方式-->
        <property name="defaultEncoding" value="UTF-8" />
        <!--单个文件的大小限制(20 m),单位:字节-->
        <property name="maxUploadSizePerFile" value="20971520"/>
        <!--上传文件的总大小限制(100 m),单位:字节-->
        <property name="maxUploadSize" value="104857600" />
    </bean>

</beans>

3.3、spring mvc环境初始化监听器

核心类:

主要用于在启动时加载相关配置文件

org.springframework.web.context.ContextLoaderListener

全局参数:

  • contextConfigLocation //可以指定例如 service、dao、数据源、事物等配置文件

web.xml中配置示例:

<!--全局参数-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-*.xml</param-value>
</context-param>
<!--spring加载配置文件的监听器
此监听器会读取 全局参数 contextConfigLocation 中的配置文件.可以将其理解为spring的父容器
-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

3.4、字符编码过滤器

核心类:

org.springframework.web.filter.CharacterEncodingFilter

初始参数:

  • encoding //指定字符集

web.xml中配置示例:

<!--加入字符集过滤器-->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <!--注意,这里要写 /* 才能拦截所有请求-->
    <url-pattern>/*</url-pattern>
</filter-mapping>

3.5、视图解析器

核心类:

org.springframework.web.servlet.view.InternalResourceViewResolver

属性:

  • prefix //前缀
  • suffix //后缀

示例:

<!--配置一个视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/jsp/" />
    <property name="suffix" value=".jsp" />
</bean>

3.6、注解相关配置

3.6.1、启用Spring MVC注解

如果你使用了注解,那么一定要记得在子容器的xml中加上启用注解的配置

虽然不写表面上看上去没事,但是一定要记得写,不然可能会有莫名其妙的问题

<mvc:annotation-driven />

3.6.2、注解扫描

扫描指定包下的类,然后根据你们自己打在类上的注解,自动装配和初始化

比如:

  • 扫描到 @controller注解的类,spring会将其当成controller处理
  • 扫描到 @RequestMapping注解,spring会为其建立路径映射
  • 扫描到 @Autowired注解,spring会为其注入对象实例
  • 扫描到 @ResponseBody注解,spring会为将其转为restful风格的数据展示
  • 扫描到 @Service注解,spring会将其当成service处理
<context:component-scan base-package="包名/类名" />

4、spring mvc常用注解

4.1、组件型注解:

以下四种注解都是注解在类上的,被注解的类将被spring初始话为一个bean,然后统一管理。

  • @Component 在类定义之前添加@Component注解,他会被spring容器识别,并转为bean。
  • @Repository 对Dao实现类进行注解 (可以将其理解为一种特殊的@Component)
  • @Service 用于对业务逻辑层进行注解, (可以将其理解为一种特殊的@Component)
  • @Controller 用于控制层注解 , (可以将其理解为一种特殊的@Component)

4.2、请求和参数型注解:

  • @RequestMapping:用于处理请求地址映射,可以作用于类和方法上。

    • value:定义request请求的映射地址

    • method:定义地request址请求的方式,包括【GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.】默认接受get请求,如果请求方式和定义的方式不一样则请求无法成功。

    • params:定义request请求中必须包含的参数值。

  • @RequestParam:用于获取传入参数的值

    • value:参数的名称
    • required:定义该传入参数是否必须,默认为true,(和@RequestMapping的params属性有点类似)
  • @PathViriable:用于定义路径参数值

    • value:参数的名称
    • required:定义传入参数是否为必须值
  • @ModelAttribute:用于把参数保存到model中,可以注解方法或参数,注解在方法上的时候,该方法将在处理器方法执行之前执行,然后把返回的对象存放在 session(前提时要有@SessionAttributes注解) 或模型属性中,@ModelAttribute(“attributeName”) 在标记方法的时候指定,若未指定,则使用返回类型的类名称(首字母小写)作为属性名称。

  • @SessionAttributes

    • 默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据存储一份到session域中。配合@ModelAttribute("user")使用的时候,会将对应的名称的model值存到session中

4.3、spring mvc 转发和重定向

4.3.1.通过ModuleAndView实现转发和重定向

设置ModelAndView对象 , 根据view的名称 , 和视图解析器跳到指定的页面 .
页面 : {视图解析器前缀} + viewName +{视图解析器后缀}

示例:某controller方法返回如下

//转发
ModelAndView mv = new ModelAndView("/index");
return mv;
//重定向
ModelAndView mv = new ModelAndView("redirect:/index");
return mv;

4.3.2.通过ServletAPI实现转发和重定向

4.3.3.通过Springmvc实现转发和重定向

示例:某controller方法返回如下

//转发
return "forward:/WEB-INF/jsp/test.jsp";
//重定向
return "redirect:/index.jsp";

5、JSR 303 后台数据校验

5.1、jsp中引入标签库

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
  • 注意启用spring mvc注解
<mvc:annotation-driven />

5.2、空检查

  • @Null 验证对象是否为null
  • @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
  • @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
  • @NotEmpty 检查约束元素是否为NULL或者是EMPTY.

5.3、Booelan检查

  • @AssertTrue 验证 Boolean 对象是否为 true
  • @AssertFalse 验证 Boolean 对象是否为 false

5.4、长度检查

  • @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
  • @Length(min=, max=) Validates that the annotated string is between min and max included.

5.5、日期检查

  • @Past 验证 Date 和 Calendar 对象是否在当前时间之前
  • @Future 验证 Date 和 Calendar 对象是否在当前时间之后
  • @Pattern 验证 String 对象是否符合正则表达式的规则

5.6、数值检查

建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null

  • @Min 验证 Number 和 String 对象是否大等于指定的值
  • @Max 验证 Number 和 String 对象是否小等于指定的值
  • @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
  • @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
  • @Digits 验证 Number 和 String 的构成是否合法
  • @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
  • @Range(min=, max=) 验证数字是否为 min ~ max区间之内.
  • @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
  • @CreditCardNumber信用卡验证
  • @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
  • @ScriptAssert(lang= ,script=, alias=)
  • @URL(protocol=,host=, port=,regexp=, flags=)

5.7、JSR303 代码示例

  • 我有一个User 实体类如下
/*
用户的实体类
 */
public class User {
    //姓名
    @NotBlank(message = "用户姓名不能为空") //校验name属性不为空字符串
    @Length(min = 0,max = 50,message = "姓名长度不能超过50个字符") //校验字符串长度在0~50之间
    private String name;
    //年龄
    @NotNull(message = "年龄不能为null")
//    @Max(value = 200,message = "年龄不能超过200岁")
//    @Min(value = 10,message = "年龄不能小于10岁")
    @Range(min = 10,max = 100,message = "年龄只能在10~100之间")
    private Integer age;
    //生日
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
    //学习的课程
    private List<String> courseList;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public List<String> getCourseList() {
        return courseList;
    }

    public void setCourseList(List<String> courseList) {
        this.courseList = courseList;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                ", courseList=" + courseList +
                '}';
    }
}

  • 测试校验的controller代码如下
/**
 * jsr 303 校验测试
 */
@RequestMapping("/validate")
@Controller
public class ValidateController {

    //跳转 jsr303页面
    @RequestMapping("/toJsr303")
    public String toJsr303(Model model){
        //可以做一些数据初始化的工作
        User u = new User();
//        u.setAge(18);
//        u.setName("小龟龟");
//
//        //
//        List<String> list = new ArrayList<String>();
//        list.add("01");//默认选中java
//        list.add("03");
//        u.setCourseList(list);

        //将u对象放入request作用域中
        model.addAttribute("user",u);
        return "jsr303";
    }
    /**
     * 测试方法1
     * @return
     */
    @RequestMapping("/test1")
    public String test1(Model model,
                        /*
                        @Valid使用jsr303 校验user对象中的属性
                         */
                        @Valid
                        User user,Errors erros){


//        System.out.println("进入 jsr 。。。name:"+name);
        if(erros.hasErrors()){


            System.out.println("校验后有错误信息:");
            //输出错误信息
//            List<ObjectError> errorList = erros.getAllErrors();
//            //遍历集合,输出所有的校验错误信息
//            errorList.stream().forEach(
//                //定义了一个匿名的方法
//                (obj) ->{
//                    System.out.println(
//                            obj.getCode()
//                            +":"+obj.getDefaultMessage());
//                }
//            );
            //获取指定属性的校验的错误信息
//            FieldError fieldError = erros.getFieldError("name");
//            System.out.println("name:"+fieldError.getDefaultMessage());
//
//            fieldError = erros.getFieldError("age");
//            System.out.println("age:"+fieldError.getDefaultMessage());

//            fieldError = erros.getFieldError("birthday");
//            System.out.println("birthday:"+fieldError.getDefaultMessage());

//            fieldError = erros.getFieldError("age");
//            System.out.println(fieldError.getDefaultMessage());
            //如果你有错误,我就将错误信息存放到requset作用域中
//            model.addAttribute("erros",erros);
            //如果有错误,就继续让对方填写表单
            return "jsr303";
        }
        //表单提交成功
        return "jsr303Sucess";
    }
}

  • 页面代码如下
    • jsr303.jsp
<%@ page import="com.zd.entity.User" %>
<%@ page
        pageEncoding="UTF-8"
        contentType="text/html;charset=UTF-8" language="java" %>
<!--引入spring的form标签库-->
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
    <title>Title</title>
    <style>
        .error{
            color: red;
        }
    </style>
</head>
<body>
<div>
    看看全局监听器中存储的contextPath:${contextPath}

    <!--
    modelAttribute: spring会去request里面寻找对应key的值,
    如果找不到会报错,如果找到了,将自动填充数据
    -->
    <form:form method="post"
               modelAttribute="user"
               action="${contextPath}/validate/test1">
        <!--path就想当与input中的name-->
        姓名:<form:input path="name" />
        <form:errors path="name" cssClass="error"/>
        <br/>
        年龄:<form:input path="age" />
        <form:errors path="age" cssClass="error" />
        <br/>
        生日:<input name="birthday" type="date" value="${dateStr}"/>
        <br/>
        课程:
        <label>
            <!--复选框,相当于 <input type="checkbox"/>-->
            <form:checkbox path="courseList" value="01"/>
            java
        </label>
        <label>
            <form:checkbox path="courseList" value="02"/>
            html
        </label>
        <label>
            <form:checkbox path="courseList" value="03"/>
            c#
        </label>
        <label>
            <form:checkbox path="courseList" value="04"/>
            数据库
        </label>
        <br/>
        <button type="submit">提交</button>
    </form:form>
</div>
</body>
</html>

    • jsr303Sucess.jsp
<%--
  Created by IntelliJ IDEA.
  User: zhuoli
  Date: 2020/11/15
  Time: 15:10
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    提交成功!
</body>
</html>

6、SpringMVC-RESTFUL风格+对应注解

6.1、什么是RESTFUL?

Restful就是一个资源定位及资源操作的风格。不是标准也不是协议,只是一种风格。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
特点包括:

  • 每一个URI代表1种资源;
  • 客户端使用GET、POST、PUT、DELETE4个表示操作方式的动词对服务端资源进行操作:
    • GET用来获取资源,
    • POST用来新建资源(也可以用于更新资源),
    • PUT用来更新资源,
    • DELETE用来删除资源;
  • 通过操作资源的表现形式来操作资源;
  • 资源的表现形式是JSON/XML/HTML;
  • 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。

6.2、示例

6.2.1、添加maven依赖:

<!-- bean转json支持,因为restful可能返回json格式的数据 -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.8</version>
</dependency>

6.2.2、静态资源访问问题

<!--静态资源映射路径
mapping : url路径
location: 映射给指定的本地路径
-->
<mvc:resources mapping="/js/**" location="/js/"/>

6.2.3、注解

  • @PostMapping ---等同于---- @RequestMapping + 限制请求方式为 post
  • @PutMapping ---等同于---- @RequestMapping + 限制请求方式为 put
  • @DeleteMapping ---等同于---- @RequestMapping + 限制请求方式为 delete
  • @GetMapping ---等同于---- @RequestMapping + 限制请求方式为 get
  • @RestController ---等同于--- @Controller + @ResponseBody 组合
  • @RequestBody -----主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)
  • @ResponseBody ----controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据。默认返回的是json格式数据,如果要返回xml格式数据,如要在返回对象的类上使用 Xml的相关注解。

返回json格式,实体类上的常用注解:

//日期转换
@JsonFormat(pattern = "yyyy-MM-dd")
  • 示例某实体类对象对转为json格式数据,其Date类型字段注解配置
//restful 转json时 日期格式处理注解
@JsonFormat(pattern = "yyyy-MM-dd")
private Date stuBirthday;

返回xml格式数据,实体类上的常用注解:

//文档更根元素
@XmlRootElement(name = "data")
//指定包装集合的标签名称(该注解强烈建议放在对应属性的getXxx方法上)
@XmlElementWrapper(name = "items")
//指定包装集合中每一项的标签名称(该注解强烈建议放在对应属性的getXxx方法上)
@XmlElement(name = "item")
  • 示例某实体类对象转换为xml格式数据,其注解配置
@XmlRootElement(name = "items")
public class XmlDo {
    private List<Student> list = null;
    @XmlElement(name = "item",type = Student.class)
    public List<Student> getList() {
        return list;
    }
    public void setList(List<Student> list) {
        this.list = list;
    }
}

注意

  • put/delete 请求方式后台需要使用 @RequestBody注解来接收数据,
  • 前端使用ajax技术,请求方式为put/delete时,需要设置请求头中的 'Content-Type'(内容类型)为 'application/json' ,才能提交json格式的数据给服务器

这里示例一段 jquery发送put请求方式的代码

function testPut(){
    //将一个js对象转换成json格式的字符串
    let jsonStr = JSON.stringify({
        id:10,
        stuName:'小丁丁',
        stuAge:18,
        stuBirthday:'2020-11-22'
    });
    //System.out.println();
    console.info(jsonStr);

    $.ajax({
        type:'put',//请求方式为 post/put/DELETE/get
        url:'${root}/rest/',//请求的路径
        //请求头参数
        headers:{
            //告诉浏览器,我提交给服务器的数据是json格式的数据
            'Content-Type':'application/json'
        },
        data:jsonStr,
        /*
                告诉jquery,返回的数据格式是json,
                让其帮我们将json格式的字符串解析为js对象
                 */
        dataType:"json",
        //请求成功调用的函数
        success:function(data){
            console.info("服务器返回的数据",data);
        },
        //请求失败调用的函数
        error:function(error){
            console.info('error',error)
        }
    });
}

7、拦截器

7.1、什么是拦截器?

Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

7.2、示例

7.2.1、自定义拦截器需要实现HandlerInterceptor接口

该接口中定义了一下几个方法

  • preHandle() 方法
    • 该方法会在控制器方法前执行,其返回值表示是否中断后续操作。
    • 当其返回值为true时,表示继续向下执行;
    • 当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。
  • postHandle()方法
    • 该方法会在控制器方法调用之后,且解析视图之前执行。
    • 可以通过此方法对请求域中的模型和视图做出进一步的修改。
  • afterCompletion()方法
    • 该方法会在整个请求完成,即视图渲染结束之后执行。
    • 可以通过此方法实现一些资源清理、记录日志信息等工作。

示例登录拦截器:

/**
 * 登录的拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        System.out.println("在调用controller的方法之前,preHandle");
        //判断用户是否登录
//        Object user = request.getSession().getAttribute("user");
//        if(user == null){
//            String root = request.getContextPath();
//            //没有登录,重定向登录页面
//            response.sendRedirect(root + "/home.jsp");
//            return false;
//        }
        //返回true,不会拦截.  返回false 拦截请求不往下走
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("调用完controller方法之后,postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        System.out.println("在页面渲染完毕之后,afterCompletion");
    }
}

示例日志记录拦截器:

/**
 * 日志拦截器
 */
public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        //在这里记录一下开始时间
        //系统当前时间
        long start = System.currentTimeMillis();
        request.setAttribute("start",start);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
        //获取系统当前时间
        long end = System.currentTimeMillis();
        long start = (long)request.getAttribute("start");

        System.out.println( request.getRequestURI() + "请求耗时"+(end - start)+"ms");
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

7.2.2、xml文件相关配置

在spring mvc子容器中配置好拦截器

<!--拦截器都配置在这对标签之中-->
<mvc:interceptors>
	<!--配置一个拦截器-->
        <mvc:interceptor>
            <!--配置拦截器的作用路径-->
            <mvc:mapping path="/**"/>
	    <!--排除的路径-->
            <mvc:exclude-mapping path=""/>
            <!--定义在<mvc:interceptor>下面的表示匹配指定路径的请求才进行拦截-->
            <bean class="实现了HandlerInterceptor接口的实现类"/>
        </mvc:interceptor>
</mvc:interceptors>

8、spring security

8.1、spring security 是什么?

是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。(安全访问控制框架)

spring security的简单原理:

使用众多的过滤器/拦截器 对url拦截,实现安全访问控制。

如:登录验证的拦截器、权限验证拦截器、记住密码拦截器、csrf安全保护拦截器、css安全保护拦截器...

注:csrf --跨站请求伪造,css --跨站脚本攻击

8.2、依赖jar

<!-- spring security 支持 start-->

<dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-web</artifactId>

    <version>4.2.3.RELEASE</version>

</dependency>

<dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-config</artifactId>

    <version>4.2.3.RELEASE</version>

</dependency>

<!-- spring security 支持 end-->

8.3、配置

8.3.1、web.xml配置 spring security过滤器链

前面讲过,spring security 是基于一系列 过滤器/拦截器 串成一个链条来实现的,所以我们要使用它的话,先要配置这根链条

核心filter-class

org.springframework.web.filter.DelegatingFilterProxy
  • filter-name为:springSecurityFilterChain,注意:该过滤器名称只能是这个

说明:该过滤器需要拦截所有的url,即对所有的url都要实现安全访问控制。DelegatingFilterProxy是一个委托的过滤器类,这个委托的类会根据名字(springSecurityFilterChain)去初始化对应的过滤器链条,所以springSecurityFilterChain 这个名字必须是固定的。

配置示例:

<!--spring security 过滤器链
    springSecurityFilterChain是固定的,
    DelegatingFilterProxy 这个委托类会根据名字去初始化对应的过滤器链条,即判断 名字如果是
    springSecurityFilterChain 那么就会初始化spring security过滤器链
    -->
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <!--对所有的url都实现安全访问控制-->
    <url-pattern>/*</url-pattern>
</filter-mapping>

8.3.2、建立spring-security.xml配置文件

注意该配置文件要加入如下schema约束

xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
	    http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd"
8.3.2.1、核心<http>标签说明及
  • security会对访问我们系统的人进行一个身份认证,既然要认证,那么认证的入口在哪?

    • <http>标签内配置一个基于<form-login>表单认证入口,并指定如下属性:

      • login-page --- 登录的页面
      • login-processing-url --- 登录处理的url,即:登录页面表单提交的 地址,security就是拦截这个地址进行登录认证
      • authentication-failure-url --- 登录认证失败跳转的地址
      • default-target-url ---登录认证成功默认跳转的地址
      <!--security的 核心标签
      use-expressions:如果为true,下面的access属性就能使用spel表达式
      spel表达式:SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言,同学们想知道其具体用法自行百度即可
      -->
      <sec:http use-expressions="true">
      	<!--...省略...-->
      	<!--身份认证的入口
      	<form-login> 基于表单登录的身份认证的标签
      	login-page:登录的表单页面
      	login-processing-url:登录处理的url,一般是和登录表单页中的action一致
      	username-parameter:表单用户名的参数key
      	password-parameter:表单密码的参数key
      	authentication-failure-url:认证失败跳转的页面
      	default-target-url:认证成功之后,默认跳转的页面
      	-->
      	<sec:form-login
      			login-page="/jsp/login.jsp"
      			login-processing-url="/user/login"
      			username-parameter="userName"
      			password-parameter="password"
      			authentication-failure-url="/jsp/login.jsp?error"
      			default-target-url="/jsp/home.jsp"
      	/>
          <!--...省略...-->
      </sec:http>
      
  • 认证入口有了,那么认证的方式是什么呢?

    • 所以这里我们要在配置一个认证管理器,用于管理我们的认证方式,标签: <authentication-manager>
  • 认证管理器中可以管理很多种认证方式(spring提供了 cas、ldap、jaas、dao、remote等认证方式),这里我们使用dao认证方式,标签 <authentication-provider>

<!--认证管理器
    管理身份认证的各种方式(cas、dao、jaas、....)
    -->
<sec:authentication-manager id="authenticationManager">
    <!--
        今天只做一种,配置dao认证方式,
        user-service-ref:bean id,实现获取用户信息对象
        -->
    <sec:authentication-provider user-service-ref="userDetailService">
        <!--dao认证方式,指定密码加密校验的方式-->
        <sec:password-encoder ref="pwdEncoder"/>
    </sec:authentication-provider>
</sec:authentication-manager>
  • 实现dao认证方式,写一个类 实现 UserDetailsService 接口
/**
 * dao认证方式 ,获取用户信息的实现类
 */
public class UserDetailServiceImpl
        implements UserDetailsService {
    /**
     * security在用户进行登录身份认证的时候,会调用这个方法。
     * @param s 用户名
     * @return
     * @throws UsernameNotFoundException
     */
    //选中类/接口 ,快捷键 ctrl + t 可以查看其子类结构
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //我们可以在这里面去查询数据库,根据用户名 把这个用户的信息加载出来
        //TODO 我这里就模拟一下查询数据库
        System.out.println("security传递过来的用户名:"+s);
        String userName = s;
        //是从数据库里面查得密码 (下面这串密文是:123456)
        String password = "$2a$10$hJsaxmYOv3xs/ts1pFqs4eavmTeF9G9K7zexd99ueywjTmUlo5VDi";
        User u = null;
        //角色集合,你们可以从数据库查询出来,我这里就模拟一下
        Collection<SimpleGrantedAuthority> coll =
                new ArrayList<SimpleGrantedAuthority>();
        //后台资源角色
        //SimpleGrantedAuthority是GrantedAuthority接口的实现类
        SimpleGrantedAuthority auth1 = new SimpleGrantedAuthority("ROLE_BACK");
        //前台资源角色
        SimpleGrantedAuthority auth2 = new SimpleGrantedAuthority("ROLE_FRONT");

        if(s.equals("admin")){
            //赋予后台资源访问的角色
            coll.add(auth1);
            //赋予前台资源访问的角色
            coll.add(auth2);
            //admin账号
            u = new User(userName, password, coll);
            //能找到用户,直接返回
            return u;
        }else if("front".equals(s)){
            //赋予前台资源访问的角色
            coll.add(auth2);
            //front账号
            u = new User(userName, password, coll);
            //能找到用户,直接返回
            return u;
        }
        //如果程序执行到这里,表示找不到用户,那么就认证失败,
        //认证失败的话,抛出一个异常即可
        //throw new UsernameNotFoundException("用户名或密码不对");
        //如果你不想让你的异常被security给抓住,你可以抛下面这个异常
        throw new RuntimeException("用户名或密码错误");
//        return null;
    }

    public static void main(String[] args) {
        BCryptPasswordEncoder pwdEncoder = new BCryptPasswordEncoder();
        //你们在注册用户的时候,肯定是将人家输入的密码加密后入库的
        //加密
        String pwd= pwdEncoder.encode("123456");
        System.out.println(pwd);
    }
}
  • <password-encoder> 配置密码加密,加密 核心类:

    org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

    此类的加密算法是bcrypt,bcrypt 使用的是布鲁斯·施内尔在1993年发布的 Blowfish 加密算法

<!--dao 认证方式中,获取用户信息对象的bean-->
<bean id="userDetailService" class="com.zd.security.UserDetailServiceImpl" />
<!--dao 认证方式中,密码加密校验方式的bean-->
<bean id="pwdEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

到这里,security 大的框框就配置完了。这里说明下spring security怎么样将我们实现的这个dao认证方式加入到spring security中呢?

因为spring security是使用众多的过滤器形成过滤器链条来拦截url实现的安全访问控制

过滤器链中有一个 UsernamePasswordAuthenticationFilter 一个基于用户名、密码认证的过滤器,该过滤完美契合dao认证方式,如果你需要更多自定义的额外的功能,你可以继承这个类,自定义一个认证过滤器来实现你想要的功能。

注意:如果你要自定义过滤器,那么在链条之中的位置必须在表单提交之前,即设置before="FORM_LOGIN_FILTER"

如果你是继承UsernamePasswordAuthenticationFilter实现的自定义过滤器,那么有如下几个属性你必须了解:

  • filterProcessesUrl属性:该身份认证过滤处理的url,一般指定登录表单提交的url即可
  • authenticationManager属性:指定该身份认证的过滤使用的 认证管理器
  • authenticationFailureHandler属性:认证失败处理器
  • authenticationSuccessHandler属性:认证成功处理器
  • usernameParameter属性:security在表单认证请求中获取用户名的参数 key(即登录表单中用户名的input框的name值)
  • passwordParameter属性:security在表单认证请求中获取密码的参数 key(即登录表单中密码的input框的name值)

8.3.2.2、细节配置

我们完成第8.3.2.1点配置后我们访问任意的url,是不是感觉还是和以前一样,没任何区别?

那是因为我们还没有个url赋予访问权限。来吧

  • <http>标签内 加入<intercept-url/>标签,此标签表示拦截url,其拥有属性如下
    • patern属性:表示匹配的url
    • access属性:表示匹配的url访问的权限。这里介绍几个常用的,下面都是spel表达式,注意将<http>标签中的属性 use-expressions="true",不然不能使用:
      • access属性值:isAuthenticated() --- 表示访问这些url必须进过身份认证之后
      • access属性值:isAnonymous() --- 表示访问这些url可以以一个匿名的身份访问,即不需要身份认证(如:不登录)也能访问
  • <intercept-url>标签可以配置任意个,拦截访问控制按照配置从上至下,其中一旦有一个匹配成功即对其进行相应的访问权限校验,校验后返回,不在进行后续权限控制匹配
    • <intercept-url>可以配合access-denied-handler标签来使用

<intercept-url>配置示例

<!--security的 核心标签
use-expressions:如果为true,下面的access属性就能使用spel表达式
spel表达式:SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言,同学们想知道其具体用法自行百度即可
-->
<sec:http use-expressions="true">
	<!--后台页面访问用户必须要有 ROLE_BACK 角色 -->
	<sec:intercept-url pattern="/jsp/back/**"
					   access="hasAnyRole('ROLE_BACK')" />
	<!--前台页面访问用户必须要有 ROLE_FRONT 角色-->
	<sec:intercept-url pattern="/jsp/front/**"
					   access="hasRole('ROLE_FRONT')" />
	<!--要求所有url都要进过 身份认证(登录)
	pattern:url规则,可以使用ant表达式
	access:访问的角色/权限
	-->
	<sec:intercept-url pattern="/**"
					   access="isAuthenticated()" />
    <!--
     没有权限或出现了其他安全方面错误导致访问失败处理的页面配置
     error-page:跳转的错误页面
     -->
	<sec:access-denied-handler
			error-page="/jsp/error.jsp"/>
	<!--。。。省略。。。-->
</sec:http>
  • 做完上面的配置后,我们发现一个问题
    • 那就是 pattern="/**" 这个规则拦截了我们所有的url,即客户访问我们系统任意url都需要经过认证才能访问
    • 但是实际情况是我们有一些url是不需要经过认证也能让客户访问的。如:登录页面、登录失败页面、错误页面...等
  • 想要解决上面的问题,我们只需指定某些url不做security安全访问控制即可

示例:

<!--
    security="none"表示不对指定的url规则做security安全访问控制-->
<!--登录页面-->
<sec:http pattern="/jsp/login.jsp*" security="none"/>
<!--登录失败页面-->
<sec:http pattern="/jsp/failure.jsp" security="none"/>
<!--错误页面-->
<sec:http pattern="/jsp/error.jsp" security="none"/>

备注:

老版本的security 对于设置为security="none"的url无法共享security管理的session,即这种url对应的controller或jsp你取不到security管理的session中保存的值。新版本没有这个问题。

  • 到这里,我们用着用着发现我们提交表单数据时,它老是给我们提示出现 CSRF 攻击,这是因为spring security默认开启CSRF保护,我们却没有按照它的机制去实现CSRF保护,但是要实现此功能是件非常麻烦的事情,如何实现同学们可自行百度,这里我们就先直接禁用掉它了
    • 禁用防 CSRF 攻击功能
<!--security的 核心标签
use-expressions:如果为true,下面的access属性就能使用spel表达式
spel表达式:SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言,同学们想知道其具体用法自行百度即可
-->
<sec:http use-expressions="true">
	<!--...省略...-->
	<!--禁用csrf 安全保护-->
	<sec:csrf disabled="true" />
	<!--...省略...-->
</sec:http>
  • 前面我们登录认证等都配置好了,那么我们如何样做登出呢?毕竟现在登录认证又不是我们实现的

    • 别急,原来spring security给我们提供了 <logout>标签,让我们做登出配置
    • <logout>有以下属性:
      • invalidate-session --- true表示登出后令之前的session失效
      • logout-url --- 访问指定的url即登出该用户
      • logout-success-url --- 登出成功后跳转的url
    <!--security的 核心标签
    use-expressions:如果为true,下面的access属性就能使用spel表达式
    spel表达式:SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言,同学们想知道其具体用法自行百度即可
    -->
    <sec:http use-expressions="true">
    	<!--...省略...-->
    	<!--登出的配置
    	invalidate-session:登出后让其session失效
    	logout-url:调用登出功能的url
    	-->
    	<sec:logout invalidate-session="true"
    				logout-url="/user/logout"/>
    	<!--...省略...-->
    </sec:http>
    
  • 假如 如果你想让你的网站像QQ一样,只能在一台电脑上登录(类似单点登录),那又该怎么做呢?

    • 我们可以思考一下,只能让我在一台电脑上登录,是不是就是我这个账户(用户名、密码)只能绑定服务器中的一个session对象
    • spring security提供了<session-management>标签,让我们来配置管理session
    • 配置示例如下:
    <!--security的 核心标签
    use-expressions:如果为true,下面的access属性就能使用spel表达式
    spel表达式:SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言,同学们想知道其具体用法自行百度即可
    -->
    <sec:http use-expressions="true">
    	<!--...省略...-->
    	<!--session管理-->
    	<sec:session-management>
    		<!--
    		max-sessions:
    			限制一个账号允许的session最大数量,设置为1就是只能在一台电脑上登录
    		error-if-maximum-exceeded:
    			为true表示超过session最大数量限制就报错,报错就会跳会登录页
    			为false,那么当超过session数量最大值,就会将之前的session挤下去(即令之前的一个session失效)
    		expired-url:当session失效的时候跳转的页面
    		-->
    		<sec:concurrency-control
    				max-sessions="1"
    				error-if-maximum-exceeded="false"
    				expired-url="/jsp/sessionExpired.jsp"/>
    	</sec:session-management>
    	<!--...省略...-->
    </sec:http
    
  • 很多网站都有记住我这个功能,利用security如何实现此功能呢?

    • 在登录页面加入一个 checkbox框,该checkbox的name值需要为 remenber-me
    • <http>标签内加入 <remember-me>标签,此标签具有如下属性
      • remember-me-parameter --- 绑定的checkbox参数key,默认为"remenber-me",如果你要自定义自己的参数key可以在这里指定。
      • token-validity-seconds --- 默认remeber me能记住14天,如果你要自定义记住的时间可以通过这个属性指定,该属性必须是一个数字,单位:秒
    • <remember-me>配置示例
<!--security的 核心标签
use-expressions:如果为true,下面的access属性就能使用spel表达式
spel表达式:SpEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言,同学们想知道其具体用法自行百度即可
-->
<sec:http use-expressions="true">
	<!--...省略...-->
	<!--记住我
	remember-me-parameter:
		参数key,和前台登录页中的 记住我 复选框的name保持一致
	token-validity-seconds:
		默认remeber me能记住14天,如果你要自定义记住的时间可以通过这个属性指定,该属性必须是一个数字,单位:秒
	-->
	<sec:remember-me
			token-validity-seconds="604800"
			remember-me-parameter="remenberMe"/>
</sec:http>

注意:登录页面 记住我 的复选框 name 属性值必须和 remember-me-parameter 值一致

image

8.3.2.3、spring security中获取用户相关信息的代码

  • 认证失败的异常信息保存在request作用域key为: SPRING_SECURITY_LAST_EXCEPTION 中
  • 获取用户信息
SecurityContextHolder.getContext().getAuthentication()
  • 获取用户名
request.getRemoteUser();  // Servlet标准,推荐使用
SecurityContextHolder.getContext().getAuthentication().getName(); //spring security提供
  • 获取用户权限信息
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
  • 判断用户是否拥有指定权限
request.isUserInRole("ADMIN");

8.4、spring security动态拦截和权限实现

注:这是额外课题,要同学们理解可能具备一定难度,若时间充足可讲。

  • 首先,我们要在security链条中嵌入我们自定义实现的过滤器

配置如下

<!--
    use-expressions 是否使用SPEL表达式的配置,
        如果声明为true,那么在access属性要用hasRole('ROLE_XXX')这样写
        如果声明为false(默认),那么access直接就是 'ROLE_XXX'
    -->
<sec:http use-expressions="true" >
    <!--。。。省略。。。-->
    <!--自定义动态url访问权限控制得 过滤器,过滤器器的位置是 url权限拦截匹配之前,身份认证之后-->
    <sec:custom-filter ref="urlInterceptFilter" before="FILTER_SECURITY_INTERCEPTOR" />
</sec:http>

<!--自定义url拦截 的过滤器,该过滤具备以下几个功能:
    1、角色/权限 能访问的 url资源  管理
    2、角色/权限 的访问策略
    -->
<bean id="urlInterceptFilter" class="com.zd.security.UrlInterceptFilter" >
    <!--注入自定义的 url规则 与 权限映射-->
    <property name="securityMetadataSource" ref="customSecurityMetadataSource" />
    <!--访问策略-->
    <property name="accessDecisionManager" ref="customAccessDecisionManager" />
    <!--由于这个过滤器内部 使用了身份认证的功能,所以这里要讲认证管理器也注入给它-->
    <property name="authenticationManager" ref="authenticationManager" />
</bean>
<!--自定义的 url规则 与 权限映射-->
<bean id="customSecurityMetadataSource" class="com.zd.security.CustomFilterInvocationSecurityMetadataSource" />
<!--决策访问器-->
<bean id="customAccessDecisionManager" class="com.zd.security.CustomAccessDecisionManager" />

<!--认证管理器-->
<sec:authentication-manager id="authenticationManager" >
    <!--认证供应商(认证方式) -->
    <sec:authentication-provider user-service-ref="userDetailService">
    </sec:authentication-provider>
</sec:authentication-manager>
<!--dao认证方式实现-->
<bean id="userDetailService" class="com.zd.security.UserDetailServiceImpl"/>
  • UrlInterceptFilter代码如下

注意:上面的配置我是将此过滤器嵌入在security链条中,其位置在 登录认证之后,权限拦截之前

/**
 * 动态 url拦截映射访问权限 过滤器
 */
public class UrlInterceptFilter extends AbstractSecurityInterceptor implements Filter {
    //xml配置文件注入
    private SecurityMetadataSource securityMetadataSource;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //security的过滤器链调用 对象
        FilterInvocation fi = new FilterInvocation(request, response,chain);
        //调用链条前的一个处理,这里处理有2个动作
        //1、调用 SecurityMetadataSource 对象中的getAttributes方法获取可以访问url该 url 的所有 角色/权限
        //2、调用 AccessDecisionManager(访问策略) 对象中的decide方法来 验证访问的权限是否充足
        //得到是拦截的一个状态 标记
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try{
            //执行security过滤器链中的下一个过滤器
            fi.getChain().doFilter(request,response);
        }finally {
            //执行完链条之后要对 拦截状态 这个标记进行一个处理
            super.afterInvocation(token,null);
        }
    }

    /**
     * 调用 security 过滤器链条需要使用 FilterInvocation类,这里直接返回 FilterInvocation.class类型即可
     * @return
     */
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    /**
     * 得到security url资源 与 角色/权限 的关系 对象
     * @return
     */
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public void setSecurityMetadataSource(SecurityMetadataSource securityMetadataSource) {
        this.securityMetadataSource = securityMetadataSource;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }
    @Override
    public void destroy() {

    }
}

  • CustomFilterInvocationSecurityMetadataSource 代码如下

该类表明url资源与角色/权限的关系

/**
 * 自定义的 security url资源 与 角色/权限 关系类
 */
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    //url 与 角色映射的集合
    private Map<String,Collection<ConfigAttribute>> resouceMap;

    //spring利用反射实例化类对象,反射实例化调用的就是这个默认的构造方法
    public CustomFilterInvocationSecurityMetadataSource(){
        //在这里我们可以去数据库中将 每个url 所对应的 角色/权限 都加载进来
        //这里我模拟一下,去查询了数据吧

        //key:url   value:能访问url的角色 集合
        Map<String,Collection<ConfigAttribute>> map = new HashMap<String,Collection<ConfigAttribute>>();
        //能够访问 /jsp/backstage/** 后台的角色
        Set<ConfigAttribute> backstageSet = new HashSet<ConfigAttribute>();
        backstageSet.add(new SecurityConfig("ROLE_ADMIN"));
        backstageSet.add(new SecurityConfig("ROLE_BACKSTAGE"));
        //缓存起来
        map.put("/jsp/backstage/**", backstageSet);

        //能够访问 /jsp/front/** 前台的角色
        Set<ConfigAttribute> frontSet = new HashSet<ConfigAttribute>();
        frontSet.add(new SecurityConfig("ROLE_FRONT"));
        //缓存起来
        map.put("/jsp/front/**",frontSet);

        //设置到对象属性中,因为 getAttributes 要用这个map
        this.resouceMap = map;

    }
    //获取哪些 角色/权限 可以访问当前url
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation)o;
        //获取当前 url
        String currentUrl = fi.getRequestUrl();
        //获取能访问  该url 的权限集合
        Set<ConfigAttribute> set = new HashSet<ConfigAttribute>();
        //遍历resouceMap 的key, 这个key 也是url
        Iterator<String> it = resouceMap.keySet().iterator();
        while(it.hasNext()){
            //这个key也是url规则,而且可以是ant表达式
            String pattern = it.next();
            //这里我们要使用ant工具类来校验 当前url是否能匹配上key
            AntPathMatcher matcher = new AntPathMatcher();
            //匹配 currentUrl 是否符合 pattern规则,如果符合 则返回true,反正返回false
            boolean b = matcher.match(pattern,currentUrl);
            if(b){
                //取得可以访问这个url规则的 角色集合
                Collection<ConfigAttribute> coll = this.resouceMap.get(pattern);
                //添加进入
                set.addAll(coll);
            }
        }
        //进过上面的遍历,能访问该url的角色都得到了,在这里返回 交给security即可
        return set;
    }
    //这个不用管
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {

        return null;
    }
    //本类是否支持security过滤器链
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}
  • CustomAccessDecisionManager 代码实现

该类表明访问的策略

/**
 * 自定义的 决策访问器
 */
public class CustomAccessDecisionManager implements AccessDecisionManager {
    /**
     * 访问策略,你可以在这个方法中实现你想要的一种访问策略
     * @param authentication 当前用户的认证信息
     * @param o 当前url
     * @param collection 访问当前url 所需的角色集合,该集合数据来源于
     *                   CustomFilterInvocationSecurityMetadataSource.getAttributes 方法
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o,
                       Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        //这里我们就做一个简单访问策略算了
        //判断当前用户 只要有 collection 中的一种角色,就让它能访问url资源
        if(collection == null || collection.isEmpty()){
            //如果集合为空,那么访问这个url不需要任何权限
            return;
        }

        //遍历访问当前url 需要的权限集合
        Iterator<ConfigAttribute> it = collection.iterator();
        while(it.hasNext()){
            //角色配置对象
            ConfigAttribute ca = it.next();
            //访问url需要的角色名称
            String needRoleName = ca.getAttribute();
            //获取当前用户 所拥有的 角色列表
            Collection<? extends GrantedAuthority> list = authentication.getAuthorities();
            for(GrantedAuthority ga : list){
                //用户的这个角色名称
                String userRoleName = ga.getAuthority();
                if(needRoleName.equals(userRoleName)){
                    //如果需要的这个 角色名称 与用户所拥有的这个角色名称一致,那么表示当前用户有权限访问这个url
                    //直接返回即可
                    return;
                }

            }
        }
        //进过上面的代码,程序若还运行到此处,那么表示 这个用于没有权限访问这个url
        //没有权限 直接抛出一个AccessDeniedException异常,
        // 让security跳转<access-denied-handler>标签中的错误页面
        throw new AccessDeniedException("没权限,你不配");

    }
    //该类是否支持security 角色/权限 访问策略
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    //该类是否支持securit过滤器链
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}
  • UserDetailServiceImpl 样例代码
/**
 * dao认证方式实现
 */
public class UserDetailServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
//        if(1 == 1){
//            throw new RuntimeException("用户名错误");
//        }
        //这里你可以根据登陆名 去查询数据库,把用户的信息都加载出来
        //TODO 模拟查询数据库
        //登录名
        String lname = loginName;
        //姓名
        String uname = "张三";
        //密码
        String password = "123456";
        //这个用户所拥有的角色,可以从数据库里面查询出来,注意角色必须以ROLE_开头
        //可以访问前端页面的角色
        GrantedAuthority auth1 = null;
        if(loginName.equals("admin")){
            auth1= new SimpleGrantedAuthority("ROLE_ADMIN");
        }else  if(loginName.equals("backstage")){
            auth1= new SimpleGrantedAuthority("ROLE_BACKSTAGE");
        }else if(loginName.equals("front")){
            auth1= new SimpleGrantedAuthority("ROLE_FRONT");
        }
        //一个用户的角色是可以有多个的
        List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
        if(null!=auth1) {
            auths.add(auth1);
        }
        /*
        根据 登录名、密码、角色来创建这个用户并返回给spring security,
        security会调用密码校验器对密码进行校验
         */
        User user = new User(lname, password, auths);
        return user;
    }
}
  • 图解 自定义过滤器类、url资源与角色/权限关系类、访问决策器类、dao认证实现类 他们的关系

ps:有时间我在画图吧

Copyright © 2024 夜雨初凉
Powered by .NET 9.0 on Kubernetes