目录
2. SpringBootWebSecurityConfiguration
6. UserDetailServiceAutoConfigutation
大纲
- 环境搭建
- 自动配置细节
一、环境搭建
- SpringBoot
- SpringSecurity
-
- 认证: 判断用户是否是系统合法用户过程
- 授权: 判断系统内用户可以访问或具有访问那些资源权限过程
1. 创建项目
1.1 创建 SpringBoot 应用
1.2 创建 controller
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
System.out.println("hello security");
return "hello security";
}
}
1.3 启动项目进行测试
http://localhost:8080/hello
2. 整合 Spring Security
2.1 引入spring security相关依赖
<!--引入spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 再次启动项目
- 启动完成后控制台生成一个密码
- 访问 hello 发现直接跳转到登录页面
2.3 登录系统
- 默认用户名为: user
- 默认密码为: 控制台打印的 uuid
这就是 Spring Security 的强大之处,只需要引入一个依赖,所有的接口就会自动保护起来!
思考🤔?
- 为什么引入 Spring Security 之后
没有任何配置所有请求就要认证
呢? - 在项目中明明没有登录界面,
登录界面
怎么来的呢? - 为什么使用
user
和控制台密码
能登陆,登录时验证数据源存在哪里呢?
二、实现原理
虽然开发者只需要引入一个依赖,就可以让 Spring Security 对应用进行保护。
Spring Security 又是如何做到的呢?
在 Spring Security 中 认证、授权 等功能都是基于过滤器完成的。
需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个FlterChainProxy 来统一
管理。
Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中。
FilterChainProxy 作为一个顶层的管理者,将统一管理 Security Filter。
FilterChainProxy 本身是通过 Spring 框架提供的 DelegatingFilterProxy 整合到原生的过滤器链中。
1. Security Filters
那么在 Spring Security 中给我们提供那些过滤器? 默认情况下那些过滤器会被加载呢?
过滤器 | 过滤器作用 | 默认是否加载 |
ChannelProcessingFilter | 过滤请求协议 HTTP 、HTTPS | NO |
| 将 WebAsyncManger 与 SpringSecurity 上下文进行集成 | YES |
| 在处理请求之前,将安全信息加载到 SecurityContextHolder 中 | YES |
| 处理头信息加入响应中 | YES |
CorsFilter | 处理跨域问题 | NO |
| 处理 CSRF 攻击 | YES |
| 处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter | 处理 OAuth2 认证重定向 | NO |
Saml2WebSsoAuthenticationRequestFilter | 处理 SAML 认证 | NO |
X509AuthenticationFilter | 处理 X509 认证 | NO |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | NO |
CasAuthenticationFilter | 处理 CAS 单点登录 | NO |
| 处理 OAuth2 认证 | NO |
Saml2WebSsoAuthenticationFilter | 处理 SAML 认证 | NO |
| 处理表单登录 | YES |
OpenIDAuthenticationFilter | 处理 OpenID 认证 | NO |
| 配置默认登录页面 | YES |
| 配置默认注销页面 | YES |
ConcurrentSessionFilter | 处理 Session 有效期 | NO |
DigestAuthenticationFilter | 处理 HTTP 摘要认证 | NO |
BearerTokenAuthenticationFilter | 处理 OAuth2 认证的 Access Token | NO |
| 处理 HttpBasic 登录 | YES |
| 处理请求缓存 | YES |
| 包装原始请求 | YES |
JaasApiIntegrationFilter | 处理 JAAS 认证 | NO |
| 处理 RememberMe 登录 | NO |
| 配置匿名认证 | YES |
| 处理OAuth2认证中授权码 | NO |
| 处理 session 并发问题 | YES |
| 处理认证/授权中的异常 | YES |
| 处理授权相关 | YES |
SwitchUserFilter | 处理账户切换 | NO |
可以看出,Spring Security 提供了 30 多个过滤器。默认情况下Spring Boot 在对 Spring Security 进入自动化
配置时,会创建一个名为SpringSecurityFilerChain 的过滤器,并注入到 Spring 容器中,这个过滤器将负责所有
的安全管理,包括用户认证、授权、重定向到登录页面等。具体可以参考WebSecurityConfiguration的源码:
2. SpringBootWebSecurityConfiguration
这个类是 spring boot 自动配置类,通过这个源码得知,默认情况下对所有请求进行权限控制:
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http.authorizeRequests().anyRequest()
.authenticated().and().formLogin().and().httpBasic();
return http.build();
}
}
这就是为什么在引入 Spring Security 中没有任何配置情况下,请求会被拦截的原因!
通过上面对自动配置分析,我们也能看出默认生效条件为:
class DefaultWebSecurityCondition extends AllNestedConditions {
DefaultWebSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class Beans {
}
}
- 条件一 classpath中存在 SecurityFilterChain.class, HttpSecurity.class
- 条件二 没有自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class
默认情况下,条件都是满足的。
WebSecurityConfigurerAdapter 这个类极其重要,Spring Security 核心配置都在这个类中:
如果要对 Spring Security 进行自定义配置,就要自定义这个类实例,
通过覆盖类中方法达到修改默认配置的目的。
3. 流程分析
- 请求 /hello 接口,在引入 spring security 之后会先经过一些列过滤器
- 在请求到达 FilterSecurityInterceptor时,发现请求并未认证。请求拦截下来,并抛出 AccessDeniedException 异常。
- 抛出 AccessDeniedException 的异常会被 ExceptionTranslationFilter 捕获,这个 Filter 中会调用
LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回 302,要求客户端进行重定向到 /login 页面。 - 客户端发送 /login 请求。
- /login 请求会再次被拦截器中 DefaultLoginPageGeneratingFilter 拦截到,并在拦截器中返回生成登录页面。
就是通过这种方式,Spring Security 默认过滤器中生成了登录页面,并返回!
4. 默认用户生成
- 查看 SpringBootWebSecurityConfiguration#defaultSecurityFilterChain 方法表单登录
- 处理登录为 FormLoginConfigurer 类中 调用 UsernamePasswordAuthenticationFilter这个类实例
- 查看类中 UsernamePasswordAuthenticationFilter#attempAuthentication 方法得知实际调用 AuthenticationManager 中authenticate 方法
- 调用 ProviderManager 类中方法 authenticate
- 调用了 ProviderManager 实现类中 AbstractUserDetailsAuthenticationProvider类中方法
- 最终调用实现类 DaoAuthenticationProvider 类中方法比较
看到这里就知道默认实现是基于 InMemoryUserDetailsManager 这个类,也就是内存的实现!
5. UserDetailService
通过刚才源码分析也能得知 UserDetailService 是顶层父接口,接口中 loadUserByUserName 方法是用来在认证
时进行用户名认证方法,默认实现使用是内存实现,如果想要修改数据库实现我们只需要自定义
UserDetailService 实现,最终返回 UserDetails 实例即可。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
6. UserDetailServiceAutoConfigutation
这个源码非常多,这里梳理了关键部分:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class },
type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {
//....
@Bean
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
//...
}
结论
- 从自动配置源码中得知当 classpath 下存在 AuthenticationManager 类
- 当前项目中,系统没有提供 AuthenticationManager.class、 AuthenticationProvider.class、UserDetailsService.class、
AuthenticationManagerResolver.class、实例
默认情况下都会满足,此时Spring Security会提供一个 InMemoryUserDetailManager 实例
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
private final User user = new User();
public User getUser() {
return this.user;
}
//....
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
//get set ...
}
}
这就是默认生成 user 以及 uuid 密码过程!
另外看明白源码之后,就知道只要在配置文件中加入如下配置可以对内存中用户和密码进行覆盖。
spring.security.user.name=root
spring.security.user.password=root
spring.security.user.roles=admin,users
7. 总结
- AuthenticationManager、ProviderManger、以及 AuthenticationProvider 关系
- WebSecurityConfigurerAdapter 扩展 Spring Security 所有默认配置
- UserDetailService 用来修改默认认证的数据源信息