Java进阶之路Spring

Spring Security_1

2019-04-25  本文已影响1人  elijah777

9.1 Spring Security简介

Spring Security

最初,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防护功能通常来讲并不是一个好主意

上一篇下一篇

猜你喜欢

热点阅读