Spring Security 入门原理
在web应用开发中,一种是不需要用户登录即可访问全部资源的,比如公司官网。而在更多的情况下web应用是需要用户登录之后才可以访问某些资源的,比如购物网站需要登录才能访问用户的账户信息,网站后台也需要管理员登录之后才可以操作。在登录过程中,应用的安全无疑是十分重要的。
选择Spring Security来保护web应用是一个非常不错的选择,Spring Security是spring项目中的一个安全模块,可以非常方便地集成到spring项目中。特别是在springBoot中使用起来更是非常简单,而且也可以非常方便地与oauth2.0 组合使用。
在接下来的篇幅中,我们会分入门原理、认证流程解析、授权流程解析、聚会oauth2、与其他安全框架的比较这几个篇幅进行介绍。每篇都会提供相应的代码示例。
本篇介绍的是入门原理。
说明: 本系列的文章使用的是 springBoot 2.7.11 (springboot 2x 最后一个稳定版本),对应的 Spring Security 是 5.7.8
流程
入门项目
pom文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</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>
代码示例
... ... ...
没有代码 没有代码 没有代码
把 security包导入之后,springboot就自动配置完成了,真的是开箱即用。
- 默认密码
在控制台有一行注释需要特别注意
UserDetailsServiceAutoConfiguration 中打印了一条日志,意思就是生成了一个安全密码,并且友情提示在开发模式下才这么使用。生成环境下需要自行去配置。
- 登录页面
我们尝试访问应用的任何一个url,最终都会跳到登录页
http://localhost:8080/login。 登录页提示我们输入账号,密码进行登录。
- 登录账号
回到上面的
UserDetailsServiceAutoConfiguration可以很清晰地看到
初始了一个Bean (实际就是一个保存在内存中的登录账号),点击其中的
User
可以看到,默认登录账号是
user, 登录密码是 UUID生成的字符串。
到这里为止,我们弄清了 Security的默认登录账号密码是如何产生的,当然我们也可以通过在application.yml文件中自由配置登录账号密码
spring:
security:
user:
name: admin
password: 1234
原理介绍
Spring Securtiy 对 web (也就是 servlet)的支持是基于 Servlet过滤器的。根据不同的功能需要,定义了很多拦截器,然后这些拦截器按照一定顺序组装成一个拦截链。
FilterChain
下图显示了处理单个HTTP请求时的流程
客户端向应用程序发送一个请求,容器根据配置创建一个 FilterChain ,其中包含 Filter 和 Servlet 。应用根据请求URL来处理 HttpServletRequest。 在Spring MVC应用程序中,Servlet是 DispatchServlet的一个实例。一个 Servlet 最多可以处理一个 HttpServletRequest 和 HttpServletResponse。
DelegatingFilterProxy
在一个web应用启动之后,会有以下几种容器
- Web容器:管理Listener(监听器)、Filter(过滤器)、间接管理
- Servlet(通过Servlet容器)
- Servlet容器:管理Servlet
- Spring容器:管理Service、Dao
- SpringMVC容器:管理Controller
- SpringBoot容器:管理所有Bean
我们在项目中配置的bean一般情况下是被spring 容器管理管理,但是拦截链是 servlet容器调用的。如何让spring容器的bean 来作为 servlet容器的拦截器使用呢?
spring securtiy 使用的 DelegatingFilterProxy 将 servlet的拦截链中某个拦截器委托给 spring 的bean 来执行拦截动作。
由于security 支持配置多个拦截链,所以可以拓展为
以上就是security 拦截链的核心原理。关于secutity是如何认证的,会在下一个篇幅进行介绍。
spring Boot 自动地:
- 启用Spring Security的默认配置,该配置将创建一个名为
springSecurityFilterChain的servletBean。这个Bean负责你的应用程序中的所有安全(保护应用程序的URL,验证提交的用户名和密码,重定向到登录表单,等等)。 - 创建一个
UserDetailsServiceBean,它的用户名是user,密码是随机生成的,会被输出到控制台。 - 在每个请求中,用一个名为
springSecurityFilterChain的 Bean 向Servlet容器注册Filter。
关于security的自动配置
两个重要自动配置类 ,都属于 spring.boot.autoconfigure 这个包中
SecurityAutoConfiguration : 负责 Security 相关配置进行装配
SecurityFilterAutoConfiguration :在上述自动装配完成之后,对SecurityFilter 进行自动装配
-
SecurityAutoConfiguration
@AutoConfiguration @ConditionalOnClass(DefaultAuthenticationEventPublisher.class) //该class 属于 spring-security包中,也就是引入了security相关包才会进行spring-security功能自动装配 @EnableConfigurationProperties(SecurityProperties.class) //将 SecurityProperties 这个配置bean加到 IOC容器 @Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class }) //导入其他配置类 public class SecurityAutoConfiguration { @Bean @ConditionalOnMissingBean(AuthenticationEventPublisher.class) //如果没有自定义配置 AuthenticationEventPublisher bean,则配置默认 public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) { return new DefaultAuthenticationEventPublisher(publisher); } }通过上述自动装配的流程,可以发现可以自定义
AuthenticationEventPublisher,看名字是认证事件通知发布器,具体作用后面再细看。默认的 SecurityConfiguration 配置类
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class SpringBootWebSecurityConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnDefaultWebSecurity //进一步了解这个注解,可以发现,这个bean是否自动装配,如果没有 WebSecurityConfigurerAdapter 或者 SecurityFilterChain 则进行装配 @ConditionalOnMissingBean({ org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.class, SecurityFilterChain.class }) static class Beans { } static class SecurityFilterChainConfiguration { @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) //默认的 SecurityFilterChain SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); //拦截全部请求 http.formLogin(); //表单登录 http.httpBasic(); //认证模式为 basic return http.build(); } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(WebInvocationPrivilegeEvaluator.class) //该bean 属于 spring-security-web中 @ConditionalOnBean(WebInvocationPrivilegeEvaluator.class) //错误页配置 static class ErrorPageSecurityFilterConfiguration { @Bean FilterRegistrationBean<ErrorPageSecurityFilter> errorPageSecurityFilter(ApplicationContext context) { FilterRegistrationBean<ErrorPageSecurityFilter> registration = new FilterRegistrationBean<>( new ErrorPageSecurityFilter(context)); registration.setDispatcherTypes(DispatcherType.ERROR); return registration; } } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) @ConditionalOnClass(EnableWebSecurity.class) //spring-security config中的类 @EnableWebSecurity static class WebSecurityEnablerConfiguration { } } 从 SpringBootWebSecurityConfiguration 中可以发现,如果可以实现一个
WebSecurityConfigurerAdapter或者SecurityFilterChain的 bean 即可改写原有的装配流程。 -
SecurityFilterAutoConfiguration
@AutoConfiguration(after = SecurityAutoConfiguration.class) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(SecurityProperties.class) @ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class }) public class SecurityFilterAutoConfiguration { private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME; @Bean @ConditionalOnBean(name = DEFAULT_FILTER_NAME) //servlet 容器依靠这个代理bean,去spring容器中寻找特定的bean,委托spring中的bean来执行具体的过滤器操作 public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration( SecurityProperties securityProperties) { DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean( DEFAULT_FILTER_NAME); registration.setOrder(securityProperties.getFilter().getOrder()); registration.setDispatcherTypes(getDispatcherTypes(securityProperties)); return registration; } private EnumSet<DispatcherType> getDispatcherTypes(SecurityProperties securityProperties) { if (securityProperties.getFilter().getDispatcherTypes() == null) { return null; } return securityProperties.getFilter() .getDispatcherTypes() .stream() .map((type) -> DispatcherType.valueOf(type.name())) .collect(Collectors.toCollection(() -> EnumSet.noneOf(DispatcherType.class))); } }
参考文章
- Spring Security 中文文档 springdoc.cn/spring-secu…
- Spring Security 入门原理及实战 www.cnblogs.com/demingblog/…
- Web服务器、Web容器、Servlet容器、Spring容器、SpringMVC容器 blog.csdn.net/JokerLJG/ar…
- spring 5.7.8 官方文档 docs.spring.io/spring-secu…