Spring 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)引入以下依赖:

<?xml version="1.0" encoding="UTF‐8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven‐4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 
 
    <groupId>com.itheima.security</groupId> 
    <artifactId>security‐springboot</artifactId> 
    <version>1.0‐SNAPSHOT</version> 
 
    <parent> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring‐boot‐starter‐parent</artifactId> 
        <version>2.1.3.RELEASE</version> 
    </parent> 
 
    <properties> 
        <project.build.sourceEncoding>UTF‐8</project.build.sourceEncoding>         <maven.compiler.source>1.8</maven.compiler.source> 
        <maven.compiler.target>1.8</maven.compiler.target> 
    </properties> 
    <dependencies> 
        <!‐‐ 以下是>spring boot依赖‐‐> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring‐boot‐starter‐web</artifactId> 
        </dependency> 
 
        <!‐‐ 以下是>spring security依赖‐‐> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring‐boot‐starter‐security</artifactId> 
        </dependency> 
 
 
        <!‐‐ 以下是jsp依赖‐‐> 
        <dependency> 
            <groupId>javax.servlet</groupId> 
            <artifactId>javax.servlet‐api</artifactId> 
            <scope>provided</scope> 
        </dependency> 
        <!‐‐jsp页面使用jstl标签 ‐‐> 
        <dependency> 
            <groupId>javax.servlet</groupId> 
            <artifactId>jstl</artifactId> 
        </dependency> 
 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring‐boot‐starter‐tomcat</artifactId> 
            <scope>provided</scope> 
        </dependency> 
        <!‐‐用于编译jsp ‐‐> 
        <dependency> 
            <groupId>org.apache.tomcat.embed</groupId> 
            <artifactId>tomcat‐embed‐jasper</artifactId> 
            <scope>provided</scope> 
        </dependency> 
         <dependency> 
            <groupId>org.projectlombok</groupId> 
            <artifactId>lombok</artifactId> 
            <version>1.18.0</version> 
          </dependency> 
    </dependencies> 
    <build> 
        <finalName>security‐springboot</finalName> 
        <pluginManagement> 
            <plugins> 
                <plugin> 
                    <groupId>org.apache.tomcat.maven</groupId> 
                    <artifactId>tomcat7‐maven‐plugin</artifactId> 
                    <version>2.2</version> 
                </plugin> 
                <plugin> 
                    <groupId>org.apache.maven.plugins</groupId> 
                    <artifactId>maven‐compiler‐plugin</artifactId> 
                    <configuration> 
                        <source>1.8</source> 
                        <target>1.8</target> 
                    </configuration> 
                </plugin> 
 
                <plugin> 
                    <artifactId>maven‐resources‐plugin</artifactId> 
                    <configuration> 
                        <encoding>utf‐8</encoding> 
                        <useDefaultDelimiters>true</useDefaultDelimiters>                         <resources> 
                            <resource> 
                                <directory>src/main/resources</directory>                                 <filtering>true</filtering> 
                                <includes> 
                                    <include>**/*</include> 
                                </includes> 
                            </resource> 
                            <resource> 
                                <directory>src/main/java</directory> 
                                <includes> 
                                    <include>**/*.xml</include> 
                                </includes> 
                            </resource> 
                        </resources> 
                    </configuration> 
                </plugin> 
            </plugins> 
        </pluginManagement> 
    </build> 
 
</project> 
 

1.3spring容器配置

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

1 Spring Boot 配置文

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

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

2Spring Boot 启动类

 @SpringBootApplication
public class SecuritySpringBootApp {
    public static void main(String[] args) {
        SpringApplication.run(SecuritySpringBootApp.class, args);    }
 
}

1.4ServletContext配置

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

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

视频解析器配置在application.properties

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

1.5安全配置

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

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

1.6测试

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

 @RestController
public class LoginController {
  //内容略..跟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 等,可以 自行查阅资料进行学习。

相关推荐

  1. SpringSecurity

    2024-06-12 23:36:02       35 阅读
  2. 【第二篇】SpringSecurity源码详解

    2024-06-12 23:36:02       32 阅读
  3. 构建安全稳定的应用SpringSecurity实用指南

    2024-06-12 23:36:02       26 阅读
  4. SpringSecurity】2. 初学SpringSecurity

    2024-06-12 23:36:02       52 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-12 23:36:02       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-12 23:36:02       106 阅读
  3. 在Django里面运行非项目文件

    2024-06-12 23:36:02       87 阅读
  4. Python语言-面向对象

    2024-06-12 23:36:02       96 阅读

热门阅读

  1. ARM 汇编 C语言 for循环

    2024-06-12 23:36:02       26 阅读
  2. day7C++

    2024-06-12 23:36:02       22 阅读
  3. 解封装类的实现【3】

    2024-06-12 23:36:02       28 阅读
  4. <题海拾贝>[递归]2.合并两个有序链表

    2024-06-12 23:36:02       31 阅读
  5. Element ui 快速入门

    2024-06-12 23:36:02       31 阅读
  6. 【x264】lookahead模块的简单分析

    2024-06-12 23:36:02       29 阅读
  7. sam_out 脱发预测

    2024-06-12 23:36:02       25 阅读
  8. web前端分离:解析其深层含义与影响

    2024-06-12 23:36:02       27 阅读
  9. dependencies?devDependencies?peerDependencies

    2024-06-12 23:36:02       32 阅读