笔记43 Spring Security简介
基于Spittr应用
一、Spring Security简介
Spring Security是为基于Spring的应用程序提供声明式安全保护的安全 性框架。Spring Security提供了完整的安全性解决方案,它能够在Web 请求级别和方法调用级别处理身份认证和授权。因为基于Spring框 架,所以Spring Security充分利用了依赖注入(dependency injection, DI)和面向切面的技术。
不管你想使用Spring Security保护哪种类型的应用程序,第一件需要做 的事就是将Spring Security模块添加到应用程序的类路径下,一共有11个模块,应用程序的类路径下至少要包含Core和Configuration这两个模块。
二、过滤Web请求
Spring Security借助一系列Servlet Filter来提供各种安全性功能。DelegatingFilterProxy是一个特殊的Servlet Filter,它本身所做 的工作并不多。只是将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring应用的上下文中, 如下图所示:
可以在web.xml进行配置,也可用Java配置。
三、编写简单的安全性配置
1.启用Web安全性功能的最简单配置
SecurityWebInitializer.java
1 package myspittr.config; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 5 import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; 6 7 @Configuration 8 @EnableWebSecurity // 启用SpringMVC安全性 9 public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer { 10 11 }
@EnableWebSecurity注解将会启用Web安全功能。Spring Security必须配置在一个实现了 WebSecurityConfigurer的bean中,或者(简单起见)扩展WebSecurityConfigurerAdapter。在Spring应用上下文中, 任何实现了WebSecurityConfigurer的bean都可以用来配置Spring Security。但如果想指定Web安全的细节,这要通过重载WebSecurityConfigurerAdapter中的一个或多个方法来实现。我们可以通过重载WebSecurityConfigurerAdapter的三 个configure()方法来配置Web安全性,这个过程中会使用传递进来的参数设置行为。
SecuritfConfig.java
1 package myspittr.config; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 8 @Configuration 9 @EnableWebSecurity 10 public class SecuritfConfig extends WebSecurityConfigurerAdapter { 11 12 @Override 13 protected void configure(HttpSecurity http) throws Exception { 14 15 http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); 16 17 } 18 }
这个简单的默认配置指定了该如何保护HTTP请求,以及客户端认证 用户的方案。通过调用authorizeRequests()和anyRequest().authenticated()就会要求所有进入应用的 HTTP请求都要进行认证。它也配置Spring Security支持基于表单的登录以及HTTP Basic方式的认证。同时,因为我们没有重 载configure(AuthenticationManagerBuilder)方法,所以 没有用户存储支撑认证过程。没有用户存储,实际上就等于没有用户。所以,在这里所有的请求都需要认证,但是没有人能够登录成功。
为了让Spring Security满足应用的需求,还需要再添加一点配置。具体来讲,我们需要:
-
-
- 配置用户存储;
- 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限;
- 提供一个自定义的登录页面,替代原来简单的默认登录页。
-
除了Spring Security的这些功能,我们可能还希望基于安全限制,有选择性地在Web视图上显示特定的内容。接下来首先介绍用户认证,即配置用户存储。
四、用户认证
1.使用基于内存的用户存储进行登录认证
因为安全配置类扩展了 WebSecurityConfigurerAdapter,因此配置用户存储的最简单 方式就是重载configure()方法,并以AuthenticationManagerBuilder作为传入参数。AuthenticationManagerBuilder有多个方法可以用来配置 Spring Security对认证的支持。通过inMemoryAuthentication() 方法,可以启用、配置并任意填充基于内存的用户存储。
SecuritfConfig.java
1 package myspittr.config; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 5 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 7 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 9 @Configuration 10 @EnableWebSecurity 11 public class SecuritfConfig extends WebSecurityConfigurerAdapter { 12 13 DataConfig dataConfig = new DataConfig(); 14 15 @Override 16 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 17 // // TODO Auto-generated method stub 18 // 启用内存用户存储 19 auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and().withUser("admin") 20 .password("password").roles("USER", "ADMIN"); 21 22 } 23 24 @Override 25 protected void configure(HttpSecurity http) throws Exception { 26 27 http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); 28 29 } 30 }
configure()方法中的 AuthenticationManagerBuilder使用构造者风格的接口来构建认证配置。通过简单地调用inMemoryAuthentication()就能启用内存用户存储。调用withUser()方法为内存用户存储添加新的用户,这个方法的参数是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder, 这个对象提供了多个进一步配置用户的方法,包括设置用户密码的 password()方法以及为给定用户授予一个或多个角色权限的 roles()方法。
上述程序中,添加了两个用户,“user”和“admin”,密码均为“password”。“user”用户具有USER角色,而“admin”用户具有 ADMIN和USER两个角色。我们可以看到,and()方法能够将多个用户的配置连接起来除了password()、roles()和and()方法以外,还有其他的几个方法可以用来配置内存用户存储中的用户信息。下表描述了 UserDetailsManagerConfigurer.UserDetailsBuilder对象所有可用的方法。
方法 | 描述 |
accountExpired(boolean) | 定义账号是否已经过期 |
accountLocked(boolean) | 定义账号是否已经锁定 |
and() | 用来连接配置 |
authorities(List<? extends GrantedAuthority>) | 授予某个用户一项或多项权限 |
authorities(GrantedAuthority) | 授予某个用户一项或多项权限 |
authorities(String) | 授予某个用户一项或多项权限 |
credentialsExpired(boolean) | 定义凭证是否已经过期 |
disabled(boolean) | 定义账号是否已被禁用 |
password(String) | 定义用户的密码 |
roles(String) | 授予某个用户一项或多项角色 |
当输入用户名和密码后才能进入应用首页。
2.基于数据库表进行认证
用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为 了配置Spring Security使用以JDBC为支撑的用户存储,我们可以使用jdbcAuthentication()方法,所需的最少配置如下所示:
1 package myspittr.config; 2 3 import javax.sql.DataSource; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 12 @Configuration 13 @EnableWebSecurity 14 public class SecuritfConfig extends WebSecurityConfigurerAdapter { 15 16 @Autowired 17 DataSource dataSource; 18 19 @Override 20 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 21 // TODO Auto-generated method stub 22 String query = "select username,password,enabled" + " from slogin where username=?"; 23 String query2 = "select username,authority from slogin where username=?"; 24 auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(query).authoritiesByUsernameQuery(query2); 25 26 } 27 28 @Override 29 protected void configure(HttpSecurity http) throws Exception { 30 http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); 31 32 } 33 }
我们必须要配置的只是一个DataSource,这样的话,就能访问关系型数据库了。在这里,DataSource是通过自动装配的技巧得到的。还需要重新设计数据库中的用户表,表名为slogin,具体结构如下所示:
*使用转码后的密码
数据库中的密码通常情况下都进行了转码,所以在用户认证的过程中,即登录过程中需要添加的一个密码转换器。
1 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 2 String query = "select username,password,enabled" + " from slogin where username=?"; 3 String query2 = "select username,authority from slogin where username=?"; 4 auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(query).authoritiesByUsernameQuery(query2) 5 .passwordEncoder(new StandardPasswordEncoder("li")); 6 7 }
密码加密:
StandardPasswordEncoder类,是PasswordEncoder接口的(唯一)一个实现类,是本文所述加密方法的核心。它采用SHA-256算法,迭代1024次,使用一个密钥(site-wide secret)以及8位随机盐对原密码进行加密。 随机盐确保相同的密码使用多次时,产生的哈希都不同; 密钥应该与密码区别开来存放,加密时使用一个密钥即可;对hash算法迭代执行1024次增强了安全性,使暴力破解变得更困难些。 盐值不需要用户提供,每次随机生成,加密后得到的密码是80位。
1 String string = "li"; 2 StandardPasswordEncoder encoder = new StandardPasswordEncoder(string); 3 System.out.println(encoder.encode("password"));
Spring Security的加密模块包括了三个这样的实现:BCryptPasswordEncoder、NoOpPasswordEncoder和 StandardPasswordEncoder。
密码“password”加密后的结果:
1 a0620349d440af0a43bf497f501efa4395ea82f9ff4255718a5d58b1bcdf3643615d46c9e9d0b49c
将此结果存入数据库当中,如下图所示:
然后运行项目,可以正确登录!
3.配置自定义的用户服务
假设我们需要认证的用户存储在非关系型数据库中,如Mongo或 Neo4j,在这种情况下,我们需要提供一个自定义的 UserDetailsService接口实现。
UserDetailsService接口非常简单:
我们所需要做的就是实现loadUserByUsername()方法,根据给定 的用户名来查找用户。loadUserByUsername()方法会返回代表给定用户的UserDetails对象。如下的程序清单展现了一 个UserDetailsService的实现,它会从给定的 SpitterRepository实现中查找用户。
UserDetails.java
1 package myspittr.config; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 import org.springframework.security.core.GrantedAuthority; 7 import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 import org.springframework.security.core.userdetails.User; 9 import org.springframework.security.core.userdetails.UserDetailsService; 10 import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 12 import myspittr.data.SpitterRepositorys; 13 import myspittr.spitter.Spitter; 14 15 public class UserDetails implements UserDetailsService { 16 private final SpitterRepositorys spitterRepositorys; 17 18 public UserDetails(SpitterRepositorys spitterRepositorys) { 19 this.spitterRepositorys = spitterRepositorys; 20 } 21 22 public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String username) 23 throws UsernameNotFoundException { 24 // TODO Auto-generated method stub 25 Spitter spitter = spitterRepositorys.findByUsername(username); 26 if (spitter != null) { 27 List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); 28 authorities.add(new SimpleGrantedAuthority("USER")); 29 return new User(spitter.getUsername(), spitter.getPassword(), authorities); 30 } 31 throw new UsernameNotFoundException("User '" + username + "' not found"); 32 } 33 34 }
UserDetails并不知道用户数据存储在什么地方。设置进来的SpitterRepository能够从关系型数据 库、文档数据库或图数据中查找Spitter对象,甚至可以伪造一 个。SpitterUserService不知道也不会关心底层所使用的数据存 储。它只是获得Spitter对象,并使用它来创建User对象。(User 是UserDetails的具体实现。) 为了使用SpitterUserService来认证用户,可以通过 userDetailsService()方法将其设置到安全配置中:
1 @Autowired 2 SpitterRepositorys spitterRepositorys; 3 @Override 4 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 5 auth.userDetailsService(new UserDetails(spitterRepositorys)); 6 }
userDetailsService()方法(类似于 jdbcAuthentication()、ldapAuthentication以及 inMemoryAuthentication())会配置一个用户存储。不过,这 里所使用的不是Spring所提供的用户存储,而是使用UserDetailsService的实现。
使用原来spitter表中注册的用户进行登录,用户名:w123123 密码:123123
spitter表: slogin表:
结果:
五、拦截请求
1.web应用路径保护
在任何应用中,并不是所有的请求都需要同等程度地保护。有些请求 需要认证,而另一些可能并不需要。有些请求可能只有具备特定权限的用户才能访问,没有这些权限的用户会无法访问。
例如,考虑Spittr应用的请求。首页当然是公开的,不需要进行保护。类似地,因为所有的Spittle都是公开的,所以展现Spittle 的页面不需要安全性。但是,创建Spittle的请求只有认证用户才能执行。同样,如果处理“/spitters/me”请求,并展现当前用户的基本信息时, 那么就需要进行认证,从而确定要展现谁的信息。
对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。如下的代码片段展现了重载的configure(HttpSecurity)方法,它为不同的URL路径有选择地应用安全性:
1 @Override 2 protected void configure(HttpSecurity http) throws Exception { 3 4 http.authorizeRequests().antMatchers("/spitter/me/**").authenticated().antMatchers(HttpMethod.POST, "/spittles") 5 .authenticated().anyRequest().permitAll().and().formLogin().and().httpBasic(); 6 }
configure()方法中得到的HttpSecurity对象可以在多个方面配 置HTTP的安全性。在这里,我们首先调用authorizeRequests(),然后调用该方法所返回的对象的方法来配置请求级别的安全性细节。其中,第一次调用antMatchers() 指定了对“/spitters/me/**”路径的请求需要进行认证。第二次调用antMatchers()更为具体,说明对“/spittles”路径的HTTP POST请求必须要经过认证。最后对anyRequests()的调用中,说明其他所有的请求都是允许的,不需要认证和任何的权限。
antMatchers()方法中设定的路径支持Ant风格的通配符。
未启用路径保护前可以对http://localhost:8080/com.li.Spittr/spitter/me/lyj123123直接进行访问,并显示用户个人信息,但是当启用了路径保护后,再次访问时就会跳转到默认的登录页面,如下所示:
我们所配置的安全性能够不仅仅限于认证 用户。例如,我们可以修改之前的configure()方法,要求用户不 仅需要认证,还要具备USER权限:
1 @Override 2 protected void configure(HttpSecurity http) throws Exception { 3 4 http.authorizeRequests().antMatchers("/spitter/me/**").hasAuthority("USER") 5 .antMatchers(HttpMethod.POST, "/spittles").authenticated().anyRequest().permitAll().and().formLogin() 6 .and().httpBasic(); 7 }
修改UserDetails中的loadUserByUsername方法,当用户名是lyj123123为其添加用户权限USER,其余添加USER_N,具体代码如下所示:
1 public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String username) 2 throws UsernameNotFoundException { 3 // TODO Auto-generated method stub 4 Spitter spitter = spitterRepositorys.findByUsername(username); 5 if (spitter != null) { 6 7 List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); 8 if (spitter.getUsername().equals("lyj123123")) { 9 authorities.add(new SimpleGrantedAuthority("USER")); 10 } else { 11 authorities.add(new SimpleGrantedAuthority("USER_N")); 12 } 13 return new User(spitter.getUsername(), spitter.getPassword(), authorities); 14 } 15 throw new UsernameNotFoundException("User '" + username + "' not found"); 16 }
所以当使用其他用户名进行登录时会报错因为缺少权限(用户名:zzc123123 密码:123123),如下所示:
我们可以将任意数量的antMatchers()、regexMatchers()和 anyRequest()连接起来,以满足Web应用安全规则的需要。但是, 我们需要知道,这些规则会按照给定的顺序发挥作用。所以,很重要 的一点就是将最为具体的请求路径放在前面,而最不具体的路径(如 anyRequest())放在最后面。如果不这样做的话,那不具体的路 径配置将会覆盖掉更为具体的路径配置。
2.强制通道的安全性
使用HTTP提交数据是一件具有风险的事情。如果使用HTTP发送无关 紧要的信息,这可能不是什么大问题。但是如果你通过HTTP发送诸 如密码和信用卡号这样的敏感信息的话,那你就是在找麻烦了。通过 HTTP发送的数据没有经过加密,黑客就有机会拦截请求并且能够看 到他们想看的数据。这就是为什么敏感信息要通过HTTPS来加密发送 的原因。通过在URL中的HTTP后添加“s”我们就能很容易地实现页面的安全性,但是忘记添加“s”同样也是很容易出现的。
作为示例,可以参考Spittr应用的注册表单。尽管Spittr应用不需要信用卡号、社会保障号或其他特别敏感的信息,但用户有可能仍然希望信息是私密的。为了保证注册表单的数据通过HTTPS传送,我们可以在配置中添加requiresChannel()方法,如下所示:
1 @Override 2 protected void configure(HttpSecurity http) throws Exception { 3 4 http.authorizeRequests().antMatchers("/spitter/me/**").hasAuthority("USER") 5 .antMatchers(HttpMethod.POST, "/spittles").authenticated().anyRequest().permitAll().and() 6 .requiresChannel().antMatchers("/spitter/register").requiresSecure().and().formLogin().and() 7 .httpBasic(); 8 }
启用前后的对比
如何启用tomcat的https协议请参考:tomcat启用https协议
3.防止跨站请求伪造
我们可以回忆一下,当一个POST请求提交到“/spittles”上 时,SpittleController将会为用户创建一个新的Spittle对象。但是,如果这个POST请求来源于其他站点的话,会怎么样呢?如果在其他站点提交如下表单,这个POST请求会造成什么样的结果呢?
1 <sf:form method="POST" action="http://localhost:8080/com.li.Spittr/spittles" enctype="multipart/form-data" modelAttribute="pubSpittle"> 2 <sf:input type="hidden" path="title" value="I'm stupid !"/> 3 <sf:input type="hidden" path="message" value="I'm stupid !"/> 4 <sf:input type="hidden" path="username" value="zzc123123"/> 5 <sf:input type="hidden" path="spittlePictureString" value="zzc123123"/> 6 <input type="submit" value="发布" /> 7 </sf:form>
假设你禁不住获得和美女聊天的诱惑,点击了按钮——那么你将会提交表单到如下地址http://localhost:8080/com.li.Spittr/spittles。如果你已经登录到了 spittr,那么这就会广播一条消息,让每个人都知道你做了一件蠢事。这是跨站请求伪造(cross-site request forgery,CSRF)的一个简单样例。从Spring Security 3.2开始,默认就会启用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”隐藏域:
如果使用JSP作为页面模板的话,如下代码所示:
更好的功能是,如果使用Spring的表单绑定标签的话,<sf:form>标 签会自动为我们添加隐藏的CSRF token标签。
处理CSRF的另外一种方式就是根本不去处理它。我们可以在配置中 通过调用csrf().disable()禁用Spring Security的CSRF防护功能, 如下所示:
演示:
因为默认CSRF是启用的,所以跨站访问的时候会报错。
当关闭CSRF防护后,再次点击发布时就会成功发送消息,如下图:
六、视图保护