创新实训 9

2021SC@SDUSC
这次分析SpringSecurity部分源码
SpringSecurity

本质是一个过滤器链

查看源码:

FilterSecurityInterceptor :

是一个方法级的权限过滤器,基于过滤器的最底层

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

    
    // ...
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    // ...
}

可以看到,对于这次请求和返回,这个拦截器会先创建一个拦截器调用实例FilterInvocation,并且调用函数invoke()

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);
        }

        InterceptorStatusToken token = super.beforeInvocation(fi);

        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        finally {
            super.finallyInvocation(token);
        }

        super.afterInvocation(token, null);
    }
}

invoke方法会检查请求中是否含有属性 FILTER_APPLIED ,如果有则直接通过,如果这个属性,且要求每次都得进行检查的话,则会添加一个 FILTER_APPLIED 属性

接着其调用了super.beforeInvocation();

protected InterceptorStatusToken beforeInvocation(Object object) {
    Assert.notNull(object, "Object was null");
    final boolean debug = logger.isDebugEnabled();

    if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException(
            "Security invocation attempted for object "
            + object.getClass().getName()
            + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
            + getSecureObjectClass());
    }

    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'");
        }

        if (debug) {
            logger.debug("Public object - authentication not attempted");
        }

        publishEvent(new PublicInvocationEvent(object));

        return null; // no further work post-invocation
    }

    if (debug) {
        logger.debug("Secure object: " + object + "; Attributes: " + attributes);
    }

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        credentialsNotFound(messages.getMessage(
            "AbstractSecurityInterceptor.authenticationNotFound",
            "An Authentication object was not found in the SecurityContext"),
                            object, attributes);
    }

    Authentication authenticated = authenticateIfRequired();

    // 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));
    }

    // 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);
    }
}

该方法主要功能是查看之前的filter是否通过。

观察这个方法,我们可以发现,他首先进行了一些必要的编程上的安全检查,然后获取到这个对象的所有属性,如果这个对象含有的属性为空,并且如果要求要拒绝非授权的调用的话(rejectPublicInvocation),则会抛出异常,并且拦截器链停止后续的工作。

然后如果当前上下文中检查身份的令牌不存在的话,会抛出异常

如果存在,则获取这个令牌,并用这个令牌尝试去验证身份,如果失败则会抛出异常并拒绝后续的访问

该过程中似乎会转换安全检测上下文,应该是令牌

afterInvocation会进行后续的操作,比如说给出这一对象的详细信息

ExceptionTranslationFilter

是一个异常处理器,判断认证授权过程中抛出的异常

UsernamePasswordAuthenticationFilter

对login的POST请求做拦截,校验表单中的用户名和密码

过滤器加载过程

  1. 使用SpringSecurity配置过滤器
  • DelegatingFilterProxy

    /// ...
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized(this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = this.findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                    }
    
                    delegateToUse = this.initDelegate(wac);
                }
    
                this.delegate = delegateToUse;
            }
        }
    
        this.invokeDelegate(delegateToUse, request, response, filterChain);
    }
    /// ...
    

    可以看到,在经过了一系列检查后,调用了InitDelegate()方法

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    String targetBeanName = this.getTargetBeanName();
    Assert.state(targetBeanName != null, "No target bean name set");
    Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
    if (this.isTargetFilterLifecycle()) {
        delegate.init(this.getFilterConfig());
    }

    return delegate;
}

获取到名为targetBeanName的javaBean “FilterChainProxy”

class FilterChainProxy {
    // ...
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (clearContext) {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                doFilterInternal(request, response, chain);
            }
            finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        }
        else {
            doFilterInternal(request, response, chain);
        }
    }
}

调用了doFilterInternal方法

private void doFilterInternal(ServletRequest request, ServletResponse response,
                              FilterChain chain) throws IOException, ServletException {

    FirewalledRequest fwRequest = firewall
        .getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse fwResponse = firewall
        .getFirewalledResponse((HttpServletResponse) response);

    List<Filter> filters = getFilters(fwRequest);

    if (filters == null || filters.size() == 0) {
        if (logger.isDebugEnabled()) {
            logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                         + (filters == null ? " has no matching filters"
                            : " has an empty filter list"));
        }

        fwRequest.reset();

        chain.doFilter(fwRequest, fwResponse);

        return;
    }

    VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
    vfc.doFilter(fwRequest, fwResponse);
}

里面有一个关键步骤:

List filters = getFilters(fwRequest);

来获取过滤器集

private List<Filter> getFilters(HttpServletRequest request) {
   for (SecurityFilterChain chain : filterChains) {
      if (chain.matches(request)) {
         return chain.getFilters();
      }
   }

   return null;
}

观察里面出现的SecurityFilterChain,有

public interface SecurityFilterChain {

   boolean matches(HttpServletRequest request);

   List<Filter> getFilters();
}

开发SpringSecurity过程中,有两个重要的接口

UserDetailService

当什么也没有配置的时候,账号和密码都是由Security自己生成的,但是实际开发中我们的账号和密码是从数据库中查出来的,所以需要配置。

在SpringSecurity中默认的过滤器是 UsernamePasswordAuthenticationFilter,该类中有一个重要的方法 attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException {
   if (postOnly && !request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
   }

   String username = obtainUsername(request);
   String password = obtainPassword(request);

   if (username == null) {
      username = "";
   }

   if (password == null) {
      password = "";
   }

   username = username.trim();

   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
         username, password);

   // Allow subclasses to set the "details" property
   setDetails(request, authRequest);

   return this.getAuthenticationManager().authenticate(authRequest);
}

显然,我们发现这个类中并没有doFilter方法,说明执行时执行的是父类的方法

观察父类 AbstractAuthenticationProcessFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {

   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;

   if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);

      return;
   }

   if (logger.isDebugEnabled()) {
      logger.debug("Request is to process authentication");
   }

   Authentication authResult;

   try {
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
   }
   catch (InternalAuthenticationServiceException failed) {
      logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
      unsuccessfulAuthentication(request, response, failed);

      return;
   }
   catch (AuthenticationException failed) {
      // Authentication failed
      unsuccessfulAuthentication(request, response, failed);

      return;
   }

   // Authentication success
   if (continueChainBeforeSuccessfulAuthentication) {
      chain.doFilter(request, response);
   }

   successfulAuthentication(request, response, chain, authResult);
}

这里面执行两个重要的方法:当认证出错时调用的 unsucessfulAuthentication 和 当认证通过时调用的 sucessfulAuthentication

如果自定义的话,只需要重写这两个方法和attemptAuthentation方法即可

查数据库的过程必须要在 UserDetailService 的实现类中实现。实现认证过程

PasswordEncoder

数据加密接口,用于返回User对象里面密码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值