深入理解Spring Security授权机制原理

简介: 在Spring Security权限框架里,若要对后端http接口实现权限授权控制,有两种实现方式。一种是基于注解方法级的鉴权,其中,注解方式又有@Secured和@PreAuthorize两种。

原创/朱季谦

在Spring Security权限框架里,若要对后端http接口实现权限授权控制,有两种实现方式。

一、一种是基于注解方法级的鉴权,其中,注解方式又有@Secured和@PreAuthorize两种。

@Secured如:

@PostMapping("/test")
@Secured({WebResRole.ROLE_PEOPLE_W})
publicvoidtest(){
......
returnnull;
}

@PreAuthorize如:

@PostMapping("save")
@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")
publicRestResponsesave(@RequestBody@ValidatedSysUsersysUser, BindingResultresult) {
ValiParamUtils.ValiParamReq(result);
returnsysUserService.save(sysUser);
}


二、一种基于config配置类,需在对应config类配置@EnableGlobalMethodSecurity(prePostEnabled = true)注解才能生效,其权限控制方式如下:

@Overrideprotectedvoidconfigure(HttpSecurityhttpSecurity) throwsException {
//使用的是JWT,禁用csrfhttpSecurity.cors().and().csrf().disable()
//设置请求必须进行权限认证               .authorizeRequests()
//首页和登录页面               .antMatchers("/").permitAll()
               .antMatchers("/login").permitAll()
// 其他所有请求需要身份认证              .anyRequest().authenticated();
//退出登录处理httpSecurity.logout().logoutSuccessHandler(...);
//token验证过滤器httpSecurity.addFilterBefore(...);
  }

这两种方式各有各的特点,在日常开发当中,普通程序员接触比较多的,则是注解方式的接口权限控制。

那么问题来了,我们配置这些注解或者类,其security框是如何帮做到能针对具体的后端API接口做权限控制的呢?

单从一行@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")注解上看,是看不出任何头绪来的,若要回答这个问题,还需深入到源码层面,方能对security授权机制有更好理解。

若要对这个过程做一个总的概述,笔者整体以自己的思考稍作了总结,可以简单几句话说明其整体实现,以该接口为例:

@PostMapping("save")
@PreAuthorize("hasAuthority('sys:user:add')")
publicRestResponsesave(@RequestBody@ValidatedSysUsersysUser, BindingResultresult) {
ValiParamUtils.ValiParamReq(result);
returnsysUserService.save(sysUser);
   }

即,认证通过的用户,发起请求要访问“/save”接口,若该url请求在配置类里设置为必须进行权限认证的,就会被security框架使用filter拦截器对该请求进行拦截认证。拦截过程主要一个动作,是把该请求所拥有的权限集与@PreAuthorize设置的权限字符“sys:user:add”进行匹配,若能匹配上,说明该请求是拥有调用“/save”接口的权限,那么,就可以被允许执行该接口资源。

 

在springboot+security+jwt框架中,通过一系列内置或者自行定义的过滤器Filter来达到权限控制,如何设置自定义的过滤器Filter呢?例如,可以通过设置httpSecurity.addFilterBefore(new JwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)来自定义一个基于JWT拦截的过滤器JwtFilter,这里的addFilterBefore方法将在下一篇文详细分析,这里暂不展开,该方法大概意思就是,将自定义过滤器JwtFilter加入到Security框架里,成为其中的一个优先安全Filter,代码层面就是将自定义过滤器添加到List<Filter> filters。

 

设置增加自行定义的过滤器Filter伪代码如下:

@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter {
       ......
@Overrideprotectedvoidconfigure(HttpSecurityhttpSecurity) throwsException {
//使用的是JWT,禁用csrfhttpSecurity.cors().and().csrf().disable()
//设置请求必须进行权限认证                  .authorizeRequests()
                  ......
//首页和登录页面                  .antMatchers("/").permitAll()
                  .antMatchers("/login").permitAll()
// 其他所有请求需要身份认证                  .anyRequest().authenticated();
          ......
//token验证过滤器httpSecurity.addFilterBefore(newJwtFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
      }
  }

该过滤器类extrends继承BasicAuthenticationFilter,而BasicAuthenticationFilter是继承OncePerRequestFilter,该过滤器确保在一次请求只通过一次filter,而不需要重复执行。这样配置后,当请求过来时,会自动被JwtFilter类拦截,这时,将执行重写的doFilterInternal方法,在SecurityContextHolder.getContext().setAuthentication(authentication)认证通过后,会执行过滤器链FilterChain的方法chain.doFilter(request, response);

publicclassJwtFilterextendsBasicAuthenticationFilter {
@AutowiredpublicJwtFilter(AuthenticationManagerauthenticationManager) {
super(authenticationManager);
       }
@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest, HttpServletResponseresponse, FilterChainchain) throwsIOException, ServletException {
// 获取token, 并检查登录状态// 获取令牌并根据令牌获取登录认证信息Authenticationauthentication=JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
     }
  }


那么,问题来了,过滤器链FilterChain究竟是什么?

这里,先点进去看下其类源码:

packagejavax.servlet;
importjava.io.IOException;
publicinterfaceFilterChain {
voiddoFilter(ServletRequestvar1, ServletResponsevar2) throwsIOException, ServletException;
   }

FilterChain只有一个 doFilter方法,这个方法的作用就是将请求request转发到下一个过滤器filter进行过滤处理操作,执行过程如下:

过滤器链就像一条铁链,中间的每个过滤器都包含对另一个过滤器的引用,从而把相关的过滤器链接起来,像一条链的样子。这时请求线程就如蚂蚁一样,会沿着这条链一直爬过去-----即,通过各过滤器调用另一个过滤器引用方法chain.doFilter(request, response),实现一层嵌套一层地将请求传递下去,当该请求传递到能被处理的的过滤器时,就会被处理,处理完成后转发返回。通过过滤器链,可实现在不同的过滤器当中对请求request做处理,且过滤器之间彼此互不干扰。

Spring Security框架上过滤器链上都有哪些过滤器呢?

 

可以在DefaultSecurityFilterChain类根据输出相关log或者debug来查看Security都有哪些过滤器,如在DefaultSecurityFilterChain类中的构造器中打断点,如图所示,可以看到,自定义的JwtFilter过滤器也包含其中:

这些过滤器都在同一条过滤器链上,即通过chain.doFilter(request, response)可将请求一层接一层转发,处理请求接口是否授权的主要过滤器是FilterSecurityInterceptor,其主要作用如下:

1. 获取到需访问接口的权限信息,即@Secured({WebResRole.ROLE_PEOPLE_W}) 或@PreAuthorize定义的权限信息;

2. 根据SecurityContextHolder中存储的authentication用户信息,来判断是否包含与需访问接口的权限信息,若包含,则说明拥有该接口权限;

3. 主要授权功能在父类AbstractSecurityInterceptor中实现;

 

我们将从FilterSecurityInterceptor这里开始重点分析Security授权机制原理的实现。

过滤器链将请求传递转发FilterSecurityInterceptor时,会执行FilterSecurityInterceptor的doFilter方法:

 

1publicvoiddoFilter(ServletRequestrequest, ServletResponseresponse,
2FilterChainchain) throwsIOException, ServletException {
3FilterInvocationfi=newFilterInvocation(request, response, chain);
4invoke(fi);
5 }

在这段代码当中,FilterInvocation类是一个有意思的存在,其实它的功能很简单,就是将上一个过滤器传递过滤的request,response,chain复制保存到FilterInvocation里,专门供FilterSecurityInterceptor过滤器使用。它的有意思之处在于,是将多个参数统一归纳到一个类当中,其到统一管理作用,你想,若是N多个参数,传进来都分散到类的各个地方,参数多了,代码多了,方法过于分散时,可能就很容易造成阅读过程中,弄糊涂这些个参数都是哪里来了。但若统一归纳到一个类里,就能很快定位其来源,方便代码阅读。网上有人提到该FilterInvocation类还起到解耦作用,即避免与其他过滤器使用同样的引用变量。

总而言之,这个地方的设定虽简单,但很值得我们学习一番,将其思想运用到实际开发当中,不外乎也是一种能简化代码的方法。

FilterInvocation主要源码如下:

1publicclassFilterInvocation {
23privateFilterChainchain;
4privateHttpServletRequestrequest;
5privateHttpServletResponseresponse;
678publicFilterInvocation(ServletRequestrequest, ServletResponseresponse,
9FilterChainchain) {
10if ((request==null) || (response==null) || (chain==null)) {
11thrownewIllegalArgumentException("Cannot pass null values to constructor");
12       }
1314this.request= (HttpServletRequest) request;
15this.response= (HttpServletResponse) response;
16this.chain=chain;
17    }
18    ......
19 }

FilterSecurityInterceptor的doFilter方法里调用invoke(fi)方法:

1publicvoidinvoke(FilterInvocationfi) throwsIOException, ServletException {
2if ((fi.getRequest() !=null)
3&& (fi.getRequest().getAttribute(FILTER_APPLIED) !=null)
4&&observeOncePerRequest) {
5//筛选器已应用于此请求,每个请求处理一次,所以不需重新进行安全检查 6fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
7    }
8else {
9// 第一次调用此请求时,需执行安全检查10if (fi.getRequest() !=null&&observeOncePerRequest) {
11fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
12       }
13//1.授权具体实现入口14InterceptorStatusTokentoken=super.beforeInvocation(fi);
15try {
16//2.授权通过后执行的业务17fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
18       }
19finally {
20super.finallyInvocation(token);
21       }
22//3.后续处理23super.afterInvocation(token, null);
24    }
25 }

授权机制实现的入口是super.beforeInvocation(fi),其具体实现在父类AbstractSecurityInterceptor中实现,beforeInvocation(Object object)的实现主要包括以下步骤:

 

一、获取需访问的接口权限,这里debug的例子是调用了前文提到的“/save”接口,其权限设置是@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')"),根据下面截图,可知变量attributes获取了到该请求接口的权限:

二、获取认证通过之后保存在 SecurityContextHolder的用户信息,其中,authorities是一个保存用户所拥有全部权限的集合;

这里authenticateIfRequired()方法核心实现:

1privateAuthenticationauthenticateIfRequired() {
2Authenticationauthentication=SecurityContextHolder.getContext()
3          .getAuthentication();
4if (authentication.isAuthenticated() &&!alwaysReauthenticate) {
5      ......
6returnauthentication;
7    }
8authentication=authenticationManager.authenticate(authentication);
9SecurityContextHolder.getContext().setAuthentication(authentication);
10returnauthentication;
11 }

在认证过程通过后,执行SecurityContextHolder.getContext().setAuthentication(authentication)将用户信息保存在Security框架当中,之后可通过SecurityContextHolder.getContext().getAuthentication()获取到保存的用户信息;

 

三、尝试授权,用户信息authenticated、请求携带对象信息object、所访问接口的权限信息attributes,传入到decide方法;

decide()是决策管理器AccessDecisionManager定义的一个方法。

1publicinterfaceAccessDecisionManager {
2voiddecide(Authenticationauthentication, Objectobject,
3Collection<ConfigAttribute>configAttributes) throwsAccessDeniedException,
4InsufficientAuthenticationException;
5booleansupports(ConfigAttributeattribute);
6booleansupports(Class<?>clazz);
7 }

AccessDecisionManager是一个interface接口,这是授权体系的核心。FilterSecurityInterceptor 在鉴权时,就是通过调用AccessDecisionManager的decide()方法来进行授权决策,若能通过,则可访问对应的接口。

AccessDecisionManager类的方法具体实现都在子类当中,包含AffirmativeBased、ConsensusBased、UnanimousBased三个子类;

AffirmativeBased表示一票通过,这是AccessDecisionManager默认类;

ConsensusBased表示少数服从多数;

UnanimousBased表示一票反对;

如何理解这个投票机制呢?

点进去AffirmativeBased类里,可以看到里面有一行代码int result = voter.vote(authentication, object, configAttributes):

这里的AccessDecisionVoter是一个投票器,用到委托设计模式,即AffirmativeBased类会委托投票器进行选举,然后将选举结果返回赋值给result,然后判断result结果值,若为1,等于ACCESS_GRANTED值时,则表示可一票通过,也就是,允许访问该接口的权限。

这里,ACCESS_GRANTED表示同意、ACCESS_DENIED表示拒绝、ACCESS_ABSTAIN表示弃权:

1publicinterfaceAccessDecisionVoter<S> {
2intACCESS_GRANTED=1;//表示同意3intACCESS_ABSTAIN=0;//表示弃权4intACCESS_DENIED=-1;//表示拒绝5    ......
6    }

那么,什么情况下,投票结果result为1呢?

这里需要研究一下投票器接口AccessDecisionVoter,该接口的实现如下图所示:

这里简单介绍两个常用的:

1. RoleVoter:这是用来判断url请求是否具备接口需要的角色,这种主要用于使用注解@Secured处理的权限;

2. PreInvocationAuthorizationAdviceVoter:针对类似注解@PreAuthorize("hasAuthority('sys:user:add') AND hasAuthority('sys:user:edit')")处理的权限;

到这一步,代码就开始难懂了,这部分封装地过于复杂,总体的逻辑,是将用户信息所具有的权限与该接口的权限表达式做匹配,若能匹配成功,返回true,在三目运算符中,

allowed ? ACCESS_GRANTED : ACCESS_DENIED,就会返回ACCESS_GRANTED ,即表示通过,这样,返回给result的值就为1了。

到此为止,本文就结束了,笔者仍存在不足之处,欢迎各位读者能够给予珍贵的反馈,也算是对笔者写作的一种鼓励。

目录
相关文章
|
24天前
|
缓存 Java 开发者
【Spring】原理:Bean的作用域与生命周期
本文将围绕 Spring Bean 的作用域与生命周期展开深度剖析,系统梳理作用域的类型与应用场景、生命周期的关键阶段与扩展点,并结合实际案例揭示其底层实现原理,为开发者提供从理论到实践的完整指导。
|
22天前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
|
4月前
|
人工智能 JSON 安全
Spring Boot实现无感刷新Token机制
本文深入解析在Spring Boot项目中实现JWT无感刷新Token的机制,涵盖双Token策略、Refresh Token安全性及具体示例代码,帮助开发者提升用户体验与系统安全性。
397 5
|
2月前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
6月前
|
存储 人工智能 自然语言处理
RAG 调优指南:Spring AI Alibaba 模块化 RAG 原理与使用
通过遵循以上最佳实践,可以构建一个高效、可靠的 RAG 系统,为用户提供准确和专业的回答。这些实践涵盖了从文档处理到系统配置的各个方面,能够帮助开发者构建更好的 RAG 应用。
2754 114
|
3月前
|
JSON 前端开发 Java
Spring MVC 核心组件与请求处理机制详解
本文解析了 Spring MVC 的核心组件及请求流程,核心组件包括 DispatcherServlet(中央调度)、HandlerMapping(URL 匹配处理器)、HandlerAdapter(执行处理器)、Handler(业务方法)、ViewResolver(视图解析),其中仅 Handler 需开发者实现。 详细描述了请求执行的 7 步流程:请求到达 DispatcherServlet 后,经映射器、适配器找到并执行处理器,再通过视图解析器渲染视图(前后端分离下视图解析可省略)。 介绍了拦截器的使用(实现 HandlerInterceptor 接口 + 配置类)及与过滤器的区别
219 0
|
3月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
119 0
|
3月前
|
监控 架构师 NoSQL
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
|
5月前
|
前端开发 Java 数据库连接
Spring核心原理剖析与解说
每个部分都是将一种巨大并且复杂的技术理念传达为更易于使用的接口,而这就是Spring的价值所在,它能让你专注于开发你的应用,而不必从头开始设计每一部分。
174 32
|
5月前
|
Java 开发者 Spring
Spring框架 - 深度揭秘Spring框架的基础架构与工作原理
所以,当你进入这个Spring的世界,看似一片混乱,但细看之下,你会发现这里有个牢固的结构支撑,一切皆有可能。不论你要建设的是一座宏大的城堡,还是个小巧的花园,只要你的工具箱里有Spring,你就能轻松搞定。
209 9