如何建设学校的微网站,网页版梦幻西游三借芭蕉扇,wordpress 仿站工具,《30天网站建设实录》阅读本文之前#xff0c;请投票支持这款 全新设计的脚手架 #xff0c;让 Java 再次伟大#xff01;
FilterSecurityInterceptor
FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说#xff0c;权限验证是一系列业务逻辑处理完成以后#xff0c;最后需要解决的…阅读本文之前请投票支持这款 全新设计的脚手架 让 Java 再次伟大
FilterSecurityInterceptor
FilterSecurityInterceptor 是负责权限验证的过滤器。一般来说权限验证是一系列业务逻辑处理完成以后最后需要解决的问题。所以默认情况下 security 会把和权限有关的过滤器放在 VirtualFilter chains 的最后一位。
FilterSecurityInterceptor.doFilter 方法定义了两个权限验证逻辑。分别是 super.beforeInvocation 方法代表的权限前置验证与 super.afterInvocation 代表的后置权限验证。
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 dont 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 中的。
InterceptorStatusToken token super.beforeInvocation(fi);再通过关联 AccessDecisionManager 的方式把身份验证通过的认证对象委托给 AccessDecisionManager 执行。
Authentication authenticated authenticateIfRequired();
CollectionConfigAttribute 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) 方法将当前上下文中的身份认证成功的对象备份然后重新拷贝一个新的鉴权成功的对象到上下文的中。
// 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 方法
finally {super.finallyInvocation(token);
}会保证自定义过滤器全部执行完毕以后再把 copy source 「归还」到上下文中。这样在后续的业务处理中就不用担心获取到一个不安全地 Authentication 对象了。
PrePostAnnotationSecurityMetadataSource.getAttributes
在 security 中使用的几种鉴权注解一共有以下四种
PreFilterPreAuthorizePostFilterPostAuthorize
其中最常用的是 PreAuthorize它的含义是在执行目标方法前执行鉴权逻辑。前置过滤器执行的第一个逻辑就是使用 getAttributes 方法获取目标方法上的注解。
public CollectionConfigAttribute 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();ArrayListConfigAttribute 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 对象后封装起来等待后续取用。
String preAuthorizeAttribute preAuthorize null ? null : preAuthorize.value();
PreInvocationAttribute pre attributeFactory.createPreInvocationAttribute(preFilterAttribute, filterObject, preAuthorizeAttribute); 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 方法进行处理。
public void decide(Authentication authentication, Object object,CollectionConfigAttribute 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 更便于计算最终得分。
public int vote(Authentication authentication, MethodInvocation method,CollectionConfigAttribute 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() 方法获取执行结果——即权限校验的结果。 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 表达式 /*** 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 表达式处理方法 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();SetString 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 类进行的高度可配置化。 Configuration(proxyBeanMethods false)
Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration
implements ImportAware, SmartInitializingSingleton, BeanFactoryAware{Beanpublic MethodSecurityMetadataSource methodSecurityMetadataSource() {ListMethodSecurityMetadataSource 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 中。
if (deny 0) {throw new AccessDeniedException(messages.getMessage(AbstractAccessDecisionManager.accessDenied, Access is denied));}
ExceptionTranslationFilter 过滤器专门处理和异常相关的事情。它的 doFilter 中定义的 AccessDeniedException 捕获处理会将异常处理交由 accessDeniedHandler.handle 处理。同时得益于组合的设计思想accessDeniedHandler.handle 也是可配置化的。
回忆之前的自定义身份验证异常处理类 GlobalExceptionHandler。现在只要增加一个新的接口实现并重写 handle 方法就可以在一个类里面处理身份验证和鉴权异常了。 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);}}
}
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, 鉴权失败);}
}