当前位置:   article > 正文

Spring Security 应用详解_spring-boot-starter-security

spring-boot-starter-security

一、 集成SpringBoot

1.1  Spring Boot 介绍

Spring Boot 是一套 Spring 的快速开发框架,基于 Spring 4.0 设计,使用 Spring Boot 开发可以避免一些繁琐的工程 搭建和配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 Spring Boot开发,例如: MyBatis Dubbo 等, Spring 家族更是如此,例如: Spring cloud Spring mvc 、 Spring security等,使用 Spring Boot 开发可以大大得高生产率,所以 Spring Boot 的使用率非常高。
本章节讲解如何通过 Spring Boot 开发 Spring Security 应用, Spring Boot 提供 spring-boot-starter-security 用于开发Spring Security 应用。

1.2 创建maven工程

1)创建maven工程 security-spring-boot,工程结构如下:

2)引入以下依赖:

  1. <?xml version="1.0" encoding="UTF‐8"?> 
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" 
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" 
  4.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven‐4.0.0.xsd"> 
  5.     <modelVersion>4.0.0</modelVersion> 
  6.  
  7.     <groupId>com.itheima.security</groupId> 
  8.     <artifactId>security‐springboot</artifactId> 
  9.     <version>1.0‐SNAPSHOT</version> 
  10.  
  11.     <parent> 
  12.         <groupId>org.springframework.boot</groupId> 
  13.         <artifactId>spring‐boot‐starter‐parent</artifactId> 
  14.         <version>2.1.3.RELEASE</version> 
  15.     </parent> 
  16.  
  17.     <properties> 
  18.         <project.build.sourceEncoding>UTF‐8</project.build.sourceEncoding>         <maven.compiler.source>1.8</maven.compiler.source> 
  19.         <maven.compiler.target>1.8</maven.compiler.target> 
  20.     </properties> 
  21.     <dependencies> 
  22.         <!‐‐ 以下是>spring boot依赖‐‐> 
  23.         <dependency> 
  24.             <groupId>org.springframework.boot</groupId> 
  25.             <artifactId>spring‐boot‐starter‐web</artifactId> 
  26.         </dependency> 
  27.  
  28.         <!‐‐ 以下是>spring security依赖‐‐> 
  29.         <dependency> 
  30.             <groupId>org.springframework.boot</groupId> 
  31.             <artifactId>spring‐boot‐starter‐security</artifactId> 
  32.         </dependency> 
  33.  
  34.  
  35.         <!‐‐ 以下是jsp依赖‐‐> 
  36.         <dependency> 
  37.             <groupId>javax.servlet</groupId> 
  38.             <artifactId>javax.servlet‐api</artifactId> 
  39.             <scope>provided</scope> 
  40.         </dependency> 
  41.         <!‐‐jsp页面使用jstl标签 ‐‐> 
  42.         <dependency> 
  43.             <groupId>javax.servlet</groupId> 
  44.             <artifactId>jstl</artifactId> 
  45.         </dependency> 
  46.  
  47.         <dependency> 
  48.             <groupId>org.springframework.boot</groupId> 
  49.             <artifactId>spring‐boot‐starter‐tomcat</artifactId> 
  50.             <scope>provided</scope> 
  51.         </dependency> 
  52.         <!‐‐用于编译jsp ‐‐> 
  53.         <dependency> 
  54.             <groupId>org.apache.tomcat.embed</groupId> 
  55.             <artifactId>tomcat‐embed‐jasper</artifactId> 
  56.             <scope>provided</scope> 
  57.         </dependency> 
  58.          <dependency> 
  59.             <groupId>org.projectlombok</groupId> 
  60.             <artifactId>lombok</artifactId> 
  61.             <version>1.18.0</version> 
  62.           </dependency> 
  63.     </dependencies> 
  64.     <build> 
  65.         <finalName>security‐springboot</finalName> 
  66.         <pluginManagement> 
  67.             <plugins> 
  68.                 <plugin> 
  69.                     <groupId>org.apache.tomcat.maven</groupId> 
  70.                     <artifactId>tomcat7‐maven‐plugin</artifactId> 
  71.                     <version>2.2</version> 
  72.                 </plugin> 
  73.                 <plugin> 
  74.                     <groupId>org.apache.maven.plugins</groupId> 
  75.                     <artifactId>maven‐compiler‐plugin</artifactId> 
  76.                     <configuration> 
  77.                         <source>1.8</source> 
  78.                         <target>1.8</target> 
  79.                     </configuration> 
  80.                 </plugin> 
  81.  
  82.                 <plugin> 
  83.                     <artifactId>maven‐resources‐plugin</artifactId> 
  84.                     <configuration> 
  85.                         <encoding>utf‐8</encoding> 
  86.                         <useDefaultDelimiters>true</useDefaultDelimiters>                         <resources> 
  87.                             <resource> 
  88.                                 <directory>src/main/resources</directory>                                 <filtering>true</filtering> 
  89.                                 <includes> 
  90.                                     <include>**/*</include> 
  91.                                 </includes> 
  92.                             </resource> 
  93.                             <resource> 
  94.                                 <directory>src/main/java</directory> 
  95.                                 <includes> 
  96.                                     <include>**/*.xml</include> 
  97.                                 </includes> 
  98.                             </resource> 
  99.                         </resources> 
  100.                     </configuration> 
  101.                 </plugin> 
  102.             </plugins> 
  103.         </pluginManagement> 
  104.     </build> 
  105.  
  106. </project> 
  107.  

1.3spring容器配置

SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器

1 Spring Boot 配置文

resources下添加application.properties,内容如下:

  1. server.port=8080
  2. server.servlet.context‐path=/security‐springboot
  3. spring.application.name = security‐springboot

2Spring Boot 启动类

  1.  @SpringBootApplication
  2. public class SecuritySpringBootApp {
  3.     public static void main(String[] args) {
  4.         SpringApplication.run(SecuritySpringBootApp.class, args);    }
  5.  
  6. }

1.4ServletContext配置

由于Springbootstarter自动装配机制,这里无需使用@EnableWebMvc@ComponentScanWebConfig如下

  1.  @Configuration
  2. public class WebConfig implements WebMvcConfigurer {
  3.  
  4.     //默认Url根路径跳转到/login,此url为spring security提供
  5.     @Override
  6.     public void addViewControllers(ViewControllerRegistry registry) {
  7.         registry.addViewController("/").setViewName("redirect:/login");    }
  8. }
  9.  

视频解析器配置在application.properties

  1. spring.mvc.view.prefix=/WEB‐INF/views/ 
  2. spring.mvc.view.suffix=.jsp 
  3.  

1.5安全配置

由于Springbootstarter自动装配机制,这里无需使用@EnableWebSecurityWebSecurityConfig内容如下

  1.  @Configuration
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3.    //内容跟Spring security入门程序一致
  4. }

1.6测试

LoginController的内容同同Springsecurity入门程序。

  1.  @RestController
  2. public class LoginController {
  3.   //内容略..跟Spring security入门程序保持一致}
测试过程:
1 、测试认证
2 、测试退出
3 、测试授权
 

二、工作原理

2.1 结构总览

        Spring Security所解决的问题就是 安全访问控制 ,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter AOP 等技术来实现, Spring Security对 Web 资源的保护是靠 Filter 实现的,所以从这个 Filter 来入手,逐步深入 Spring Security 原理。
        当初始化Spring Security 时,会创建一个名为 SpringSecurityFilterChain Servlet 过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了 javax.servlet.Filter ,因此外部的请求会经过此类。
        下图是Spring Security过滤器链结构图:
 

FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy SecurityFilterChain 所包含的各个 Filter ,同时 这些Filter 作为 Bean Spring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的 ,也不直接处理用户的 授权 ,而是把它们交给了 认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager) 进行处理,下图是 FilterChainProxy 相关类的 UML 图示。

spring Security 功能的实现主要是由一系列过滤器链相互配合完成。

下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter 这个 Filter 是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext ,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository ,同时清除 securityContextHolder 所持有的 SecurityContext
UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler
AuthenticationFailureHandler ,这些都可以根据需求做相关改变;
FilterSecurityInterceptor 是用于保护 web 资源的,使用 AccessDecisionManager 对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException ,其它的异常它会继续抛出。

2.2.认证流程

让我们仔细分析认证过程:

1. 用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication ,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
2. 然后过滤器将 Authentication 提交至认证管理器( AuthenticationManager )进行认证
3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4. SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication ,通过
SecurityContextHolder.getContext().setAuthentication(…) 方法,设置到其中。
可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager 。而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 List<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道 web 表单的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService 负责 UserDetails 的获取。最终 AuthenticationProvider将 UserDetails 填充至 Authentication
认证核心组件的大体关系如下:

2.3AuthenticationProvider介绍

通过前面的 Spring Security 认证流程 我们得知,认证管理器( AuthenticationManager )委托
AuthenticationProvider 完成认证工作。
AuthenticationProvider 是一个接口,定义如下:

authenticate () 方法定义了 认证的实现过程 ,它的参数是一个 Authentication ,里面包含了登录用户所提交的用 户、密码等。而返回值也是一个Authentication ,这个 Authentication 则是在认证成功后,将用户的权限及其他信 息重新组装后生成。
Spring Security 中维护着一个 List<AuthenticationProvider> 列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider 。如使用用户名密码登录时,使用 AuthenticationProvider1 ,短信登录时使用 AuthenticationProvider2等等这样的例子很多。
每个 AuthenticationProvider 需要实现 supports () 方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security 会生成 UsernamePasswordAuthenticationToken ,它是一个 Authentication ,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider 来处理它?
我们在 DaoAuthenticationProvider 的基类 AbstractUserDetailsAuthenticationProvider 发现以下代码:

也就是说当web表单提交用户名密码时,Spring SecurityDaoAuthenticationProvider处理。

最后,我们来看一下 Authentication ( 认证信息 ) 的结构,它是一个接口,我们之前提到的
UsernamePasswordAuthenticationToken 就是它的实现之一:

1 Authentication spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName() 方法。
2 getAuthorities() ,权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系
列字符串。
3 getCredentials() ,凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
4 getDetails() ,细节信息, web 应用中的实现接口通常为 WebAuthenticationDetails ,它记录了访问者的 ip
址和 sessionId 的值。
5 getPrincipal() ,身份信息,大部分情况下返回的是 UserDetails 接口的实现类, UserDetails 代表用户的详细 信息,那从Authentication 中取出来的 UserDetails 就是当前登录用户信息,它也是框架中的常用接口之一。

2.4.UserDetailsService 介绍

1 )认识 UserDetailsService
现在咱们现在知道 DaoAuthenticationProvider 处理了 web 表单的认证逻辑,认证成功后既得到一个
Authentication(UsernamePasswordAuthenticationToken 实现 ) ,里面包含了身份信息( Principal )。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为 UserDetails 对象。  
DaoAuthenticationProvider 中包含了一个 UserDetailsService 实例,它负责根据用户名提取用户信息 UserDetails(包含密码 ) ,而后 DaoAuthenticationProvider 会去对比 UserDetailsService 提取的用户密码与用户提交 的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为 spring bean 来定 义自定义身份验证。

很多人把 DaoAuthenticationProvider UserDetailsService 的职责搞混淆,其实 UserDetailsService 只负责从特定 的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider 的职责更大,它完成完整的认 证流程,同时会把UserDetails 填充至 Authentication
上面一直提到 UserDetails 是用户信息,咱们看一下它的真面目:

它和 Authentication 接口很类似,比如它们都拥有 username authorities Authentication getCredentials() 与 UserDetails中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication 中的 getAuthorities() 实际是由 UserDetails getAuthorities() 传递而形 成的。还记得Authentication 接口中的 getDetails() 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。
通过实现 UserDetailsService UserDetails ,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security 提供的 InMemoryUserDetailsManager( 内存认证 ) JdbcUserDetailsManager(jdbc 认证 ) 就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

2 )测试
自定义 UserDetailsService

屏蔽安全配置类中 UserDetailsService 的定义

重启工程,请求认证, SpringDataUserDetailsService loadUserByUsername 方法被调用 ,查询用户信息。

2.5PasswordEncoder介绍

1 )认识 PasswordEncoder
DaoAuthenticationProvider 认证处理器通过 UserDetailsService 获取到 UserDetails 后,它是如何与请求Authentication中的密码做对比呢?
        在这里Spring Security 为了适应多种多样的加密类型,又做了抽象, DaoAuthenticationProvider 通过 PasswordEncoder接口的 matches 方法进行密码的对比,而具体的密码对比细节取决于实现:

Spring Security 提供很多内置的 PasswordEncoder ,能够开箱即用,使用某种 PasswordEncoder 只需要进行如下声明即可,如下:

NoOpPasswordEncoder 采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1 、用户输入密码(明文 )
2 DaoAuthenticationProvider 获取 UserDetails (其中存储了用户的正确密码)
3 DaoAuthenticationProvider 使用 PasswordEncoder 对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncoder 的校验规则拿 输入的密码和 UserDetails 中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则 校验失败。
实际项目中推荐使用 BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder 等,感兴趣 的大家可以看看这些PasswordEncoder 的具体实现。
2 )使用 BCryptPasswordEncoder
1 、配置 BCryptPasswordEncoder
在安全配置类中定义:

测试发现认证失败,提示: Encoded password does not look like BCrypt
原因:
由于 UserDetails 中存储的是原始密码(比如: 123 ),它不是 BCrypt 格式。
跟踪 DaoAuthenticationProvider 33 行代码查看 userDetails 中的内容 ,跟踪第 38 行代码查看
PasswordEncoder 的类型。
2 、测试 BCrypt
通过下边的代码测试 BCrypt 加密及校验的方法
添加依赖:

编写测试方法:

3 、修改安全配置类
UserDetails 中的原始密码修改为 BCrypt 格式

实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。

2.6授权流程

2.6.1授权流程

通过 快速上手 我们知道, Spring Security 可以通过 http.authorizeRequests() web 请求进行授权保护。 Spring Security使用标准 Filter 建立了对 web 请求的拦截,最终实现对资源的授权访问。
Spring Security 的授权流程如下:

分析授权流程:
1. 拦截请求 ,已认证用户访问受保护的 web 资源将被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子类拦截。
2. 获取资源访问策略 FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限
Collection<ConfigAttribute>
SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读 取访问策略如:

3. 最后, FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
AccessDecisionManager (访问决策管理器)的核心接口如下 :

这里着重说明一下 decide 的参数:
authentication :要访问资源的访问者的身份
object :要访问的受保护资源, web 请求对应 FilterInvocation
configAttributes :是受保护资源的访问策略,通过 SecurityMetadataSource 获取。
decide 接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

2.6.2授权决策

AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。

通过上图可以看出, AccessDecisionManager 中包含的一系列 AccessDecisionVoter 将会被用来对 Authentication
是否有权访问受保护对象进行投票, AccessDecisionManager 根据投票结果,做出最终决策。
AccessDecisionVoter 是一个接口,其中定义有三个方法,具体结构如下所示。

vote() 方法的返回结果会是 AccessDecisionVoter 中定义的三个常量之一。 ACCESS_GRANTED 表示同意, ACCESS_DENIED表示拒绝, ACCESS_ABSTAIN 表示弃权。如果一个 AccessDecisionVoter 不能判定当前 Authentication是否拥有访问对应受保护对象的权限,则其 vote() 方法的返回值应当为弃权 ACCESS_ABSTAIN
Spring Security 内置了三个基于投票的 AccessDecisionManager 实现类如下,它们分别是
AffirmativeBased ConsensusBased UnanimousBased
AffirmativeBased 的逻辑是:
1 )只要有 AccessDecisionVoter 的投票为 ACCESS_GRANTED 则同意用户进行访问;
2 )如果全部弃权也表示通过;
3 )如果没有一个人投赞成票,但是有人投反对票,则将抛出 AccessDeniedException
Spring security 默认使用的是 AffirmativeBased
ConsensusBased 的逻辑是:
1 )如果赞成票多于反对票则表示通过。
2 )反过来,如果反对票多于赞成票则将抛出 AccessDeniedException
3 )如果赞成票与反对票相同且不等于 0 ,并且属性 allowIfEqualGrantedDeniedDecisions 的值为 true ,则表 示通过,否则将抛出异常AccessDeniedException 。参数 allowIfEqualGrantedDeniedDecisions 的值默认为 true
4 )如果所有的 AccessDecisionVoter 都弃权了,则将视参数 allowIfAllAbstainDecisions 的值而定,如果该值 为true 则表示通过,否则将抛出异常 AccessDeniedException 。参数 allowIfAllAbstainDecisions 的值默认为 false
UnanimousBased 的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递 给AccessDecisionVoter 进行投票,而 UnanimousBased 会一次只传递一个 ConfigAttribute 给 AccessDecisionVoter进行投票。这也就意味着如果我们的 AccessDecisionVoter 的逻辑是只要传递进来的 ConfigAttribute中有一个能够匹配则投赞成票,但是放到 UnanimousBased 中其投票结果就不一定是赞成了。
UnanimousBased 的逻辑具体来说是这样的:
1 )如果受保护对象配置的某一个 ConfigAttribute 被任意的 AccessDecisionVoter 反对了,则将抛出 AccessDeniedException。
2 )如果没有反对票,但是有赞成票,则表示通过。
3 )如果全部弃权了,则将视参数 allowIfAllAbstainDecisions 的值而定, true 则通过, false 则抛出
AccessDeniedException
Spring Security 也内置一些投票者实现类如 RoleVoter AuthenticatedVoter WebExpressionVoter 等,可以 自行查阅资料进行学习。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/870221
推荐阅读
相关标签
  

闽ICP备14008679号