一、 集成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
2)Spring Boot 启动类
@SpringBootApplication
public class SecuritySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(SecuritySpringBootApp.class, args); }
}
1.4ServletContext配置
由于Springbootstarter自动装配机制,这里无需使用@EnableWebMvc与@ComponentScan,WebConfig如下
@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自动装配机制,这里无需使用@EnableWebSecurity,WebSecurityConfig内容如下
@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 Security由DaoAuthenticationProvider处理。
最后,我们来看一下 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 等,可以 自行查阅资料进行学习。