说实话,其实Spring Security并没有看起来那么复杂(附源码)

2023-11-16

权限管理是每个项目必备的功能,只是各自要求的复杂程度不同,简单的项目可能一个 Filter 或 Interceptor 就解决了,复杂一点的就可能会引入安全框架,如 Shiro, Spring Security 等。
其中 Spring Security 因其涉及的流程、类过多,看起来比较复杂难懂而被诟病。但如果能捋清其中的关键环节、关键类,Spring Security 其实也没有传说中那么复杂。本文结合脚手架框架的权限管理实现(jboost-auth 模块,源码获取见文末),对 Spring Security 的认证、授权机制进行深入分析。

使用 Spring Security 认证、鉴权机制

Spring Security 主要实现了

  • Authentication(认证——你是谁?)
  • Authorization(鉴权——你能干什么?)

认证(登录)流程

Spring Security 的认证流程及涉及的主要类如下图,

SpringSecurity认è¯

认证入口为 AbstractAuthenticationProcessingFilter,一般实现有 UsernamePasswordAuthenticationFilter

  1. filter 解析请求参数,将客户端提交的用户名、密码等封装为 Authentication,Authentication 一般实现有 UsernamePasswordAuthenticationToken
  2. filter 调用 AuthenticationManager 的 authenticate() 方法对 Authentication 进行认证,AuthenticationManager 的默认实现是
    ProviderManager
  3. ProviderManager 认证时,委托给一个 AuthenticationProvider 列表,调用列表中 AuthenticationProvider 的 authenticate()
    方法来进行认证,只要有一个通过,则认证成功,否则抛出 AuthenticationException 异常(AuthenticationProvider 还有一个 supports() 方法,用来判断该 Provider
    是否对当前类型的 Authentication 进行认证)
  4. 认证完成后,filter 通过 AuthenticationSuccessHandler(成功时) 或 AuthenticationFailureHandler(失败时)来对认证结果进行处理,如返回 token 或 认证错误提示

认证涉及的关键类

  1. 登录认证入口 UsernamePasswordAuthenticationFilter

项目中 RestAuthenticationFilter 继承了 UsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter 将客户端提交的参数封装为
UsernamePasswordAuthenticationToken,供 AuthenticationManager 进行认证。

RestAuthenticationFilter 覆写了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(request,response) 方法逻辑,根据
loginType 的值来将登录参数封装到认证信息 Authentication 中,(loginType 为 USER 时为 UsernameAuthenticationToken,
loginType 为 Phone 时为 PhoneAuthenticationToken),供下游 AuthenticationManager 进行认证。

  1. 认证信息 Authentication

使用 Authentication 的实现来保存认证信息,一般为 UsernamePasswordAuthenticationToken,包括

  • principal:身份主体,通常是用户名或手机号
  • credentials:身份凭证,通常是密码或手机验证码
  • authorities:授权信息,通常是角色 Role
  • isAuthenticated:认证状态,表示是否已认证

本项目中的 Authentication 实现:

  • UsernameAuthenticationToken: 使用用户名登录时封装的 Authentication

    • principal => username
    • credentials => password
    • 扩展了两个属性: uuid, code,用来验证图形验证码
  • PhoneAuthenticationToken: 使用手机验证码登录时封装的 Authentication

    • principal => phone(手机号)
    • credentials => code(验证码)

两者都继承了 UsernamePasswordAuthenticationToken。

  1. 认证管理器 AuthenticationManager

认证管理器接口 AuthenticationManager,包含一个 authenticate(authentication) 方法。
ProviderManager 是 AuthenticationManager 的实现,管理一个 AuthenticationProvider(具体认证逻辑提供者)列表。在其 authenticate(authentication ) 方法中,对 AuthenticationProvider 列表中每一个 AuthenticationProvider,调用其 supports(Class<?> authentication) 方法来判断是否采用该
Provider 来对 Authentication 进行认证,如果适用则调用 AuthenticationProvider 的 authenticate(authentication)
来完成认证,只要其中一个完成认证,则返回。

  1. 认证提供者 AuthenticationProvider

由3可知认证的真正逻辑由 AuthenticationProvider 提供,本项目的认证逻辑提供者包括

  • UsernameAuthenticationProvider: 支持对 UsernameAuthenticationToken 类型的认证信息进行认证。同时使用 PasswordRetryUserDetailsChecker
    来对密码错误次数超过5次的用户,在10分钟内限制其登录操作
  • PhoneAuthenticationProvider: 支持对 PhoneAuthenticationToken 类型的认证信息进行认证

两者都继承了 DaoAuthenticationProvider —— 通过 UserDetailsService 的 loadUserByUsername(String username) 获取保存的用户信息
UserDetails,再与客户端提交的认证信息 Authentication 进行比较(如与 UsernameAuthenticationToken 的密码进行比对),来完成认证。

  1. 用户信息获取 UserDetailsService

UserDetailsService 提供 loadUserByUsername(username) 方法,可获取已保存的用户信息(如保存在数据库中的用户账号信息)。

本项目的 UserDetailsService 实现包括

  • UsernameUserDetailsService:通过用户名从数据库获取账号信息
  • PhoneUserDetailsService:通过手机号码从数据库获取账号信息
  1. 认证结果处理

认证成功,调用 AuthenticationSuccessHandler 的 onAuthenticationSuccess(request, response, authentication) 方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 时进行了设置。 本项目中认证成功后,生成 jwt token返回客户端。

认证失败(账号校验失败或过程中抛出异常),调用 AuthenticationFailureHandler 的 onAuthenticationFailure(request, response, exception) 方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 时进行了设置,返回错误信息。

以上关键类及其关联基本都在 SecurityConfiguration 进行配置。

  1. 工具类

SecurityContextHolder 是 SecurityContext 的容器,默认使用 ThreadLocal 存储,使得在相同线程的方法中都可访问到 SecurityContext。
SecurityContext 主要是存储应用的 principal 信息,在 Spring Security 中用 Authentication 来表示。在
AbstractAuthenticationProcessingFilter 中,认证成功后,调用 successfulAuthentication() 方法使用 SecurityContextHolder 来保存
Authentication,并调用 AuthenticationSuccessHandler 来完成后续工作(比如返回token等)。

使用 SecurityContextHolder 来获取用户信息示例:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

鉴权流程

Spring Security 的鉴权(授权)有两种实现机制:

  • FilterSecurityInterceptor:通过 Filter 对 HTTP 资源的访问进行鉴权
  • MethodSecurityInterceptor:通过 AOP 对方法的调用进行鉴权。在 GlobalMethodSecurityConfiguration 中注入,
    需要在配置类上添加注解 @EnableGlobalMethodSecurity(prePostEnabled = true) 使 GlobalMethodSecurityConfiguration 配置生效。

鉴权流程及涉及的主要类如下图,

  1. 登录完成后,一般返回 token 供下次调用时携带进行身份认证,生成 Authentication
  2. FilterSecurityInterceptor 拦截器通过 FilterInvocationSecurityMetadataSource 获取访问当前资源需要的权限
  3. FilterSecurityInterceptor 调用鉴权管理器 AccessDecisionManager 的 decide 方法进行鉴权
  4. AccessDecisionManager 通过 AccessDecisionVoter 列表的鉴权投票,确定是否通过鉴权,如果不通过则抛出 AccessDeniedException 异常
  5. MethodSecurityInterceptor 流程与 FilterSecurityInterceptor 类似

鉴权涉及的关键类

  1. 认证信息提取 RestAuthorizationFilter

对于前后端分离项目,登录完成后,接下来我们一般通过登录时返回的 token 来访问接口。

在鉴权开始前,我们需要将 token 进行验证,然后生成认证信息 Authentication 交给下游进行鉴权(授权)。

本项目 RestAuthorizationFilter 将客户端上报的 jwt token 进行解析,得到 UserDetails, 并对 token 进行有效性校验,并生成
Authentication(UsernamePasswordAuthenticationToken),通过
SecurityContextHolder 存入 SecurityContext 中供下游使用。

  1. 鉴权入口 AbstractSecurityInterceptor

三个实现:

  • FilterSecurityInterceptor:基于 Filter 的鉴权实现,作用于 Http 接口层级。FilterSecurityInterceptor 从 SecurityMetadataSource 的实现 DefaultFilterInvocationSecurityMetadataSource 获取要访问资源所需要的权限
    Collection,然后调用 AccessDecisionManager 进行授权决策投票,若投票通过,则允许访问资源,否则将禁止访问。
  • MethodSecurityInterceptor:基于 AOP 的鉴权实现,作用于方法层级。
  • AspectJMethodSecurityInterceptor:用来支持 AspectJ JointPoint 的 MethodSecurityInterceptor
  1. 获取资源权限信息 SecurityMetadataSource

SecurityMetadataSource 读取访问资源所需的权限信息,读取的内容,就是我们配置的访问规则,如我们在配置类中配置的访问规则:

@Override
protected void configure(HttpSecurity http) throws Exception{
    http.authorizeRequests()
        .antMatchers(excludes).anonymous()
        .antMatchers("/api1").hasAuthority("permission1")
        .antMatchers("/api2").hasAuthority("permission2")
        ...
}

我们可以自定义一个 SecurityMetadataSource 来从数据库或其它存储中获取资源权限规则信息。

  1. 鉴权管理器 AccessDecisionManager

AccessDecisionManager 接口的 decide(authentication, object, configAttributes) 方法对本次请求进行鉴权,其中

  • authentication:本次请求的认证信息,包含 authority(如角色) 信息
  • object:当前被调用的被保护对象,如接口
  • configAttributes:与被保护对象关联的配置属性,表示要访问被保护对象需要满足的条件,如角色

AccessDecisionManager 接口的实现者鉴权时,最终是通过调用其内部 List<AccessDecisionVoter<?>> 列表中每一个元素的 vote(authentication, object, attributes)
方法来进行的,根据决策的不同分为如下三种实现

  • AffirmativeBased:一票通过权策略。只要有一个 AccessDecisionVoter 通过(AccessDecisionVoter.vote 返回 AccessDecisionVoter.
    ACCESS_GRANTED),则鉴权通过。为默认实现
  • ConsensusBased:少数服从多数策略。多数 AccessDecisionVoter 通过,则鉴权通过,如果赞成票与反对票相等,则根据变量 allowIfEqualGrantedDeniedDecisions
    的值来决定,该值默认为 true
  • UnanimousBased:全票通过策略。所有 AccessDecisionVoter 通过或弃权(返回 AccessDecisionVoter.
    ACCESS_ABSTAIN),无一反对则通过,只要有一个反对就拒绝;如果全部弃权,则根据变量 allowIfAllAbstainDecisions 的值来决定,该值默认为 false
  1. 鉴权投票者 AccessDecisionVoter

与 AuthenticationProvider 类似,AccessDecisionVoter 也包含 supports(attribute) 方法(是否采用该 Voter 来对请求进行鉴权投票) 与 vote (authentication, object, attributes) 方法(具体的鉴权投票逻辑)

FilterSecurityInterceptor 的 AccessDecisionManager 的投票者列表(AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor() 中设置)包括:

  • WebExpressionVoter:验证 Authentication 的 authenticated。

MethodSecurityInterceptor 的 AccessDecisionManager 的投票者列表(GlobalMethodSecurityConfiguration.accessDecisionManager()
中设置)包括:

  • PreInvocationAuthorizationAdviceVoter: 如果 @EnableGlobalMethodSecurity 注解开启了 prePostEnabled,则添加该 Voter,对使用了 @PreAuthorize 注解的方法进行鉴权投票
  • Jsr250Voter:如果 @EnableGlobalMethodSecurity 注解开启了 jsr250Enabled,则添加该 Voter,对 @Secured 注解的方法进行鉴权投票
  • RoleVoter:总是添加, 如果 ConfigAttribute.getAttribute() 以 ROLE_ 开头,则参与鉴权投票
  • AuthenticatedVoter:总是添加,如果 ConfigAttribute.getAttribute() 值为
    IS_AUTHENTICATED_FULLYIS_AUTHENTICATED_REMEMBEREDIS_AUTHENTICATED_ANONYMOUSLY 其中一个,则参与鉴权投票
  1. 鉴权结果处理

ExceptionTranslationFilter 异常处理 Filter, 对认证鉴权过程中抛出的异常进行处理,包括:

  • authenticationEntryPoint: 对过滤器链中抛出 AuthenticationException 或 AccessDeniedException 但 Authentication 为
    AnonymousAuthenticationToken 的情况进行处理。如果 token 校验失败,如 token 错误或过期,则通过 ExceptionTranslationFilter 的 AuthenticationEntryPoint 进行处理,本项目使用 RestAuthenticationEntryPoint 来返回统一格式的错误信息
  • accessDeniedHandler: 对过滤器链中抛出 AccessDeniedException 但 Authentication 不为 AnonymousAuthenticationToken 的情况进行处理,本项目使用 RestAccessDeniedHandler 来返回统一格式的错误信息

如果是 MethodSecurityInterceptor 鉴权时抛出 AccessDeniedException,并且通过 @RestControllerAdvice 提供了统一异常处理,则将由统一异常处理类处理,因为
MethodSecurityInterceptor 是 AOP 机制,可由 @RestControllerAdvice 捕获。

本项目中, RestAuthorizationFilter 在 Filter 链中位于 ExceptionTranslationFilter 的前面,所以其中抛出的异常也不能被 ExceptionTranslationFilter 捕获, 由 cn.jboost.base.starter.web.ExceptionHandlerFilter 捕获处理。

也可以将 RestAuthorizationFilter 放入 ExceptionTranslationFilter 之后,但在 RestAuthorizationFilter 中需要对 SecurityContextHolder.getContext().getAuthentication() 进行 AnonymousAuthenticationToken 的判断,因为 AnonymousAuthenticationFilter 位于 ExceptionTranslationFilter 前面,会对 Authentication 为空的请求生成一个
AnonymousAuthenticationToken,放入 SecurityContext 中。

总结

安全框架一般包括认证与授权两部分,认证解决你是谁的问题,即确定你是否有合法的访问身份,授权解决你是否有权限访问对应资源的问题。Spring Security 使用 Filter 来实现认证,使用 Filter(接口层级) + AOP(方法层级)的方式来实现授权。本文相对偏理论,但也结合了脚手架中的实现,对照查看,应该更易理解。

本文基于 Spring Boot 脚手架中的权限管理模块编写,该脚手架提供了前后端分离的权限管理实现。可关注作者公众号 【Java烂猪皮】,回复 【666】 获取源码地址。


为帮助开发者们提升面试技能、有机会入职BATJ等大厂公司,特别制作了这个专辑——这一次整体放出。

大致内容包括了: Java 集合、JVM、多线程、并发编程、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大厂面试题等、等技术栈! 

需要获取以下这些面试题答案以及学习资料得话麻烦一键三连之后微信扫描下图作者助手的微信:( wjn168178 )添加即可免费获取到哦

å¨è¿éæå¥å¾çæè¿°

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章ing

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

说实话,其实Spring Security并没有看起来那么复杂(附源码) 的相关文章

随机推荐

  • JavaScript分支语句总结

    注 js变量算术运算符和逻辑运算符知识点的补充 1 的区别 表示值相等 表示值相等 数据类型也必须相等 案例 的区别 表示值相等 表示值相等 数据类型也必须相等 var x 10 var y 10 console log x y true
  • 图像降质

    1 逆滤波和维纳滤波 附Matlab完整代码 https blog csdn net weixin 41730407 article details 80455612 2 python 运动模糊 退化模型 点扩散函数 逆滤波与维纳滤波 ht
  • GG-CNN代码学习

    文章目录 1 源码网址 https github com dougsm ggcnn 2 数据集格式转化 下载后的康奈尔数据集 解压完之后里面的格式 里面的 tiff图像通过 txt文件转化得到 python m utils dataset
  • layui 数据表格 sort排序,filter过滤——soulTable

    1 效果图 2 页面代码 div class fp table style margin left 0 5 width 86 table style margin bottom 0px table div 3 js代码 引入扩展组件 lay
  • 【学vue跟玩一样】快速搞懂vue渲染

    Vue的渲染分为条件渲染和列表渲染 那究竟什么式渲染呢 1 条件渲染 1 v if写法 1 v if 表达式 2 v else if 表达式 3 v else 表达式 和我们曾经学过的JavaScript里面的if语句几乎一样 适用于 切换
  • Quartz misfire详解

    一 前言 最近在学习Quartz 看到misfire这一部分 发现官方文档上讲解的很简单 没有看明白 然后去搜索了一下网上的讲解 发现讲的也都大同小异 也没有看明白 最后只能自己动手做测试 总结了一下 这篇文章把自己总结的记录下来 方便自己
  • 使用 HEX 参数在 Python 中实现六边形图像的显示数据关系

    使用 HEX 参数在 Python 中实现六边形图像的显示数据关系 在数据可视化中 六边形图被广泛应用于显示多元数据之间的关系 本文将介绍如何使用 Python 中的 hex 参数来设置六边形图像 并展示如何使用这种方法来显示数据的关系 首
  • Spring Boot —— Security 控制按钮权限

    文章目录 Spring Boot Security 控制按钮权限 前言 实现 引入对应的依赖 配置标签 Spring Boot Security 控制按钮权限 前言 在freemarker中 通过Security根据用户角色控制页面按钮或菜
  • win8.1仅允许运行使用网络级别身份认证的远程桌面计算机连接,使用Win10通过Mstsc远程连接 Server 2012 R2 时出现 身份验证错误,要求的函数不受支持,这可能是由于CredSSP...

    使用Win10通过Mstsc远程连接 Server 2012 R2 时出现 身份验证错误 要求的函数不受支持 这可能是由于CredSSP加密Oracle修正 最终解决方法 原因 因为CVE 2018 0886 的 CredSSP 2018
  • unity shader 之基础四 数学

    4 2 笛卡尔坐标系 笛卡尔坐标系分为二维和三维坐标系 4 2 1二位坐标系 OpenGL 和 DirectX 二位坐标系是不同的 OpenGL 和 DirectX 是不同的图形访问接口 用来和硬件交互的 二维坐标系 是可以相互转换的 既
  • 【经典】centos 安装 mysql

    CentOS第一次安装MySQL的完整步骤 目录 1 官方安装文档 2 下载 Mysql yum包 3 安转软件源 4 安装mysql服务端 5 首先启动mysql 6 接着检查mysql 的运行状态 7 修改临时密码 7 1 获取MySQ
  • [转] 英文写作中分号和冒号的使用

    我们先来了解下分号和冒号的作用 分号的主要作用是来连接两个在语法上平等的成分 冒号的主要作用是引起读者对冒号后面内容的注意力 下面总结下规则 用分号的情况 1 用分号连接两个独立的句子 两个独立的句子不能够用逗号隔开 如果用逗号 必须逗号后
  • idea忽略.iml文件

    1 点击file文件下的设置中 2 点下file types 文件类型 进入到file types窗口 如图 然后点击忽略文件那添加需要忽略的类型
  • 自用HTML+CSS学习笔记

    HTML CSS学习笔记 1 Web标准 Web标准也称为网页标准 由一系列的标准组成 大部分由W3C World Wide Web Consortium 万维网联盟 负责制定 由三个组成部分 HTML 负责网页的结构 页面元素和内容 CS
  • IT的教育

    IT的教育 李颜芯 CSDN的网友大家好 欢迎大家收看这一起的CSDN视频访谈节目 今天我们请到了两位嘉宾 一位是 金旭亮 老师 一位是 金戈 老师 两位老师作一下自我介绍怎么样 金旭亮 我先介绍一下吧 我叫金旭亮是北京理工大学的讲师 我在
  • 怎样把pdf转换成word-多语言ocr支持

    http jingyan baidu com article 86fae34699bb4e3c49121a23 html PDF格式良好的视觉阅读性和通用性使得PDF文件的使用越来越广泛了 网络上的PDF资料也越来越多 但是我们往往想要提出
  • 【大屏】 amap + echarts 踩坑以及避免办法

    amap echarts 踩坑以及避免办法 大屏 踩坑 代码 大屏 html body container margin 0 padding 0 width 5376px height 1944px background color 000
  • softmax用于分类问题/逻辑回归

    参考 d2l 线性回归问题最后输出一个参数用于预测 多分类问题最后输出多个维度的数据 多少个output channels就有多少个类别 softmax是一种激活函数 它常见于分类问题的最后一层激活函数 目的是让输出属于一个概率密度函数 我
  • AI「领悟」有理论解释了!谷歌:两种脑回路内部竞争,训练久了突然不再死记硬背...

    梦晨 发自 凹非寺量子位 公众号 QbitAI 谷歌PAIR团队不久前撰文介绍了AI的 领悟 Grokking 现象 训练久了突然不再死记硬背 而是学会举一反三 有了泛化能力 不出一个月 另一只团队 主要成员来自DeepMind 表示 已经
  • 说实话,其实Spring Security并没有看起来那么复杂(附源码)

    权限管理是每个项目必备的功能 只是各自要求的复杂程度不同 简单的项目可能一个 Filter 或 Interceptor 就解决了 复杂一点的就可能会引入安全框架 如 Shiro Spring Security 等 其中 Spring Sec