【SpringSecurity系列2】基于SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析
源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-stateless
一、前言
在上一篇,我们实现了基于 SpringSecurity 实现前后端分离无状态 Rest API 的权限控制,在本篇我们将对其原理进行分析,从而加深对 SpringSecurity 的认识。
二、原理分析
1、SpringSecurity 中的过滤器及功能分析
SpringSecurity 的各种强大功能,是借助于多个过滤器组成一个过滤器链来实现的。我们在 DEBUG 查看 SpringSecurity 源码时,经常是跳来跳去,最后就跳晕了,没办法耐心多尝试几次吧。
以下是我列出的一些 SpringSecurity 的过滤器,根据名称我们能大概猜到他们的作用,我们选取几个重要的分析一下。
org.springframework.security.web.session.DisableEncodeUrlFilter
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.authentication.logout.LogoutFilter
com.ning.config.TokenAuthenticationFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
(1)SecurityContextPersistenceFilter
SecurityContextPersistenceFilter 的主要作用是在执行过滤器链的 doFilter 方法之前,从 SecurityContextRepository 中加载 SecurityContext。在执行完滤器链的 doFilter 方法之后,将 SecurityContext 保存到 SecurityContextRepository。
对应源码如下:
// 从 SecurityContextRepository 中加载 SecurityContext
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
// 将 SecurityContext 保存到 SecurityContextRepository
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
这里要说明下几个重要接口的作用:
SecurityContext:用于存取 Authentication,SpringSecurity 实现权限控制所必须的认证信息和权限集合,都是存储在 Authentication 中。
SecurityContextRepository:用于加载和保存 SecurityContext,它有三个实现类,分别是 HttpSessionSecurityContextRepository(从 HttpSession 中存取 SecurityContext)、RequestAttributeSecurityContextRepository(从
HttpServletRequest 中存取 SecurityContext)、NullSecurityContextRepository(不存储 SecurityContext)
SecurityContextHolder:将 SecurityContext 与当前执行线程相关联。它有四种不同的策略,分别是 ThreadLocalSecurityContextHolderStrategy (保存在 ThreadLocal 中,默认策略)、InheritableThreadLocalSecurityContextHolderStrategy(保存在 InheritableThreadLocal 中)、
GlobalSecurityContextHolderStrategy(全局)、CustomStrategy(自定义实现 SecurityContextHolderStrategy 接口)。
【PS】个人理解 SecurityContextRepository 和 SecurityContextHolder 的不同在于,SecurityContextRepository 是用于不同请求之间 SecurityContext 的存取策略,而 SecurityContextHolder 是用于同一个请求 SecurityContext 的存取。个人见解,如有不对请各位大佬指正。
(2)LogoutFilter
LogoutFilter 主要是用于处理退出的逻辑,默认的退出请求为 /logout。它涉及两个比较重要的接口 LogoutHandler 和 LogoutSuccessHandler。
LogoutHandler:处理退出逻辑,主要是清除 SecurityContext 和 清除 SecurityContext 中的 Authentication。
LogoutSuccessHandler:处理退出成功后的逻辑,默认的实现类是在退出成功后将请求重定向。
(3)UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter 主要是用于处理经由表单提交的用户名和密码的登录逻辑,默认的登录请求为 POST 方法的 /login 请求。在整个过滤器链中,真正执行的是其父类 AbstractAuthenticationProcessingFilter 的 doFilter 方法,而 AbstractAuthenticationProcessingFilter
的认证逻辑,是执行的其子类的 attemptAuthentication 方法后返回 Authentication。若认证成功则执行 successfulAuthentication 方法,认证失败则执行 unsuccessfulAuthentication 方法。对应源码如下:
// 交由子类执行具体的认证
Authentication authenticationResult = this.attemptAuthentication(request, response);
// 认证成功后处理逻辑
this.successfulAuthentication(request, response, chain, authenticationResult);
// 认证失败后处理逻辑
this.unsuccessfulAuthentication(request, response, var5);
(4)ExceptionTranslationFilter
ExceptionTranslationFilter 的作用是专门用于处理和 SpringSecurity 相关的异常,主要是 AuthenticationException 和 AccessDeniedException。若是 AuthenticationException,将执行 AuthenticationEntryPoint 的 commence 方法,默认情况
会进入 LoginUrlAuthenticationEntryPoint 中,会将请求重定向到 /login 执行登录认证。若是 AccessDeniedException,会判断当前的认证信息是否为匿名用户,不是匿名用户则交由 AccessDeniedHandler 处理,默认情况下会响应 403 的错误码,若是匿名用户,
则执行 AuthenticationEntryPoint 的 commence 方法,默认情况下也是重定向到 /login 执行登录认证。看下相应源码:
if (exception instanceof AuthenticationException) {
this.handleAuthenticationException(request, response, chain, (AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
this.handleAccessDeniedException(request, response, chain, (AccessDeniedException)exception);
}
// 默认情况进入 LoginUrlAuthenticationEntryPoint 请求重定向到 /login 执行登录认证
this.authenticationEntryPoint.commence(request, response, reason);
// 交由 AccessDeniedHandler 处理非匿名用户的 AccessDeniedException
this.accessDeniedHandler.handle(request, response, exception);
(5)FilterSecurityInterceptor
FilterSecurityInterceptor 中在执行 invoke 方法前,会先执行 beforeInvocation 方法。在 beforeInvocation 方法中,会根据配置的 SpringSecurity 属性,对当前用户的认证信息和权限集合进行校验,若校验失败则会抛出 AuthenticationException 或者 AccessDeniedException。
2、请求受保护资源,跳转登录流程分析
在上一小节中,我们分析了几个重要的过滤器的大致功能,下面我们来具体分析下,在未登录认证前,请求受保护资源,跳转到登录的具体流程。
1、拿上一篇《【SpringSecurity系列1】基于SpringSecurity实现前后端分离无状态Rest API的权限控制》的代码举例,我们先在 FilterSecurityInterceptor 打个断点,然后发送请求 http://localhost:8080/index。
2、我们跟随 DEBUG 进入 FilterSecurityInterceptor 类中的 beforeInvocation 方法,我们可以看到 ConfigAttribute 只有一个 “authenticated”,而它匹配的范围是 “any request”。这是因为我们在 WebSecurityConfig 中配置了
“http.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated()”。
3、继续往下,我们看到这个时候的 Authentication 是一个匿名用户,对应的 principal 是 “anonymousUser”,这是因为在经过 AnonymousAuthenticationFilter 后,SpringSecurity 帮我们创建了一个匿名用户放在了上下文中。
4、接着会进入 attemptAuthorization 方法,对权限做进一步的校验。经过 AccessDecisionVoter 投票器的结果,若 deny 数大于0,则会抛出 AccessDeniedException “org.springframework.security.access.AccessDeniedException: Access is denied”。
5、抛出的 AccessDeniedException 会被 ExceptionTranslationFilter 捕获,从而进入 handleSpringSecurityException 类中的 handleSpringSecurityException 方法逻辑。由于此时上下文中是一个匿名用户(anonymousUser),因此会执行 sendStartAuthentication 方法,
然后调用 authenticationEntryPoint 的 commence 方法。(此时的 authenticationEntryPoint 是默认的 LoginUrlAuthenticationEntryPoint)
6、在 LoginUrlAuthenticationEntryPoint 的 commence 方法中,我们可以看到最终的 redirectUrl 是 “http://localhost:8080/login”。到此时,请求就由 /index 重定向到了 /login。
7、那么在请求被重定向到 /login 后,又是怎样跳转到默认的登录页面的呢?我们先得修改下 WebSecurityConfig 中的配置将 .and().formLogin().loginPage("/login") 改为 .and().formLogin(),让 SpringSecurity 跳转默认的登录页面。改完配置后,我们重启项目。
8、项目重启完后,我们可以在控制台看到多了 DefaultLoginPageGeneratingFilter 和 DefaultLogoutPageGeneratingFilter 两个过滤器,顾名思义跳转默认登录页面的逻辑,就应该是在 DefaultLoginPageGeneratingFilter 中了。
9、最后,我就直接贴出 DefaultLoginPageGeneratingFilter 跳转默认登录页面的源码了,比较简单:
String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
10、到此请求受保护资源,跳转登录的流程分析完毕。
3、请求未授权资源,跳转异常处理流程分析
1、在使用账号密码登录(经过身份认证),访问未授权资源时,我们经过 DEBUG 可以看到,在 FilterSecurityInterceptor 中 Authentication 是我们登录后的用户,对应的 principal 是 “org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED]...”。
2、请求在顺利经过 FilterSecurityInterceptor 的 beforeInvocation 方法后,会继续进入后续的过滤器链,并最终进入 ApplicationFilterChain 的 this.servlet.service(request, response); 方法。
3、我们可以看到此时的 servlet 正是传说中的 DispatcherServlet,对于熟悉 Spring MVC 的同学就明白,之后将会经历请求分发,并最终找到对应的 Controller 进行方法的反射调用,此处我就不展开分析了。从 DispatcherServlet 到找对最终的 Controller 的代码还是比较复杂的,感兴趣的同学可以多 DEBUG 几次。
4、我在这里就直接讲结论了,由于我们在 Controller 的方法上,配置了 @PreAuthorize("hasRole('home')")。框架在找到目标对象 IndexController 后,会使用 Cglib 创建出代理对象,通过 AOP 的方式进行权限的判断,关键的类是 MethodSecurityInterceptor。
5、在 MethodSecurityInterceptor 类中的 beforeInvocation 方法,我们可以看到此时的 ConfigAttribute 是 “[authorize: 'hasRole('home')', filter: 'null', filterTarget: 'null']”。和身份认证的一样,经过 AccessDecisionVoter 投票器的结果,若 deny 数大于0,则会抛出 AccessDeniedException。
6、抛出的 AccessDeniedException 会被 ExceptionTranslationFilter 捕获,从而进入 handleSpringSecurityException 类中的 handleSpringSecurityException 方法逻辑。由于此时上下文中是经过身份认证的用户,故而会交由 AccessDeniedHandler 会处理相应异常。
7、默认的 AccessDeniedHandlerImpl 会将请求响应改为 403,对应源码如下:
response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
8、到此请求未授权资源,跳转异常处理的流程分析完毕。
三、配置的原理分析。
在上一篇《【SpringSecurity系列1】基于SpringSecurity实现前后端分离无状态Rest API的权限控制》,我们修改了一些配置,从而使 SpringSecurity 满足了前后端分离架构的需要,那么我们这么配置的依据是什么呢?
1、为什么改写 Get 方法的 /login 请求?
经过上文的分析,我们知道默认的 /login 请求会跳转到 DefaultLoginPageGeneratingFilter 生成的登录页面,这不符合前后端分离的需要。所以,我们自定义了 /login 请求,并向客户端响应 JSON。
2、为什么需要 TokenAuthenticationSuccessHandler 和 TokenAuthenticationFailureHandler
分析了 UsernamePasswordAuthenticationFilter 的源码之后,我们知道经过对账号密码校验后,会返回 Authentication。由于 SpringSecurity 默认是基于 session 对 Authentication 进行管理,为了达到实现前后端分离架构
的需要,我们需要自己实现对 Authentication 的管理。因此我们定义了 AuthenticationRepository,在身份认证成功后,我们创建出一个 token 并和 Authentication 关联起来存储在 AuthenticationRepository 中,若是身份
认证失败,我们则在 TokenAuthenticationFailureHandler 向客户端响应登录失败的 JSON 提示。
3、为什么需要 TokenLogoutSuccessHandler
默认的 SpringSecurity 配置是在 DefaultLogoutPageGeneratingFilter 处理退出请求 /logout,并向客户端响应一个退出页面。因此,我们需要创建 TokenLogoutSuccessHandler,在退出成功后,向客户端响应 JSON。
4、为什么需要 TokenAccessDeniedHandler
经过上文的分析,我们知道默认的 AccessDeniedHandlerImpl 会将请求响应改为 403。所以,我们需要在 TokenAccessDeniedHandler 中向客户端响应 JSON。
5、为什么需要 TokenAuthenticationFilter 并在 UsernamePasswordAuthenticationFilter 之前
TokenAuthenticationFilter 源码很简单,就是根据 header 中的 token,从 AuthenticationRepository 中找到对应的 Authentication 并放入 SecurityContext。
6、为什么需要配置 csrf().disable()
为了防止 CSRF,默认 SpringSecurity 开启防 CSRF 配置,会在 CsrfFilter 中对登录表单进行 token 校验。
四、总结
经过本篇的分析,我们对 SpringSecurity 会有更深刻的理解,在源码分析的过程中,DEBUG 跳来跳去会令人头痛,大家多多尝试,习惯就好了 O(∩_∩)O 哈哈~
在下一篇,我们将尝试使用 Spring Webflux 集成 SpringSecurity 来实现同样的效果,大家多多关注哦~
源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-stateless
【打个广告】推荐下个人的基于 SpringCloud 开源项目,供大家学习参考,欢迎大家留言进群交流
Gitee:https://gitee.com/ningzxspace/exam-ning-springcloud-v1
Github:https://github.com/ningzuoxin/exam-ning-springcloud-v1
【SpringSecurity系列2】基于SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析的更多相关文章
- 【SpringSecurity系列3】基于Spring Webflux集成SpringSecurity实现前后端分离无状态Rest API的权限控制
源码传送门: https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/02-springsecurity-state ...
- 【SpringSecurity系列1】基于SpringSecurity实现前后端分离无状态Rest API的权限控制
源码传送门: https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-state ...
- 用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo
简介 完整代码 https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom 运行展示 后端 主要展示 Spring Security ...
- 基于 koajs 的前后端分离实践
一.什么是前后端分离? 前后端分离的概念和优势在这里不再赘述,有兴趣的同学可以看各个前辈们一系列总结和讨论: 系列文章:前后端分离的思考与实践(1-6) slider: 淘宝前后端分离实践 知乎提问: ...
- (转)也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)
原文链接:http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/ 随着不同终端(pad/mobile/pc)的兴起 ...
- 也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)
前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们重新思考了“前后端”的定义,引入前端同学都熟悉的NodeJS,试图 ...
- 基于NodeJS进行前后端分离
1.什么是前后端分离 传统的SPA模式:所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现. 从某种意义上来说,SPA确实做到了前后端分离,但这种方式存在两个问题 ...
- [原创]基于VueJs的前后端分离框架搭建之完全攻略
首先请原谅本文标题取的有点大,但并非为了哗众取宠.本文取这个标题主要有3个原因,这也是写作本文的初衷: (1)目前国内几乎搜索不到全面讲解如何搭建前后端分离框架的文章,讲前后端分离框架思想的就更少了, ...
- [转] 基于NodeJS的前后端分离的思考与实践(五)多终端适配
前言 近年来各站点基于 Web 的多终端适配进行得如火如荼,行业间也发展出依赖各种技术的解决方案.有如基于浏览器原生 CSS3 Media Query 的响应式设计.基于云端智能重排的「云适配」方案等 ...
随机推荐
- String能变化吗?和StringBuffer的区别是什么
[新手可忽略不影响继续学习]看 过上面例子的童鞋一定会觉得很奇怪,s = s + s1.charAt(i); 马克-to-win, s不是老在变化吗?其实s = "";时,虚拟机会 ...
- java中的访问控制有什么用?如何用法?请举例
9.访问控制 [新手可忽略不影响继续学习] 访问控制有什么用?在软件公司里是这么用的,我们想像一种场景,在你的类中,你编了三个私有方法,马克-to-win,别人当然都用不了,但在类外,你也是用不了的, ...
- java基础-字符流
字符流 * 字符流是可以直接读写字符的IO流 * 字符流读取字符, 就要先读取到字节数据, 然后转为字符. 如果要写出字符, 需要把字符转为字节再写出. FileReader * FileRea ...
- 解决zabbix5字体中文口口乱码
环境信息 系统:Ubuntu20.04 zabbix版本:5.4 解决方法一 此方法比较偷懒,就是不改变zabbix相关配置,直接用原名替换字体文件. 原字体字体名称为DejaVuSans.将方法二的 ...
- SprigCloud入门踩坑之创建bean失败
昨天也是报同样的错误,恰好要去吃饭,着急就没找到问题,把项目删了,后续看了路飞大佬的通过pom导入依赖的方式,但是资源导入太慢随放弃. 昨晚熄灯前二十分钟又从头敲了一遍,敲好就断电了,没来得及启动,今 ...
- B. Lord of the Values 思维数学建构 附加 英文翻译
原题链接 Problem - 1523B - Codeforces 题目及部分翻译 While trading on(贸易,利用) his favorite exchange trader Willi ...
- Not Adding - 题解【数学,枚举】
题面 原题链接(CF1627D) You have an array \(a_1,a_2,-,a_n\) consisting of \(n\) distinct integers. You are ...
- 《手把手教你》系列基础篇(九十一)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-下篇(详解教程)
1.简介 为了方便查看和归档:(1)不同包的日志可能要放到不同的文件中,如service层和dao层的日志:(2)不同日志级别:调试.信息.警告和错误等也要分文件输出.所以宏哥今天主要介绍和分享的是: ...
- RxJava + Retrofit源码解析
RxJava + Retrofit怎么请求网络,具体的用法这里就不讲了,本文只讲一些重点源码. 版本如下: okhttp : "com.squareup.okhttp3:okhttp:3.1 ...
- Antd Modal 可拖拽移动
一 目标: 实现antd Modal 弹窗或者其他弹窗的点击标题进行拖拽的效果 二 准备及思录: 1.使用antd Modal 组件,要想改变位置需要改变Modal style 的left 和top ...