目录
- 一、Spring Security 原理
- 二、Spring Security认证流程 (源码跟踪)
- 1、AuthenticationProvider
- 2、UserDetailsService
- 3、PasswordEncoder
- 三、Spring Security授权流程 (源码跟踪)
- 1、授权决策
一、Spring Security 原理
跳转到目录
- 是基于
过滤器链
来拦截用户发送的请求
; Spring Security
所解决的问题就是安全访问控制
,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源(就是看用户有没有权限)。根据前边知识的学习,可以通过Filter
或AOP
等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。- 当
初始化Spring Security
—> public class SpringSecurityApplicationInitializer
extends AbstractSecurityWebApplicationInitializer,会创建一个名为SpringSecurityFilterChain
的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy
,它实现了javax.servlet.Filter
,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图
上图:
-
UsernamePasswordAuthenticationFilter
—> 对应实际干活的是AuthenenticationManager
-
FilterSecurityInterceptor
—> 对应实际干活的是AccessDecisionManager
FilterChainProxy
是一个代理
,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager
和决策(授权)管理器 (AccessDecisionManager)
进行处理,下图是FilterChainProxy相关类的UML图示。
一、Spring Security认证流程 (源码跟踪)
跳转到目录
根据上面的时序图, 在程序中找到UsernamePasswordAuthenticationFilter
和DaoAuthenticationProvider
两个类, 通过断点来分析上面时序图
的流程;1、首先进去登录页面, 输入正确的账号密码 zhangsan, 123
2、因为输入账号密码后, 会进入到AbstractAuthenticationProcessingFilter
的doFilter
方法,
3、认证器(AuthenticationManager)
主要委托DaoAuthenticationProvider
来认证用户的账号密码; 找到该类的retrieveUser
方法
在UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
这一步, 会调用我们编写的userDetailsService
方法, 从数据库中取出用户的账号密码;
4、对用户输入的账号密码 和 数据库中的账号密码, 进行匹配
, 会进入到DaoAuthenticationProvider
的父类AbstractUserDetailsAuthenticationProvider
进行判断操作
进入additionalAuthenticationChecks
方法, 进行账号密码的匹配
此时就登录成功;
认证核心组件
的大体关系如下:
1、AuthenticationProvider
跳转到目录
2、UserDetailsService
跳转到目录
- 认识UserDetailsService
现在咱们现在知道DaoAuthenticationProvider
处理了web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现)
,里面包含了身份信息(Principal)。这个身份信息就是一个Object ,大多数情况下它可以被强转为UserDetails
对象。
DaoAuthenticationProvider中包含了一个UserDetailsService
实例,它负责根据用户名提取用户信息 UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService
提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定 义自定义身份验证。
重要 : 很多人把DaoAuthenticationProvider
和UserDetailsService
的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息
,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程
,同时会把UserDetails填充至Authentication。
- 它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与 UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形 成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。
- 通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
- Spring Security提供的
InMemoryUserDetailsManager(内存认证)
,JdbcUserDetailsManager(jdbc认证)
就是 UserDetailsService的实现类,主要区别无非就是从内存
还是从数据库
加载用户。
测试:
自定义UserDetailsService
@Service
public class MyUserDetailService implements UserDetailsService {
// 根据账号查询信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 将来连接数据库根据账号来查询用户信息
// 现在先模拟
System.out.println("username = " + username);
UserDetails userDetails = User.withUsername("zhangsan1").password("123").authorities("p1").build();
return userDetails;
}
}
屏蔽安全配置类
中UserDetailsService的定义
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置用户信息服务
// @Bean
// public UserDetailsService userDetailsService() {
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
// manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
// return manager;
// }
重启工程,请求认证,MyUserDetailsService的loadUserByUsername方法被调用 ,查询用户信息。
3、PasswordEncoder
跳转到目录认识PasswordEncoder
DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?
在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches
方法进行密码的对比,而具体的密码对比细节取决于实现:
而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如 下声明即可,如下
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
NoOpPasswordEncoder
采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1、用户输入密码(明文 )
2、DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通 过,否则校验失败。
- NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行
字符串
比较,字符串内容一致 则校验通过,否则校验失败 - 实际项目中推荐使用
BCryptPasswordEncoder
, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感兴趣的大家可以看看这些PasswordEncoder的具体实现
使用BCryptPasswordEncoder
2 测试BCrypt
@SpringBootTest
public class TestBCrypt {
@Test
public void testBCrypt() {
// 对密码进行加密
String hashpw = BCrypt.hashpw("123", BCrypt.gensalt());
String hashpw2 = BCrypt.hashpw("456", BCrypt.gensalt());
System.out.println("hashpw = " + hashpw);
System.out.println("hashpw2 = " + hashpw2);
// 校验密码
boolean checkpw1 = BCrypt.checkpw("123", "$2a$10$QfQYXOtc/2oSgiuYi.9x6.8VcFZ4RuQOq7WmzwkkhXoiD.hB5swP.");
boolean checkpw2 = BCrypt.checkpw("123", "$2a$10$ptyf4yyfbc1oL.OJPfSKMOk.hO4eRS1SQj44MBhhHnSZFrphjGHK.");
System.out.println("checkpw1 = " + checkpw1);
System.out.println("checkpw2 = " + checkpw2);
}
}
3、修改安全配置类
/**
* Description: 安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
*
* @author zygui
* @date Created on 2020/7/22 15:11
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("$2a$10$QfQYXOtc/2oSgiuYi.9x6.8VcFZ4RuQOq7WmzwkkhXoiD.hB5swP.").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("$2a$10$LYa/9GkXYzhc/UjD7S/D5OWE2F7RXHVgANsDHC4XSp8OiEfi1Fk4e").authorities("p2").build());
return manager;
}
// 对密码进行编码, 使用不加密的对比
// @Bean
// public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
// }
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置安全拦截机制
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
.formLogin()//允许表单登录
.successForwardUrl("/login-success");//自定义登录成功的页面地址
}
}
三、Spring Security授权流程 (源码跟踪)
跳转到目录 通过快速上手我们知道,Spring Security可以通过 http.authorizeRequests()
对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security的授权流程如下:
在AccessDecisiontManager
中的Decide()
方法中拿到当前访问资源所需要的权限信息
和用户信息中的权限信息
作对比
, 如果符合, 则授权成功;
AccessDecisionManager(访问决策管理器)的核心接口如下:
public interface AccessDecisionManager {
void decide(Authentication var1, Object var2, Collection<ConfigAttribute> var3) throws AccessDeniedException, InsufficientAuthenticationException;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
}
这里着重说明一下decide的参数:
- authentication:要访问资源的访问者的身份
- object:要访问的受保护资源,web请求对应FilterInvocation
- configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
1、授权决策
跳转到目录 AccessDecisionManager采用投票
的方式来确定是否能够访问受保护资源。
注意:
默认是采用AffirmativeBased的方式进入该类, 在decide方法下打断点