SpringInAction笔记(九)——保护Web应用(上)

       安全性是超越应用程序功能的一个关注点。应用系统的绝大部分内容都不应该参与 到与自己相关的安全性处理中。尽管我们可以直接在应用程序中编写 安全性功能相关的代码(这种情况并不少见),但更好的方式还是将安全性相关的关注点与应用程序本身的关注点进行分离。
       Spring Security是一种基于Spring AOP和Servlet规范中的Filter实现的安全框架。

9.1 Spring Security简介
        Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。Spring Security提供了完整的安全性解决方案,它能够在Web 请求级别和方法调用级别处理身份认证和授权。因为基于Spring框架,所以Spring Security充分利用了依赖注入(dependency injection, DI)和面向切面的技术。

        Spring Security的最新版本为3.2(事实上现在最新版本是5.0.3)。Spring Security从两个角度来解决安全性问题。 它使用Servlet规范中的Filter保护Web请求并限制URL级别的访问。 Spring Security还能够使用Spring AOP保护方法调用——借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。


9.1.1 理解Spring Security的模块
       不管你想使用Spring Security保护哪种类型的应用程序,第一件需要做 的事就是将Spring Security模块添加到应用程序的类路径下。Spring Security 3.2分为11个模块,如表9.1所示。
SpringInAction笔记(九)——保护Web应用 - 叶主任 - 叶主任的博客
      
        应用程序的类路径下至少要包含Core和Configuration这两个模块。 Spring Security经常被用于保护Web应用,这显然也是Spittr应用的场景,所以我们还需要添加Web模块。同时我们还会用到Spring Security 的JSP标签库,所以我们需要将这个模块也添加进来。

9.1.2 过滤Web请求

       Spring Security借助一系列Servlet Filter来提供各种安全性功能。这是否意味着我们需要在web.xml 或WebApplicationInitializer中配置多个Filter呢?实际上,借助于Spring的小技巧,我们只需配置一个Filter就可以了。
       DelegatingFilterProxy是一个特殊的Servlet Filter,它本身所做的工作并不多。只是将工作委托给一个javax.servlet.Filter实 现类,这个实现类作为一个<bean>注册在Spring应用的上下文中, 如图9.1所示。
SpringInAction笔记(九)——保护Web应用 - 叶主任 - 叶主任的博客
     图9.1 DelegatingFilterProxy把Filter的处理逻辑委托给Spring应用 上下文中所定义的一个代理Filter bean

准备以下jar包:
spring-security-web-4.2.3.RELEASE
spring-security-config-4.2.3.RELEASE
spring-security-core-4.2.3.RELEASE
(注意:由于本人使用JDK1.7,如果使用spring-security 5.0.x的话,spring-security 5.0.x是基于JDK1.8 ,会编译报错)

web.xml配置

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterproxy</filter-class>
</filter>

借助WebApplicationInitializer以Java的方式来配置Delegating-FilterProxy,那么所需要做的就是创建一个扩展的新类:
package spittr.config;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
      AbstractSecurityWebApplicationInitializer实现了 WebApplication-Initializer,因此Spring会发现它,并用它在Web容器中注册DelegatingFilterProxy。尽管可以重载它的appendFilters()或insertFilters()方法来注册自己选择的Filter,但是要注册DelegatingFilterProxy的话,我们并不需要重载任何方法。     
      不管是通过web.xml还是通过 AbstractSecurityWebApplicationInitializer的子类来配置DelegatingFilterProxy,它都会拦截发往应用中的请求,并将请求委托给ID为springSecurityFilterChain bean。       
      springSecurityFilterChain本身是另一个特殊的Filter,它也被称为FilterChainProxy。它可以链接任意一个或多个其他的 Filter。Spring Security依赖一系列Servlet Filter来提供不同的安全特性。但是,你几乎不需要知道这些细节,因为你不需要显式声明springSecurityFilterChain以及它所链接在一起的其他Filter。当我们启用Web安全性的时候,会自动创建这些Filter。

9.1.3 编写简单的安全性配置       
      Spring 3.2引入了新的Java配置方案,完全不再需要通过XML来配置安 全性功能了。如下的程序清单展现了Spring Security最简单的Java配置。 
程序清单9.1 启用Web安全性功能的最简单配置

package spittr.config;

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

@Configuration
@EnableWebSecurity //启用Spring MVC安全性,@EnableWebMvcSecurity已过时
public class SecurityConfig extends WebSecurityConfigurerAdapter {	
}
@EnableWebMvcSecurity已过时,@EnableWebSecurity已有这样的功能,使用@EnableWebSecurity即可。
       除了其他的内容以外,@EnableWebSecurity注解还配置了一 个Spring MVC参数解析解析器(argument resolver),这样的话处理器方法就能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。它同时还配置了一个bean, 在使用Spring表单绑定标签库来定义表单时,这个bean会自动添加一 个隐藏的跨站请求伪造(cross-site request forgery,CSRF)token输入域。
       程序清单9.1的配置类会给应用产生很大的影响。其中任何一种配置都会将应用严格锁定,导致没有人能够进入该系统了!
       我们可能希望指定Web安全的细节,这要通 过重载WebSecurityConfigurerAdapter中的一个或多个方法来 实现。我们可以通过重载WebSecurityConfigurerAdapter的三 个configure()方法来配置Web安全性,这个过程中会使用传递进 来的参数设置行为。表9.2描述了这三个方法。
       表9.2 重载WebSecurityConfigurerAdapter的configure()方法
SpringInAction笔记(九)——保护Web应用 - 叶主任 - 叶主任的博客

       重新看一下程序清单9.2,可以看到它没有重写上述三 个configure()方法中的任何一个,这就说明了为什么应用现在是 被锁定的。尽管对于我们的需求来讲默认的Filter链是不错的,但是默认的configure(HttpSecurity)实际上等同于如下所示:
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
                   .anyRequest().authenticated()
		   .and().formLogin()
		   .and().httpBasic();
	}
这个简单的默认配置指定了该如何保护HTTP请求,以及客户端认证用户的方案。通过调用 authorizeRequests()和 anyRequest().authenticated()就会要求所有进入应用的 HTTP请求都要进行认证。它也配置Spring Security支持基于表单的登 录以及HTTP Basic方式的认证。
       同时,因为我们没有重 载configure(AuthenticationManagerBuilder)方法,所以没有用户存储支撑认证过程。没有用户存储,实际上就等于没有用户。所以,在这里所有的请求都需要认证,但是没有人能够登录成功。
       为了让Spring Security满足我们应用的需求,还需要再添加一点配置。 具体来讲,我们需要:
  • 配置用户存储;
  • 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限;
  • 提供一个自定义的登录页面,替代原来简单的默认登录页。
       除了Spring Security的这些功能,我们可能还希望基于安全限制,有选择性地在Web视图上显示特定的内容。
       但首先,我们看一下如何在认证的过程中配置访问用户数据的服务。

9.2 选择查询用户详细信息的服务
       我们所需要的是用户存储,也就是用户名、密码以及其他信息存储的地方,在进行认证决策的时候,会对其进行检索。
       好消息是,Spring Security非常灵活,能够基于各种数据存储来认证用户。它内置了多种常见的用户存储场景,如内存、关系型数据库以及 LDAP。但我们也可以编写并插入自定义的用户存储实现。 借助Spring Security的Java配置,我们能够很容易地配置一个或多个数 据存储方案。那我们就从最简单的开始:在内存中维护用户存储。

9.2.1 使用基于内存的用户存储
       因为我们的安全配置类扩展了 WebSecurityConfigurerAdapter,因此配置用户存储的最简单方式就是重载configure()方法,并以AuthenticationManagerBuilder作为传入参数。AuthenticationManagerBuilder有多个方法可以用来配置 Spring Security对认证的支持。通过inMemoryAuthentication() 方法,我们可以启用、配置并任意填充基于内存的用户存储。
       例如,在如程序清单9.3中,SecurityConfig重载了 configure()方法,并使用两个用户来配置内存用户存储。
   程序清单9.3 配置Spring Security使用内存用户存储
package spittr.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity //启用Spring MVC安全性,@EnableWebMvcSecurity已过时
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(AuthenticationManagerBuilder auth)
			throws Exception {
		// TODO Auto-generated method stub
        auth.inMemoryAuthentication()
             .withUser("user").password("password").roles("USER").and()
             .withUser("admin").password("password").roles("USER", "ADMIN");
	}
}
注意:如果编译报错The method withUser(UserDetails) is ambiguous for the type InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder>,这个问题是由于jdk的版本导致的,因为本人使用JDK1.7,所以对应使用Spring Security4.X版本。
       configure()方法中的 AuthenticationManagerBuilder使用构造者风格的接口来构建认证配置。通过简单地调用
inMemoryAuthentication()就能启用内存用户存储。需要调用withUser()方法为内存用户存储添加新的用 户,这个方法的参数是username。withUser()方法返回的 是UserDetailsManagerConfigurer.UserDetailsBuilder, 这个对象提供了多个进一步配置用户的方法,包括设置用户密码的 password()方法以及为给定用户授予一个或多个角色权限的 roles()方法。
       在程序清单9.3中,我们添加了两个用户,“user”和“admin”,密码均 为“password”。“user”用户具有USER角色,而“admin”用户具有 ADMIN和USER两个角色。可以看到,and()方法能够将多个用户的配置连接起来。 除了password()、roles()和and()方法以外,还有其他的几个 方法可以用来配置内存用户存储中的用户信息。
       表9.3描述了UserDetailsManagerConfigurer.UserDetailsBuilder对 象所有可用的方法。 需要注意的是,roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会添加一个“ROLE_”前缀,并将其作为权限授予给用户。实际上,如下的用户配置与程序清单9.3是等价的:
        auth.inMemoryAuthentication()
             .withUser("user").password("password").authorities("ROLE_USER").and()
             .withUser("admin").password("password").authorities("ROLE_USER", "ROLE_ADMIN");
     表9.3 配置用户详细信息的方法
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客

结果如下:
输入URL:http://localhost:8080/spittr/,会自动跳转到登录认证页面(默认支持HTTP Basic方式的登录认证):
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
输入admin,password之后点击登录,就可看到spittr的主页了
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
 
9.2.2 基于数据库表进行认证
       用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为 了配置Spring Security使用以JDBC为支撑的用户存储,我们可以使 用jdbcAuthentication()方法,所需的最少配置如下所示:
package spittr.config;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity //启用Spring MVC安全性,@EnableWebMvcSecurity已过时
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private DataSource dataSource;
	
	@Autowired
	public SecurityConfig(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth)
			throws Exception {
		// TODO Auto-generated method stub
		auth.jdbcAuthentication()
		     .dataSource(dataSource);
	}
}
必须要配置的只是一个DataSource,这样的话,就能访问关系 型数据库了。在这里,DataSource是通过自动装配的技巧得到的。

重写默认的用户查询功能
      
       尽管默认的最少配置能够让一切运转起来,但是它对我们的数据库模 式有一些要求。它预期存在某些存储用户数据的表。更具体来说,下面的代码片段来源于Spring Security内部,这块代码展现了当查找用户信息时所执行的SQL查询语句:
public static final String DEP_USERS_BY_USERNAME_QUERY = 
       "select username,password,enabled " +
       "from users " +
       "where username = ?";
public static final String DEP_AUTHORITIES_BY_USERNAME_QUERY = 
       "select username,authority " +
       "from authorities " +
       "where username = ?";
public static final String DEP_GROUP_AUTHORITIES_BY_USERNAME_QUERY = 
       "select g.id, g.group_name, ga.authority " +
       "from groups g, group_members gm, group_authorities ga " +
       "where gm.username = ? " +
       "and g.id = ga.group_id " +
       "and g.id = gm.group_id";
 在第一个查询中,我们获取了用户的用户名、密码以及是否启用的信息,这些信息会用来进行用户认证。接下来的查询查找了用户所授予的权限,用来进行鉴权,最后一个查询中,查找了用户作为群组的成员所授予的权限。
       如果你能够在数据库中定义和填充满足这些查询的表,那么基本上就 不需要你再做什么额外的事情了。使用H2数据库,编写如下脚本:
清单 /spittr/src/spittr/data/h2_security.sql
DROP TABLE IF EXISTS users;
create table users (
	id identity,
	username varchar(50) unique not null,
	password varchar(20) not null,
	enabled tinyint(4) default null
);

DROP TABLE IF EXISTS authorities;
create table authorities (
	id identity,
	username varchar(50) unique not null,
	authority varchar(50) default null
);

DROP TABLE IF EXISTS groups;
create table groups (
	id int PRIMARY KEY,
	group_name varchar(50) not null,
);

DROP TABLE IF EXISTS group_members;
create table group_members (
	id identity,
	group_id int(11) not null,
	username varchar(50) not null
);

DROP TABLE IF EXISTS group_authorities;
create table group_authorities (
	id identity,
	group_id int(11) not null,
	authority varchar(50) default null
);

INSERT INTO users (username, password, enabled) VALUES ('username', '123456', 1);
INSERT INTO authorities (username, authority) VALUES ('username', 'ROLE_USER');
INSERT INTO groups (id, group_name) VALUES (1, 'group_name');
INSERT INTO group_members (group_id, username) VALUES (1, 'username');
INSERT INTO group_authorities (group_id, authority) VALUES (1, 'ROLE_USER');


运行效果如下:输入URL:http://localhost:8080/spittr/
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
 输入用户名username,密码123456,点击登录:
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
 
但是,也可能你的数据库与上面所述并不一致,那么你就会希望在查询上有更多的控制权。如果是这样的话,我们可以按照如下的方式配置自己的查询:
@Override
protected void configure(Authentication 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=?");
}
       在本例中,我们只重写了认证和基本权限的查询语句,但是通过调用group-AuthoritiesByUsername()方法,我们也能够将群组权限重写为自定义的查询语句。
       将默认的SQL查询替换为自定义的设计时,很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名、密码以及启用状态信息。权限查询会选取零行或多行包含该用户名及其权限信息的数据。群组权限查询会选取零行或多行数据,每行数据中都会包含群组ID、群组名称以及权限。

9.3 拦截请求
       在任何应用中,并不是所有的请求都需要同等程度地保护。有些请求需要认证,而另一些可能并不需要。有些请求可能只有具备特定权限的用户才能访问,没有这些权限的用户会无法访问。
       例如,考虑Spittr应用的请求。首页当然是公开的,不需要进行保护。类似地,因为所有的Spittle都是公开的,所以展现Spittle 的页面不需要安全性。但是,创建Spittle的请求只有认证用户才能执行。同样,尽管用户基本信息页面是公开的,不需要认证,但是,如果要处理“/spitters/me”请求,并展现当前用户的基本信息时, 那么就需要进行认证,从而确定要展现谁的信息。
       对每个请求进行细粒度安全性控制的关键在于重 载configure(HttpSecurity)方法。如下的代码片段展现了重载的configure(HttpSecurity)方法,它为不同的URL路径有选择地应用安全性:
@Override
protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        http.formLogin()          //基于表单的登 录认证
              .and().httpBasic()  //HTTP Basic方式的认证
              .and()
              .authorizeRequests()
              .antMatchers("/spitter/me").authenticated() 

                      //指定了对“/spitters/me”路径的请求需要进行认证
              .antMatchers(HttpMethod.POST, "/spittles").authenticated() 

                       //对“/spittles”路径的HTTP POST请 求必须要经过认证
              .anyRequest().permitAll()   //其他所 有的请求都是允许的, 不需认证
              .and().csrf().disable();    //关闭csrf token的认证 
}

   configure()方法中得到的HttpSecurity对象可以在多个方面配 置HTTP的安全性。在这里,我们首先调用authorizeRequests(),然后调用该方法所返回的对象的方法来配置请求级别的安全性细节。其中,第一次调用antMatchers() 指定了对“/spitters/me”路径的请求需要进行认证。第二次调 用antMatchers()更为具体,说明对“/spittles”路径的HTTP POST请求必须要经过认证。最后对anyRequests()的调用中,说明其他所有的请求都是允许的,不需要认证和任何的权限。

        注意:基于Java配置的Spring Security默认开启CSR保护。      

       antMatchers()方法中设定的路径支持Ant风格的通配符。在这里 我们并没有这样使用,但是也可以使用通配符来指定路径,如下所 示:

.antMatchers("/spittlers/**")
也可以在一个对antMatchers()方法的调用中指定多个路径:
.antMatchers("/spittlers/**", "/spittles/mine").authenticated()
antMatchers()方法所使用的路径可能会包括Ant风格的通配符, 而regexMatchers()方法则能够接受正则表达式来定义请求路径。例如,如下代码片段所使用的正则表达式与“/spitters/**”(Ant风格)功 能是相同的:
.regexMatchers("/spitters/.*").authenticated()
       除了路径选择,还通过authenticated()和permitAll()来定义该如何保护路径。authenticated()要求在执行该请求时,必须已经登录了应用。如果用户没有认证的话,Spring Security的Filter将会捕获该请求,并将用户重定向到应用的登录页面。同 时,permitAll()方法允许请求没有任何的安全限制。
       除了authenticated()和permitAll()以外,还有其他的一些方 法能够用来定义该如何保护请求。表9.4描述了所有可用的方案。
        表9.4 用来定义如何保护路径的配置方法
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客

       我们可以将任意数量的antMatchers()、regexMatchers()和 anyRequest()连接起来,以满足Web应用安全规则的需要。但是, 我们需要知道,这些规则会按照给定的顺序发挥作用。所以,很重要 的一点就是将最为具体的请求路径放在前面,而最不具体的路径(如 anyRequest())放在最后面。如果不这样做的话,那不具体的路径配置将会覆盖掉更为具体的路径配置。

9.3.2 强制通道的安全性
        通过 HTTP发送的数据没有经过加密,黑客就有机会拦截请求并且能够看到他们想看的数据。这就是为什么敏感信息要通过HTTPS来加密发送的原因。
        传递到configure()方法中的HttpSecurity对象,除了具有authorizeRequests()方法以外,还有一个requiresChannel()方法,借助这个方法能够为各种URL模式声明所要求的通道。
       作为示例,可以参考Spittr应用的注册表单。尽管Spittr应用不需要信 用卡号、社会保障号或其他特别敏感的信息,但用户有可能仍然希望 信息是私密的。为了保证注册表单的数据通过HTTPS传送,我们可以在配置中添加requiresChannel()方法,如下所示:
     程序清单9.5 requiresChannel()方法会为选定的URL强制使用 HTTPS
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// TODO Auto-generated method stub
		http.formLogin()          //基于表单的登 录认证
		      .and().httpBasic()  //HTTP Basic方式的认证
		      .and()
		      .authorizeRequests()
		      .antMatchers("/spitter/me").authenticated()  
		      .antMatchers(HttpMethod.POST, "/spittles").authenticated() 
		      .anyRequest().permitAll()  
		      .and()
		      .requiresChannel().antMatchers(HttpMethod.POST, "/spitter/register").requiresSecure()
		      .and().csrf().disable();    //关闭csrf token的认证
	}
     不论何时,只要是对“/spitter/register”的POST请求,Spring Security都视为需要安全通道(通过调用requiresChannel()确定的)并自动将请求重定向到HTTPS上。        
       如果使用的是Tomcat服务器,需要借助Java自带的keytool来生成证书,并配置到Tomcat的server.xml文件中。
     (1)输入命令:keytool -genkeypair -alias "tomcat" -keyalg "RSA" -keystore "f:\tomcat.keystore"
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
生成RSA加密的证书
     (2)修改tomcat的server.xml文件,详细请参考
  配置Tomcat使用https协议(配置SSL协议) tomcat7 配置 https安全访问运行结果:访问注册页面,点击提交:
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客点击“是”,跳转到https://localhost:8443/spittr/spitter/register页面,这时候可以正常提交数据了
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
SpringInAction笔记(九)——保护Web应用(上) - 叶主任 - 叶主任的博客
        与之相反,有些页面并不需要通过HTTPS传送。例如,首页不包含任何敏感信息,因此并不需要通过HTTPS传送。我们可以使 用requiresInsecure()代替requiresSecure()方法,将首页声明为始终通过HTTP传送:
.requiresChannel().antMatchers("/").requiresInsecure()


 9.3.3 防止跨站请求伪造

       当一个POST请求提交到/spittles上时,SpittleController将会为用户创建一个新的Spittle对象。但是,如果这个POST请求来源于其他站点的话,会怎么样呢?如果在其他站点提交如下表单,这个POST请求会造成什么样的结果呢?
<form input="POST" action="http://www.spittr.com/spittles">
    <input type="hidden" name="message" value="I'm stupid!" />
    <input type="submit" value="Click here to win a new car!" />
</form>
       假设你禁不住获得一辆新汽车的诱惑,点击了按钮——那么你将会提交表单到如下地址http://www.spittr.com/spittles。如果你已经登录到了spittr.com,那么这就会广播一条消息,让每个人都知道你做了一件蠢事。
       这是跨站请求伪造(cross-site request forgery,CSRF)的一个简单样例。简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,这可能会带来消极的后果。
       从Spring Security 3.2开始,默认就会启用CSRF防护。实际上,除非你采取行为处理CSRF防护或者将这个功能禁用,否则的话,在应用中提交表单时,可能会遇到问题。
       Spring Security通过一个同步token的方式来实现CSRF防护的功能。它将会拦截状态变化的请求(例如,非GET、HEAD、OPTIONS和TRACE的请求)并检查CSRF token。如果请求中不包含CSRF token的话,或者token不能与服务器端的token相匹配,请求将会失败,并抛出CsrfException异常。
       这意味着在你的应用中,所有的表单必须在一个_csrf域中提交token,而且这个token必须要与服务器端计算并存储的token一致,这样的话当表单提交的时候,才能进行匹配。
       好消息是,Spring Security已经简化了将token放到请求的属性中这一任务。如果你使用Thymeleaf作为页面模板的话,只要<form>标签的action属性添加了Thymeleaf命名空间前缀,那么就会自动生成一个_csrf隐藏域:
<form method="POST" th:action="@{/spittles}">
    ...
</form>
使用JSP如下:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
更好的功能是,如果使用Spring的表单绑定标签的话,<sf:form>标签会自动为我们添加隐藏的CSRF token标签。

清单  /spittr/WebRoot/WEB-INF/views/spittles.html:

扫描二维码关注公众号,回复: 1048096 查看本文章
<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="http://blog.163.com/yetong1985@126/blog/@{/resources/style.css}" ></link>
  </head>
  <body>
    <div id="header" th:include="page :: header"></div>
  
    <div id="content">
      <div class="spittleForm">
        <h1>Spit it out...</h1>
        <form method="POST" name="spittleForm">
          <input type="hidden" name="latitude" />
          <input type="hidden" name="longitude" />
          <textarea name="message" cols="80" rows="5"></textarea><br/>
          
          <input type="hidden" 
                 th:name="${_csrf.parameterName}"
                 th:value="${_csrf.token}" />
                 
          <input type="submit" value="Add" />
        </form>
      </div>
      <div class="listTitle">
        <h1>Recent Spittles</h1>
        <ul class="spittleList">
          <li th:each="spittle : ${spittleList}" 
              th:id="'spittle_' + ${spittle.id}">
            <div class="spittleMessage" th:text="${spittle.message}">Spittle message</div>
            <div>
              <span class="spittleTime" th:text="${spittle.time}">spittle timestamp</span>
              <span class="spittleLocation" th:text="'{' + ${spittle.latitude} + ', ' + ${spittle.longitude} + ')'">lat, long</span>
            </div>
          </li>
        </ul>
      </div>
    </div>
    
    <div id="footer" th:include="page :: copy"></div>
  </body>
</html>

SecurityConfig中取消对CSRF:

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// TODO Auto-generated method stub
		http.formLogin()          //基于表单的登 录认证
		      .and().httpBasic()  //HTTP Basic方式的认证
		      .and()
		      .authorizeRequests()
		      .antMatchers("/spitter/me").authenticated()  
		      .antMatchers(HttpMethod.POST, "/spittles").authenticated() 
		      .anyRequest().permitAll();   
	}
处理CSRF的另外一种方式就是根本不去处理它。我们可以在配置中通过调用csrf().disable()禁用Spring Security的CSRF防护功能,如下所示:
http.csrf().disable();

猜你喜欢

转载自blog.csdn.net/ganmaotong/article/details/80377243