10k

Spring实战(第四版)- part2(SpringMVC构建Web应用实例)

第二部分 Web中的Spring

第五章 Spring MVC基本用法。如何编写处理 Web 请求的控制器以及如何透明地绑定请求参数和负载到业务对象上,同时它还提供了数据检验和错误处理的功能。

第六章 如何渲染Web视图。如何得到 Spring MVC 控制器所生成的模型数据,并将其渲染为用户浏览器中的 HTML。(JSP,Apache Tiles,Thymeleaf)

第七章 Spring MVC高级技术。包括自定义 Spring MVC 配置、处理 multipart 文件上传、处理异常以及使用 flash 属性跨请求传递数据。

第八章如何使用 Spring Web Flow 来构建会话式、基于流程的 Web 应用程序。

第九章如何使用 Spring Security 来为 Web 应用程序提供安全性。

第五章 构建Spring Web Application

  • 映射请求到Spring控制器
  • 透明的绑定表单参数
  • 校验表单提交

5.1 Spring MVC起步

Spring将请求在调度Servlet、处理器映射(handler mapping)、控制器以及视图解析器之间移动。

5.1.1 跟踪Spring MVC的请求

img

  1. 单例的前端控制器(DispatcherServlet)根据请求携带的URL信息查询一个或多个处理器映射(handler mapping),然后将请求发送给Spring MVC的控制器controller。
  2. 良好的控制器一般不怎么处理工作,它会将业务逻辑委托给服务对象进行处理;
  3. Controller完成逻辑后产生的信息(model),标示出用来渲染的视图,打包发给DispatcherServlet;
  4. DispatcherServlet通过返回的逻辑视图名称,利用视图解析器view resolver找到特定的视图实现;
  5. 将数据model交付给视图,视图渲染数据并返回给客户端。
5.1.2 搭建Spring MVC

配置DispatcherServlet:使用Java将DispatcherServlet 配置在 Servlet 容器中

package spittr.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" }; // 映射请求
  }
  
  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
  } 

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
  }

}

容器查找SpringServletContainerInitializer类将配置工作交给它,SpringServletContainerInitializer又会去找WebApplicationInitializer的实现并将配置任务交给这个实现。AbstractAnnotationConfigDispatcherServletInitializer是WebApplicationInitializer的基础实现,SpitterWebInitializer继承它也是实现了WebApplicationInitializer

启动Spring MVC

一个最简单的配置:

package spittr.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
public class WebConfig {
}

问题1:缺少视图解析器的话会使用默认的BeanNameViewResolver,这个视图解析器会查找 ID 与视图名称匹配的 bean,并且查找的 bean 要实现 View 接口,它以这样的方式来解析视图。;

问题2:没启动组件扫描,只能找到显式声明的控制器;

问题3:DispatcherServlet 会映射为应用的默认 Servlet,所以它会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这可能并不是你想要的效果)。

所以我们加一些东西:

package spittr.web;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
  
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable(); // 通过调用 DefaultServletHandlerConfigurer 的 enable() 方法,我们要求 DispatcherServlet 将对静态资源的请求转发到 Servlet 容器中默认的 Servlet 上,而不是使用 DispatcherServlet 本身来处理此类请求。
  }
}

因为聚焦Web开发,所以RootConfig可以带过:

package spittr.config;

import java.util.regex.Pattern;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableMvc;

@Configuration
@ComponentScan(basePackages={"spittr"}, 
    excludeFilters={
        @Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
    })
public class RootConfig {
}
5.1.3 Spittr应用简介

Spitter(应用的用户)和 Spittle(用户发布的简短状态更新)。

5.2 编写基本的控制器

  1. 控制器是类里的方法上加了@RequestMapping 注解的类,这个注解声明了它们要处理的请求。

    ```java package spittr.web;

    import static org.springframework.web.bind.annotation.RequestMethod.*;

    import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping;

    @Controller public class HomeController {

    @RequestMapping(value="/", method = GET) public String home(Model model) { return "home"; }

    } ```

    @Controller 是基于@Component,辅助组件实现扫描。Spring会在看到它后将它声明为一个bean。

5.2.1 测试控制器
package spittr.web;

import static org.junit.Assert.asser¡12tEquals;
import org.junit.Test;
import spittr.web.HomeController;

public class HomeControllerTest {

  @Test
  public void testHomePage() throws Exception {
    HomeController controller = new HomeController();
    assertEuqals("home", controller.home());
  }

}

这个测试的问题在于它并没有以MVC的视角去测试,它只是看了controller里面会调用home()方法以及它会返回什么。

从Spring3.2开始,可以按照控制器的方式测试,它会不启动服务器发起请求:

package spittr.web;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;

import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;

import spittr.web.HomeController;

public class HomeControllerTest {

  @Test
  public void testHomePage() throws Exception {
    HomeController controller = new HomeController();
    MockMvc mockMvc = standaloneSetup(controller).build();
    mockMvc.perform(get("/"))
           .andExpect(view().name("home"));
  }

}
5.2.2 定义类级别的请求处理
  1. 可以将@RequestMapping拆分并将路径映射部分放到类级别上

    ```java package spittr.web;

    import static org.springframework.web.bind.annotation.RequestMethod.*;

    import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping;

    @Controller @RequestMapping("/") // @RequesiMapping({"/", "/homepage"}) public class HomeController {

    @RequestMapping(method = GET) public String home(Model model) { return "home"; }

    } ```

    类级别的注解会应用到它下面的所有处理器方法,处理器方法上的@RequestMapping会对其进行补充。

  2. @RequestMapping 的 value 属性能够接受一个 String 类型的数组。

5.2.3 传递模型数据到视图中
  1. 需要定义新方法来处理对于Spittr数据的展现

  2. 首先定义一个数据访问的Repository,接口可以实现解耦(参考此书第一部分)。

    ```java package spittr.data;

    import java.util.List; import spittr.Spittle;

    public interface SpittleRepository {

    List<Spittle> findSpittles(long max, int count);

    } ```

  3. 定义Spittle类

    ```java package spittr;

    import java.util.Date;

    import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;

    public class Spittle {

    private final Long id; private final String message; private final Date time; private Double latitude; private Double longitude;

    public Spittle(String message, Date time) { this(null, message, time, null, null); }

    public Spittle(Long id, String message, Date time, Double longitude, Double latitude) { this.id = id; this.message = message; this.time = time; this.longitude = longitude; this.latitude = latitude; }

    public long getId() { return id; }

    public String getMessage() { return message; }

    public Date getTime() { return time; }

    public Double getLongitude() { return longitude; }

    public Double getLatitude() { return latitude; }

    @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that, "id", "time"); }

    @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, "id", "time"); }

    } ```

  4. 对于这个Spittle对象,写一个测试来断言预期

    ```java @Test public void shouldShowPagedSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(50); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findSpittles(238900, 50)) .thenReturn(expectedSpittles);

    SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")) .build();

    mockMvc.perform(get("/spittles")) .andExpect(view().name("spittles")) .andExpect(model().attributeExists("spittleList")) .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); }

    ...

    private List<Spittle> createSpittleList(int count) { List<Spittle> spittles = new ArrayList<Spittle>(); for (int i=0; i < count; i++) { spittles.add(new Spittle("Spittle " + i, new Date())); } return spittles; } ```

    我们预期创建50个对象,并放进一个list中。当请求包含参数的地址时,可以返回名字为“spittles”的逻辑视图,包含了一个spittleList对象。

  5. 此处例子中因为MockMvc会因为无法区分视图路径和控制器路径因此失败,所以加上这个方法从而让它只使用这个view实例。

    Sets up a single ViewResolver that always returns the provided view instance. This is a convenient shortcut if you need to use one View instance only。

5.3 接收请求的输入

Spring MVC 允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:

  • 查询参数(Query Parameter)
  • 表单参数(Form Parameter)
  • 路径变量(Path Variable)
5.3.1 处理查询参数
  1. 在 Spittr 应用中,我们可能需要处理的一件事就是展现分页的 Spittle 列表。

  2. 在确定该如何实现时,假设我们要查看某一页 Spittle 列表,这个列表会按照最新的 Spittle 在前的方式进行排序。因此,下一页中第一条的 ID 肯定会早于当前页最后一条的 ID。所以,为了显示下一页的 Spittle,我们需要将一个 Spittle 的 ID 传入进来,这个 ID 要恰好小于当 前页最后一条 Spittle 的 ID。另外,你还可以传入一个参数来确定要展现的 Spittle 数量。

  3. 重写之前的测试,主要区别在于GET路径加入了查询参数:

    ```java @Test public void shouldShowRecentSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(50); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findSpittles(238900, 50)) .thenReturn(expectedSpittles);

    SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")) .build();

    mockMvc.perform(get("/spittles?max=238900&count=50")) .andExpect(view().name("spittles")) .andExpect(model().attributeExists("spittleList")) .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); } ```

  4. 为了让SpittleController同时满足此处和5.2节中的请求,可以给它加上参数并且赋予默认值

    ```java private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);

    @RequestMapping(method=RequestMethod.GET) public List<Spittle> spittles( @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max, @RequestParam(value="count", defaultValue="20") int count) { return spittleRepository.findSpittles(max, count); } ```

5.3.2 通过路径参数接受输入
  1. 构建面向资源的请求。

  2. 假设我们的应用程序需要根据给定的 ID 来展现某一个 Spittle 记录。按照上一节通过查询参数方式构造的请求会像这样:/spittles/show?spittle_id=12345, 从面向资源的角度不太理想。

  3. 理想状态是应该通过路径进行标识,而不是通过查询参数。对 /spittles/12345 发起 GET 请求要更好。

  4. /spittles/12345 能够识别出要查询的资源,而另外一种通过查询描述的是带有参数的一个操作 —— 本质上是通过 HTTP 发起的 RPC。

  5. 继续修改测试

    ```java @Test public void testSpittle() throws Exception { Spittle expectedSpittle = new Spittle("Hello", new Date()); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);

    SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(get("/spittles/12345")) .andExpect(view().name("spittle")) .andExpect(model().attributeExists("spittle")) .andExpect(model().attribute("spittle", expectedSpittle)); } ```

  6. 在@RequestMapping中添加占位符可以实现利用路径参数的请求。

    java @RequestMapping(value="/{spittleId}", method=RequestMethod.GET) public String spittle( @PathVariable("spittleId") long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }

    如果 @PathVariable 中没有 value 属性的话,它会假设占位符的名称与方法的参数名相同。

5.4 处理表单

  1. 新建一个SpitterController来处理表单的提交

    ```java package spittr.web;

    import static org.springframework.web.bind.annotation.RequestMethod.*;

    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping;

    import spittr.Spitter; import spittr.data.SpitterRepository;

    @Controller @RequestMapping("/spitter") public class SpitterController {

    @RequestMapping(value="/register", method=GET) public String showRegistrationForm() { return "registerForm"; }

    } ```

  2. 写一个测试

    ```java @Test public void shouldShowRegistration() throws Exception { SpitterController controller = new SpitterController(); MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(get("/spitter/register")).andExpect(view().name("registerForm")); } ```

  3. 渲染注册表单的JSP

    ```jsp <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ page session="false" %> <html> <head> <title>Spitter</title> <link rel="stylesheet" type="text/css" href="\<c:url value="/resources/style.css" />" > </head> <body>

    Register

    <form method="POST">
      First Name: <input type="text" name="firstName" /><br/>
      Last Name: <input type="text" name="lastName" /><br/>
      Email: <input type="email" name="email" /><br/>
      Username: <input type="text" name="username" /><br/>
      Password: <input type="password" name="password" /><br/>
      <input type="submit" value="Register" />
    </form>
    

    </body> </html> ```

    这里的标签中并没有设置 action 属性。在这种情况下,当表单提交时它会提交到与展现时相同的 URL 路径上,即提交到 /spitter/register 上。这意味着需要在服务器端处理该 HTTP POST 请求。

5.4.1 编写处理表单的控制器
  1. 为了处理表单POST请求,控制器会接收表单数据并保存为Spittr对象。为了防止用户刷新重复提交会做重定向处理。

  2. 写个测试, 构建一下预期(TDD?)

    ```java @Test public void shouldProcessRegistration() throws Exception { SpitterRepository mockRepository = mock(SpitterRepository.class); Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); when(mockRepository.save(unsaved)).thenReturn(saved);

    SpitterController controller = new SpitterController(mockRepository); MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(post("/spitter/register") .param("firstName", "Jack") .param("lastName", "Bauer") .param("username", "jbauer") .param("password", "24hours") .param("email", "jbauer@ctu.gov")) .andExpect(redirectedUrl("/spitter/jbauer"));

    verify(mockRepository, atLeastOnce()).save(unsaved); } ```

  3. 定义Controller

    ```java package spittr.web;

    import static org.springframework.web.bind.annotation.RequestMethod.*;

    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping;

    import spittr.Spitter; import spittr.data.SpitterRepository;

    @Controller @RequestMapping("/spitter") public class SpitterController {

    private SpitterRepository spitterRepository;

    @Autowired public SpitterController(SpitterRepository spitterRepository) { this.spitterRepository = spitterRepository; }

    @RequestMapping(value="/register", method=GET) public String showRegistrationForm() { return "registerForm"; }

    @RequestMapping(value="/register", method=POST) public String processRegistration(Spitter spitter) {
    spitterRepository.save(spitter); return "redirect:/spitter/" + spitter.getUsername(); }

    } ```

  4. InternalResourceViewResolver 还能识别forward。forward做一次请求,redirect做两次。

    两者区别:A找B借钱,B再去找C,这就是forward;A让B去找C,相当于A又去发一次借的请求,这是redirect。

  5. 当我们重定向到用户个人信息页面,需要添加一个方法来处理这个Spittr对象将其展示:

    java @RequestMapping(value="/{username}", method=GET) public String showSpitterProfile(@PathVariable String username, Model model) { Spitter spitter = spitterRepository.findByUsername(username); model.addAttribute(spitter); return "profile"; }

5.4.2 校验表单
  1. 对用户提交的数据基于安全性(e.g. 不能是一些注入语句)或者是业务要求(日期、电话、卡号要满足一定的格式),都需要进行校验。

  2. 在processRegistration里面虽然可以加if...else...,但是随着越来越多的信息和业务逻辑,这个地方会变得异常庞大和复杂,且搞乱了本身要做的事情。

  3. 从 Spring 3.0 开始,在 Spring MVC 中提供了对 Java 校验 API 的支持。

    注解 描述
    @AssertFalse 所注解的元素必须是 Boolean 类型,并且值为 false
    @AssertTrue 所注解的元素必须是 Boolean 类型,并且值为 true
    @DecimalMax 所注解的元素必须是数字,并且它的值要小于或等于给定的 BigDecimalString 值
    @DecimalMin 所注解的元素必须是数字,并且它的值要大于或等于给定的 BigDecimalString 值
    @Digits 所注解的元素必须是数字,并且它的值必须有指定的位数
    @Future 所注解的元素的值必须是一个将来的日期
    @Max 所注解的元素必须是数字,并且它的值要大于或等于给定的值
    @Min 所注解的元素必须是数字,并且它的值要小于或等于给定的值
    @NotNull 所注解元素的值必须不能为 null
    @Null 所注解元素的值必须为 null
    @Past 所注解的元素的值必须是一个已过去的日期
    @Pattern 所注解的元素的值必须匹配给定的正则表达式
    @Size 所注解的元素的值必须是 String、集合或数组,并且它的长度要符合给定的范围

    也可以自定义一些注解限制。

  4. Spittr可以加上一些注解

    ```java package spittr;

    import javax.validation.constraints.NotNull; import javax.validation.constraints.Size;

    import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;

    public class Spitter {

    private Long id;

    @NotNull @Size(min=5, max=16) private String username;

    @NotNull @Size(min=5, max=25) private String password;

    @NotNull @Size(min=2, max=30) private String firstName;

    @NotNull @Size(min=2, max=30) private String lastName;

    ... } ```

  5. Spitter 参数添加了 @Valid 注解,这会告知 Spring,需要确保这个对象满足校验限制。

    ```java @RequestMapping(value="/register", method=POST) public String processRegistration( @Valid Spitter spitter, Errors errors) { if (errors.hasErrors()) { return "registerForm"; }

    spitterRepository.save(spitter); return "redirect:/spitter/" + spitter.getUsername(); } ```

    如果有校验出现错误的话,那么这些错误可以通过 Errors 对象进行访问,现在这个对象已作为 processRegistration() 方法的参数。(很重要一点需要注意: Errors 参数要紧跟在带有 @Valid 注解的参数后面,@Valid 注解所标注的就是要检验的参数。)processRegistration() 方法所做的第一件事就是调用Errors.hasErrors() 来检查是否有错误。

5.5 小结

第六章 渲染Web视图

  • 将模型数据渲染为 HTML
  • 使用 JSP 视图
  • 通过 tiles 定义视图布局
  • 使用 Thymeleaf 视图

本章讨论视图以及渲染视图。

6.1 理解视图解析

  1. 将请求处理的逻辑和视图渲染解耦也是Spring MVC重要特性

  2. Spring视图解析器的内部实现

    java public interface ViewResolver { View resolverViewName(String viewName, Locale locale) throws Exception; }

    resolverViewName会返回一个View实例。

    ```java public interface View {

    String getContentType();

    void render(Map<String, ?> model, HttpServletRequest request, HttpServlectResponse response) throws Exception; } ```

  3. Spring提供了多个内置实现

    视图解析器 描述
    BeanNameViewResolver 将视图解析为 Spring 应用上下文中的 bean,其中 bean 的 ID 与视图的名字相同
    ContentNegotiatingViewResolver 通过考虑客户端需要的内容类型来解析视图, 委托给另外一个能够产生对应内容类型的视图解析器
    FreeMarkerViewResolver 将视图解析为 FreeMarker 模板
    InternalResourceViewResolver 将视图解析为 Web 应用的内部资源(一般为 JSP)
    JasperReportsViewResolver 将视图解析为 JasperReports 定义
    ResourceBundleViewResolver 将视图解析为资源 bundle(一般为属性文件)
    TilesViewResolver 将视图解析为 Apache Tile 定义,其中 tile ID 与视图名称相同。注意有两个不同的 TilesViewResolver 实现,分别对应于 Tiles 2.0 和 Tiles 3.0
    UrlBasedViewResolver 直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义
    VelocityLayoutViewResolver 将视图解析为 Velocity 布局,从不同的Velocity 模板中组合页面
    VelocityViewResolver 将视图解析为 Velocity 模板
    XmlViewResolver 将视图解析为特定 XML 文件中的 bean 定义。类似于 BeanNameViewResolver
    XsltViewResolver 将视图解析为 XSLT 转换后的结果

6.2 创建JSP视图

Spring提供支持JSP方式

  • InternalResourceViewResolver 会将视图名解析为 JSP 文件,如果在你的 JSP 页面中使用了 JSP 标准标签库 (JavaServer Pages Standard Tag Library,JSTL)的话,InternalResourceViewResolver 能够将视图名解析为 JstlView 形式的 JSP 文件,从而将 JSTL 本地化和资源 bundle 变量暴露给JSTL的格式化(formatting)和信息(message)标签。
  • Spring 提供了两个 JSP 标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类特性。
6.2.1 配置适用于JSP的视图解析
  1. InternalResourceViewResolver遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个 Web 应用中视图资源的物理路径。

  2. 前文中home页面的例子

    java @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views"); resolver.setSuffix(".jsp"); return resolver; }

    img

  3. 也可以基于XML去配置。

  4. 当逻辑视图带有斜线时,如:books/detail 将会解析为 /WEB-INF/views/books/detail.jsp, 它可以带入到物理视图的渲染。这样可以允许我们组织文件,并且清晰的使用层级结构。

解析JSTL视图

  1. 如果这些 JSP 使用 JSTL 标签来处理格式化和信息的话,那么我们会希望 InternalResourceViewResolver 将视图解析为 JstlView

  2. 让InternalResourceViewResolver将视图渲染为jstlView只需要设置一下setViewClass即可(XML也可以配置):

    java @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views"); resolver.setSuffix(".jsp"); resolver.setViewClass(org.springframework.web.service.view.JstlView.class); return resolver; }

6.2.2 使用Spring的JSP库
  1. Spring提供的标签可以避免在脚本中写入过多的Java代码
  2. Spring提供两种,一种是将基于HTML标签的,将表单绑定到模型,另一种是一些工具标签。

将表单绑定到模型

  1. 首先需要声明:<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>

  2. 一共提供了14种标签

    JSP标签 描述
    \<sf:checkbox> 渲染成一个HTML 标签,其中type属性设置 为checkbox
    \<sf:checkboxes> 渲染成多个 HTML \<input> 标签,其中 type 属性设置为 checkbox
    \<sf:errors> 在一个 HTML \<span> 中渲染输入域的错误
    \<sf:form> 渲染成一个 HTML \<form> 标签,并为其内部标签暴露绑定路径,用于数据绑定
    \<sf:hidden> 渲染成一个 HTML \<input> 标签,其中 type 属性设置为 hidden
    \<sf:input> 渲染成一个 HTML \<input> 标签,其中 type 属性设置为 text
    \<sf:label> 渲染成一个 HTML \<label> 标签
    \<sf:option> 渲染成一个 HTML 标签,其 selected 属性根据所绑定的值进行设置
    \<sf:options> 按照绑定的集合、数组或 Map,渲染成一个 HTML 标签的列表
    \<sf:password> 渲染成一个HTML 标签,其中type属性设置 为password
    \<sf:radiobutton> 渲染成一个 HTML \<input> 标签,其中 type 属性设置为 radio
    \<sf:radiobuttons> 渲染成多个 HTML \<input> 标签,其中 type 属性设置为 radio
    \<sf:select> 渲染为一个 HTML \<select> 标签
    \<sf:textarea> 渲染为一个 HTML \<textarea> 标签
  3. 应用到Spittr应用的用户注册页面:

    jsp <sf:form method="POST" commandName="spitter" > First Name: <sf:input path="firstName" /><br/> Last Name: <sf:input path="lastName" /><br/> Email: <sf:input path="email" /><br/> Username: <sf:input path="username" /><br/> Password: <sf:password path="password" /><br/> <input type="submit" value="Register" /> </sf:form>

    <sf:form> 会渲染会一个 HTML \<form> 标签,但它也会通过 commandName 属性构建针对某个模型对象的上下文信息。

    所以相应的。在controller中需要传一个对象给它:

    java @RequestMapping(value="/register", method=GET) public String showRegistrationForm(Model model) { model.addAttribute(new Spitter()); return "registerForm"; }

  4. 还可以指定文本域的类型信息Email: <sf:input path="email" type="email" /><br/>

展现错误

  1. 如果存在校验错误的话,请求中会包含错误的详细信息,这些信息是与模型数据放到一起的。

    jsp <sf:form method="POST" commandName="spitter"> First Name: <sf:input path="fisrtName" /> <sf:errors path="firstName" /><br/> ... </sf:form>

    如果没错,则不会渲染error这一块,如果有错误则会渲染如下:

    html First Name: <input id="firstName" name="firstName" type="text" value="J" /> <span id="firstName.errors">size must be between 2 and 30</span>

  2. 本地化让应用变得更加友好易读。

    1. 首先修改下Spittr定义类,如果没有大括号的话,message 中的值将会作为展现给用户的错误信息。但是使用了大括号之后,我们使用的就是属性文件中的某一个属性,该属性包含了实际的信息。

      ```java @NotNull @Size(min=5, max=16, message="{username.size}") private String username;

      @NotNull @Size(min=5, max=25, message="{password.size}") private String password;

      @NotNull @Size(min=2, max=30, message="{firstName.size}") private String firstName;

      @NotNull @Size(min=2, max=30, message="{lastName.size}") private String lastName;

      @NotNull @Email(message="{email.valid}") private String email; ```

    2. 定义一个property文件,放在根目录下:

      properties firstName.size=First name must be between {min} and {max} characters long. lastName.size=Last name must be between {min} and {max} characters long. username.size=Username must be between {min} and {max} characters long. password.size=Password must be between {min} and {max} characters long. email.valid=The email address must be valid.

      min和max会取自 @Size 注解上所设置的 min 和 max 属性。

    3. 将这些错误信息抽取到属性文件中还会带来一个好处,那就是我们可以通过创建地域相关的属性文件,为用户展现特定语言和地域的信息。例如,如果用户的浏览器设置成了西班牙语,那么就应该用西班牙语展现错误信息,我们需要创建一个名为 ValidationErrors_es.properties 的文件

    Spring通用标签库

    1. 同样首先要声明:<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>

    2. 有10个标签

      标签 描述
      \<s:bind> 将绑定属性的状态导出到一个名为 status 的页面作用域属性 中,与 \<s:path> 组合使用获取绑定属性的值
      \<s:escapeBody> 将标签体中的内容进行 HTML 和 / 或 JavaScript 转义
      \<s:hasBindErrors> 根据指定模型对象(在请求属性中)是否有绑定错误,有条件地渲染内容
      \<s:htmlEscape> 为当前页面设置默认的 HTML 转义值
      \<s:message> 根据给定的编码获取信息,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)
      \<s:nestedPath> 设置嵌入式的 path,用于 \<s:bind> 之中
      \<s:theme> 根据给定的编码获取主题信息,然后要么进行渲染(默认行 为),要么将其设置为页面作用域、请求作用域、会话作用 域或应用作用域的变量(通过使用 var 和 scope 属性实现)
      \<s:transform> 使用命令对象的属性编辑器转换命令对象中不包含的属性
      \<s:url> 创建相对于上下文的 URL,支持 URI 模板变量以及 HTML/XML/JavaScript 转义。可以渲染 URL(默认行为),也可以将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)
      \<s:eval> 计算符合 Spring 表达式语言(Spring Expression Language, SpEL)语法的某个表达式的值,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)

      展现国际化信息

      1. 如果你在模板文件中写的是中文,但是这个应用部署到其他国家,所有用户看的都是中文,而且如果要改需要去改全部模板。(类似硬编码)

      2. 对于渲染文本来说,是很好的方案,文本能够位于一个或多个属性文件中。借助 <s:message>,我们可以将硬编码的欢迎信息替换为如下的形式:<h1><s:message code="spitter.welcome" /></h1>

      3. ResourceBundleMessageSource。它会从一个属性文件中加载信息,这个属性文件的名称是根据基础名称(base name)衍生而来的。如下的 @Bean 方法配置了 ResourceBundleMessageSource:

        java @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); return messageSource; }

        在这个 bean 声明中,核心在于设置 basename 属性。你可以将其设置为任意你喜欢的值,在这里,我将其设置为 message。将其设置为 message 后,ResourceBundleMessageSource 就会试图在根路径的属性文件中解析信息,这些属性文件的名称是根据这个基础名称衍生得到的。

      4. 另外的可选方案是使用 ReloadableResourceBundleMessageSource,它的工作方式与 ResourceBundleMessageSource 非常类似,但是它能够重新加载信息属性,而不必重新编译或重启应用。如下是配置ReloadableResourceBundleMessageSource 的样例:

        java @Bean public MessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("file:///etc/spitter/messages"); messageSource.setCacheSeconds(10); return messageSource; }

      5. 创建默认属性文件messages.properties,目前来说至少要包含spitter.welcome=Welcome to Spittr!

      6. 为了国际化,创建另外一个名为 messages_es.properties 的属性文件spittr.welcome=Bienvenidos a Spittr!

      创建URL

      <s:url>: 创建 URL,然后将其赋值给一个变量或者渲染到响应中。是<c:url>的替代者。

      • 如果希望在 URL 上添加参数的话,那么你可以使用 <s:param> 标签。比如,如下的 <s:url> 使用两个内嵌的 <s:param> 标签,来设置 /spittles 的 max 和 count 参数:

        jsp <s:url href="/spittles" var="spittlesUrl"> <s:param name="max" value="60" /> <s:param name="count" value="20" /> </s:url>

      • 如果希望创建带有路径参数的url,可以使用<s:url>特有的:

        jsp <s:url href="/spitter/{username}" var="spitterUrl"> <s:param name="username" value="jbauer" /> </s:url>

      转义内容

      <s:escapeBody> 标签是一个通用的转义标签。它会渲染标签体中内嵌的内容,并且在必要的时候进行转义。例如,假设你希望在页面上展现一个 HTML 代码片段。为了正确显示,我们需要将 <> 字符替换为 <>,否则的话,浏览器将会像解析页面上其他 HTML 那样解析这段 HTML 内容。

6.3 使用Apache Tiles视图定义布局

6.3.1 配置Tiles视图解析器

6.4 使用Thymeleaf

  • JSP标签库与HTML混和降低可读性,而且因为其不是真正的HTML,在不同的浏览器渲染的也不尽人意可能。
  • JSP规范和Servlet规范耦合,只能应用在基于Servlet的Web应用。
6.4.1 配置Thymeleaf视图解析器

为了要在 Spring 中使用 Thymeleaf,我们需要配置三个启用 Thymeleaf 与 Spring 集成的 bean:

  • ThymeleafViewResolver:将逻辑视图名称解析为 Thymeleaf 模板视图;
  • SpringTemplateEngine:处理模板并渲染结果;
  • TemplateResolver:加载 Thymeleaf 模板。
6.4.2 定义Thymeleaf模板
  1. 就是HTML,没标签库。发挥作用依靠自定义命名空间

    ```html <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Spitter</title> <link rel="stylesheet" type="text/css" th:href="@{/resources/style.css}"></link> </head> <body>

    Welcome to Spitter

      <a th:href="@{/spittles}">Spittles</a> | 
      <a th:href="@{/spitter/register}">Register</a>
    

    </body> </html> ```

6.5 小结

第七章 Spring MVC 高级技术

  • SpringMVC配置的替代方案
  • 处理文件上传
  • 在控制器中处理异常
  • 使用flash属性

7.1 Spring MVC配置的替代方案

AbstractAnnotationConfigDispatcherServletInitializer基本的配置可能并不满足我们的需求;除了DispatcherServlet以外我们还需要额外的Servlet和Filter;可能需要对DispatcherServlet本身做一些配置;亦或者需要将应用配置到Spring3.0之前的容器,需要将Servlet配置在web.xml中。

7.1.1 自定义DispatcherServlet配置
  1. 之前的SpitterServletInitializer只是实现了必须实现的三个方法,还有其他方法可以实现进行额外配置。
  2. 例如customizeRegistration()。在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后,就会调用这个方法,可以对DispatcherServlet进行额外配置。
    • setLoadOnStartup() 设置 load-on-startup 优先级
    • setInitParameter() 设置初始化参数
    • setMultipartConfig() 配置 Servlet 3.0 对 multipart 的支持
7.1.2 添加其他的Servlet和Filter
  1. 基于Java的initializer可以让我们定义任意数量的初始化器类。如果想在Web容器注册其他组件,只需要创建一个新的初始化器即可。

    ```java package com.myapp.config;

    import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration.Dynamic; import org.springframework.web.WebApplicationInitializer; import com.myapp.MyServlet;

    public class MyServletInitializer extends WebApplicationInitializer { @Override public void onStartup(ServeletContext servletContext) throws ServletException { Dynamic myServlet = servletContext.addServlet("myServelet", MyServlet.calss);

        myServelet.addMapping("/custom/**");
    }
    

    } ```

  2. 类似的也可以创建新的WebApplicationInitializer实现来注册Listener和Filter,Spring3.0提供了一种通用方式:

    ```java @Override public void onStartup(ServlectContext servletContext) throws ServletException {

    javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);

    filter.addMappingForUrlPatterns(null, false, "/custom/**"); } ```

    另外还有更快捷方式,只需重载getServletFilters方法:

    java @Override protected Filter() getServletFilters() { return new Filter[] { new MyFilter() }; }

    他返回的所有filter都会映射到DispatcherServlet上。

7.1.3 在web.xml中声明DispatcherServlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" >

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/root-context.xml</param-value>
  </context-param>
  
  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>
  
  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

而且还可以通过指定配置类的位置,在web.xml中使用Java配置类。

7.2 处理multipart形式的数据

multipart 格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了 multipart 的请求体:

-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW
Content-Disposition: form-data; name="firstName"

Charles
-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW
Content-Disposition: form-data; name="lastName"

Xavier
-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW
Content-Disposition: form-data; name="email"

charles@xmen.com
-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW
"Content-Disposition: form-data; name="username"

professorx
-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW
Content-Disposition: form-data; name="password"

letmeinO1
-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"

Content-Type: image/jpeg

  [[ Binary image data goes here ]]
-----------WebKitFormBoundaryqgkaBn8lHJCuNmiW--
7.2.1 配置multipart解析器
  1. 从 Spring 3.1 开 始,Spring 内置了两个 MultipartResolver 的实现供我们选择:

    • CommonsMultipartResolver:使用 Jakarta Commons FileUpload 解析 multipart 请求;

    • StandardServletMultipartResolver:依赖于 Servlet 3.0 对 multipart 请求的支持(始于 Spring 3.1)。(常选)

  2. multipart的配置需要放到DispatcherServlet配置的一部分。

7.2.2 处理multipart请求
  1. 最常见的方式就是在某个控制器方法参数上添加 @RequestPart 注解。

7.3 处理异常

Spring 提供了多种方式将异常转换为响应:

  • 特定的 Spring 异常将会自动映射为指定的 HTTP 状态码;
  • 异常上可以添加 @ResponseStatus 注解,从而将其映射为某一个 HTTP 状态码;
  • 在方法上可以添加 @ExceptionHandler 注解,使其用来处理异常。
7.3.1 将异常映射为HTTP状态码
  1. 在默认情况下,Spring 会将自身的一些异常自动转换为合适的状态码。表 7.1 列出了这些映射关系。
Spring 异常 HTTP 状态码
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad Request
  1. Spring可以通过@ResponseStatus注解将异常映射为HTTP状态码。例子:

    java @RequestMapping(value="/{spittleId}", method=RequestMethod.GET) public String spittle( @Pathvariable("spittleId") long spittleId, Model model) { Spittle spittle = spittleRepository.findOne(spittleId); if (spittle == null) { throw new SpittleNotFoundException(); } model.addAttribute(spittle); return "spittle"; }

    当findOne找不到spittle为空时,抛出自定义异常SpittleNotFoundException,默认会产生500状态码,但是对于资源没找到,比较精确的描述应该是是404,所以可以自定义映射SpittleNotFoundException来进行变更:

    ```java package spittr.web;

    import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus;

    @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found") public class SpittleNotFoundException extends RuntimeException { } ```

7.3.2 编写异常处理的方法
  1. 前文已经对于错误可以成功映射了,但是有时我们还想在exception中包含产生的错误,我们此时需要将异常视为请求来处理。

  2. 例子:当用户提交的spittle和已创建的文本一模一样,则save()方法会抛出DuplicateSpittle Exception,那么SpittleController的saveSpittle就要处理这个异常。基本操作:

    java @RequestMapping(method=RequestMethod.POST) public String saveSpittle(SpittleForm form, Model model) { try { spittleRepository.save(new Spittle(null, form.getMessage(), new Date(), form.getLongitude(), form.getLatitude())); return "redirect:/spittles"; } catch (DuplicateSpittleException e) { return "error/duplicate"; } }

    基本操作稍微复杂。可以改为如下设计:

    1. 首先剥离异常处理

      java @RequestMapping(method=RequestMethod.POST) public String saveSpittle(SpittleForm form, Model model) { spittleRepository.save(new Spittle(null, form.getMessage(), new Date(), form.getLongitude(), form.getLatitude())); return "redirect:/spittles"; }

    2. 为异常处理增加一个方法在controller,handleDuplicateSpittle() 方法上添加了 @ExceptionHandler 注解,当抛出 DuplicateSpittleException 异常的时候,将会委托该方法来处理

      java @ExceptionHandler(DuplicateSpittleException.class) public String handleDuplicateSpittle() { return "error/duplicate"; }

  3. 这个异常处理可以处理整个controller的DuplicateSpittleException。

7.4 为控制器添加通知

  1. 如果有handler可以处理整个项目相同类型而不是局限于一个controller就好了。

  2. 控制器通知(controller advice)是任意带有 @ControllerAdvice 注解的类,这个类会包含一个或多个如下类型的方法:

    • @ExceptionHandler 注解标注的方法;
    • @InitBinder 注解标注的方法;
    • @ModelAttribute 注解标注的方法。
  3. 最为实用的一个场景就是将所有的 @ExceptionHandler 方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。例如:

    ```java package spittr.web;

    import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler;

    @ControllerAdvice public class AppWideExceptionHandler {

    @ExceptionHandler(DuplicateSpittleException.class) public String handleNotFound() { return "error/duplicate"; }

    } ```

7.5 跨重定向请求传递数据

model数据以请求参数形式复制到请求中,重定向会丢失。

7.5.1 通过URL模板进行重定向
  1. 当构建 URL 或 SQL 查询语句的时候,使用 String 连接是很危险的-> 注入。

  2. 除了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。

  3. 使用模板的方式可以避免安全问题,因为不安全字符都会被转义。

    java @RequestMapping(value="/register", method=POST) public String processRegistration( Spitter spitter, Model model) { spitterRepository.save(spitter); model.addAttribute("username", spitter.getUsername()); model.addAttribute("spitterId", spitter.getId()); return "redirect:/spitter/{username}"; }

  4. 但是他的限制就是只能用来发送相对简单的值。

7.5.2 使用flash属性
  1. 假设我们要发一个Spitter对象。

  2. 将Spitter model放在session中用以重定向之后的使用,但是要在使用后清除掉。

  3. Spring认为我们不需要管理这些数据,提供了flash属性。flash会一直携带这些数据直到下一次请求,然后才会消失。

  4. Spring 提供了通过 RedirectAttributes 设置 flash 属性的方法,这是 Spring 3.1 引入的 Model 的一个子接口。RedirectAttributes 提供了 Model 的所有功能。 具体来讲,RedirectAttributes 提供了一组addFlashAttribute() 方法来添加 flash 属性。

    java @RequestMapping(value="/register", method=POST) public String processRegistration( Spitter spitter, RedirectAttribute model) { spitterRespository.save(spitter); model.addAttribute("username", spitter.getUsername()); model.addFlashAttribute("spitter", spitter); // 甚至可以不指定key参数,让它根据类型自行推断。 return "redirect:/spitter/{username}"; }

    原理还是利用session:

    img

  5. 使用(下游消费Spitter),在数据库查询之前,先看会话是否带有Spitter对象

    java @RequestMapping(value="/{username}", method=GET) public String showSpitterProfile( @PathVariable String username, Model model) { if (!model.containsAttribute("spitter")) { model.addAttribute(spitterRepository.findByUsername(username)); } return "profile"; }

7.6 小结

第八章 使用Spring Web Flow

第九章 保护Web应用

  • Spring Sesecurity介绍
  • 使用Servlet规范中的Filter保护Web应用
  • 基于数据库和LDAP进行认证

Spring Security是基于Spring AOP的Servlet规范中的Filter实现的安全框架。

9.1 Spring Security简介

  1. 在Web请求级别和方法调用级别处理身份认证和授权
  2. 基于Spring,充分利用DI和AOP
9.1.1 理解Spring Security的模块
模块 描述
ACL 支持通过访问控制列表为域对象提供安全性
切面 当使用Spring Security注解时,会使用基于AspectJ的切面而不是标准的SpringAOP
CAS客户端 提供与Jasig的中心认证服务进行集成的功能
配置 包含通过XML和Java配置SPring Security的功能支持核心提供Spring Security基本库
加密 提供了加密和密码编码的功能
LDAP 支持基于LDAP进行认证
OpenID 支持基于LDAP进行认证
Remoting 提供了对Spring Remoting的支持
标签库 Spring Security 的 JSP 标签库
Web 提供了 Spring Security 基于 Filter 的 Web 安全性支持
9.1.2 过滤Web请求
  1. 只需要配置一个Filter,DelegatingFilterProxy 是一个特殊的 Servlet Filter,它本身所做的工作并不多。只是将工作委托给一个 javax.servlet.Filter 实现类,这个实现类作为一个注册在 Spring 应用的上下文中。

img

  1. 可以通过Java或者xml配置这个filter。他会拦截发往应用中请求,并将请求委托给ID为springSecurityFilterChain bean。
  2. springSecurityFilterChain也称作FilterChainProxy,他可以链接任一或多个其他的filter。你不需要显式声明这些,它会自动创建。
9.1.3 编写简单的安全性配置
  1. @EnableWebSecurity 注解将会启用 Web 安全功能。任何实现了WebSecurityConfigurer的bean都可以用来配置Spring Security。

  2. 但是当你是Spring MVC开发者的时候,你还可以选择@EnableWebMvcSecurity

    ```java package spitter.config;

    import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

    @Configuration @EnableWebMvcSecurity // @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { } ```

  3. 通过重载WebSecurityConfigurerAdapter的configure方法配置。

    方法 描述
    configure(WebSecurity) 通过重载配置SpringSecurity的Filter 链
    configure(HttpSecurity) 通过重载,配置如何通过拦截器保护请求
    configure(AuthenticationManagerBuilder) 通过重载,配置userdetail服务

9.2 选择查询用户详细信息的服务

9.2.1 使用基于内存的用户存储
  1. 通过 inMemoryAuthentication() 方法,我们可以启用、配置并任意填充基于内存的用户存储。

    ```java package spittr.config;

    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

    @Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("user").password("password").roles("USER");
    }
    

    } ```

  2. roles方法是authorities方法的简写,roles方法所给定的值都会添加一个”ROLE_"前缀。

    java auth .inMemoryAuthentication() .withUser("user").password("password").authorities("ROLE_USER") .and() .withUser("admin").password("password").authorities("ROLE_USER", "ROLE_ADMIN");

  3. 方法列表

    方法 描述
    accountExpired(boolean) 定义账号是否已过期
    accountLocked(boolean) 定义账号是否已锁定
    and() 用来连接配置
    authorities(GrantedAuthority...) 授予某个用户一项或多项权限
    authorities(List<?\<GrantedAuthority>) 授予某个用户一项或多项权限
    authorities(String...) 授予某个用户一项或多项权限
    credentialsExprired(boolean) 定义凭证是否已经过期
    disabled(boolean) 定义账号是否已被禁用
    password(String) 用户定义的密码
    roles(String) 授予某个用户一项或多项角色
  4. 基于内存的方式对于开发测试友好,但是不适合生产环境。生产环境的用户数据比较适合保存在数据库中,

9.2.2 基于数据表进行认证
  1. 为了配置Spring Security使用JDBC为支持的用户存储,可以使用jdbc-Authentication()方法。

    java @Autowired DataSource dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource); }

  2. Spring内部实现了默认的用户表查询,但是当你的数据库用户表不是默认实现的那些表名和列,侧需要自己额外配置:

    java @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthtication() .dataSource(dataSource) .usersByUsernameQuery( "select username, password, true from Spitter where username=?") .authoritiesByUsernameUuery( "select username, 'ROLE_USER' from Spitter where username=?"); }

  3. 将默认的SQL查询替换为自定义设计时,要遵循查询的及本协议:

    1. 所有查询用户名作为唯一的查询参数
    2. 认证查询会选取用户名、密码以及启用状态信息。
  4. 数据库不要使用明文存储,所以在存储之前,要对密码做一步转码处理,将转码后的数据存入数据库,当认证的时候用同样的流程转码,然后对比数据库中的存储。

    java @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery( "select username, password, true " + "from Spitter where username=?") .authoritiesByUsernameQuery( "select username, 'ROLE_USER' from Spitter where username=?") .passwordEncoder(new StandardPasswordEnconder("123456")); }

  5. passworkEncoder方法可以接受PasswordEncoder接口的任意实现(BCryptPasswordEncoder、NoOpPasswordEncoder 和 StandardPasswordEncoder),当默认的三种不能满足时,你还可以自定义实现。

    java public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matched(CharSequence rawPassword, String encodePassword); }

9.2.3 基于LDAP进行认证

9.2.4 配置自定义的用户服务

9.3 拦截请求

9.3.1 使用SPring表达式进行安全保护
安全表达式 计算结果
authentication 用户的认证对象
denyAll 结果始终为false
hasAnyRole(list of roles) 如果用户被授予了列表中任意角色,返回true
hasRole(role) 如果用户被授予指定角色,返回true
hasIpAddress(IP Address) 如果用户来自指定IP,返回true
isAnonymous() 如果当前用户为匿名用户,结果为true
isAuthenticated() 如果当前用户进行了认证的话,结果为true
isFullyAuthenticated() 如果当前用户进行了认证的话(不是用过Remember-me功能进行的认证),结果为true
isRemeberMe() 如果当前用户是通过 Remember-me 自动认证的,结果为 true
permitAll 结果始终为true
principal 用户的principal对象
9.3.2 强制通道的安全性
  1. 传递到 configure() 方法中的 HttpSecurity 对象有一个 requiresChannel() 方法,借助这个方法能够为各种 URL 模式声明所要求的通道

    java @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/spitter/me").hasRole("SPITTER") .antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER") .anyRequest().permitAll() .and() .requeresChannel() .antMatchers("/spitter/form").requiresSecure(); }

  2. 有些页面(e.g. 首页)不需要HTTPS传送,可以使用requiresSecure()方法,生命为始终通过HTTP传送:

    java .antMatchers("/").requiresInecure(); // regexMathcers()

9.3.3 防止跨站请求伪造
  1. Spring Security 3.2开始就默认启动SCRF防护了。

9.4 认证用户

9.4.1 添加自定义的登录页
9.4.2 启动HTTP Basic认证
  1. HTTP Basic 认证(HTTP Basic Authentication)会直接通过 HTTP 请求本身,对要访问应用程序的用户进行认证。你可能在以前见过 HTTP Basic 认证。当在 Web 浏览器中使用时,它将向用户弹出一个简单的模态对话框。

    java @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginPage("/login"); .and() .httpBasic() .realmName("Spittr") .and() ... }

9.4.3 启动Remember-me功能
  1. 默认情况下,这个功能是通过在 cookie 中存储一个 token 完成的,这个 token 最多两周内有效。我们也可以指定validation的时间。
9.4.4 退出
  1. 退出功能是通过 Servlet 容器中的Filter实现的(默认情况下),这个 Filter 会拦截针对 “/logout” 的请求。
  2. 当用户点击这个链接的时候,会发起对 “/logout” 的请求,这个请求会被 Spring Security 的 LogoutFilter 所处理。用户会退出应用,所有的 Remember-me token 都会被清除掉。在退出完成后,用户浏览器将会 重定向到 “/login?logout”,从而允许用户进行再次登录。 如果你希望用户被重定向到其他的页面,如应用的首页,那么可以在 configure() 中进行配置。

9.5 保护视图

Spring Security 本身提供了一个 JSP 标签库,而 Thymeleaf 通过特定的方言实现了与 Spring Security 的集成。

9.6 小结

在前面的几章中,我们看到了如何将 Spring 运用到应用程序的前端,后面会介绍Spring如何在后端发挥作用。

Thoughts? Leave a comment