SpringSecurity原理解析(五):HttpSecurity 类处理流程
1、SpringSecurity 在spring boot中与SSM项目中基于配置文件的区别
通过前边的笔记我们可以知道,在传统的SSM项目中 SpringSecurity的使用是基于配置文件
的,然后spring 容器初始化的时候将 SpringSecurity 中的各种标签解析成对应的Bean对象,
SpringSecurity 配置文件如下所示:
<!-- SpringSecurity配置文件 -->
<!--
auto-config:表示自动加载SpringSecurity的配置文件
use-expressions:表示使用Spring的EL表达式
-->
<security:http auto-config="true" use-expressions="true">
<!--定义匿名访问,跳转到登录页面 -->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--
拦截资源
pattern="/**" 拦截所有的资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER 这个角色可以访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')" ></security:intercept-url>
<!--
配置认证(用户登录)信息,覆盖security 默认的登录页面
login-page:登录页面地址
login-processing-url:登录的请求url
default-target-url:登录成功皇后的目标地址
authentication-failure-url:登录校验失败后的地址
-->
<security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/home.jsp" authentication-failure-url="/error.jsp"/>
<!--开启csrf校验 -->
<security:csrf disabled="true"/>
<!--
开启“记住我” 登录用户缓存功能,该功能默认是关闭的,需要手动开启
remember-me-parameter 是登录页面配置的 “记住我” 功能的属性名称,如login.jsp 中的 "remember-me"
token-validity-seconds 设置 “记住我” 登录的数据保存的超时时间,
注意:当前 这种配置只是把“登录数据” 临时保存在页面的Cookie(token) 中,保存在页面中的数据安全性很差,很容故意被盗取;
为了解决这个问题 spring security提供了把“记住我” 功能的数据保存到数据库中,需要在 <security:remember-me>
中添加配置属性 data-source-ref,并 指定数据源,如:data-source-ref="dataSource"
-->
<security:remember-me
token-validity-seconds="1200"
data-source-ref="dataSource"
remember-me-parameter="remember-me"/>
<!--自定义错误页面 -->
<security:access-denied-handler error-page="error.jsp"/>
</security:http>
<!--向IOC容器注入一个bean-->
<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="bCryptPasswordEncoder"/>
<!-- 认证用户信息 -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl"><!--指向自己定义的认证service,在service 中根据登录用户与数据库的数据进行用户认证处理,这样保密性比较好 -->
<!--
<security:user-service >
设置一个账号 zhangsan 密码123 {noop} 表示不加密 具有的角色是 ROLE_USER
<security:user name="zhangsan" authorities="ROLE_USER" password="{noop}123" ></security:user>
<security:user name="lisi" authorities="ROLE_USER" password="{noop}123456" ></security:user>
</security:user-service>
-->
<!--引入用户认证密码加密方式 -->
<security:password-encoder ref="bCryptPasswordEncoder"></security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
也就是在配置文件中通过 security:http 等标签来定义了认证需要的相关信息,但是在
SpringBoot项目中,我们慢慢脱离了xml配置文件的方式,通过配置类的方式来配置
SpringSecurity。SpringSecurity配置类需要继承类 WebSecurityConfigurerAdapter,并重写
configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http) 方法;
SpringSecurity 配置类如下所示:
/**
* SpringSecurity的配置文件
* WebSecurityCofniguration中 @Bean注解 把 FilterChainProxy 注入到了容器中 而且名称为springSecurityFilterChain
* 而 FilterChainProxy 对象是通过 WebSecurity 构建的
*
* @EnableWebSecurity
*/
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService)// 数据库认证,绑定需要执行用户认证操作的service
.passwordEncoder(bCryptPasswordEncoder); // 设置加密处理的方式
//auth.inMemoryAuthentication().withUser("root").password("123")
}
/**
* 容器中注入 BCryptPasswordEncoder
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* HttpSecurity 相当于 SpringSecurity配置文件中 http 标签
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//匿名访问资源
.antMatchers("/login.html","/css/**","/img/**")
// 配置需要放过的资源,表示前边 antMatchers 配置的资源都需要放过
.permitAll()
.antMatchers("/**")
//认证的资源及所具备的权限
.hasAnyRole("USER")
.anyRequest()//表示所有请求都需要认证
.authenticated()//需要认证
//and() 返回一个HttpSecurity
.and()
// 配置登录表单相关的信息
.formLogin()
// 指定自定义的登录页面
//todo 注意:
// 对于前后端分离的项目,不需要指定跳转的页面,只需要 loginProcessingUrl
// 设置请求资源url就行了
.loginPage("/login.html") //认证表单相关信息
.loginProcessingUrl("/login") // 表单提交的登录地址
.defaultSuccessUrl("/home.html")
//表示上边与form表单提交得资源都要放过
.permitAll()
.and()
.rememberMe() // 放开 记住我 的功能
.tokenRepository(persistentTokenRepository) // 持久化
.and()
//csrf设置
.csrf()
.disable();
HttpSecurity http1 = http.authorizeRequests()
// 配置需要放过的资源
.antMatchers("/login.html", "/css/**", "/img/**")
.permitAll()//表示放过前边 antMatchers 配置得资源
.antMatchers("/**")
.hasAnyRole("USER")
.anyRequest()
.authenticated()
.and();
}
/**
* 向Spring容器中注入 PersistentTokenRepository 对象
* @param dataSource
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
//绑定数据源
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = "admin";
// 每次都会生成一个随机的salt,同一个明文加密多次编码得到的密文其实都不一样
System.out.println(encoder.encode(password));
System.out.println(encoder.encode(password));
System.out.println(encoder.encode(password));
}
}
在SpringSecurity中提供了HttpSecurity等工具类,这里HttpSecurity就等同于我们在配置
文件中定义的<security:http>标签,而。
通过代码结果来看和配置文件的效果是一样的。基于配置文件的方式我们之前分析过,是通过
标签对应的handler来解析处理的,那么HttpSecurity这块是如何处理的呢?
2、HttpSecurity 类的处理过程
2.1、HttpSecurity 类图:
由该类图可以清晰得发现,HttpSecurity 继承了父类 AbstractConfiguredSecurityBuilder
并实现了接口 SecurityBuilder 和 HttpSecurityBuilder
HttpSecurity 类的定义如下:
2.2、SecurityBuilder 接口
SecurityBuilder 定义如下:
public interface SecurityBuilder<O> {
//构建 SecurityBuilder 指定泛型类型的对象
O build() throws Exception;
}
由接口 SecurityBuilder 的定义可以发现,SecurityBuilder 接口只提供了一个 build() 方法,用
来构建 SecurityBuilder 泛型指定类型的bean对象。
结合 HttpSecurity 类中实现 SecurityBuilder 接口时的泛型是什么,就知道在 HttpSecurity 类
中 SecurityBuilder 是用来创建什么对象,HttpSecurity 定义如下:
由 HttpSecurity 的定义可以发现,在 HttpSecurity 类中 SecurityBuilder 指定的泛型是
DefaultSecurityFilterChain,DefaultSecurityFilterChain 是拦截器链SecurityFilterChain
一个默认实现,所以 DefaultSecurityFilterChain 是一个拦截器链,所以在 HttpSecurity
中,SecurityBuilder 是用来创建拦截器链的。
2.2.1、SecurityBuilder.build() 方法的实现
下面看下 SecurityBuilder.build() 的实现过程,及连接器链的创建过程
SecurityBuilder 的默认实现是类 AbstractSecurityBuilder
SecurityBuilder.build() 方法的实现如下:
public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> {
//CAS类型
private AtomicBoolean building = new AtomicBoolean();
//返回创建的O的对象
private O object;
public AbstractSecurityBuilder() {
}
//创建泛型O的对象
public final O build() throws Exception {
//基于CAS,保证在整个环境中O只被创建一次
if (this.building.compareAndSet(false, true)) {
//真正创建泛型O的对象
this.object = this.doBuild();
return this.object;
} else {
throw new AlreadyBuiltException("This object has already been built");
}
}
//返回O的对象
public final O getObject() {
if (!this.building.get()) {
throw new IllegalStateException("This object has not been built");
} else {
return this.object;
}
}
//抽象方法,由子类实现
protected abstract O doBuild() throws Exception;
}
由 AbstractSecurityBuilder 的定义可以发现,真正创建泛型O(在 HttpSecurity 中O是
拦截器链 DefaultSecurityFilterChain )的对象是在doBuild 发给发中完成,而doBuild是
一个抽象方法,由子类 AbstractConfiguredSecurityBuilder 实现,
doBuild 方法实现如下:
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
//执行当前类的init方法进行初始化操作
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
//获取构建的对象,上面的方法可以先忽略
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
performBuild() 是一个抽象方法,由AuthenticationManagerBuilder、HttpSecurity、
WebSecurity 三个子类实现,在这里我们应该看 HttpSecurity 中的实现
HttpSecurity.performBuild() 方法的实现如下:
@Override
protected DefaultSecurityFilterChain performBuild() {
//filters:保存所有的过滤器
// 对所有的过滤器做排序
this.filters.sort(OrderComparator.INSTANCE);
List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
for (Filter filter : this.filters) {
sortedFilters.add(((OrderedFilter) filter).filter);
}
// 然后生成 DefaultSecurityFilterChain
return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}
DefaultSecurityFilterChain 构造方法如下:
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
if (!filters.isEmpty()) {
logger.info(LogMessage.format("Will not secure %s", requestMatcher));
} else {
logger.info(LogMessage.format("Will secure %s with %s", requestMatcher, filters));
}
//绑定请求匹配规则
this.requestMatcher = requestMatcher;
//绑定过滤器集合
this.filters = new ArrayList(filters);
}
在HttpSecurity构造方法中绑定了对应的请求匹配器和过滤器集合。
对应的请求匹配器则是 AnyRequestMatcher 匹配所有的请求。当然我们会比较关心
默认的过滤器链中的过滤器是哪来的,这块儿我们继续来分析
2.3、AbstractConfiguredSecurityBuilder
AbstractConfiguredSecurityBuilder 是一个抽象类,也可以看成是 SecurityBuilder 接口的
实现,AbstractConfiguredSecurityBuilder 类图如下所示:
类型 | 功能 |
SecurityBuilder | 声明了build方法 |
AbstractSecurityBuilder | 提供了获取对象的方法以及控制一个对象只能build一次 |
AbstractConfiguredSecurityBuilder | 除了提供对对象细粒度的控制外还扩展了对configurer的操作 |
AbstractConfiguredSecurityBuilder 对应的三个实现类,如下所示:
2.3.1、BuildState
AbstractConfiguredSecurityBuilder 中定义了一个枚举类BuildState,将整个构建过程
分为 5 种状态,也可 以理解为构建过程生命周期的五个阶段,通过这些阶段来管理需
要构建的对象的不同阶段 如下:
private enum BuildState {
/**
* 还没开始构建
*/
UNBUILT(0),
/**
* 构建中
*/
INITIALIZING(1),
/**
* 配置中
*/
CONFIGURING(2),
/**
* 构建中
*/
BUILDING(3),
/**
* 构建完成
*/
BUILT(4);
private final int order;
BuildState(int order) {
this.order = order;
}
public boolean isInitializing() {
return INITIALIZING.order == this.order;
}
/**
* Determines if the state is CONFIGURING or later
* @return
*/
public boolean isConfigured() {
return this.order >= CONFIGURING.order;
}
}
2.3.2、AbstractConfiguredSecurityBuilder 常见方法
2.3.2.1、add() 方法
add 方法,这相当于是在收集所有的配置类。将所有的 xxxConfigure 收集起来存储到
configurers 中,将来再统一初始化并配置,configurers 本身是一个 LinkedHashMap ,
key 是配置类的 class, value 是一个集合,集合里边放着 xxxConfigure 配置类。当
需要对这些配置类进行集中配置的时候, 会通过 getConfigurers 方法获取配置类,
这个获取过程就是把 LinkedHashMap 中的 value 拿出来, 放到一个集合中返回。
add 方法代码如下:
private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
Assert.notNull(configurer, "configurer cannot be null");
//configurer必须是 SecurityConfigurer 的子类
Class<? extends SecurityConfigurer<O, B>> clazz = configurer.getClass();
synchronized(this.configurers) {
if (this.buildState.isConfigured()) {
throw new IllegalStateException("Cannot apply " + configurer + " to already built object");
} else {
List<SecurityConfigurer<O, B>> configs = null;
if (this.allowConfigurersOfSameType) {
configs = (List)this.configurers.get(clazz);
}
List<SecurityConfigurer<O, B>> configs = configs != null ? configs : new ArrayList(1);
((List)configs).add(configurer);
this.configurers.put(clazz, configs);
if (this.buildState.isInitializing()) {
this.configurersAddedInInitializing.add(configurer);
}
}
}
}
//获取指定的配置类
@SuppressWarnings("unchecked")
public <C extends SecurityConfigurer<O, B>> List<C> getConfigurers(Class<C> clazz) {
List<C> configs = (List<C>) this.configurers.get(clazz);
if (configs == null) {
return new ArrayList<>();
}
return new ArrayList<>(configs);
}
2.3.2.2、doBuild()方法
@Override
protected final O doBuild() throws Exception {
synchronized (this.configurers) {
this.buildState = BuildState.INITIALIZING;
beforeInit(); //是一个预留方法,没有任何实现
init(); // 就是找到所有的 xxxConfigure,挨个调用其 init 方法进行初始化,完成默认过滤器的初始化
this.buildState = BuildState.CONFIGURING;
beforeConfigure(); // 是一个预留方法,没有任何实现
configure(); // 就是找到所有的 xxxConfigure,挨个调用其 configure 方法进行配置。
this.buildState = BuildState.BUILDING;
O result = performBuild();
// 是真正的过滤器链构建方法,但是在 AbstractConfiguredSecurityBuilder中 performBuild 方法只是一个抽象方法,具体的实现在 HttpSecurity 中
this.buildState = BuildState.BUILT;
return result;
}
}
init方法:完成所有相关过滤器的初始化
private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.init((B) this); // 初始化对应的过滤器
}
for (SecurityConfigurer<O, B> configurer : this.configurersAddedInInitializing) {
configurer.init((B) this);
}
}
configure方法:完成HttpSecurity和对应的过滤器的绑定。
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}
2.4、HttpSecurity
HttpSecurity 做的事情,就是对各种各样的 xxxConfigurer 进行配置;
HttpSecurity 部分方法列表如下:
HttpSecurity 中有大量类似的方法,过滤器链中的过滤器就是这样一个一个配置的。我们
就不一一介绍 了。每个配置方法的结尾都会调用一次 getOrApply 方法,getOrApply
方法是做什么的?
getOrApply 方法如下:
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer)
throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}
getConfigurer 方法是在它的父类 AbstractConfiguredSecurityBuilder 中定义的,目的就
是去查看当前 这个 xxxConfigurer 是否已经配置过了。
如果当前 xxxConfigurer 已经配置过了,则直接返回,否则调用 apply 方法,这个 apply
方法最终会调 用到 AbstractConfiguredSecurityBuilder#add 方法,将当前配置
configurer 收集起来 HttpSecurity 中还有一个 addFilter 方法.
addFilter 方法如下所示:
@Override
public HttpSecurity addFilter(Filter filter) {
Integer order = this.filterOrders.getOrder(filter.getClass());
if (order == null) {
throw new IllegalArgumentException("The Filter class " + filter.getClass().getName()
+ " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.");
}
this.filters.add(new OrderedFilter(filter, order));
return this;
}
这个 addFilter 方法的作用,主要是在各个 xxxConfigurer 进行配置的时候,会调用到这
个方法, (xxxConfigurer 就是用来配置过滤器的),把 Filter 都添加到 fitlers 变量中。
3、总结
这就是 HttpSecurity 的一个大致工作流程。把握住了这个工作流程,剩下的就只是一些简
单的重 复的 xxxConfigurer 配置了