Spring Security内置过滤器详解

news2024/11/17 13:41:59

相关文章:

  1. OAuth2的定义和运行流程
  2. Spring Security OAuth实现Gitee快捷登录
  3. Spring Security OAuth实现GitHub快捷登录
  4. Spring Security的过滤器链机制
  5. Spring Security OAuth Client配置加载源码分析

文章目录

  • 前言
  • OAuth2AuthorizationRequestRedirectFilter
  • OAuth2LoginAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

前言

根据前面的示例,我们已经知道启动时会加载18个过滤器,并且已经知道了请求会匹配到DefaultSecurityFilterChain并依次通过这18个过滤器。

DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
OAuth2AuthorizationRequestRedirectFilter
OAuth2LoginAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor

我们从OAuth2AuthorizationRequestRedirectFilter开始看,OAuth2开头这非常明显。

OAuth2AuthorizationRequestRedirectFilter

OAuth2 客户端认证核心过滤器,通过重定向到authorization_uri来获取code
该过滤器并没有doFilter()方法,只有doFilterInternal,而doFilter()存在于其父类OncePerRequestFilter过滤器中
如下是核心代码:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
    try {
        //从request、ClientRegistration中构造出OAuth2的授权请求对象
        OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
        if (authorizationRequest != null) {
            //重定向到authorizationUri
            this.sendRedirectForAuthorization(request, response, authorizationRequest);
            return;
        }
    }
    catch (Exception ex) {
        this.unsuccessfulRedirectForAuthorization(request, response, ex);
        return;
    }
    try {
        //执行下一个过滤器
        filterChain.doFilter(request, response);
    }
    catch (IOException ex) {
        throw ex;
    }
    catch (Exception ex) {
        //判断是否有ClientAuthorizationRequiredException,如果有单独处理
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
        ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException) this.throwableAnalyzer
            .getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
        if (authzEx != null) {
            try {
                OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request,
                                                                                                            authzEx.getClientRegistrationId());
                if (authorizationRequest == null) {
                    throw authzEx;
                }
                this.sendRedirectForAuthorization(request, response, authorizationRequest);
                this.requestCache.saveRequest(request, response);
            }
            catch (Exception failed) {
                this.unsuccessfulRedirectForAuthorization(request, response, failed);
            }
            return;
        }
        if (ex instanceof ServletException) {
            throw (ServletException) ex;
        }
        if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
        }
        throw new RuntimeException(ex);
    }
}

OAuth2LoginAuthenticationFilter

说明:核心OAuth登录过滤器,首先从URL中提取code,然后使用code获取access_token,接着借助access_token获取用户信息,最终构建出OAuth2AuthenticationToken认 证对象,表明认证成功。
该过滤器会在授权服务器调用回调接口的时候起作用,本例中回调的URL为/login/oauth2/code/gitee?code=c52e1e1f8ab954d680f4bb78e33ce303b15ed3cb1040bd47089067e9a8ed9ea5&state=k2Ci-0eyt0To1Me_ThgInRX2CPv2EtKSiu5xTrWyN3Y%3D
doFilter代码位于父类AbstractAuthenticationProcessingFilter中,核心代码如下:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    //通过requestMatcher判断request请求是否需要处理
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    try {
        //获取身份认证结果,并创建认证对象,由子类实现,OAuth2 使用的是OAuth2LoginAuthenticationFilter
        Authentication authenticationResult = attemptAuthentication(request, response);
        if (authenticationResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            return;
        }
        //对会话进行处理,防止会话固定攻击(session-fixation详情可网上查询)
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);
        //认证成功后,是否继续执行后面的过滤器
        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        //处理认证成功后的处理逻辑,委托给AuthenticationSuccessHandler,此处为SavedRequestAwareAuthenticationSuccessHandler
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
        //认证失败处理
        unsuccessfulAuthentication(request, response, failed);
    }
    catch (AuthenticationException ex) {
        // Authentication failed
        //认证失败处理
        unsuccessfulAuthentication(request, response, ex);
    }
}

其中attemptAuthentication处的代码量很大,作用是:获取身份认证结果,并创建认证对象,由子类实现,OAuth2 使用的是OAuth2LoginAuthenticationFilter,我们来看看这部分

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    //从授权服务器回调的URL请求中,将请求参数转换为map格式,key=参数名,value=参数值
    MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
    //根据code、status、error字段判断是否为返回请求
    if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
        OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    //返回OAuth2AuthorizationRequest,该对象在OAuth2LoginAuthenticationFilter中构建
    //removeAuthorizationRequest官方的话是,但不太理解为什么要remove
    //Removes and returns the OAuth2AuthorizationRequest associated to the provided 
    //HttpServletRequest and HttpServletResponse or if not available returns null.
    OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
        .removeAuthorizationRequest(request, response);
    if (authorizationRequest == null) {
        OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
    ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
    if (clientRegistration == null) {
        OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
                                                  "Client Registration not found with Id: " + registrationId, null);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    // @formatter:off
    String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
        .replaceQuery(null)
        .build()
        .toUriString();
    // @formatter:on
    //构建OAuth2授权响应对象
    OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params,
                                                                                                 redirectUri);
    //从request构建一个完整的身份认证信息
    Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
    //构建OAuth2认证Token
    OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
                                                                                              new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
    authenticationRequest.setDetails(authenticationDetails);
    //请求授权服务器的token-uri,进行身份认证,返回完整的OAuth2认证Token
    //请求user-info-uri,获取资源所有者主体信息
    //会轮询所有的provider,直到遇到支持的provider,该处为OAuth2LoginAuthenticationProvider
    OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
        .getAuthenticationManager().authenticate(authenticationRequest);
    //转换成需要返回的OAuth2AuthenticationToken
    OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
        .convert(authenticationResult);
    Assert.notNull(oauth2Authentication, "authentication result cannot be null");
    oauth2Authentication.setDetails(authenticationDetails);
    //构建最终的OAuth2 授权客户端
    OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
        authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
        authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
    //保存授权相关信息,此处保存到本地
    this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
    return oauth2Authentication;
}

DefaultLoginPageGeneratingFilter

说明:默认的登录页面
当没有配置自定义登录页时,将使用该默认登录页
如下的doFilter核心代码:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    //当前请求是否是failureUrl指定的地址
    boolean loginError = isErrorPage(request);
    //当前请求是否是logoutSuccessUrl指定的地址
    boolean logoutSuccess = isLogoutSuccess(request);
    //是否是登录请求,是否是重定向的错误请求,是否是登出地址
    if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
        //生成一个默认的登录页
        String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
        response.setContentType("text/html;charset=UTF-8");
        response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
        response.getWriter().write(loginPageHtml);
        return;
    }
    //否则执行下一个过滤器
    chain.doFilter(request, response);
}

DefaultLogoutPageGeneratingFilter

说明:默认的登出页面
当请求地址为GET请求,/logout时,生成一个默认的登出页面
核心代码如下:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    //地址是/logout
    if (this.matcher.matches(request)) {
        //生成一个登出页面
        renderLogout(request, response);
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Did not render default logout page since request did not match [%s]",
                    this.matcher));
        }
        filterChain.doFilter(request, response);
    }
}

界面如下:
在这里插入图片描述

RequestCacheAwareFilter

说明:
从session中获取SavedRequest,如果当前请求信息和SaveRequest信息一致(一般是登录成功后重定向),则返回SavedRequestAwareWrapper的HttpServletRequest包装类

SecurityContextHolderAwareRequestFilter

说明:
通过HttpServletRequestFactory将HttpServletRequest请求包装成SecurityContextHolderAwareRequestWrapper,它实现了HttpServletRequest,并进行了扩展,添加一些额外的方法,比如:getPrincipal()方法等。这样就可以那些需要Principal等参数的Controller就可以接收到对应参数了。除了这个地方的应用,在其他地方,也可以直接调用request#getUserPrincipal()获取对应信息。

AnonymousAuthenticationFilter

说明:
匿名过滤器,如果执行到该过滤器时还没有主体,则创建一个匿名主体

OAuth2AuthorizationCodeGrantFilter

说明:
OAuth2授权码授权过滤器,跟OAuth2LoginAuthenticationFilter很像,不知道在这里的作用是什么?
是为了兜底?

SessionManagementFilter

Session管理过滤器

ExceptionTranslationFilter

处理过滤器链中抛出的AccessDeniedException和AuthenticationException 异常

FilterSecurityInterceptor

对资源的过滤处理
核心代码如下:

public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
    //该请求已经运行过该过滤,跳过,执行下一个过滤器
    if (isApplied(filterInvocation) && this.observeOncePerRequest) {
        // filter already applied to this request and user wants us to observe
        // once-per-request handling, so don't re-do security checking
        filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        return;
    }
    // first time this request being called, so perform security checking
    if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
        filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
    }
    //获取当前的权限配置
    InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
    try {
        filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
    }
    finally {
        //设置SecurityContextHolderStrategy context
        super.finallyInvocation(token);
    }
    //最后的处理
    super.afterInvocation(token, null);
}

获取当前 request 对应的权限配置,首先是调用基类的 beforeInvocation 方法 。

看一下基类的 beforeInvocation 方法,从配置好的 SecurityMetadataSource 中获取当前 request 所对应的 ConfigAttribute,即权限信息。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
        .getAttributes(object);
    if (attributes == null || attributes.isEmpty()) {
        if (rejectPublicInvocations) {
            throw new IllegalArgumentException(
                "Secure object invocation "
                + object
                + " was denied as public invocations are not allowed via this interceptor. "
                + "This indicates a configuration error because the "
                + "rejectPublicInvocations property is set to 'true'");
        }
        ......
    }
}

这里需要注意一下 rejectPublicInvocations 属性,默认为 false。此属性含义为拒绝公共请求。如果从配置好的 SecurityMetadataSource 中获取不到当前 request 所对应的 ConfigAttribute 时,即认为当前请求为公共请求。如配置 rejectPublicInvocations 属性为 true,则系统会抛出 IllegalArgumentException 异常,即当前请求需要配置权限信息。

接下来,就要判断是否需要进行身份认证了,即调用 authenticateIfRequired 方法。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......

    Authentication authenticated = authenticateIfRequired();

    ......
}

而判断及身份认证逻辑也并不复杂,首先会判断当前用户是否已通过身份认证,如果已通过身份认证,则直接返回;如果尚未通过身份认证,则调用身份认证管理器 AuthenticationManager 进行认证,就如同登录时一样。认证通过后,同样会在当前的安全上下文中存储一份认证后的 authentication。

private Authentication authenticateIfRequired() {
    Authentication authentication = SecurityContextHolder.getContext()
        .getAuthentication();
    if (authentication.isAuthenticated() && !alwaysReauthenticate) {
        if (logger.isDebugEnabled()) {
            logger.debug("Previously Authenticated: " + authentication);
        }
        return authentication;
    }
    authentication = authenticationManager.authenticate(authentication);
    // We don't authenticated.setAuthentication(true), because each provider should do
    // that
    if (logger.isDebugEnabled()) {
        logger.debug("Successfully Authenticated: " + authentication);
    }
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return authentication;
}

然后,使用获取到的 ConfigAttribute ,继续调用访问控制器 AccessDecisionManager 对当前请求进行鉴权。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......
    // Attempt authorization
    try {
          this.accessDecisionManager.decide(authenticated, object, attributes);
        }
    catch (AccessDeniedException accessDeniedException) {
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                                                   accessDeniedException));
        throw accessDeniedException;
    }
    if (debug) {
        logger.debug("Authorization successful");
    }
    if (publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }
}

注意,无论鉴权通过或是不通后,Spring Security 框架均使用了观察者模式,来通知其它Bean,当前请求的鉴权结果。
如果鉴权不通过,则会抛出 AccessDeniedException 异常,即访问受限,然后会被 ExceptionTranslationFilter 捕获,最终解析后调转到对应的鉴权失败页面。

如果鉴权通过,AbstractSecurityInterceptor 通常会继续请求。但是,在极少数情况下,用户可能希望使用不同的 Authentication 来替换 SecurityContext 中的 Authentication。该身份认证就会由 RunAsManager 来处理。这在某些业务场景下可能很有用,录入服务层方法需要调用远程系统并呈现不同的身份。因为 Spring Security 会自动将安全标识从一个服务器传播到另一个服务器(假设使用的是正确配置的 RMI 或 HttpInvoker 远程协议客户端),这就可能很有用。

在 AccessDecisionManager 鉴权成功后,将通过 RunAsManager 在现有 Authentication 基础上构建一个新的Authentication,如果新的 Authentication 不为空则将产生一个新的 SecurityContext,并把新产生的Authentication 存放在其中。这样在请求受保护资源时从 SecurityContext中 获取到的 Authentication 就是新产生的 Authentication。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......

        // 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-invocation
        return 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-invocation
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
}

注意,AbstractSecurityInterceptor 默认持有的是 RunAsManager 的空实现 NullRunAsManager。

public abstract class AbstractSecurityInterceptor implements InitializingBean,
    ApplicationEventPublisherAware, MessageSourceAware {
  ......
  private RunAsManager runAsManager = new NullRunAsManager();
    ......
 }

待请求完成后会在 finallyInvocation() 中将原来的 SecurityContext 重新设置给SecurityContextHolder。

protected void finallyInvocation(InterceptorStatusToken token) {
    if (token != null && token.isContextHolderRefreshRequired()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Reverting to original Authentication: "
                         + token.getSecurityContext().getAuthentication());
        }

        SecurityContextHolder.setContext(token.getSecurityContext());
    }
}

然而,无论正常调用,亦或是请求异常等,都会触发 finallyInvocation()。

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    if ((fi.getRequest() != null)
       ......
    }
    else {
        ......

        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        finally {
            // 无论是否成功、抛异常与否,均会执行
            super.finallyInvocation(token);
        }

        // 正常请求结束,最后也会执行(afterInvocation 内部会调用finallyInvocation )
        super.afterInvocation(token, null);
    }
}

即便是正常执行结束,依然会执行 finallyInvocation()(afterInvocation 内部会调用finallyInvocation )。

protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
    ......

    finallyInvocation(token); // continue to clean in this method for passivity

    ......
}

此外,Spring Security 对 RunAsManager 有一个还有一个非空实现类 RunAsManagerImpl,其构造新 Authentication 的逻辑如下:

如果受保护对象对应的 ConfigAttribute 中拥有以“RUN_AS_”开头的配置属性,则在该属性前加上“ROLE_”,然后再把它作为一个 SimpleGrantedAuthority 赋给将要创建的 Authentication(如ConfigAttribute 中拥有一个“RUN_AS_ADMIN”的属性,则将构建一个“ROLE_RUN_AS_ADMIN”的SimpleGrantedAuthority),最后再利用原 Authentication 的 principal、权限等信息构建一个新的 Authentication 并返回;如果不存在任何以“RUN_AS_”开头的 ConfigAttribute,则直接返回null。

public Authentication buildRunAs(Authentication authentication, Object object,
      Collection<ConfigAttribute> attributes) {
    List<GrantedAuthority> newAuthorities = new ArrayList<>();

    for (ConfigAttribute attribute : attributes) {
        if (this.supports(attribute)) {
            GrantedAuthority extraAuthority = new SimpleGrantedAuthority(
                getRolePrefix() + attribute.getAttribute());
            newAuthorities.add(extraAuthority);
        }
    }

    if (newAuthorities.size() == 0) {
        return null;
    }

    // Add existing authorities
    newAuthorities.addAll(authentication.getAuthorities());

    return new RunAsUserToken(this.key, authentication.getPrincipal(),
                              authentication.getCredentials(), newAuthorities,
                              authentication.getClass());
}

AccessDecisionManager 是在访问受保护的对象之前判断用户是否拥有该对象的访问权限。然而,有时候我们可能会希望在请求执行完成后对返回值做一些修改或者权限校验,当然,也可以简单的通过AOP来实现这一功能。

同样的,Spring Security 提供了 AfterInvocationManager 接口,它允许我们在受保护对象访问完成后对返回值进行修改或者进行权限校验,权限校验不通过时抛出 AccessDeniedException,并使用观察者模式通知其它Bean。

protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
    ......

    if (afterInvocationManager != null) {
        ......
        catch (AccessDeniedException accessDeniedException) {
            AuthorizationFailureEvent event = new AuthorizationFailureEvent(
                token.getSecureObject(), token.getAttributes(), token
                .getSecurityContext().getAuthentication(),
                accessDeniedException);
            publishEvent(event);

            throw accessDeniedException;
        }
    }
  ......  
}

其将由 AbstractSecurityInterceptor 的子类进行调用,如默认子类 FilterSecurityInterceptor 。

需要特别注意的是,AfterInvocationManager 需要在受保护对象成功被访问后才能执行。

类似于AuthenticationManager,AfterInvocationManager 同样也有一个默认的实现类AfterInvocationProviderManager,其中有一个由 AfterInvocationProvider 组成的集合属性。

public class AfterInvocationProviderManager implements AfterInvocationManager,
    InitializingBean {
  ......

  private List<AfterInvocationProvider> providers;

    ......
}

非常有趣的是,AfterInvocationProvider 与 AfterInvocationManager 具有相同的方法定义。此一来,在调用AfterInvocationProviderManager 中的方法时,实际上就是依次调用其中成员属性 providers 中的AfterInvocationProvider 接口对应的方法。

public Object decide(Authentication authentication, Object object,
                     Collection<ConfigAttribute> config, Object returnedObject)
    throws AccessDeniedException {
    Object result = returnedObject;
    for (AfterInvocationProvider provider : providers) {
        result = provider.decide(authentication, object, config, result);
    }
    return result;
}

而 AfterInvocationProvider 的默认实现类 PostInvocationAdviceProvider 中的 PostInvocationAuthorizationAdvice,其默认实现类 ExpressionBasedPostInvocationAdvice,不正是对应着后置权限注解 @PostAuthorize 吗?

最后,关于 FILTER_APPLIED 常量,在 FilterSecurityInterceptor 中是这么使用的:

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 checking
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    }
    else {
        // first time this request being called, so perform security checking
        if (fi.getRequest() != null && observeOncePerRequest) {
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }

        ......
    }
}

其主要作用,是用于阻止请求的重复安全检查。

原理也简单,第一次执行时,检查 request 中 FILTER_APPLIED 属性值为空,则放入值;后续该 request 再次请求时,FILTER_APPLIED 属性值不为空,代表已经进行过安全检查,则该请求直接通过,不再重复进行安全检查。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/800264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

matlab多线程,parfor循环进度,matlab互斥锁

一. 内容简介 matlab多线程&#xff0c;parfor循环进度&#xff0c;matlab互斥锁 二. 软件环境 2.1 matlab 2022b 2.2代码链接 https://gitee.com/JJW_1601897441/csdn 三.主要流程 3.1 matlab多线程 有好几种&#xff0c;最简单的&#xff0c;最好理解的就是parfor&am…

cpolar内网穿透外网远程访问本地网站

文章目录 cpolar内网穿透外网远程访问本地网站 cpolar内网穿透外网远程访问本地网站 在现代人的生活中&#xff0c;电脑是离不开的重要设备&#xff0c;大家看到用到的各种物品都离不开电脑的支持。尽管移动电子设备发展十分迅速&#xff0c;由于其自身存在的短板&#xff0c;…

Leetcode-每日一题【剑指 Offer 66. 构建乘积数组】

题目 给定一个数组 A[0,1,…,n-1]&#xff0c;请构建一个数组 B[0,1,…,n-1]&#xff0c;其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]A[0]A[1]…A[i-1]A[i1]…A[n-1]。不能使用除法。 示例: 输入: [1,2,3,4,5]输出: [120,60,40,30,24] 提示&#xff1a; 所…

SSIS对SQL Server向Mysql数据转发表数据 (三)

1、在控制流界面&#xff0c;在左侧的组件里&#xff0c;添加一个“序列容器组件”和一个“数据流任务组件” 2、双击数据流任务&#xff0c;进入到数据流界面&#xff0c;然后再在左面添加一个OLE DB 源组件、目标源组件 3、右键源组件&#xff0c;编辑&#xff0c;选择好相关…

虎年现货黄金投资布局图

参与现货黄金交易的主要目的&#xff0c;是为了根据行情走势的变动&#xff0c;把握一些较佳的获利机会&#xff0c;在这样的一个过程中&#xff0c;如果投资者能够提前把布局的图表画好&#xff0c;那么就可能获得事半功倍的效果&#xff0c;而本文将为大家简单的介绍&#xf…

C++——STL容器之list链表的讲解

目录 一.list的介绍 二.list类成员函数的讲解 2.2迭代器 三.添加删除数据&#xff1a; 3.1添加&#xff1a; 3.2删除数据 四.排序及去重函数&#xff1a; 错误案例如下&#xff1a; 方法如下&#xff1a; 一.list的介绍 list列表是序列容器&#xff0c;允许在序列内的任何…

前端面试题 —— React (二)

目录 一、React 组件中怎么做事件代理&#xff1f;它的原理是什么&#xff1f; 二、React.Component 和 React.PureComponent 的区别 三、Component, Element, Instance 之间有什么区别和联系&#xff1f; 四、React声明组件有哪几种方法&#xff0c;有什么不同&#xff1f…

数学建模-MATLAB三维作图

导出图片用无压缩tif会更清晰 帮助文档&#xff1a;doc 函数名 matlab代码导出为PDF 新建实时脚本或右键文件转换为实时脚本实时编辑器-全部运行-内嵌显示保存为PDF

120个颠覆你认知的gpt使用案例汇总,办公效率提高500%

文章目录 介绍1.代码生成2.代码注释3.代码解释器4.充当 Linux 终端5.代码纠正6.英语口语练习7.专业的翻译8.面试官9.写任何考科目的作业10.快速解决学习中的任何问题11.网站推荐12.网络工具软件推荐13.快速学习新技能14.快速总结长文本的核心思想15.解决日常办公问题16.制作各种…

【CASA】生态系统NPP及碳源、碳汇模拟(土地利用变化、未来气候变化、空间动态模拟)

碳中和可以从碳排放&#xff08;碳源&#xff09;和碳固定&#xff08;碳汇&#xff09;这两个侧面来理解。陆地生态系统在全球碳循环过程中有着重要作用&#xff0c;准确地评估陆地生态系统碳汇及碳源变化对于研究碳循环过程、预测气候变化及制定合理政策具有重要意义。CASA(C…

【QT 网络云盘客户端】——实现文件属性窗口

目录 文件属性对话框 设置字体样式 获取文件的信息 显示文件属性对话框 当我们点击文件中的属性&#xff0c;则会弹出一个属性对话框&#xff1a; 实现过程&#xff1a; 0.设置 属性 菜单项的槽函数。 1.鼠获取鼠标选中的QListWidgetItem,它包含 图标和文件名 2.根据文件…

Dooring-Saas低代码技术详解

hello, 大家好, 我是徐小夕, 今天和大家分享一下基于 H5-Dooring零代码 开发的全新零代码搭建平台 Dooring-Saas 的技术架构和设计实现思路. 背景介绍 3年前我上线了第一版自研零代码引擎 H5-Dooring, 至今已迭代了 300 多个版本, 主要目的是快速且批量化的生产业务/营销过程中…

CSS Flex 笔记

1. Flexbox 术语 Flex 容器可以是<div> 等&#xff0c;对其设置属性&#xff1a;display: flex, justify-content 是沿主轴方向调整元素&#xff0c;align-items 是沿交叉轴对齐元素。 2. Cheatsheet 2.1 设置 Flex 容器&#xff0c;加粗的属性为默认值 2.1.1 align-it…

Spring 事务的使用、隔离级别、@Transactional的使用

Spring事务是Spring框架提供的一种机制&#xff0c;用于管理应用程序中的数据库事务。 事务是一组数据库操作的执行单元&#xff0c;要么全部成功提交&#xff0c;要么全部失败回滚&#xff0c;保证数据的一致性和完整性。 Spring事务提供了声明式事务和编程式事务两种方式&am…

[Tools: Camera Conventions] NeRF中的相机矩阵估计

参考&#xff1a;NeRF代码解读-相机参数与坐标系变换 - 知乎 在NeRF中&#xff0c;一个重要的步骤是确定射线&#xff08;rays&#xff09;的初始点和方向。根据射线的初始点和方向&#xff0c;和设定射线深度和采样点数量&#xff0c;可以估计该射线成像的像素值。估计得到的…

Live Market:中国“一带一路”十周年,品牌出海跨境电商成为新引擎

​中国提出的“一带一路”倡议已经迎来10周年。高质量共建“数字丝绸之路”是这一倡议的重点方向&#xff0c;能够巩固互联互通合作基础、拓展国际合作新空间、扎牢风险防控网络&#xff0c;实现更高合作水平、更高投入效益、更高供给质量、更高发展韧性&#xff0c;推动共建“…

Python教程三:Python基本概念

1、Python基本语法 Python中严格区分大小写Python中每一行就是一条语句&#xff0c;每条语句以换行结束每一行语句不建议过长&#xff08;一般不建议超过80个字符&#xff09;一条语句可以多行编写&#xff0c;语句后加\结尾Python是缩进严格的语言&#xff0c;所以在Python中…

简单认识NoSQL的Redis配置与优化

文章目录 一、关系型数据库与非关系型数据库1、关系型数据库&#xff1a;2、非关系型数据库3、关系型数据库和非关系型数据库区别&#xff1a;4、非关系型数据库应用场景 二.Redis1、简介2、优点&#xff1a;3、Redis为什么这么快&#xff1f; 三、Redis 安装部署1、安装配置2、…

IDEA配置maven3.6.1时报错: 不支持发行版本 5 或 java: 不再支持源选项 5。请使用 7 或更高版本。

环境&#xff1a;Win10 IDEA 2022.3.3&#xff0c;JDK16&#xff0c;配置maven3.6.1 生成工程后&#xff0c;运行程序&#xff0c;结果报错如下&#xff1a; 不支持发行版本 5 好&#xff0c;此时更改以下选项&#xff1a; 此处我改为16&#xff0c;因为我的JDK是16版本的…

C#实现数字验证码

开发环境&#xff1a;VS2019&#xff0c;.NET Core 3.1&#xff0c;ASP.NET Core API 1、建立一个验证码控制器 新建两个方法Create和Check&#xff0c;Create用于创建验证码&#xff0c;Check用于验证它是否有效。 声明一个静态类变量存放列表&#xff0c;列表中存放包含令…