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保护方法调用——借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。
不管你想使用Spring Security保护哪种类型的应用程序,第一件需要做 的事就是将Spring Security模块添加到应用程序的类路径下。Spring Security 3.2分为11个模块,如表9.1所示。
应用程序的类路径下至少要包含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所示。
准备以下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()方法
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满足我们应用的需求,还需要再添加一点配置。 具体来讲,我们需要:
- 配置用户存储;
- 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限;
- 提供一个自定义的登录页面,替代原来简单的默认登录页。
但首先,我们看一下如何在认证的过程中配置访问用户数据的服务。
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描述了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 配置用户详细信息的方法
结果如下:
输入URL:http://localhost:8080/spittr/,会自动跳转到登录认证页面(默认支持HTTP Basic方式的登录认证):
输入admin,password之后点击登录,就可看到spittr的主页了
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');
@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描述了所有可用的方案。
我们可以将任意数量的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文件中。
生成RSA加密的证书
(2)修改tomcat的server.xml文件,详细请参考
.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:
<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/aaa@qq.com/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();
上一篇: DecisionTree --- 决策树
下一篇: 决策树算法推导&python应用