Spring Security 如何进行权限验证

devtools/2024/10/19 12:27:03/

阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!

FilterSecurityInterceptor

FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说,权限验证是一系列业务逻辑处理完成以后,最后需要解决的问题。所以默认情况下 security 会把和权限有关的过滤器,放在 VirtualFilter chains 的最后一位。

FilterSecurityInterceptor.doFilter 方法定义了两个权限验证逻辑。分别是 super.beforeInvocation 方法代表的权限前置验证与 super.afterInvocation 代表的后置权限验证。

java">public void invoke(FilterInvocation fi) throws IOException, ServletException {if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) {// filter already applied to this request and user wants us to observe// once-per-request handling, so don't re-do security checkingfi.getChain().doFilter(fi.getRequest(), fi.getResponse());} else {// first time this request being called, so perform security checkingif (fi.getRequest() != null && observeOncePerRequest) {fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());}finally {super.finallyInvocation(token);}super.afterInvocation(token, null);}
}

和身份验证的设计类似,前置权限验证的业务处理也是参考模板方法设计模式的理念,将核心处理流程定义在父类(super) AbstractSecurityInterceptor 中的。

java">InterceptorStatusToken token = super.beforeInvocation(fi);

再通过关联 AccessDecisionManager 的方式,把身份验证通过的认证对象委托给 AccessDecisionManager 执行。

java">Authentication authenticated = authenticateIfRequired();
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);// Attempt authorization
try {this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));throw accessDeniedException;
}// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);if (runAs == null) {if (debug) {logger.debug("RunAsManager did not change Authentication object");}// no further work post-invocationreturn new InterceptorStatusToken(SecurityContextHolder.getContext(), false,attributes, object);
}
else {if (debug) {logger.debug("Switching to RunAs Authentication: " + runAs);}SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);// need to revert to token.Authenticated post-invocationreturn new InterceptorStatusToken(origCtx, true, attributes, object);
}

在前置权限校验执行完毕后,会使用 this.runAsManager.buildRunAs 与 SecurityContextHolder.getContext().setAuthentication(runAs) 方法将当前上下文中的身份认证成功的对象备份,然后重新拷贝一个新的鉴权成功的对象到上下文的中。

java">// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);

这个处理初看会有点难以理解,但是仔细思考一下,这其实是一个友好且细腻的操作。

原因是 FilterSecurityInterceptor 在大部分情况下是最后一个默认过滤器,如果说还有后续过滤操作的话,那这些肯定是你的自定义的过滤器对象行为产生的。安全起见,security 将权限认证过后的 authenticated 对象拷贝了一份副本,方便用户在后续的自定义过滤器中使用。

即使自定义过滤器中不小心修改了全局上下文中的 authenticate 对象也无妨,因为 super.finallyInvocation 方法

java">finally {super.finallyInvocation(token);
}

会保证自定义过滤器全部执行完毕以后,再把 copy source 「归还」到上下文中。这样,在后续的业务处理中就不用担心获取到一个不安全地 Authentication 对象了。

PrePostAnnotationSecurityMetadataSource.getAttributes

在 security 中使用的几种鉴权注解一共有以下四种:

  • PreFilter
  • PreAuthorize
  • PostFilter
  • PostAuthorize

其中最常用的是 PreAuthorize,它的含义是在执行目标方法前执行鉴权逻辑。前置过滤器执行的第一个逻辑,就是使用 getAttributes 方法获取目标方法上的注解。

java">public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {if (method.getDeclaringClass() == Object.class) {return Collections.emptyList();}logger.trace("Looking for Pre/Post annotations for method '" + method.getName()+ "' on target class '" + targetClass + "'");PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);PreAuthorize preAuthorize = findAnnotation(method, targetClass,PreAuthorize.class);PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);// TODO: Can we check for void methods and throw an exception here?PostAuthorize postAuthorize = findAnnotation(method, targetClass,PostAuthorize.class);if (preFilter == null && preAuthorize == null && postFilter == null&& postAuthorize == null) {// There is no meta-data so returnlogger.trace("No expression annotations found");return Collections.emptyList();}String preFilterAttribute = preFilter == null ? null : preFilter.value();String filterObject = preFilter == null ? null : preFilter.filterTarget();String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();String postFilterAttribute = postFilter == null ? null : postFilter.value();String postAuthorizeAttribute = postAuthorize == null ? null : postAuthorize.value();ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(preFilterAttribute, filterObject, preAuthorizeAttribute);if (pre != null) {attrs.add(pre);}PostInvocationAttribute post = attributeFactory.createPostInvocationAttribute(postFilterAttribute, postAuthorizeAttribute);if (post != null) {attrs.add(post);}attrs.trimToSize();return attrs;}

当通过 getAttributes 获取到对应的鉴权注解后,通过 createPreInvocationAttribute 将注解的 value——一般是 el 表达式,解析成 Express 对象后封装起来等待后续取用。

java">String preAuthorizeAttribute = preAuthorize == null ? null : preAuthorize.value();
PreInvocationAttribute pre = attributeFactory.createPreInvocationAttribute(preFilterAttribute, filterObject, preAuthorizeAttribute);
java">
public PreInvocationAttribute createPreInvocationAttribute(String preFilterAttribute,String filterObject, String preAuthorizeAttribute) {try {// TODO: Optimization of permitAllExpressionParser parser = getParser();Expression preAuthorizeExpression = preAuthorizeAttribute == null ? parser.parseExpression("permitAll") : parser.parseExpression(preAuthorizeAttribute);Expression preFilterExpression = preFilterAttribute == null ? null : parser.parseExpression(preFilterAttribute);return new PreInvocationExpressionAttribute(preFilterExpression,filterObject, preAuthorizeExpression);}catch (ParseException e) {throw new IllegalArgumentException("Failed to parse expression '"+ e.getExpressionString() + "'", e);}}

AccessDecisionManager.decide -> AffirmativeBased.decide

说完了 getAttribute,让我们回到 beforeInvocation 方法的后续处理。你可能已经猜到了,和身份认证一样 filter 是不会实现实际的权限验证逻辑的。权限验证被交给了过滤器关联的 AccessDecisionManage.decide 方法来决定。

AccessDecisionManager 是一个接口,它的实现一共有 3 个类。AbstractAccessDecisionManager 是实现接口的抽象类,提供模板方法的定义。AffirmativeBased、ConsensusBased、UnanimousBased 是三个继承 AbstractAccessDecisionManager 的子类实现,分别代表了三种不同的记分策略。

以 AffirmativeBased 作为代表来说明的话,主要内容就是把 authentication 与封装的鉴权注解 el 表达式传递给关联的成员 decisionVoters 的 vote 方法进行处理。

java">public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {int deny = 0;for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);if (logger.isDebugEnabled()) {logger.debug("Voter: " + voter + ", returned: " + result);}switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();}

PreInvocationAuthorizationAdviceVoter.vote

和上面的架构设计一样, AccessDecisionVoter 的实现 PreInvocationAuthorizationAdviceVoter.vote 看起来像是要开始处理真正意义上的权限问题了,但是其实依然没到最核心的部分。
vote 方法会返回一个 preAdvice.before 的结果,这个结果会被转换成 ACCESS_GRANTED : ACCESS_DENIED 这两个数字。为什么 preAdvice.before 的调用不直接返回 boolean 类型的鉴权结果?想想之前的 AffirmativeBased.decide 方法的设计思路:根据多个不同的 voter 返回的结果,我们可以使用不同的投票策略来对分数进行判定。返回一个数字,而不是 boolean 更便于计算最终得分。

java">public int vote(Authentication authentication, MethodInvocation method,Collection<ConfigAttribute> attributes) {// Find prefilter and preauth (or combined) attributes// if both null, abstain// else call advice with themPreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);if (preAttr == null) {// No expression based metadata, so abstainreturn ACCESS_ABSTAIN;}boolean allowed = preAdvice.before(authentication, method, preAttr);return allowed ? ACCESS_GRANTED : ACCESS_DENIED;}

ExpressionBasedPreInvocationAdvice.before

这次我向你保证,ExpressionBasedPreInvocationAdvice.before 真的是专门负责判定权限校验是否通过的方法了。由于之前已经封装好了 el 表达式的 Express 对象,所以调用 ExpressionUtils.evaluateAsBoolean 就可以直接根据表达式与执行上下文 ctx -> WebSecurityExpressionRoot extends SecurityExpressionRoot 使用 getValue() 方法获取执行结果——即权限校验的结果。

java">
public boolean before(Authentication authentication, MethodInvocation mi,PreInvocationAttribute attr) {PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,mi);Expression preFilter = preAttr.getFilterExpression();Expression preAuthorize = preAttr.getAuthorizeExpression();if (preFilter != null) {Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);expressionHandler.filter(filterTarget, preFilter, ctx);}if (preAuthorize == null) {return true;}return ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx);}

举一反三一下,既然鉴权逻辑就是一个运行时的 el 表达式解析后的结果,那利用 el 表达式可以调用类方法的特性,是不是自定义一个专门的表达式处理程序就可以随心所欲地返回鉴权结果了呢?

自定义 EL 表达式

java">    /*** sysdomain + edit permission*/public static final String SYSDOMAIN_AND_EDITPERMISSION = SYSDOMAIN + " && " + EDIT_PERMISSION;public static final String EDIT_PERMISSION_MENU = "@authorizeAppService.hasPermission('EDIT_PERMISSION_MENU')";

自定义 EL 表达式处理方法

java">
public class AuthorizeAppService implements AuthorizeAppInterface {@Overridepublic boolean hasDomain(String domain) throws IllegalArgumentException {if (StringUtils.isEmpty(domain)) {throw new IllegalArgumentException("hasDomain accepted a invalid express parameter");}AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return StringUtils.equals(domain, user.getDomain().getCode());}@Overridepublic boolean hasRole(String... role) throws IllegalArgumentException {if (ArrayUtils.isEmpty(role)) {throw new IllegalArgumentException("hasRole accepted a invalid express parameter\"");}AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return user.getAuthRoles().stream().map(AuthRole::getCode).collect(Collectors.toList()).containsAll(CollectionUtils.arrayToList(role));}@Overridepublic boolean hasPermission(String... permission) throws IllegalArgumentException {if (ArrayUtils.isEmpty(permission)) {throw new IllegalArgumentException("hasPermission accepted a invalid express parameter\"");}AuthUser user = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();Set<String> permissions = user.getAllpermissions().stream().map(AuthPermission::getCode).collect(Collectors.toSet());return permissions.containsAll(CollectionUtils.arrayToList(permission));}
}

最后,我们来探讨一下 security 为什么要费尽周折,通过一长串的聚合和依赖把权限校验这个处理流程传递这么长的路径来实现?为什么不在一开始就是 make el Express and return el.value() 搞定所有的事情?

我想这是因为 security 是一个 architect first 的设计产物。架构优先的设计,高扩展性都特别的重要。这一系列的聚合和依赖,都是「用组合解决问题」思想的体现。尽量用组合来解决问题会带来了很多好处,比如通过 GlobalMethodSecurityConfiguration 类进行的高度可配置化。

java">
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware{@Beanpublic MethodSecurityMetadataSource methodSecurityMetadataSource() {List<MethodSecurityMetadataSource> sources = new ArrayList<>();ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(getExpressionHandler());MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();if (customMethodSecurityMetadataSource != null) {sources.add(customMethodSecurityMetadataSource);}boolean hasCustom = customMethodSecurityMetadataSource != null;boolean isPrePostEnabled = prePostEnabled();boolean isSecuredEnabled = securedEnabled();boolean isJsr250Enabled = jsr250Enabled();if (!isPrePostEnabled && !isSecuredEnabled && !isJsr250Enabled && !hasCustom) {throw new IllegalStateException("In the composition of all global method configuration, " +"no annotation support was actually activated");}if (isPrePostEnabled) {sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));}if (isSecuredEnabled) {sources.add(new SecuredAnnotationSecurityMetadataSource());}if (isJsr250Enabled) {GrantedAuthorityDefaults grantedAuthorityDefaults =getSingleBeanOrNull(GrantedAuthorityDefaults.class);Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context.getBean(Jsr250MethodSecurityMetadataSource.class);if (grantedAuthorityDefaults != null) {jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());}sources.add(jsr250MethodSecurityMetadataSource);}return new DelegatingMethodSecurityMetadataSource(sources);}}

异常处理

voter 与 advicer 只做他们的份内事——对鉴权结果投票并返回。不同的得分策略,决定了同样的分数在不同的策略下,可能会有不同的最终结果。
所以 AffirmativeBased.decide 如果得出了鉴权失败的结论的话就会抛出一个 AccessDeniedException 异常。这个异常会一直沿着调用链往上抛,直到抛到 FilterSecurityIntercptor 的调用者,ExceptionTranslationFilter 中。

java">if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}

ExceptionTranslationFilter 过滤器专门处理和异常相关的事情。它的 doFilter 中定义的 AccessDeniedException 捕获处理会将异常处理交由 accessDeniedHandler.handle 处理。同时得益于组合的设计思想,accessDeniedHandler.handle 也是可配置化的。

回忆之前的自定义身份验证异常处理类 GlobalExceptionHandler。现在只要增加一个新的接口实现并重写 handle 方法,就可以在一个类里面处理身份验证和鉴权异常了。

java">
private void handleSpringSecurityException(HttpServletRequest request,HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {if (exception instanceof AuthenticationException) {logger.debug("Authentication exception occurred; redirecting to authentication entry point",exception);sendStartAuthentication(request, response, chain,(AuthenticationException) exception);}else if (exception instanceof AccessDeniedException) {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {logger.debug("Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",exception);sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication","Full authentication is required to access this resource")));}else {logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler",exception);accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);}}
}
java">@RestControllerAdvice
public class GlobalExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {log.error("spring security 认证发生异常。", e);HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_UNAUTHORIZED, "身份认证失败");}@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {log.error("spring security 鉴权发生异常。", e);HttpResponseWriter.sendError(httpServletResponse, HttpServletResponse.SC_FORBIDDEN, "鉴权失败");}
}

http://www.ppmy.cn/devtools/127006.html

相关文章

python 区间循环数

目录 区间循环加上步长2&#xff1a; 区间循环加上步长1&#xff1a; 方法3&#xff1a; 区间循环加上步长2&#xff1a; for i in range(0, 11, 2):print(i) 区间循环加上步长1&#xff1a; for i in range(start, stop):# 你的代码逻辑pass 方法3&#xff1a; if __nam…

Redis——事务

文章目录 Redis 事务Redis 的事务和 MySQL 事务的区别:事务操作MULTIEXECDISCARDWATCHUNWATCHwatch的实现原理 总结 Redis 事务 什么是事务 Redis 的事务和 MySQL 的事务 概念上是类似的. 都是把⼀系列操作绑定成⼀组. 让这⼀组能够批量执行 Redis 的事务和 MySQL 事务的区别:…

Spring Boot 3 声明式接口:能否完全替代 OpenFeign?

Spring Boot 3 声明式接口&#xff1a;能否完全替代 OpenFeign&#xff1f; 在微服务架构中&#xff0c;服务间的通信是一个核心问题。OpenFeign&#xff0c;作为一个声明式的HTTP客户端&#xff0c;极大地简化了服务调用和负载均衡的实现。然而&#xff0c;随着Spring Boot 3…

通过前端UI界面创建VUE项目

通过前端UI界面创建VUE项目&#xff0c;是比较方面的一种方式&#xff0c;下面我们详细分析一下流程&#xff1a; 1、找到合适目录 右键鼠标&#xff0c;点击在终端打开 2、开始创建 输入 vue ui 浏览器弹出页面 3、点击Create项目 显示已有文件列表&#xff0c;另外可以点击…

如何快速学会盲打

今天就来给大家分享一下如何快速学会盲打 盲打的基本方法和步骤 手指放置&#xff1a;将双手放在键盘上&#xff0c;左手食指放在F键上&#xff0c;右手食指放在J键上&#xff0c;其他手指分别放在相邻的键位上。熟悉键盘布局&#xff1a;学习26个字母的位置&#xff0c;以及…

框架一 Mybatis Spring SpringMVC(东西居多 后边的没怎么处理)

Mybatis 使用简单的XML或注解来配置和映射原生类型、接 口和Java的POJO (Plain Old Java Objects,普通老式Java对象)为数据库中的记录。 ${}和#{}的区别是 ${}替换成变量的值 #{}替换成&#xff1f; Mybatis中&#xff0c;resultType和ResultMap的区别是 如果数据库列名和…

【AAOS】Android Automotive 13模拟器源码下载及编译

源码下载 repo init -u https://android.googlesource.com/platform/manifest -b android-13.0.0_r69 repo sync -c --no-tags --no-clone-bundle 源码编译 source build/envsetup.sh lunch sdk_car_x86_64-userdebug make -j8 运行效果 emualtor Home Map All apps Sett…

复习:如何理解 React 的 redux,怎么使用

React 的 Redux 是一个状态管理工具,它允许你在 React 应用中以可预测的方式管理应用的状态。Redux 的核心思想是单一数据源(Single Source of Truth)和状态是只读的(State is Read-Only),并且只能通过纯函数(Reducer)来更新状态(Changes are made with Pure Function…