笔记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防护后,再次点击发布时就会成功发送消息,如下图:

 

六、视图保护

 

posted @ 2018-06-11 16:24  雨落忧伤-  阅读(314)  评论(0编辑  收藏  举报