本博文由阅读官网文档 https://spring.io/guides/topicals/spring-security-architecture 分析整理得到。
前言:
安全方面的内容一直是一个应用不可或缺的,在Java应用中,Spring套件作为框架的主流,配套的Spring-Security框架也是经常被用于安全方面,对鉴权和授权,即Authenticatio和Authorization两块的功能进行支持。在实际的使用上,能容易的找到相关的示例代码进行使用,但是在原理方面由于代码本身较多,难以一览全貌,通过官方文档理解整体的结构,有助于后续进行深入的理解。
本次阅读官方介绍的起因,是因为在项目中,需要同时配置针对于用户端(app)以及后台端的鉴权,2套鉴权分别使用了token和session的方式,为了实现上述目的,针对2套体系实现了2个Filter,并在继承WebSecurityConfigurerAdapter重写方法,设置HttpSecurity。通过调用.antMatcher("/api/v1/business/**").addFilterBefore()的方式,对特定路径添加过滤器,但是后续实际发现过滤器是全局生效的。过滤器的全局生效,会导致本地开发测试时部分功能出现非预期的情况,比如说在本地切换商户和用户角色,但是由于商户先登录之后,后台会记录session,后续带入token的方式访问用户接口,就会出现商户和用户的2个Filter均进行处理,先后处理session和token,由于其顺序导致了鉴权设置异常。因为这个情况,抽了时间去看本文最初提到的文档,最终解决了上述问题。
正文:
正文将根据文档内容,提取相应的关键点进行描述,通过相应的关键点,可以了解大体的工作原理,并能清楚笔者之前问题的出现原因以及解决方案。
鉴权和授权,授权和访问控制的目的一致,后续用访问控制进行区分。
1. 鉴权的核心类是AuthenticationManager,该接口提供方法 Authentication authenticate(Authentication authentication) throws AuthenticationException 。该方法接收Authentication对象,并判断是否鉴权通过,不通过会抛出异常。
AuthenticationManager的主要实现类是ProviderManager,ProviderManager有2个重要的局部变量,一个是parent(类型统一为ProviderManager),使得ProviderManager具有层级结构;另一个为AuthenticationProvider的list,实际上是使用了委派模式,实际的工作是有AuthenticationProvider的list完成的。示意图如下:
2. 如果想要自定义构建AuthenticationManager,可以注入AuthenticationManagerBuilder对象修改(注入的对象是全局的),或通过继承WebSecurityConfigurerAdapter并重写void configure(AuthenticationManagerBuilder auth)方法(该方式是局部的)
3. 访问控制的核心类是AccessDecisionManager,实际上的实现类,内部会使用委派模式,维护一个AccessDecisionVoter列表(实际的判断操作是由AccessDecisionVoter实现)。
AccessDecisionVoter的核心方法是 int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); 三个参数中,第一个即之前的鉴权对象Authentication,第二个为需要访问的资源描述(元数据,可能是web页面,可能是某个java方法),第三个采参数是访问需要的权限描述(例如访问该资源需要拥有角色ROLE_ADMIN这样的描述)。
4. 在web容器层面上,Spring Security只有一个过滤器被直接管理,即FilterChainProxy(如下图),通过委派的方式(又是委派)使用内部的Filter执行逻辑,内部的Filter则由Spirng Security框架本身进行管理(这样可以解释为什么spring security能通过配置直接filter对部分路径进行处理)。
(在此处官方文档中明确声明了,如果需要将Filter通过Spring Security进行管理,而不是通过容器进行管理,那么这些Filter就不能使用@Bean或@Component,或者需要使用FilterRegistrationBean时声明disable该Filter)
(在该处笔者发现了之前的原因,之前虽然在继承类重写方法上正常,但是由于本身构建Filter需要注入其他对象,在类上增加了@Component注解,导致Filter被全局容器代理,导致了最开始说的冲突情况,解决方法就很简单,使用文章中提到了,在FilterRegistrationBean中disable该Filter即可,代码如下:
@Bean
public FilterRegistrationBean<BusinessRequestFilter> businessRequestFilterFilterRegistrationBean(BusinessRequestFilter businessRequestFilter) {
FilterRegistrationBean<BusinessRequestFilter> filterRegistrationBean = new FilterRegistrationBean<>(businessRequestFilter);
filterRegistrationBean.setEnabled(false);
return filterRegistrationBean;
}
)
5. 在spring security的FilterChainProxy内部,根据配置创建了多个filter chain(如下图),不同的filter chain之间有不同的匹配规则,而实际运行时,会根据该规则进行优先匹配,匹配后即执行该filter chain的逻辑。
重点: 一个请求,只会走一条filter chain的逻辑。
6. 创建自定义的filter chain。 这一部分是核心部分,也是开发者进行自定义逻辑的部分,通过继承WebSecurityConfigurerAdapter,指定order(order关系到实际的排列顺序),并重写方法(一般为protected void configure(HttpSecurity http)方法),在运行时会动态创建filter chain,从而实现相关的自定义鉴权和访问控制。示例代码如下:
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
.authorizeRequests()
.antMatchers("/foo/bar").hasRole("BAR")
.antMatchers("/foo/spam").hasRole("SPAM")
.anyRequest().isAuthenticated();
}
}
上述代码中出现了antMather()和antMathcers(),需要注意,前者是进行请求的匹配,后者是进行访问权限的控制。
7. 关于方法级别的安全,Spring Security可以通过注解@EnableGlobalMethodSecurity开启,@Secured("ROLE_USER")或@PreAuthorize或@PostAuthorize等注解进行使用
从上到下,其实大致的运行逻辑已经相对清楚了,我这边再简单的描述一下。
1. 继承WebSecurityConfigurerAdapter并重写 protected void configure(HttpSecurity httpSecurity) 、 public void configure(WebSecurity web) 方法。
2. 启动时会调用HttpSecurity对象的performBuild()方法,创建DefaultSecurityFilterChain,即Spring Security内部的管理的filter chain。创建时会更新配置时的输入值进行构建,如构建地址匹配以及Filter列表,内部的鉴权和访问控制对象。
3. 运行后,请求访问到FilterChainProxy时会进入SpringSecurity的内部逻辑,比如根据地址进行匹配,匹配完成后,执行对应Filter chain中的逻辑。