前言

分析SpringSecurity的核心原理的时候,那么我们要从哪开始分析?以及我们要分析哪些内容?

  1. 系统启动的时候SpringSecurity做了哪些事情?
  2. 第一次请求执行的流程是什么?
  3. SpringSecurity中认证流程是怎么样的?

1.系统启动

当我们的Web服务启动的时候,SpringSecurity做了哪些事情?
当系统启动的时候,肯定会加载我们的配置的web.xml文件

1.1 加载web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app  version="2.5"
          xmlns="http://java.sun.com/xml/ns/javaee"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  <display-name>Archetype Created Web Application</display-name>

  <!--初始化spring容器-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!--
  配置一个SpringMVC的前端控制器
  目的是所有的客户端的请求都会被DispatcherServlet处理
  -->
  <servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
  </servlet>



  <servlet-mapping>
    <!--支持Restful风格编程-->
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <!--配置字符编码的过滤器-->
  <!--设置设置编码的过滤器-->
  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceRequestEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
    <init-param>
      <param-name>forceResponseEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!--配置过滤器链 springSecurityFilterChain 名称固定-->
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!--default 防止静态资源-->
  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.jpg</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.png</url-pattern>
  </servlet-mapping>


</web-app>

web.xml中配置的信息:

  1. Spring的初始化(会加载解析SpringSecurity的配置文件)
  2. SpringMVC的前端控制器初始化(SpringMVC的初始化和SpringSecurity其实是没有多大关系的)
  3. 加载DelegatingFilterProxy过滤器(拦截所有的请求。而且这个过滤器本身适合SpringSecurity是没有关系的!!!!在之前介绍的Shiro的时候,和Spring整合的时候我们也是使用的这个过滤器。其实就是完成从IOC容器中获取DelegatingFilterProxy这个过滤器配置的FilterName的对象。)

1.1.1 Spring初始化加载解析SpringSecurity源码浅析

springsecurity oauth2 登出 springsecurity登录流程_初始化


springsecurity oauth2 登出 springsecurity登录流程_spring_02


springsecurity oauth2 登出 springsecurity登录流程_初始化_03

springsecurity oauth2 登出 springsecurity登录流程_初始化_04


springsecurity oauth2 登出 springsecurity登录流程_初始化_05


springsecurity oauth2 登出 springsecurity登录流程_初始化_06


springsecurity oauth2 登出 springsecurity登录流程_spring_07


springsecurity oauth2 登出 springsecurity登录流程_xml_08


springsecurity oauth2 登出 springsecurity登录流程_xml_09


springsecurity oauth2 登出 springsecurity登录流程_初始化_10


springsecurity oauth2 登出 springsecurity登录流程_spring_11


springsecurity oauth2 登出 springsecurity登录流程_spring_12

springsecurity oauth2 登出 springsecurity登录流程_xml_13


springsecurity oauth2 登出 springsecurity登录流程_初始化_14


springsecurity oauth2 登出 springsecurity登录流程_初始化_15


springsecurity oauth2 登出 springsecurity登录流程_xml_16


springsecurity oauth2 登出 springsecurity登录流程_初始化_17


springsecurity oauth2 登出 springsecurity登录流程_spring_18


springsecurity oauth2 登出 springsecurity登录流程_spring_19


springsecurity oauth2 登出 springsecurity登录流程_xml_20


springsecurity oauth2 登出 springsecurity登录流程_xml_21


springsecurity oauth2 登出 springsecurity登录流程_初始化_22


springsecurity oauth2 登出 springsecurity登录流程_初始化_23


springsecurity oauth2 登出 springsecurity登录流程_xml_24

springsecurity oauth2 登出 springsecurity登录流程_初始化_25


springsecurity oauth2 登出 springsecurity登录流程_xml_26


springsecurity oauth2 登出 springsecurity登录流程_初始化_27

1.1.2 DelegatingFilterProxy过滤器

拦截所有的请求。而且这个过滤器本身适合SpringSecurity是没有关系的!!!!在之前介绍的Shiro的时候,和Spring整合的时候我们也是使用的这个过滤器。其实就是完成从IOC容器中获取DelegatingFilterProxy这个过滤器配置的FilterName的对象。

系统启动的时候会执行DelegatingFilterProxy的init方法

springsecurity oauth2 登出 springsecurity登录流程_初始化_28

springsecurity oauth2 登出 springsecurity登录流程_spring_29

springsecurity oauth2 登出 springsecurity登录流程_初始化_30


springsecurity oauth2 登出 springsecurity登录流程_初始化_31

protected void initFilterBean() throws ServletException {
        synchronized(this.delegateMonitor) {
        //如果委托对象为 null 进入
            if (this.delegate == null) {
            // 如果targetBeanName == null
                if (this.targetBeanName == null) {
                // targetBeanName = 'springSecurityFilterChain'
                    this.targetBeanName = this.getFilterName();
                }

                WebApplicationContext wac = this.findWebApplicationContext();
                if (wac != null) {
                //初始化代理对象
                    this.delegate = this.initDelegate(wac);
                }
            }

        }
    }
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    //SpringSecurityFilterChain
        String targetBeanName = this.getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        //从IOC容器中获取SpringSecurityFilterChain的类型为Filter的对象
        Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
        if (this.isTargetFilterLifecycle()) {
            delegate.init(this.getFilterConfig());
        }

        return delegate;
    }

init方法的作用是:从IOC容器中获取FilterChainProxy的实例对象,并赋值给DelegatingFilterProxy的delegate属性。

2.第一次请求

客户发送请求会经过很多个Web Filter拦截。

springsecurity oauth2 登出 springsecurity登录流程_xml_32


然后经过系统启动的分析,我们知道有一个我们定义的过滤器会拦截客户端的所有的请求。DelegatingFilterProxy

springsecurity oauth2 登出 springsecurity登录流程_初始化_33


当用户请求进来的时候会被doFilter方法拦截

springsecurity oauth2 登出 springsecurity登录流程_初始化_34

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
        // 如果delegateToUse 为空 那么完成init中的初始化操作
            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);
    }

invokeDelegate

protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    //delegate.doFilter() FilterChainProxy
        delegate.doFilter(request, response, filterChain);
    }

所以在此处我们发现DelegatingFilterProxy最终是调用的委托代理对象的doFilter方法

springsecurity oauth2 登出 springsecurity登录流程_初始化_35


FilterChainProxy

过滤器链的代理对象:增强过滤器链(具体处理请求的过滤器还不是FilterChainProxy)根据客户端的请求匹配合适的过滤器链来处理请求

public class FilterChainProxy extends GenericFilterBean {
    private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
    private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
    // 过滤器链的集合 保存的有很多个过滤器链 一个过滤器链中包含的有多个过滤器
    private List<SecurityFilterChain> filterChains;
    private FilterChainProxy.FilterChainValidator filterChainValidator;
    private HttpFirewall firewall;
    .....
    }

springsecurity oauth2 登出 springsecurity登录流程_spring_36

//处理用户请求
    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);
                this.doFilterInternal(request, response, chain);
            } finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        } else {
            this.doFilterInternal(request, response, chain);
        }

    }

doFilterInternal

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
        HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
        // 根据当前的请求获取对应的过滤器链
        List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
        if (filters != null && filters.size() != 0) {
            FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
            vfc.doFilter(fwRequest, fwResponse);
        } else {
            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);
        }
    }

getFilters

private List<Filter> getFilters(HttpServletRequest request) {
        Iterator var2 = this.filterChains.iterator();

        SecurityFilterChain chain;
        do {
            if (!var2.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var2.next();
        } while(!chain.matches(request));

        return chain.getFilters();

SpringSecurity中处理请求的过滤器中具体处理请求的方法

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

                this.firewalledRequest.reset();
                this.originalChain.doFilter(request, response);
            } else {
                ++this.currentPosition;
                Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
                if (FilterChainProxy.logger.isDebugEnabled()) {
                    FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
                }

                nextFilter.doFilter(request, response, this);
            }

        }

springsecurity oauth2 登出 springsecurity登录流程_初始化_37


ExceptionTranslationFilter

ExceptionTranslationFilter是我们看到的过滤器链中的倒数第二个,作用是捕获倒数第一个过滤器抛出来的异常信息。

springsecurity oauth2 登出 springsecurity登录流程_xml_38


FilterSecurityInterceptor

做权限相关的内容

public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }
			// 抛出异常 ExceptionTranslationFilter就会捕获异常
            InterceptorStatusToken token = super.beforeInvocation(fi);

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

            super.afterInvocation(token, (Object)null);
        }

    }

ExceptionTranslationFilter处理异常的代码

springsecurity oauth2 登出 springsecurity登录流程_xml_39

springsecurity oauth2 登出 springsecurity登录流程_xml_40


springsecurity oauth2 登出 springsecurity登录流程_spring_41


springsecurity oauth2 登出 springsecurity登录流程_spring_42


springsecurity oauth2 登出 springsecurity登录流程_xml_43


springsecurity oauth2 登出 springsecurity登录流程_spring_44


springsecurity oauth2 登出 springsecurity登录流程_spring_45


当用第二次提交http://localhost:8082/login时,我们要关注的是 DefaultLoginPageGeneratingFilter 这个过滤器

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        boolean loginError = this.isErrorPage(request);
        boolean logoutSuccess = this.isLogoutSuccess(request);
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
        //正常的业务请求就直接放过
            chain.doFilter(request, response);
        } else {
        // 需要跳转到登录页面的请求
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
            //直接相应登录页面
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }

generateLoginPageHtml

private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
        String errorMsg = "Invalid credentials";
        if (loginError) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                AuthenticationException ex = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
                errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
            }
        }

        StringBuilder sb = new StringBuilder();
        sb.append("<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Please sign in</title>\n    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n  </head>\n  <body>\n     <div class=\"container\">\n");
        String contextPath = request.getContextPath();
        if (this.formLoginEnabled) {
            sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n        <h2 class=\"form-signin-heading\">Please sign in</h2>\n" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n          <label for=\"username\" class=\"sr-only\">Username</label>\n          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n        </p>\n        <p>\n          <label for=\"password\" class=\"sr-only\">Password</label>\n          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n        </p>\n" + this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request) + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n      </form>\n");
        }

        if (this.openIdEnabled) {
            sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n        <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n          <label for=\"username\" class=\"sr-only\">Identity</label>\n          <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n        </p>\n" + this.createRememberMe(this.openIDrememberMeParameter) + this.renderHiddenInputs(request) + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n      </form>\n");
        }

        if (this.oauth2LoginEnabled) {
            sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
            sb.append(createError(loginError, errorMsg));
            sb.append(createLogoutSuccess(logoutSuccess));
            sb.append("<table class=\"table table-striped\">\n");
            Iterator var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator();

            while(var7.hasNext()) {
                Entry<String, String> clientAuthenticationUrlToClientName = (Entry)var7.next();
                sb.append(" <tr><td>");
                String url = (String)clientAuthenticationUrlToClientName.getKey();
                sb.append("<a href=\"").append(contextPath).append(url).append("\">");
                String clientName = HtmlUtils.htmlEscape((String)clientAuthenticationUrlToClientName.getValue());
                sb.append(clientName);
                sb.append("</a>");
                sb.append("</td></tr>\n");
            }

            sb.append("</table>\n");
        }

        sb.append("</div>\n");
        sb.append("</body></html>");
        return sb.toString();
    }

3.认证流程

UsernamePasswordAuthenticationFilter:专门处理用户认证请求的

在父类中AbstractAuthenticationProcessingFilter中查看doFilter的逻辑

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
        // 如果不是必须要认证的请求就直接放过
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }
		
            Authentication authResult;
            try {
            	// 获取认证的信息 客户端提交的表单信息
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

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

attemptAuthentication在UsernamePasswordAuthenticationFilter实现

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }
			// 空处理
            username = username.trim();
            // 账号密码封装为对应的对象
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            // 认证操作
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

authenticate

springsecurity oauth2 登出 springsecurity登录流程_初始化_46


变量获取每个认证提供者,然后认证处理

springsecurity oauth2 登出 springsecurity登录流程_xml_47

springsecurity oauth2 登出 springsecurity登录流程_spring_48

springsecurity oauth2 登出 springsecurity登录流程_xml_49


springsecurity oauth2 登出 springsecurity登录流程_初始化_50


此处就会进入到我们自定义的UserServiceImpl中

springsecurity oauth2 登出 springsecurity登录流程_spring_51