FilterChainProxy是spring-security的入口,包含默认的和用户自定义过滤器。
当访问任意接口,例如: http://localhost:8035/,请求将进入FilterChainProxy打个断点可以看到只使用用户名密码登录时默认有11个过滤器:
几个重要的过滤器:
其成员变量repo就是持久化仓库,默认实现是HttpSessionSecurityContextRepository,即将SpringContext保存到Session中。
http.authorizeRequests() .mvcMatchers("/login").permitAll() //匿名用户可访问/login .anyRequest().authenticated()//代表其他请求需要认证 .and() .formLogin()//表示开启表单认证 .loginPage("/login") .loginProcessingUrl("/doLogin"); |
loginPage:指定自定义登录页面,当拒绝访问时会重定向该页面,注意:一旦自定义登录页面,必须指定登录url
loginProcessingUrl:指定用户名密码登录请求url,即UsernamePasswordAuthenticationFilter处理的请求
服务启动后,在如下图示FilterChainProxy中打断点,浏览器新页面访问http://localhost:8035/
请求进入如下断点:
日志中可可以看到/请求进入:
放行直到nextFilter是SecurityContextPersistenceFilter时,进入该Filter
可以看到先从会话中提取SpringSecurityContext,放入本次请求的ThreadLocal对象中,用于后续处理,因此还未登录,因此这次提取的上下文对象是null,继续后续处理,点击进入chain.doFilter方法,可发现会重新进入FilterChainProxy:
同样断点放行,直到nextNextFilter是UsernamePasswordAuthenticationFilter时进入Filter:
进入requireAuthentication方法:
可发现当前URI是/,并不是/doLogin,因此返回false,然后将执行下一个Filter
放行断点直到AnonymousAuthenticationFilter,并进入:
可以看到构造的Authentication的authorizes是ROLE_ANONYMOUS,同时可以注意到details中有当前会话的sessionId;
放行断点并进入ExceptionTranslationFilter :
可以看到就是为了捕获后续过滤器的异常的!
放行断点并进入FilterSecurityInterceptor :
并进入beforeInvocation方法:
进入attemptAuthorization方法:
继续进入decide方法:
进入vote方法:
这符合我们的配置;是怎么判断的呢?肯定是根据配置。比如授权服务器处理/oauth/authorize接口需要authorities包含【ROLE_USER】,当用户未登录或者没有该权限时,这里肯定也返回拒绝!
点击下一步直到抛出异常:
debug点击下一步直到进入ExceptionTranslateFilter:
进入handleSpringSecurityException方法:
进入handleAccessDeniedException方法:
进入sendStartAuthentication(发送到开始认证的页面,即登录页):
进入commence方法:
断点放行进入FilterChainProxy,直到日志打印:Securing GET /login ,说明这是浏览器在请求登录页面!
和之前访问localhost:8035/的请求一样,还是会认证为一个匿名用户,然后进入FilterSecurityIntercepter进行授权校验:
然后在LoginController中打断点:
断点放行,可发现进入了LoginController:
断点放行,浏览器渲染登录页面
保持FilterChainProxy的断点,输入用户名root密码root点击登录按钮,请求进入断点:
日志也可看到当前请求是/doLogin;
断点放行,直到进入UsernamePasswordAuthenticationFilter:
这次requiresAuthentication方法判断为真了,因此进入认证方法:
可以看到其实进入的类是AbstractAuthencationProcessingFilter,就是各种认证处理过滤器的抽象父类;
进入attemptAuthentication后,就是子类具体实现了:
进入authenticate:
在如下图中标记断点,并放行到该断点:
可发现能处理UsernamePasswordAuthenticationToken的provider是DaoAuthenticationProvider,进入authenticate方法:
进入retrieveUser方法:
因为demo中自定义了InMemoryUserDetailService,因此会进入该Service:
查询返回User:
debug点击下一步,进入方法this.preAuthenticationChecks.check(user):
可返现校验了用户的状态;
点击下一步进入additionalAuthenticationChecks方法:
进行密码校验,通过后进入 this.postAuthenticationChecks.check(user):
debug点击下一步直到createSuccessAuthentication:
可以看到最终构造了一个Authentication对象,而且authenticated=true,这个方法可以注意下,当我们向构建登录态时,也可以构建这个对象,比如cookie单点登录设置登录态。
返回SuccessAuthentication后
debug点击下一步,回到抽象父类AbstractAuthenticationProcessingFilter:
可以执行方法successfulAuthentication,并且没有传入filterChain,也就是说肯定不会调用filterChain.doFilter(),因此UsernamePasswordAuthenticationFilter后面的Filter不会被执行了!
进入方法successfulAuthentication:
该方法中可以看到首先保存认证结果到SpringContext,这是为了使SecurityContextPersistenceFilter能够持久化
然后调用了successHandler方法,默认实现是SavedRequestAwareAuthenticationSuccessHandler,用户可以自定义覆盖这个默认实现,进入onAuthenticationSuccess方法:
进入onAuthenticationSuccess方法:
进入handle方法:
debug点击下一步,直到进入SecurityContextPersistenceFilter:
可以看到保存SpringContext到repo中!
保持FilterChainProxy断点:
debug放行,浏览器中可以看到doLogin重定向到localhost:8035/
再次进入FilterChainProxy断点:
日志显示当前请求是 GET /
在根路径中打断点:
debug放行进入SecurityContextPersistenceFilter:
可以看到从repo中加载了前面认证成功后的SecurityContext,并放入SecurityContextHolder中;
因请求根路径,所以不会执行UsernamePasswordAuthenticationFilter的认证方法;
debug放行,进入AnonymousAuthenticationFilter :
因为SecurityContextHolder中已存在认证对象,因此不会构造匿名认证对象了;
debug放行,直到进入FilterSecurityInterceptor :
未抛出AccessDeniedException,则校验通过!
debug放行,请求将进入Controller:
debug放行,渲染页面,请求完成:
先进入登录页面,然后如下打断点:
当输入错误密码并提交,进入断点:
debug点击下一步,密码错误,抛出BadCredentialsException;点击下一步直到回到抽象父类异常捕获方法:
进入unsuccessfulAuthentication:
继续执行认证失败处理器,默认handler是SimpleUrlAuthenticationFailureHandler,进入:
重定向到登录页面!
上一篇:数据结构--二叉树