一.构建Spring Web应用

1.Spring MVC中用户请求处理

跟踪Spring MVC的请求 (1).png

    上图展示了请求使用Spring MVC所经历的所有站点。

    1:在请求离开浏览器时,会带有所有请求内容的信息,至少会包含请求的URL。

        请求通过Spring的DispatcherServlet前端控制器将请求发送给Spring MVC控制器(Controller)。

    2:DispatcherServlet会查询一个或多个处理器映射(handler mapping)来确定请求的下一站在哪里。

        处理器映射会根据请求所携带的URL信息来进行决策。

    3:一旦选择了合适的控制器(Controller),DispatcherServlet会将请求发送给选中的控制器。

        到了控制器,请求会卸下提交的信息并等待控制器处理这些信息。

    4:控制器处理完之后会产生一些信息即模型(model)。控制器将模型数据打包,并标示出用于渲染输出的视图名。然后将请求连同模型和视图名发送回DispatcherServlet。

    5:DispatcherServlet会使用视图解析器(view resolve)来将逻辑视图名匹配为一个特定的视图实现。

    6:视图的实现,交付模型数据。

    7:视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端。


2.基本控制器

@Controller:声明为一个控制器;

@RequestMapping:声明他们所要处理的请求:

    @RequestMapping(vaule="/", method=GET):当收到对"/"的HTTP GET请求时,就会调用home()该注解的方法。

@RequestParam:接收请求输入,将请求作为查询参数;

@PathVariable:使用占位符,将请求作为路径的一部分;

    两者不同点使用@RequestParam时,URL是这样的:http://host:port/path?参数名=参数值

                        使用@PathVariable时,URL是这样的:http://host:port/path/参数值

@RequestMapping(value="/user",method = RequestMethod.GET)  
   public @ResponseBody  
   User printUser(@RequestParam(value = "id", required = false, defaultValue = "0")  
   int id) {  
    User user = new User();  
       user = userService.getUserById(id);  
       return user;  
   }  
     
   @RequestMapping(value="/user/{id:\\d+}",method = RequestMethod.GET)  
   public @ResponseBody  
   User printUser2(@PathVariable int id) {  
       User user = new User();  
       user = userService.getUserById(id);  
       return user;  
   }


    

@Valid:告知Spring,需要确保这个对象满足校验限制。

    Java校验API所提供的校验注解:

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


@Controller
@RequestMapping(DetailController.VIEW_PREFIX)
public class DetailController extends AjaxBaseController {
public static final String VIEW_PREFIX = "/ajax/detail";
public static final String GET_DETAIL = "/getDetailList";
public static final String FIND = "/find";
@Autowired
private JobDetailService jobDetailService;
/**
* 查找
*/
@ResponseBody
@RequestMapping(value = FIND, method = RequestMethod.GET)
public AjaxPackVo<DetailVo> find(@RequestParam("id") Long id){
AjaxPackVo<DetailVo> packVo = new AjaxPackVo<>();
DetailVo dr = jobDetailService.findVo(id);
packVo.setVo(dr);
return packVo;
}
@ResponseBody
@RequestMapping(value = GET_DETAIL, method = RequestMethod.POST)
public AjaxPackVo<DetailVo> getDetail(@RequestBody DetailSo detailSo){
AjaxPackVo<DetailVo> detailVoAjaxPackVo = new AjaxPackVo<>();
PageList<DetailVo> detailVoPageList = jobDetailService.getPageList(detailSo);
detailVoAjaxPackVo.setPageList(detailVoPageList);
return detailVoAjaxPackVo;
}



@ResponseStatus:异常上添加注解,从而将其映射为某一个HTTP状态码;

    @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")  //将异常映射为HTTP状态404

@ExceptionHandler:方法上添加注解,使其用来处理异常;

    

二.保护Web应用

    使用Spring Security为Web应用提供安全性,保护应用中的信息。

    Spring Security提供了完整的安全性解决方案,能够在Web请求级别和方法调用级别处理身份认证和授权。充分利用了依赖注入和面向切面技术。

    Spring Security从两个角度解决安全性问题:

        (1)使用Servlet规范中的Filter保护Web请求并限制URL级别的访问;

        (2)Spring Security还能够使用Spring AOP保护方法调用--借助于对象代理和使用通知。能够确保只有具备适当权限的用户才能访问安全保护的方法。

    1.Spring Security被分成11个模块:

模块描述
ACL支持通过访问控制列表(ACL)为域对象提供安全性
切面(Aspects)一个很小的模块,当使用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP
CAS客户端提供与Jasig的中心认证服务(Central Authentication Service,CAS)进行集成的功能
配置(Configuration)包含通过XML和Java配置Spring Security的功能支持
核心(Core)提供Spring Security基本库
加密(Cryptography)提供了加密和密码编码的功能
LDAP支持基于LDAP进行认证
OpenID支持使用OpenID进行集中式认证
Remoting提供了对Spring Remoting的支持
标签库(Tag Library)Spring Security的JSP标签库
Web提供了Spring Security基于Filter的Web安全性支持


    @EnableWebSecurity:该注解将会启用Web安全功能,它必须配置在一个实现了WebSecurityConfigure的bean中;

    @EnableWebMvcSecurity:如果你的应用正好是使用Spring MVC开发的,那么就应该考虑使用该注解代替@EnableWebSecurity;

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{}

    @EnableWebMvcSecurity注解还配置了一个Spring MVC参数解析解析器(argument resolver),这样的话处理器方法能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。

        它同时还配置了一个bean,在使用Spring表单绑定标签库来定义表单时,这个bean会自动添加一个隐藏的跨站请求伪造(cross-site request forgery,CSRF)token输入域。

        通过重载WebSecurityConfigureAdapter中的configure()方法配置Web安全性,这个过程中会使用传递进来的参数设置行为。

方法描述
configure(WebSecurity)通过重载,配置Spring Security的Filter链
configure(HttpSecurity)通过重载,配置如何拦截保护请求
configure(AuthenticationManagerBuilder)通过重载,配置user-detail服务

    

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

        配置用户存储:configure(AuthenticationManagerBuilder)

        AuthenticationManagerBuilder有多个方法可以用来配置SpringSecurity对认证的支持。通过InMemoryAuthentication()方法,可以启用、配置并任意填充基于内存的用户数存储。

方法描述
accountExpired(boolean)定义账号是否已经过期
accountLocked(boolean)定义账号是否已经锁定
and()用来连接配置
authorities(GrantedAthority...)授予某个用户一项或多项权限
authorities(List<? extends GrantedAuthority>)授予某个用户一项或多项权限
authorities(String...)授予某个用户一项或多项权限
credentialsExpired(boolean)定义凭证是否已经过期
disable(boolean)定义账号是否已经被禁用
password(String)定义用户的密码
roles(String...)授予某个用户一项或多项角色
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER", "ADMIN");
    }
}

    基于数据库表进行认证:jdbcAuthentication();

        使用转码后的密码:passwordEncoder(),该方法可以接受Spring Security中passwordEncoder接口的任意实现。包括了:BCryptPasswordEncoder、NoOpPasswordEncoder和standardPasswordEncoder。

@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 StandardPasswordEncoder("53cr3t"));
    }

    基于LDAP进行认证:ldapAuthentication();

        ldapAuthentication()功能与jdbcAuthentication()类似。ldapAuthentication()可以配置密码比对,密码可指定转码器,可饮用远程的LDAP服务器。

        配置自定义的用户服务:

            提供一个UserDetailsService接口,实现loadUserByUsername方法。

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new xxx);
    }


    3.拦截请求

        对每个请求进行细粒度安全控制:configure(HttpSecurity);

        定义如何保护路径的配置方法:

方法作用
access(String)如果给定的SqEL表达式计算结果为true,就允许访问
anonymous()允许匿名用户访问
authenticated()允许认证过的用户访问
denyAll()无条件拒绝所有访问
fullyAuthenticated()如果用户是完整认证(不是通过Remember-me功能认证的),就允许访问
hasAnyAuthority(String ...)如果用户具备给定权限中的某一个,就允许访问
hasAnyRole(String)如果用户具备给定角色中的某一个,就允许访问
hasAuthority(String)如果用户具备给定权限,就允许访问
hasIpAddress(String)如果请求来自给定IP地址,就允许访问
hasRole(String)如果用户具备给定角色的话,就允许访问
not()对其他访问方法的结果求反
permitAll()无条件允许访问
rememberMe()如果用户是通过Remember-me功能认证的就允许访问

         使用Spring表达式进行安全保护: 

.antMatchers("/spitter/me")
    .access("hasRole('ROLE_SPITTER') and hasIpAddress('192.168.1.3')")
安全表达式计算结果
authentication用户认证的对象
denyAll结果始终为false
hasAnyRole(lis of roles)如果用户被授予了列表中任意的指定角色,结果为true
hasRole(role)如果用户被授予了指定的角色,结果为true
hasIpAddress(IP Address)如果请求来自指定IP,结果为true
isAnonymous()如果当前用户为匿名用户,结果为true
isAuthenticated()如果当前用户进行了认证的话,结果为true
isFullyAuthenticated()如果当前用户进行了完整认证的话(不是通过Remember-me功能进行认证),结果为true
isRememberMe()如果当前用户是通过Remember-me自动认证的,结果为true
permitAll结果始终为true
principal用户的principal对象

        强制通道:HTTPS:requiresChannel()

                        HTTP:requiresInsecure()

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

    

    4.用户认证

        启用Remember-me功能:(token中的用户名、密码、过期时间和私钥在写入cookie前都进行了MD5hash)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/login")
        .and()
        .rememberMe()
            .tokenValiditySeconds(2419200)        --指定这个token最多四周内有效
            .key("spitterKey")                    --私钥名为spitterKey
    ...
}

        用户登录表单: 

<input id="remember_me" name="remember-me" type="checkbox" />
<label for="remember_me" class="inline">Remember me</label>