Spring Security请求全过程解析

Spring Security是一款基于Spring的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架Apache Shiro相比,它拥有更为强大的功能。Spring Security也可以轻松的自定义扩展以满足各种需求,并且对常见的Web安全攻击提供了防护支持。如果你的Web框架选择的是Spring,那么在安全方面Spring Security会是一个不错的选择。

这里我们使用Spring Boot来集成Spring Security,Spring Boot版本为2.5.3,Spring Security版本为5.5.1

开启Spring Security

使用IDEA创建一个Spring Boot项目,然后引入spring-boot-starter-security

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.projectlombok:lombok:1.18.8'
    annotationProcessor 'org.projectlombok:lombok:1.18.8'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

接下来我们创建一个HelloController,对外提供一个/hello服务:

@RestController
public class HelloController {
    @GetMapping("hello")
    public String hello() {
        return "hello world";
    }
}

这时候我们直接启动项目,访问http://localhost:8080/hello,可以看到页面跳转到一个登陆页面:

 默认的用户名为user,密码由Sping Security自动生成,回到IDEA的控制台,可以找到密码信息:

Using generated security password: 4f06ba04-37e9-4bdd-a085-3305260da0d6

输入用户名user,密码4f06ba04-37e9-4bdd-a085-3305260da0d6后,我们便可以成功访问/hello接口。

基本原理

Spring Security默认为我们开启了一个简单的安全配置,下面让我们来了解其原理。

当Spring Boot项目配置了Spring Security后,Spring Security的整个加载过程如下图所示:

​​​​​​​

而当我们访问http://localhost:8080/hello时,代码的整个执行过程如下图所示:

 如上图所示,Spring Security包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。

下面我们通过debug来验证这个过程:

首先,通过前面可以知道,当有请求来到时,最先由DelegatingFilterProxy负责接收,因此在DelegatingFilterProxydoFilter()的首行打上断点:

 接着DelegatingFilterProxy会将请求委派给FilterChainProxy进行处理,在FilterChainProxy的首行打上断点:

 FilterChainProxy会在doFilterInternal()中生成一个内部类VirtualFilterChain的实例,以此来调用Spring Security的整条过滤器链,在VirtualFilterChaindoFilter()首行打上断点:

接下来VirtualFilterChain会通过currentPosition依次调用存在additionalFilters中的过滤器,其中比较重要的几个过滤器有:UsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterAnonymousAuthenticationFilterExceptionTranslationFilterFilterSecurityInterceptor,我们依次在这些过滤器的doFilter()的首行打上断点:

 

准备完毕后,我们启动项目,然后访问http://localhost:8080/hello,程序首先跳转到DelegatingFilterProxy的断点上:

此时delegate还是null的,接下来依次执行代码,可以看到delegate最终被赋值一个FilterChainProxy的实例:

接下来程序依次跳转到FilterChainProxydoFilter()VirtualFilterChaindoFilter()中:

接着程序跳转到AbstractAuthenticationProcessingFilterUsernamePasswordAuthenticationFilter的父类)的doFilter()中,通过requiresAuthentication()判定为false(是否是POST请求):

接着程序跳转到DefaultLoginPageGeneratingFilterdoFilter()中,通过isLoginUrlRequest()判定为false(请求路径是否是/login):

接着程序跳转到AnonymousAuthenticationFilterdoFilter()中,由于是首次请求,此时SecurityContextHolder.getContext().getAuthentication()为null,因此会生成一个AnonymousAuthenticationToken的实例:

接着程序跳转到ExceptionTranslationFilterdoFilter()中,ExceptionTranslationFilter负责处理FilterSecurityInterceptor抛出的异常,我们在catch代码块的首行打上断点:

接着程序跳转到FilterSecurityInterceptordoFilter()中,依次执行代码后程序停留在其父类(AbstractSecurityInterceptor)的attemptAuthorization()中:

accessDecisionManagerAccessDecisionManager(访问决策器)的实例,AccessDecisionManager主要有3个实现类:AffirmativeBased(一票通过),ConsensusBased(少数服从多数)、UnanimousBased(一票否决),此时AccessDecisionManager的的实现类是AffirmativeBased,我们可以看到程序进入AffirmativeBaseddecide()中:

从上图可以看出,决策的关键在voter.vote(authentication, object, configAttributes)这句代码上,通过跟踪调试,程序最终进入AuthenticationTrustResolverImplisAnonymous()中:

isAssignableFrom()判断前者是否是后者的父类,而anonymousClass被固定为AnonymousAuthenticationToken.class,参数authentication由前面AnonymousAuthenticationFilter可以知道是AnonymousAuthenticationToken的实例,因此isAnonymous()返回true,FilterSecurityInterceptor抛出AccessDeniedException异常,程序返回ExceptionTranslationFilter的catch块中:

接着程序会依次进入DelegatingAuthenticationEntryPointLoginUrlAuthenticationEntryPoint中,最后由LoginUrlAuthenticationEntryPointcommence()决定重定向到/login

后续对/login的请求同样会经过之前的执行流程,在DefaultLoginPageGeneratingFilterdoFilter()中,通过isLoginUrlRequest()判定为true(请求路径是否是/login),直接返回login.html,也就是我们开头看到的登录页面。

当我们输入用户名和密码,点击Sign in,程序来到AbstractAuthenticationProcessingFilterdoFilter()中,通过requiresAuthentication()判定为true(是否是POST请求),因此交给其子类UsernamePasswordAuthenticationFilter进行处理,UsernamePasswordAuthenticationFilter会将用户名和密码封装成一个UsernamePasswordAuthenticationToken的实例并进行校验,当校验通过后会将请求重定向到我们一开始请求的路径:/hello

后续对/hello的请求经过过滤器链时就可以一路开绿灯直到最终交由HelloController返回"Hello World"。

参考链接:

1. Spring Security Reference

2. Spring Boot中开启Spring Security

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值