前言:本系列文章主要讲述SpringSecurity在认证授权时的一系列过程,方便为各位小白答疑解惑,后续文章也会定时更新。谢谢大家的支持。
认证流程图(大概的基本流程)
默认情况下
- 当前端发起请求时,经过一系列过滤器之后,被SpringSecurity中的过滤器进行拦截
- 当拦截到该请求之后,会委派到身份验证管理其中进行认证,AuthenticationManager是一个接口,主要实现类为ProviderManager
- 身份验证程序(AuthenticationProvider)实现了主要的认证逻辑,包括检查用户名,密码等
- 认证完成之后,会将认证的结果依次返回,最终将认证的结果保存在SpringContext的上下文对象中
构建项目基础环境(jdk8)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
搭建完成之后,输入http://localhost:8080/ 地址之后,可以看到一个默认的登录页面
idea控制台中会生成一个加密之后的密码字符串
此时表示环境已经搭建成功。用户名为user密码为控制台中生成的字符串(默认用户)
实现原理(过滤器链)DelegatingFilterProxy
当前端请求后端接口时,会首先经过一些原生的filter,在这些filter中会经过DelegatingFilterProxy委派到SpringSecurity所提供的的一系列过滤器链(SecurityFilterChain)
官方解释:Spring 提供了一个名为 DelegatingFilterProxy 的 Filter 实现,允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。Servlet容器允许通过使用自己的标准来注册 Filter 实例,但它不知道 Spring 定义的 Bean。你可以通过标准的Servlet容器机制来注册 DelegatingFilterProxy,但将所有工作委托给实现 Filter 的Spring Bean。
官网图片 (https://springdoc.cn/spring-security/servlet/architecture.html)
默认情况下会加载部分的过滤器
过滤器 | 过滤器作用 | 默认是否加载 |
---|---|---|
ChannelProcessingFilter | 过滤请求协议 HTTP 、HTTPS | NO |
WebAsyncManagerIntegrationFilter | 将 WebAsyncManger 与 SpringSecurity 上下文进行集成 | YES |
SecurityContextPersistenceFilter | 在处理请求之前,将安全信息加载到 SecurityContextHolder 中 | YES |
HeaderWriterFilter | 处理头信息加入响应中 | YES |
CorsFilter | 处理跨域问题 | NO |
CsrfFilter | 处理 CSRF 攻击 | YES |
LogoutFilter | 处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter | 处理 OAuth2 认证重定向 | NO |
Saml2WebSsoAuthenticationRequestFilter | 处理 SAML 认证 | NO |
X509AuthenticationFilter | 处理 X509 认证 | NO |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | NO |
CasAuthenticationFilter | 处理 CAS 单点登录 | NO |
OAuth2LoginAuthenticationFilter | 处理 OAuth2 认证 | NO |
Saml2WebSsoAuthenticationFilter | 处理 SAML 认证 | NO |
UsernamePasswordAuthenticationFilter | 处理表单登录 | YES |
OpenIDAuthenticationFilter | 处理 OpenID 认证 | NO |
DefaultLoginPageGeneratingFilter | 配置默认登录页面 | YES |
DefaultLogoutPageGeneratingFilter | 配置默认注销页面 | YES |
ConcurrentSessionFilter | 处理 Session 有效期 | NO |
DigestAuthenticationFilter | 处理 HTTP 摘要认证 | NO |
BearerTokenAuthenticationFilter | 处理 OAuth2 认证的 Access Token | NO |
BasicAuthenticationFilter | 处理 HttpBasic 登录 | YES |
RequestCacheAwareFilter | 处理请求缓存 | YES |
SecurityContextHolder<br />AwareRequestFilter | 包装原始请求 | YES |
JaasApiIntegrationFilter | 处理 JAAS 认证 | NO |
RememberMeAuthenticationFilter | 处理 RememberMe 登录 | NO |
AnonymousAuthenticationFilter | 配置匿名认证 | YES |
OAuth2AuthorizationCodeGrantFilter | 处理OAuth2认证中授权码 | NO |
SessionManagementFilter | 处理 session 并发问题 | YES |
ExceptionTranslationFilter | 处理认证/授权中的异常 | YES |
FilterSecurityInterceptor | 处理授权相关 | YES |
SwitchUserFilter | 处理账户切换 | NO |
SpringSecurity中的默认配置
每次启动项目之后,控制台中都会生成一次默认的随机密码字符串,包括默认的密码加密方式(PasswordEncoder)、默认的登录页面、认证、授权过滤器等。这些都是由SpringBoot来完成.
- 首先讲解UserdetailService和PasswordEncoder
UserdetailService
可以看到UserDetailService是一个接口,只有一个loadUserByUsername的方法,根据用户名获取一个UserDetails(后续说明) 对象,从内存中或者数据库中查询用户信息(默认是基于内存中查询,也就是登录也中输入的user用户名)。
说明:Userdetails:
- Collection<? extends GrantedAuthority> getAuthorities();获取用户权限信息
- String getPassword();获取密码
- String getUsername();获取用户名
- boolean isAccountNonExpired(); 账户是否不过期
- boolean isAccountNonLocked();账户是否没被锁定
- boolean isCredentialsNonExpired();凭据是否不过期
- boolean isEnabled();是否开启
- 从数据库或内存中获取的用户信息,将其封装为用UserDetails对象进行返回,工上层使用(后续在原码中可以看到)
PasswordEncoder
-
String encode(CharSequence rawPassword);对密码进行加密返回
-
boolean matches(CharSequence rawPassword, String encodedPassword);对密码进行比较,即,将用户传入的密码进行加密之后与数据库或内存中加密之后的密码进行匹配,匹配成功返回true
-
default boolean upgradeEncoding(String encodedPassword)对密码进行升级(多种加密方式)
-
默认情况下,在项目启动时,会对加密对象(PasswordEncoder)进行多种加密方式的配置。
-
PasswordEncoderFactories类中的源码如下,可以看到有多种的加密方式,
-
return new DelegatingPasswordEncoder(encodingId, encoders);默认指定为BCryptPasswordEncoder,一种单向的hash加密方式
加密方式: -
BCryptPasswordEncoder
BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptP asswordEncoder “为自己带盐”开发者不需要额外维护一个“盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。
-
Argon2PasswordEncoder
Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。
-
Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2
算法也是一种故意降低运算速度的算法,当需要 FIPS (Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。
-
SCryptPasswordEncoder
SCryptPasswordEncoder 使用scrypt 算法对密码进行加密,和前面的几种类似,serypt 也是一种故意降低运算速度的算法,而且需要大量内存。
DelegatingPasswordEncoder(实现多种编码策略)
可以看到虽然实现了PasswordEncoder这个接口,但是他并不是一种加密方式,也没有实现加密的操作,只是将加密等一系列的操作委派给实现了同一个接口下的其他xxxPasswordEncoder实现类进行相应的处理,PasswordEncoderFactories中提供的就是DelegatingPasswordEncoder可以进行委派的加密方式列表。
例如:{noop}123321 ,{noop}当带有这个前缀时DelegatingPasswordEncoder会将其委派给NoOpPasswordEncoder进行密码的一系列处理,其他同理。
如何更改SpringSecurity中UserDetailsService 和 PasswordEncoder这个两个默认配置(核心配置类WebSecurityConfigurerAdapter)?
- 方式一:通过这种方式覆盖掉SpringBoot提供的默认配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
// 基于内存的方式创建一个UserDetails对象
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();//创建了一个基于内存的管理对象,UserDetailsService 实现子类之一
//User是SpringSecurity提供的一个专门用来构建UserDetails的类
UserDetails details = User.withUsername("admin") //用户名
.password("123") // 密码
.authorities("ADMIN")// 指定当前用户所拥有的权限(后续说明,暂时指定为ADMIN或者别的也可以)
.build();
manager.createUser(details);
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//后续详细说明,暂时先按如下配置
http.authorizeRequests() //开启请求认证
.anyRequest() // 任何请求
.authenticated()//需要认证
.and()
.formLogin();//基于表单的形式,也就是默认生成的登录页面
}
}
- 方式二:通过WebSecurityConfigurerAdapter中提供的configure方法进行自定义配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
UserDetails details = User.withUsername("admin")
.password("123")
.authorities("ADMIN")
.build();
manager.createUser(details);
auth.userDetailsService(manager).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
}
- 方式三:混合配置(不推荐)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
UserDetails details = User.withUsername("admin")
.password("123")
.authorities("ADMIN")
.build();
manager.createUser(details);
auth.userDetailsService(manager);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
}
此时当我们再次进入登录页面时,就可以输入我们自定义的内存用户名和密码进行登录了。