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

     

    六、视图保护

  • 相关阅读:
    25 Groovy 相关资料
    24 使用Maven 或 Gradle构建groovy
    UVA
    UVA
    UVA
    UVA
    UVA
    【JZOJ4235】序列【数论,数学】
    【JZOJ4235】序列【数论,数学】
    【JZOJ5230】队伍统计【状压DP】
  • 原文地址:https://www.cnblogs.com/lyj-gyq/p/9167672.html
Copyright © 2011-2022 走看看