【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 的响应式设计.基于云端智能重排的「云适配」方案等 ...
随机推荐
- Mybatis实现多级菜单查询
写在前面 最近实现一个小需求,前端需要菜单的信息,需要向后端发起获取菜单的请求,菜单又是一个多级菜单,后端我用的mybatis进行数据库查询,实现的方法我这里想到有两种,欢迎大家补充. 1. 在Men ...
- 让我们写一个 Win32 文本编辑器吧 - 2. 计划和显示
让我们写一个 Win32 文本编辑器吧 - 2. 计划和显示 如果你已经阅读了简介,相信你已经对我们接下来要做的事情有所了解. 本文,将会把简介中基础程序修改为一个窗体应用程序.并对编辑器接下来的编辑 ...
- 设计模式学习笔记(十六)迭代器模式及其在Java 容器中的应用
迭代器(Iterator)模式,也叫做游标(Cursor)模式.我们知道,在Java 容器中,为了提高容器遍历的方便性,把遍历逻辑从不同类型的集合类中抽取出来,避免向外部暴露集合容器的内部结构. 一. ...
- Alibaba Java诊断工具Arthas查看Dubbo动态代理类
原创/朱季谦 阅读Dubbo源码过程中,会发现,Dubbo消费端在做远程调用时,默认通过 Javassist 框架为服务接口生成动态代理类,接着再去调用代理类实现远程接口调用.在阅读这部分源码时,最后 ...
- 4.Java开发环境的搭建
Java开发环境搭建 一.JDK下载与安装 JDK8下载地址 选择目录,点击下一步 二.配置环境变量 变量名:JAVA_HOME 变量值:JDK安装路径 变量值:CLASSPATH 变量值:.;%JA ...
- PowerDotNet平台化软件架构设计与实现系列(13):应用监控平台
本文再写一篇和具体业务逻辑几乎无关的公共服务应用监控平台.PowerDotNet自研的应用监控平台系统,是服务治理的重要拼图,和服务治理平台配合使用效果更好. 监控开源产品非常丰富,站在巨人的肩膀上, ...
- Halo 开源项目学习(七):缓存机制
基本介绍 我们知道,频繁操作数据库会降低服务器的系统性能,因此通常需要将频繁访问.更新的数据存入到缓存.Halo 项目也引入了缓存机制,且设置了多种实现方式,如自定义缓存.Redis.LevelDB ...
- 网络协议之:Domain name service DNS详解
目录 简介 DNS的功能 DNS的组成 域名空间Domain name space Name servers DNS的工作流程 DNS资源记录 DNS消息的结构 总结 简介 现在是互联网的世界,大家从 ...
- Linux网络重点知识总结性梳理
一个执着于技术的公众号 1 OSI七层模型 层次 说明 功能/协议 应用层 应用程序及接口 提供应用程序的接口:FTP telnet http pop3等 表示层 对数据进行转换.加密和压缩 将上层的 ...
- EdgeFormer: 向视觉 Transformer 学习,构建一个比 MobileViT 更好更快的卷积网络
前言 本文主要探究了轻量模型的设计.通过使用 Vision Transformer 的优势来改进卷积网络,从而获得更好的性能. 欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结.最新技术跟 ...