跳转到目录
是基于过滤器链
来拦截用户发送的请求;
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图示。
跳转到目录
根据上面的时序图, 在程序中找到UsernamePasswordAuthenticationFilter
和DaoAuthenticationProvider
两个类, 通过断点来分析上面时序图
的流程;
1、首先进去登录页面, 输入正确的账号密码 zhangsan, 123
2、因为输入账号密码后, 会进入到AbstractAuthenticationProcessingFilter
的doFilter
方法,
3、认证器(AuthenticationManager)主要委托DaoAuthenticationProvider
来认证用户的账号密码; 找到该类的retrieveUser
方法
在UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
这一步, 会调用我们编写的userDetailsService
方法, 从数据库中取出用户的账号密码;
4、对用户输入的账号密码 和 数据库中的账号密码, 进行匹配
, 会进入到DaoAuthenticationProvider
的父类AbstractUserDetailsAuthenticationProvider
进行判断操作
进入additionalAuthenticationChecks
方法, 进行账号密码的匹配
此时就登录成功;
跳转到目录
现在咱们现在知道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方法被调用 ,查询用户信息。
跳转到目录
认识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可以通过 http.authorizeRequests()
对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security的授权流程如下:
拿到当前访问资源所需要的权限信息
和用户信息中的权限信息
作对比, 如果符合, 则授权成功;
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的参数:
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
跳转到目录
AccessDecisionManager采用投票
的方式来确定是否能够访问受保护资源。