spring-security

1. 带着疑问去学习

在使用spring-security时,我们应该去思考一些问题:

  • 系统在启动spring-security时做了哪些事
  • 默认的认证界面如何出现的
  • 默认的认证流程如何实现

2. 流程分析

客户端发起请求到最终的servlet可能会经过如下过程:


spring-security_第1张图片
请求到servlet

我们可以思考一下,权限认证是不是也是其中某一个或者某几个filter呢?

基于XML分析


  springSecurityFilterChain 
  org.springframeword.web.filter.DelegatingFilterProxy


  springSecurityFilterChain
  /*

DelegatingFilterProxy并不是SpringSecurity提供,而是Spring框架本身就存在的。它继承自GenericFilterBean,Servlet容器在启动的时候就会执行GenericFilterBean的init方法。

public final void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");

        this.filterConfig = filterConfig;

        // Set bean properties from init parameters.
        PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
                Environment env = this.environment;
                if (env == null) {
                    env = new StandardServletEnvironment();
                }
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            }
            catch (BeansException ex) {
                String msg = "Failed to set bean properties on filter '" +
                    filterConfig.getFilterName() + "': " + ex.getMessage();
                logger.error(msg, ex);
                throw new NestedServletException(msg, ex);
            }
        }

        // 子类扩展,初始化自己的filter.
        initFilterBean();

        if (logger.isDebugEnabled()) {
            logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
        }
    }

我们看看子类DelegatingFilterProxy的initFilterBean()方法里面做了什么事

protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                if (this.targetBeanName == null) {
                    //这里获取的就是web.xml中配置的filterName=springSecurityFilterChain
                    this.targetBeanName = getFilterName();
                }
                // 获取IOC容器对象.
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    //获取委托处理请求的过滤器,这里的实际过滤是通过FilterChainProxy处理的
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        // 获取的值是springSecurityFilterChain
        String targetBeanName = getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        // SpringIOC容器中是根据springSecurityFilterChain这个名称获取bean对象的,
        // 所以web.xml中必须使用springSecurityFilterChain命名filter, 否则获取不到
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

在debug模式下我们可以看到真实的具体信息


spring-security_第2张图片
FilterChainProxy

通过上面源码的解析我们能够发现DelegatingFilterProxy这个过滤器在初始的时候从Spring容器中获取了 FilterChainProxy 这个过滤器链的代理对象,并且把这个对象保存在了DelegatingFilterProxy 的delegate属性中。那么当请求到来的时候会执行DelegatingFilterProxy的doFilter方法,那么我们就可以来看下这个方法里面又执行了什么

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        //if中的逻辑在初始化阶段已经完成
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }

        // 核心代码,调用委托对象处理
        invokeDelegate(delegateToUse, request, response, filterChain);
    }


protected void invokeDelegate(
            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //这里的delegate属性在之前的初始化阶段被赋值成FilterChainProxy
        delegate.doFilter(request, response, filterChain);
    }

实际流程变为:


spring-security_第3张图片
spring-security过滤流程
FilterChainProxy请求处理分析
private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        // 对request对象做防火墙检查,校验提交方式是否合法【post, get等】
        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);
        // 核心方法,这里是获取这个请求过滤链中的所有过滤器
        List 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);
    }

我们需要了解的概念,SpringSecurity中可以存在多个过滤器链,而每个过滤器链又可以包含多个过滤器


spring-security_第4张图片
多个过滤器链

spring-security_第5张图片
多个过滤器链
spring-security中15个核心过滤器
spring-security_第6张图片
15个核心过滤器

SpringSecurity中的主要过滤器

  • ChannelProcessingFilter
    处理https,没配置就没有?

  • WebAsyncManagerIntegrationFilter
    将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成

  • SecurityContextPersistenceFilter
    new HttpRequestResponseHolder(request,response)
    HttpSessionSecurityContextRepository#loadContext
    从request中获取session,从Session中取出已认证用户的信息保存在SecurityContext中,提高效率, 避免
    每一次请求都要解析用户认证信息,方便接下来的filter直接获取当前的用户信息
    如果是第一次请求,session没有相关信息,那么会创建一个新的SecurityContext
    包装request、response
    SecurityContextHolder.setContext(contextBeforeChainExecution);
    finally
    将上下文保存到HttpSessionSecurityContextRepository
    清除Holder中的上下文

  • HeaderWriterFilter
    往该请求的Header中添加相应的信息,在http标签内部使用security:headers来控制

  • CorsFilter
    未配置就没有

  • CsrfFilter
    对需要验证的请求验证是否包含csrf的token信息,如果不包含,则报错。 这样攻击网站无法获取到
    token信息,则跨域提交的信息都无法通过过滤器的校验

  • LogoutFilter
    根据request的请求方法和路径匹配判断当前是否为注销URL
    默认匹配 POST /logout
    this.handler.logout(request, response, auth);
    CsrfLogoutHandler
    SecurityContextLogoutHandler
    使session失效
    清除remember me
    清除SecurityContextHolder的SecurityContext
    LogoutSuccessEventPublishingLogoutHandler
    通知注销事件
    SimpleUrlLogoutSuccessHandler#onLogoutSuccess(request, response, auth);
    重定向,默认为/login?logout
    ...

  • UsernamePasswordAuthenticationFilter
    遍历本Manager和父Manager的所有AuthenticationProvider 如果有AuthenticationProvider支持处理当前类型的Authentication

    if (!provider.supports(toTest)) {
      continue;
    }
    try {     
    result = provider.authenticate(authentication);
        if (result != null) {
            copyDetails(authentication, result);
            break;
        }
    }
    /** 穿插一点使用的知识点
    1. 可自定义Authenticaton类,实现Authentication接口完成
    2. 自定义provider进行用户身份鉴权,支持类型是自己自定义的Authenticaton即可
    /
    
    try {     
        user = retrieveUser(username,authentication);
    }catch (UsernameNotFoundException notFound){
        throw new BadCredentialsException(xx)
    }
    protected final UserDetails retrieveUser(String username,
              UsernamePasswordAuthenticationToken authentication)
              throws AuthenticationException {
          prepareTimingAttackProtection();
          try {
                          //核心处理,这里可以自定义实现
              UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
              if (loadedUser == null) {
                  throw new InternalAuthenticationServiceException(
                          "UserDetailsService returned null, which is an interface contract violation");
              }
              return loadedUser;
          }
    ...
      }
    // 上述自定义实现,可以通过实现UserDetailService接口,实现loadUserByUsername方法,加载自己业务的用户信息,从数据库中读取啥的
    

    用户相关前置判断
    preAuthenticationChecks.check(user);
    AbstractUserDetailAuthenticationProvider.DefaultPreAuthenticationChecks
    !user.isAccountNonLocked() 账号是否被锁定
    !user.isEnabled() 账号是否可用
    !user.isAccountNonExpired() 账号是否过期
    验证密码是否正确
    additionalAuthenticationChecks(user,authentication)
    如果验证成功
    擦除所有地方的密码信息
    发布AuthenticationSuccessEvent事件
    如果验证失败
    抛出AuthenticationException
    清除SecurityContext
    ...

  • DefaultLoginPageGeneratingFilter

    if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
              //这里是根据身份验证结果,硬编码生成html页面返回
              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;
          }
    
  • DefaultLogoutPageGeneratingFilter

    if (this.matcher.matches(request)) {//匹配url  "/logout"
            renderLogout(request, response);//硬编码返回html页面
        } else {
            filterChain.doFilter(request, response);
        }
    
  • ConcurrentSessionFilter
    取出session
    若过期,进入注销逻辑,return
    没过期,更新session
    session就跳过
    ...

  • BasicAuthenticationFilter
    没配置就没有
    处理HTTP请求中的BASIC authorization头部,把认证结果写入SecurityContextHolder

  • RequestCacheAwareFilter
    从缓存中寻找是否已经有解析过的请求,若有,替换掉原生请求
    否则,继续
    SecurityContextHolderAwareRequestFilter
    封装Request,丰富API
    ...

  • RememberMeAuthenticationFilter
    当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统
    AnonymousAuthenticationFilter
    检测 SecurityContextHolder 中的SecurityContext是否存在 Authentication
    如果不存在为其提供一个匿名 Authentication key随机生成,username为anonymous,权限只有ROLE_Anonymous
    authenticated为true
    否则跳过
    ...

  • SessionManagementFilter
    防止会话固定保护攻击
    限制已认证用户可以同时打开多少个会话

  • ExceptionTranslationFilter
    直接chain.doFilter(request, response)
    通过catch处理下一个Filter或应用逻辑产生的异常
    AuthenticationException
    AccessDeniedException
    当前是匿名认证或者认证信息不全
    sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(xx))
    其他
    accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);

  • FilterSecurityInterceptor
    主要用于鉴权逻辑的处理,之前的UsernamePasswordAuthenticationFilter等都是用于登录逻辑处理。

过滤器如何执行

先前我们讲到,会构造出一个虚拟链路执行对应的filter。VirtualFilterChain

public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " reached end of additional filter chain; proceeding with original chain");
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                // size=15  currentPosition初始化为0  会从0-14一个一个取出过滤器执行
                currentPosition++;
                Filter nextFilter = additionalFilters.get(currentPosition - 1);

                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " at position " + currentPosition + " of " + size
                            + " in additional filter chain; firing Filter: '"
                            + nextFilter.getClass().getSimpleName() + "'");
                }
                // 调用取出的过滤器执行对应的过滤方法,此处用到责任链模式
                nextFilter.doFilter(request, response, this);
            }
        }
详细分析ExceptionTranslationFilter、FilterSecurityInterceptor
  1. 在整个过滤器链中,ExceptionTranslationFilter是倒数第二个执行的过滤器,它的作用是通过catch处理下一个Filter【也就是FilterSecurityInterceptor】或应用逻辑产生的异常
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(request, response);
        }catch (Exception ex) {
//....省略相关代码
            if (ase != null) {
                                //...省略相关非核心代码
                  //异常核心处理方法
                handleSpringSecurityException(request, response, chain, ase);
            }
        }
    }

private void handleSpringSecurityException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, RuntimeException exception)
            throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
           //开始登录认证异常处理
            sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);//这里实际上是调用了AuthenticationEntryPoint.commence()方法来处理认证异常
        }
        else if (exception instanceof AccessDeniedException) {
                 //开始鉴权异常处理
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                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);
            }
        }
    }
public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {

            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be
                // used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response,
                        authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        }
        else {
            // 获取重定向的地址, 默认是http://localhost:8080/login

            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }
                //重定向到指定路径页面
        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
/**
使用说明:
1. 这里的AuthenticationEntryPoint可以自定义逻辑处理,实现对应的commence方法即可
2. 可根据相关逻辑重定向或者直接返回认证失败的信息,依据自身业务来
/
  1. FilterSecurityInterceptor是SpringSecurity过滤器链中的最后一个过滤器,作用是先判断是否身份
    验证,然后在做权限的验证。第一次访问的时候处理的在doFilter中的方法的关键代码如下:
try {
  this.accessDecisionManager.decide(authenticated, object, attributes); 
}catch (AccessDeniedException accessDeniedException){
   //发布AuthorizationFailureEvent事件 
  throw accessDeniedException; }

decide方法在做投票选举,第一次的时候回抛出AccessDeniedException异常,而抛出的异常会
被ExceptionTranslationFilter中的catch语句块捕获,进而执行handleSpringSecurityException方法。

基于SpringBoot方式分析

 
  org.springframework.boot 
  spring-boot-starter-security 

基于SpringBoot的自动装配,第三方框架要加载的信息在spring.factories中


spring-security_第7张图片
自动装配security
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\

这三个类中与DelegatingFilterProxy有关系的是SecurityFilterAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {

    private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

    @Bean
    @ConditionalOnBean(name = DEFAULT_FILTER_NAME)//name = "springSecurityFilterChain"
    public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
            SecurityProperties securityProperties) {
                //将filter添加到spring容器中
        DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
                DEFAULT_FILTER_NAME);
        registration.setOrder(securityProperties.getFilter().getOrder());
        registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
        return registration;
    }
}

我们来看一下这里面的构建逻辑是如何处理的


spring-security_第8张图片
类图结构

从ServletContextInitializer.onStartU()开始分析

  • RegistrationBean的onStartUp方法
  • DynamicRegistrationBean的register方法
    protected final void register(String description, ServletContext servletContext) {
          //1. 添加过滤器,DelegatingFilterProxy
          D registration = addRegistration(description, servletContext);
          if (registration == null) {
              logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
              return;
          }
          //2. 设置拦截url  默认是 /* 拦截所有
          configure(registration);
      }
    
    protected Dynamic addRegistration(String description, ServletContext servletContext) {
          Filter filter = getFilter();
          return servletContext.addFilter(getOrDeduceName(filter), filter);
      }
    //这里就很清晰了,显示的声明了一个DelegatingFilterProxy,并且指明beanName=springSecurityFilterChain
    public DelegatingFilterProxy getFilter() {
          return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {
    
              @Override
              protected void initFilterBean() throws ServletException {
                  // Don't initialize filter bean on init()
              }
    
          };
      }
    
    AbstractFilterRegistrationBean
    protected void configure(FilterRegistration.Dynamic registration) {
          .....
          Set servletNames = new LinkedHashSet<>();
          for (ServletRegistrationBean servletRegistrationBean : this.servletRegistrationBeans) {
              servletNames.add(servletRegistrationBean.getServletName());
          }
          servletNames.addAll(this.servletNames);
          // DelegatingFilterProxyRegistrationBean 创建时没有指定ServletRegistrationBeans, urlPatterns。为空
          if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
              // DEFAULT_URL_MAPPINGS="/*", 默认拦截所有
              registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS);
          }
          else {
              if (!servletNames.isEmpty()) {
                  registration.addMappingForServletNames(dispatcherTypes, this.matchAfter,
                          StringUtils.toStringArray(servletNames));
              }
              if (!this.urlPatterns.isEmpty()) {
                  registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter,
                          StringUtils.toStringArray(this.urlPatterns));
              }
          }
      }
    

至此,我们可以看到在springboot中,通过DelegatingFilterProxyRegistrationBean创建了一个,DelegatingFilterProxy过滤器并且执行了拦截地址是 /*。后续的流程就和xml流程一致,通过FilterChainProxy处理。

你可能感兴趣的:(spring-security)