- Spring Security介绍
- 使用Servlet规范中的Filter保护Web应用
9.1 Spring Security简介
Spring Security
基于Spring的应用程序提供声明式安全保护的安全性框架
提供了完整的安全性解决方案,它能够在Web请求级别和方法调用级别处理身份认证和授权。
充分利用了依赖注入(dependency injection,DI)和面向切面的技术
最初,Spring Security被称为Acegi Security。Acegi是一个强大的安全框架,但是它存在一个严 重的问题:那就是需要大量的XML配置。
9.1.1 Spring Security的模块
表9.1 Spring Security被分成了11个模块
模 块 | 描 述 |
---|---|
ACL | 支持通过访问控制列表(access control list,ACL)为域对象提供安全性 |
切面(Aspects) | 一个很小的模块,当使用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP |
CAS客户端 | (CASClient) 提供与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安全性支持 |
应用程序的类路径下至少要包含Core和Configuration这两个模块。Spring Security经常被用于保 护Web应用
9.1.2 过滤Web请求
Spring Security借助一系列Servlet Filter来提供各种安全性功能。只需配置一个Filter就可以了
DelegatingFilterProxy是一个特殊的Servlet Filter,它本身所做的工作并不多。只是将 工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在 Spring应用的上下文中,如
<!--spring security配置-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
</filter>
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class SpittrWebAppInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
}
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 编写简单的安全性配置
启用Web安全性功能的最简单配置
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
@EnableWebMvcSecurity注解还配置了一个Spring MVC参数解析解析器(argument resolver),这样的话处理器方法就能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。
它同时还配置了一个bean,在使用Spring表单绑定标签库来定义表单时,这个bean会自动添加一个隐藏的跨站请求伪造(cross-site request forgery,CSRF)token输入域。
看起来似乎并没有做太多的事情,但程序清单9.1和9.2中的配置类会给应用产生很大的影响。 其中任何一种配置都会将应用严格锁定,导致没有人能够进入该系统了! 尽管不是严格要求的,但我们可能希望指定Web安全的细节,这要通过重 载WebSecurityConfigurerAdapter中的一个或多个方法来实现。我们可以通过重 载WebSecurityConfigurerAdapter的三个configure()方法来配置Web安全性,这个 过程中会使用传递进来的参数设置行为。表9.2描述了这三个方法。
表9.2 重载WebSecurityConfigurerAdapter的configure()方法
方 法 | 描 述 |
---|---|
configure(WebSecurity) | 通过重载,配置Spring Security的Filter链 |
configure(HttpSecurity) | 通过重载,配置如何通过拦截器保护请求 |
configure(AuthenticationManagerBuilder) | 通过重载,配置user-detail服务 |
默认的configure(HttpSecurity)实际上等同于如下所示:
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
通过调用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使用内存用户存储
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.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").and()
.withUser("admin").password("password").roles("USER","ADMIN");
}
}
我们可以看到,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").roles("ROLE_USER").and()
.withUser("admin").password("password").roles("ROLE_USER","ROLE_ADMIN");
表9.3 配置用户详细信息的方法
方 法 | 描 述 |
---|---|
accountExpired(boolean) | 定义账号是否已经过期 |
accountLocked(boolean) | 定义账号是否已经锁定 |
and() | 用来连接配置 |
authorities(GrantedAuthority...) | 授予某个用户一项或多项权限 |
authorities(List<? extends GrantedAuthority>) | 授予某个用户一项或多项权限 |
authorities(String...) | 授予某个用户一项或多项权限 |
credentialsExpired(boolean) | 定义凭证是否已经过期 |
disabled(boolean) | 定义账号是否已被禁用 |
password(String) | 定义用户的密码 |
roles(String...) | 授予某个用户一项或多项角色 |
对于调试和开发人员测试来讲,基于内存的用户存储是很有用的,但是对于生产级别的应用来讲,这就不是最理想的可选方案了。为了用于生产环境,通常最好将用户数据保存在某种类型的数据库之中。
9.2.2 基于数据库表进行认证
用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为了配置Spring Security使用以JDBC为支撑的用户存储,我们可以使用jdbcAuthentication()方法,所需的最少配置如下所示:
@Autowired
DataSource dataSource;
/**
* 使用jdbc进行访问
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 启用内存用户储存
auth.jdbcAuthentication()
.dataSource(dataSource);
}
我们必须要配置的只是一个DataSource,这样的话,就能访问关系型数据库了。在这里,DataSource是通过自动装配的技巧得到的。 重写默认的用户查询功能尽管默认的最少配置能够让一切运转起来,但是它对我们的数据库模式有一些要求。它预期存在某些存储用户数据的表。更具体来说,下面的代码片段来源于Spring Security内部,这块代码展现了当查找用户信息时所执行的SQL查询语句:
/**
* 在第一个查询中,我们获取了用户的用户名、密码以及是否启用的信息,这些信息会用来进行用户认证。
* 接下来的查询查找了用户所授予的权限,用来进行鉴权,
* 最后一个查询中,查找了用户作为群组的成员所授予的权限
*/
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username, password, enabled from users where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username, authority from authorities where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority " +
"from group g, group_members gm, group_authorities ga " +
"where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
当数据库与上面所述并不一致,那么你就会希望在查询上有更多的控制权。如果是这样的话,我们可以按照如下的方式配置自己的查询:
/**
* 使用jdbc进行访问,只重写了认证和基本权限的查询语句
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 启用内存用户储存
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, true from Spitter where username = ?")
.authoritiesByUsernameQuery("selcet username, `ROLE_USER` from Spitter where username= ?");
}
在本例中,我们只重写了认证和基本权限的查询语句,但是通过调用group�AuthoritiesByUsername()方法,我们也能够将群组权限重写为自定义的查询语句。
将默认的SQL查询替换为自定义的设计时,很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名、密码以及启用状态信息。权限查询会选取零行或多行包含该用户名及其权限信息的数据。
群组权限查询会选取零行或多行数据,每行数据中都会包含群组ID、群组名称以及权限。
使用转码后的密码看一下上面的认证查询,它会预期用户密码存储在了数据库之中。这里唯一的问题在于如果密码明文存储的话,会很容易受到黑客的窃取。但是,如果数据库中的密码进行了转码的话,那么认证就会失败,因为它与用户提交的明文密码并不匹配。
为了解决这个问题,我们需要借助passwordEncoder()方法指定一个密码转码器(encoder):
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 启用内存用户储存
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username, password, true from Spitter where username = ?")
.authoritiesByUsernameQuery("selcet username, 'ROLE_USER' from Spitter where username= ?")
.passwordEncoder(new StandardPasswordEncoder("33"));
}
passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意实现。 Spring Security的加密模块包括了三个这样的实现:BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。 上述的代码中使用了StandardPasswordEncoder,但是如果内置的实现无法满足需求时,你可以提供自定义的实现。PasswordEncoder接口非常简单:
public interface PasswordEncoder {
String encoude(CharSequence rawPassworc);
boolean matches(CharSequence rawPassword, String encodeedPassword);
}
不管你使用哪一个密码转码器,数据库中的密码是永远不会解码的。所采取的策略与之相反,用户在登录时输入的密码会按照相同的算法进行转码,然后再与数据库中已经转码过的密码进行对比。这个对比是在PasswordEncoder的matches()方法中进行的。
9.2.3 基于LDAP进行认证
为了让Spring Security使用基于LDAP的认证,我们可以使用ldapAuthentication()方法。 这个方法在功能上类似于jdbcAuthentication(),只不过是LDAP版本。如下的configure()方法展现了LDAP认证的简单配置:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 启用内存用户储存
auth.ldapAuthentication()
.userSearchFilter("(uid={0})")
.groupSearchFilter("(member={0})");
}
方法userSearchFilter()和groupSearchFilter()用来为基础LDAP查询提供过滤条件,它们分别用于搜索用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LDAP层级结构的根开始。但是我们可以通过指定查询基础来改变这个默认行为:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 启用内存用户储存
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("(member={0})");
}
userSearchBase()属性为查找用户提供了基础查询。同样,groupSearchBase()为查找组指定了基础查询。我们声明用户应该在名为people的组织单元下搜索而不是从根开始。而组应该在名为groups的组织单元下搜索。
配置密码比对基于LDAP进行认证的默认策略是进行绑定操作,直接通过LDAP服务器认证用户。另一种可选的方式是进行比对操作。这涉及将输入的密码发送到LDAP目录上,并要求服务器将这个密码和用户的密码进行比对。因为比对是在LDAP服务器内完成的,实际的密码能保持私密。 如果你希望通过密码比对进行认证,可以通过声明passwordCompare()方法来实现
默认情况下,在登录表单中提供的密码将会与用户的LDAP条目中的userPassword属性进行比对。如果密码被保存在不同的属性中,可以通过passwordAttribute()方法来声明密码属性的名称:
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("(member={0})")
.passwordCompare()
.passwordEncoder(new MD5PasswordEncode())
.passwordAttribute("passcode");
在本例中,我们指定了要与给定密码进行比对的是“passcode”属性。另外,我们还可以指定密码转码器。在进行服务器端密码比对时,有一点非常好,那就是实际的密码在服务器端是私密的。但是进行尝试的密码还是需要通过线路传输到LDAP服务器上,这可能会被黑客所拦截。为了避免这一点,我们可以通过调用passwordEncoder()方法指定加密策略。 在本示例中,密码会进行MD5加密。这需要LDAP服务器上密码也使用MD5进行加密。 引用远程的LDAP服务器到目前为止,我们忽略的一件事就是LDAP和实际的数据在哪里。我们很开心地配置Spring使用LDAP服务器进行认证,但是服务器在哪里呢?默认情况下,Spring Security的LDAP认证假设LDAP服务器监听本机的33389端口。但是,如果你的LDAP服务器在另一台机器上,那么可以使用contextSource()方法来配置这个地址:
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("(member={0})")
.contextSource().root("dc=habuma,dc=com");
当LDAP服务器启动时,它会尝试在类路径下寻找LDIF文件来加载数据。LDIF(LDAP Data Interchange Format,LDAP数据交换格式)是以文本文件展现LDAP数据的标准方式。每条记录可以有一行或多行,每项包含一个名值对。记录之间通过空行进行分割。 如果你不想让Spring从整个根路径下搜索LDIF文件的话,那么可以通过调用ldif()方法来明确指定加载哪个LDIF文件:
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("(member={0})")
.contextSource().root("dc=habuma,dc=com").ldif("classpath:users.ldif")
https://www.jianshu.com/p/7e4d99f6baaf LDAP入门
9.2.4 配置自定义的用户服务
假设我们需要认证的用户存储在非关系型数据库中,如Mongo或Neo4j,在这种情况下,我们需要提供一个自定义的UserDetailsService接口实现。
UserDetailsService接口非常简单:
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public interface UserDetailService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
我们所需要做的就是实现loadUserByUsername()方法,根据给定的用户名来查找用户。loadUserByUsername()方法会返回代表给定用户的UserDetails对象。如下的程序清单展现了一个UserDetailsService的实现,它会从给定的SpitterRepository实现中查找用户。
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public interface UserDetailService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public class SpitterUserDetailService implements UserDetailService {
@Autowired
private SpittleRepository spittleRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Spitter spitter = spittleRepository.findByUsername(username);
if (spitter !=null) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_SPITER"));
return new User(spitter.getUsername(),
spitter.getPassword(),
authorities);
}
throw new UsernameNotFoundException("User '" + username + " 'not found.");
}
}
SpitterUserService有意思的地方在于它并不知道用户数据存储在什么地方。设置进来的SpitterRepository能够从关系型数据库、文档数据库或图数据中查找Spitter对象,甚至可以伪造一个。SpitterUserService不知道也不会关心底层所使用的数据存储。它只是获得Spitter对象,并使用它来创建User对象。(User是UserDetails的具体实现。) 为了使用SpitterUserService来认证用户,我们可以通过userDetailsService()方法将其设置到安全配置中:
9.3 拦截请求
一个特别简单的Spring Security配置,在这个默认的配置中,会要求所有请求都要经过认证。有些人可能会说,过多的安全性总比安全性太少要好。但也有一种说法就是要适量地应用安全性。
在任何应用中,并不是所有的请求都需要同等程度地保护。有些请求需要认证,而另一些可能并不需要。有些请求可能只有具备特定权限的用户才能访问,没有这些权限的用户会无法访问。
对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。如下的代码片段展现了重载的configure(HttpSecurity)方法,它为不同的URL路径有选择地应用安全性:
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/spitters/me")
.authenticated()
.antMatchers(HttpMethod.POST, "/spittles")
.authenticated()
.anyRequest()
.permitAll();
}
configure()方法中得到的HttpSecurity对象可以在多个方面配置HTTP的安全性。在这里,我们首先调用authorizeRequests(),然后调用该方法所返回的对象的方法来配置请求级别的安全性细节。
其中,第一次调用antMatchers()指定了对“/spitters/me”路径的请求需要进行认证。
第二次调用antMatchers()更为具体,说明对“/spittles”路径的HTTP POST请求必须要经过认证。
最后对anyRequests()的调用中,说明其他所有的请求都是允许的,不需要认证和任何的权限。 antMatchers()方法中设定的路径支持Ant风格的通配符。在这里我们并没有这样使用,但是也可以使用通配符来指定路径,如下所示:
.antMatchers("/spitter/**").authenticated();
我们也可以在一个对antMatchers()方法的调用中指定多个路径:
.antMatchers("/spitter/**","/spitters/mine").authenticated();</pre>
除了路径选择,我们还通过authenticated()和permitAll()来定义该如何保护路径。authenticated()要求在执行该请求时,必须已经登录了应用。如果用户没有认证的话,Spring Security的Filter将会捕获该请求,并将用户重定向到应用的登录页面。同时,permitAll()方法允许请求没有任何的安全限制。
除了authenticated()和permitAll()以外,还有其他的一些方法能够用来定义该如何保护请求。
用来定义如何保护路径的配置方法
方 法 | 能够做什么 |
---|---|
access(String) | 如果给定的SpEL表达式计算结果为true,就允许访问 |
anonymous() | 允许匿名用户访问 |
authenticated() | 允许认证过的用户访问 |
denyAll() | 无条件拒绝所有访问 |
fullyAuthenticated() | 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问 |
hasAnyAuthority(String...) | 如果用户具备给定权限中的某一个的话,就允许访问如果用户具备给定角色中的某一个的话,就允许访问 |
。。 | 。。。 |
我们可以将任意数量的antMatchers()、regexMatchers()和anyRequest()连接起来,以满足Web应用安全规则的需要。但是,我们需要知道,这些规则会按照给定的顺序发挥作用。所以,很重要的一点就是将最为具体的请求路径放在前面,而最不具体的路径(如anyRequest())放在最后面。如果不这样做的话,那不具体的路径配置将会覆盖掉更为具体的路径配置。
9.3.1 使用Spring表达式进行安全保护
使用hasRole()限制某个特定的角色, 但是我们不能在相同的路径上同时通过hasIpAddress()限制特定的IP地址。
望限制某个角色只能在星期二进行访问
SpEL更强大的原因在于,hasRole()仅是Spring支持的安全相关表达式中的一种
在掌握了Spring Security的SpEL表达式后,我们就能够不再局限于基于用户的权限进行访问限制了。例如,如果你想限制“/spitter/me” URL的访问,不仅需要ROLE_SPITTER,还需要来自指定的IP地址,那么我们可以按照如下的方式调用access()方法
httpSecurity.antMatcher("/s/me").access("hasRole('ROLE_SPITTER') and hadIpAddress('192.168.1.2')");
可以使用SpEL实现各种各样的安全性限制
9.3.2 强制通道的安全性
使用HTTP提交数据是一件具有风险的事情。如果使用HTTP发送无关紧要的信息,这可能不是什么大问题。但是如果你通过HTTP发送诸如密码和信用卡号这样的敏感信息的话,那你就是在找麻烦了。通过HTTP发送的数据没有经过加密,黑客就有机会拦截请求并且能够看到他们想看的数据。这就是为什么敏感信息要通过HTTPS来加密发送的原因。
使用HTTPS似乎很简单。你要做的事情只是在URL中的HTTP后加上一个字母“s”就可以了。是这样吗? 这是真的,但这是把使用HTTPS通道的责任放在了错误的地方。通过添加“s”我们就能很容易地实现页面的安全性,但是忘记添加“s”同样也是很容易出现的。如果我们的应用中有多个链接需要HTTPS,估计在其中的一两个上忘记添加“s”的概率还是很高的。
另一方面,你可能还会在原本并不需要HTTPS的地方,误用HTTPS。
传递到configure()方法中的HttpSecurity对象,除了具有authorizeRequests()方法以外,还有一个requiresChannel()方法,借助这个方法能够为各种URL模式声明所要求的通道。
作为示例,可以参考Spittr应用的注册表单。尽管Spittr应用不需要信用卡号、社会保障号或其他特别敏感的信息,但用户有可能仍然希望信息是私密的。为了保证注册表单的数据通过HTTPS传送,我们可以在配置中添加requiresChannel()方法,如下所示:
程序清单9.5 requiresChannel()方法会为选定的URL强制使用HTTPS
传递到configure()方法中的HttpSecurity对象,除了具有authorizeRequests()方法以外,还有一个requiresChannel()方法,借助这个方法能够为各种URL模式声明所要求的通道。
作为示例,可以参考Spittr应用的注册表单。尽管Spittr应用不需要信用卡号、社会保障号或其他特别敏感的信息,但用户有可能仍然希望信息是私密的。为了保证注册表单的数据通过HTTPS传送,我们可以在配置中添加requiresChannel()方法,如下所示: 程序清单9.5 requiresChannel()方法会为选定的URL强制使用HTTPS
// 需要HTTPS
httpSecurity.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spitter/me").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/from")
.requiresSecure();</pre>
9.3.3 防止跨站请求伪造
当一个POST请求提交到“/spittles”上时,SpittleController将会为用户创建一个新的Spittle对象。但是,如果这个POST请求来源于其他站点的话,会怎么样呢?如果在其他站点提交如下表单,
<h1>跨域请求</h1>
<form method="post" action="http://www.spittr.com/spittle/register">
<input type="hidden" name="message" value=" I`m stupid!" />
<input type="submit" value="Click here to win a new car!" />
</form></pre>
这是跨站请求伪造(cross-site request forgery,CSRF)的一个简单样例。简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,这可能会带来消极的后果。尽管提交“I’m stupid!”这样的信息到微博站点算不上什么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="@{/spittle/register}*">
</form>
处理CSRF的另外一种方式就是根本不去处理它。我们可以在配置中通过调用csrf().disable()禁用Spring Security的CSRF防护功能,如 http.. .csrf( ).dsibale( ); 但禁用CSRF防护功能通常来讲并不是一个好主意