Spring security 是一种基于Spring Aop和Servlet 过滤器的安全框架。

在ApplicationFilterChain 中通过DelegatingFilterProxyRegistrationBean创建DelegatingFilterProxy:

DelegatingFilterProxyRegistrationBean:

  1. @Override
  2. public Filter getFilter() {
  3. return new DelegatingFilterProxy(this.targetBeanName,
  4. getWebApplicationContext()) {
  5. @Override
  6. protected void initFilterBean() throws ServletException {
  7. // Don't initialize filter bean on init()
  8. }
  9. };
  10. }

通过调用AbstractFilterRegistrationBean的onStartup 方法调用DelegatingFilterProxy中具体的filter方法:
AbstractFilterRegistrationBean:

  1. @Override
  2. public void onStartup(ServletContext servletContext) throws ServletException {
  3. Filter filter = getFilter();
  4. Assert.notNull(filter, "Filter must not be null");
  5. String name = getOrDeduceName(filter);
  6. if (!isEnabled()) {
  7. this.logger.info("Filter " + name + " was not registered (disabled)");
  8. return;
  9. }
  10. FilterRegistration.Dynamic added = servletContext.addFilter(name, filter);
  11. if (added == null) {
  12. this.logger.info("Filter " + name + " was not registered "
  13. + "(possibly already registered?)");
  14. return;
  15. }
  16. configure(added);
  17. }

最后通过DelegatingFilterProxy 执行FilterChainProxy的doFilterInternal方法。
在doFilterInternal方法中首先会根据request获取所有的filter

  1. /**
  2. * Returns the first filter chain matching the supplied URL.
  3. 返回第一个匹配的url的过滤器
  4. *
  5. * @param request the request to match
  6. * @return an ordered array of Filters defining the filter chain
  7. */
  8. private List<Filter> getFilters(HttpServletRequest request) {
  9. for (SecurityFilterChain chain : filterChains) {
  10. if (chain.matches(request)) {
  11. return chain.getFilters();
  12. }
  13. }
  14. return null;
  15. }

filterchains中的filter 是通过以下方法配置:

  1. @Configuration
  2. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Autowired
  4. private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
  5. @Autowired
  6. private LoginSuccessHandler loginSuccessHandler;
  7. @Autowired
  8. private CustomLogoutSuccessHandler logoutSuccessHandler;
  9. @Override
  10. protected void configure(HttpSecurity http) throws Exception {
  11. loginSuccessHandler.setDefaultTargetUrl("/");
  12. loginSuccessHandler.setForwardToDestination(false);
  13. logoutSuccessHandler.setDefaultTargetUrl("/login");
  14. http.authorizeRequests().anyRequest().authenticated()
  15. .and()
  16. .formLogin().successHandler(loginSuccessHandler)
  17. .permitAll()
  18. .and()
  19. .logout().logoutSuccessHandler(logoutSuccessHandler)
  20. .permitAll()
  21. ;
  22. http.sessionManagement().maximumSessions(1);
  23. http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
  24. http.exceptionHandling().accessDeniedPage("/403").authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"));
  25. }
  26. @Override
  27. public void configure(WebSecurity web) throws Exception {
  28. web.ignoring().antMatchers("/resources/static/**") //这会被解析成一个SecurityFilterChain
  29. .and().ignoring().antMatchers("/static/**")
  30. .and().ignoring().antMatchers("/bootstrap/**")
  31. .and().ignoring().antMatchers("/fragments/**")
  32. .and().ignoring().antMatchers("/**/*.js")
  33. .and().ignoring().antMatchers("/**/*.css")
  34. .and().ignoring().antMatchers("/**/*.woff");
  35. }
  36. }

Spring security 源码解析 - 图1spring security 默认有13个filter, 通过下面配置可添加自己额外的filter:

  1. http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);

之后将这13个filter 封装在VirtualFilterChain 中:

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

下面讲解这13个Filter各自的作用(按顺序执行):

  1. WebAsyncManagerIntegrationFilter:

提供了对securityContext和WebAsyncManager的集成。方式是通过SecurityContextCallableProcessingInterceptor的beforeConcurrentHandling(NativeWebRequest, Callable)方法来讲SecurityContext设置到Callable上。

  1. SecurityContextPersistenceFilter:

这个过滤器主要是加载或者创建新的SecurityContext,并把它保存到全局的SecurityContextHolder中去。
看如下代码:

  1. HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
  2. response);
  3. //1. 加载或创建SecurityContext
  4. SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
  5. try {
  6. //2. 保存securityContext 到 SecurityContextHolder中去
  7. SecurityContextHolder.setContext(contextBeforeChainExecution);
  8. chain.doFilter(holder.getRequest(), holder.getResponse());
  9. }
  10. finally {
  11. SecurityContext contextAfterChainExecution = SecurityContextHolder
  12. .getContext();
  13. // Crucial removal of SecurityContextHolder contents - do this before anything
  14. // else.
  15. SecurityContextHolder.clearContext();
  16. repo.saveContext(contextAfterChainExecution, holder.getRequest(),
  17. holder.getResponse());
  18. request.removeAttribute(FILTER_APPLIED);
  19. if (debug) {
  20. logger.debug("SecurityContextHolder now cleared, as request processing completed");
  21. }
  22. }

在看loadContext()方法的实现,这个方法的默认实现在HttpSessionSecurityContextRepository类中:
代码如下:

  1. public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
  2. HttpServletRequest request = requestResponseHolder.getRequest();
  3. HttpServletResponse response = requestResponseHolder.getResponse();
  4. HttpSession httpSession = request.getSession(false);
  5. // 1. 从session中读取securitycontext,
  6. //实际调用httpSession.getAttribute("SPRING_SECURITY_CONTEXT");
  7. SecurityContext context = readSecurityContextFromSession(httpSession);
  8. if (context == null) {
  9. if (logger.isDebugEnabled()) {
  10. logger.debug("No SecurityContext was available from the HttpSession: "
  11. + httpSession + ". " + "A new one will be created.");
  12. }
  13. //2. 如果session中不存在securityContext, 则会创建一个empty securitycontext,具体如何创建,往下看
  14. context = generateNewContext();
  15. }
  16. //3. 下面代码将response封装成SaveToSessionResponseWrapper对象,
  17. // 如果是servlet3, 则将response封装成Servlet3SaveToSessionRequestWrapper对象:
  18. SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
  19. response, request, httpSession != null, context);
  20. requestResponseHolder.setResponse(wrappedResponse);
  21. if (isServlet3) {
  22. requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper(
  23. request, wrappedResponse));
  24. }
  25. return context;
  26. }

如果session中不存在securityContext , spring会根据3中不同的策略创建空的securityContext:

  • GlobalSecurityContextHolderStrategy

  • InheritableThreadLocalSecurityContextHolderStrategy

  • ThreadLocalSecurityContextHolderStrategy(默认使用)

我们可以在application.properties 中通过配置 spring.security.strategy 来选择使用哪个strategy来创建空的securityContext;
下面是ThreadLocalSecurityContextHolderStrategy创建securityContext的代码:

public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
  1. HeaderWriterFilter:

这个过滤器主要是在response中加入一些spring security关于安全的头部信息,默认会有以下5中HeaderWriter需要写入头信息:

  • XContentTypeOptionsHeaderWriter

  • XXssProtectionHeaderWriter

  • CacheControlHeaderWriter

  • HstsHeaderWriter

  • XFrameOptionsHeaderWriter

接下来看每一种HeaderWriter都写入了哪些头信息:
a. XContentTypeOptionsHeaderWriter
这个写入了”X-Content-Type-Options” = “nosniff”, 这个设置可防止内容嗅探攻击,详细解释请自行百度

public final class XContentTypeOptionsHeaderWriter extends StaticHeadersWriter {

    /**
     * Creates a new instance
     */
    public XContentTypeOptionsHeaderWriter() {
        super("X-Content-Type-Options", "nosniff");
    }
}

public StaticHeadersWriter(String headerName, String... headerValues) {
        this(Collections.singletonList(new Header(headerName, headerValues)));
    }

b. XXssProtectionHeaderWriter
这个写入 X-XSS-Protection = 1;mode=block, 这个设置可以阻止用户在地址栏中执行插入的脚本,详细解释请自行百度

public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(XSS_PROTECTION_HEADER, headerValue);
    }

c. CacheControlHeaderWriter
这个写入三个header, 分别是Cache-Control, Expires, Pragma

private static List<Header> createHeaders() {
        List<Header> headers = new ArrayList<Header>(2);
        headers.add(new Header(CACHE_CONTROL,
                "no-cache, no-store, max-age=0, must-revalidate"));
        headers.add(new Header(PRAGMA, "no-cache"));
        headers.add(new Header(EXPIRES, "0"));
        return headers;
    }

d. HstsHeaderWriter
这个写入Strict-Transport-Security = “max-age=31536000;includeSubDomains”, 具体解释请自行百度

private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";

public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        if (this.requestMatcher.matches(request)) {
            response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue);
        }
        else if (this.logger.isDebugEnabled()) {
            this.logger
                    .debug("Not injecting HSTS header since it did not match the requestMatcher "
                            + this.requestMatcher);
        }
    }

private void updateHstsHeaderValue() {
        String headerValue = "max-age=" + this.maxAgeInSeconds;
        if (this.includeSubDomains) {
            headerValue += " ; includeSubDomains";
        }
        this.hstsHeaderValue = headerValue;
    }

e.XFrameOptionsHeaderWriter
这个写入 X-Frame-Options = DENY, 这个响应头为了阻止CSRF 攻击,具体解释请自行百度

public enum XFrameOptionsMode {
        DENY("DENY"), SAMEORIGIN("SAMEORIGIN"), ALLOW_FROM("ALLOW-FROM");
}

public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        if (XFrameOptionsMode.ALLOW_FROM.equals(frameOptionsMode)) {
            String allowFromValue = allowFromStrategy.getAllowFromValue(request);
            if (allowFromValue != null) {
                response.setHeader(XFRAME_OPTIONS_HEADER,
                        XFrameOptionsMode.ALLOW_FROM.getMode() + " " + allowFromValue);
            }
        }
        else {
            response.setHeader(XFRAME_OPTIONS_HEADER, frameOptionsMode.getMode());
        }
    }
  1. CsrfFilter

这个过滤器主要防止网站CSRF攻击, 具体逻辑看下面代码:

//通过HttpSessionCsrfTokenRepository.loadToken() 去session中
// 去加载 key = org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN
//的值
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        final boolean missingToken = csrfToken == null;
        if (missingToken) {
    //如果session中不存在Csrf Token,则自动生成并存入tokenRepository中
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        //将csrfToken 加入request属性中
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);

        //如果request不需要csrf检查,则跳过这个filter
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 从请求头不中获取csrf Token 的信息,
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        if (actualToken == null) {
            //如果请求头部不存在,再从请求参数中获取
            actualToken = request.getParameter(csrfToken.getParameterName());
        }
        // 如果实际请求的csrf token 和repository中的token不一致,则拒绝访问
        if (!csrfToken.getToken().equals(actualToken)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid CSRF token found for "
                        + UrlUtils.buildFullRequestUrl(request));
            }
            if (missingToken) {
                this.accessDeniedHandler.handle(request, response,
                        new MissingCsrfTokenException(actualToken));
            }
            else {
                this.accessDeniedHandler.handle(request, response,
                        new InvalidCsrfTokenException(csrfToken, actualToken));
            }
            return;
        }
  1. LogoutFilter

首先根据请求url 是否匹配/logout 并且 requestMethod = Post 来判断是否需需要执行这个filter。
如果需要,看如下代码:

if (requiresLogout(request, response)) {
            // 从SecurityContextHolder中获取当前登录用户的授权信息Authentication
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();

            if (logger.isDebugEnabled()) {
                logger.debug("Logging out user '" + auth
                        + "' and transferring to logout destination");
            }
            // this.handler = CompositeLogoutHandler, 它会遍历所有的LogoutHandler的实例,
            //并执行他们各自的logout方法。 logoutHandler可在WebSecurityConfig配置
            this.handler.logout(request, response, auth);

            // logoutSuccessHandler 在所有logoutHandler执行完成后用户需要执行额外的逻辑
            // 这个handler 也可以通过在WebSecurityConfig 配置
            logoutSuccessHandler.onLogoutSuccess(request, response, auth);

            return;
        }
  1. UsernamePasswordAuthenticationFilter

首先判断请求的url是否需要授权,默认执行AntPathRequestMatcher.matches(request)去验证。默认/login 才需要授权验证

public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

如果是/login访问,则首先调用 attempAuthentication(request,response)方法尝试获取Authentication.

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

获取Authentication的主要逻辑在这句话中

this.getAuthenticationManager().authenticate(authRequest);

this.getAuthenticationManager()返回的是ProviderManager对象, 接下来看ProviderManager 是如何实现authenticate 方法的:
首先在ProviderManager中要获取AuthenticationProvider, 第一个ProviderManager找到的是AnonymousAuthenticationProvider,它的父ProviderManager找到的是DaoAuthenticationProvider

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
        // 当前的toTest = UsernamePasswordAuthenticationToken, 
            // 所以只有支持这个token的authenticationProvider才能执行authenticate()
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
        .............
//如果当前的providerManager 没有找到合适AuthenticateProvider ,则从父的providerManager中查找
            if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
        }
    ......................
.........................
}

DaoAuthenticationProvider:

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
            //查找用户,通过userDetailsService 接口查找user ,
//用户可自己实现UserDetailsService接口的loadUserByUsername();
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
....................

try {
        //预检查user 的 授权信息, 比如用户是否账号被锁,用户账号是否可用,用户账号是否过期
            preAuthenticationChecks.check(user);
//密码MD5校验
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
....................

return createSuccessAuthentication(principalToReturn, authentication, user);

createSuccessAuthentication()

private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                // user.getAuthorities()返回的是GrantedAuthority
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

authoritiesMapper有三种实现: NullAuthorititesMapper(默认),RoleHierarchyAuthoritiesMapper,SimpleAuthorityMapper
接下来执行successfulAuthentication()来更新SecurityContextHolder中Authentication的信息

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
        //最后调用sucessHandler 来处理 authentication成功之后的跳转逻辑
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }
  1. ConcurrentSessionFilter

  1. RequestCacheAwareFilter

  1. SecurityContextHolderAwareRequestFilter

    通过HttpServlet3RequestFactory创建一个HttpServletRequest:
@Override
    public HttpServletRequest create(HttpServletRequest request,
            HttpServletResponse response) {
//this.rolePrefix == “ROLE_“
        return new Servlet3SecurityContextHolderAwareRequestWrapper(request,
                this.rolePrefix, response);
    }
  1. AnonymousAuthenticationFilter

if (SecurityContextHolder.getContext().getAuthentication() == null) {
        // 这里创建一个匿名的AuthenticationToken : AnonymousAuthenticationToken
            SecurityContextHolder.getContext().setAuthentication(
                    createAuthentication((HttpServletRequest) req));
}
  1. SessionManagementFilter

static final String FILTER_APPLIED = "__spring_security_session_mgmt_filter_applied";

if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }
else{
    //如果HttpSessionSecurityContextRepository中没有这次请求对应的securityContext
    if (!securityContextRepository.containsContext(request)) {
        Authentication authentication = SecurityContextHolder.getContext()
                    .getAuthentication();
        //如果在SecurityContextHolder上下文中存在Authentication信息并且不是匿名的,
        //证明这个用户在当前请求中已经被授权了,可只见调用sessionAuthenticationStrategy重新绑定request
        //对应的authentication, RegisterSessionAuthenticationStrategy会调用
        // sessionRegistry.registerNewSession(request.getSession().getId(),authentication.getPrincipal());
        if (authentication != null && !trustResolver.isAnonymous(authentication)) {
                // The user has been authenticated during the current request, so call the
                // session strategy
                try {
                    sessionAuthenticationStrategy.onAuthentication(authentication,
                            request, response);
                }
                catch (SessionAuthenticationException e) {
                    // The session strategy can reject the authentication
                    logger.debug(
                            "SessionAuthenticationStrategy rejected the authentication object",
                            e);
                    SecurityContextHolder.clearContext();
                    failureHandler.onAuthenticationFailure(request, response, e);

                    return;
                }
                // Eagerly save the security context to make it available for any possible
                // re-entrant
                // requests which may occur before the current request completes.
                // SEC-1396.
                securityContextRepository.saveContext(SecurityContextHolder.getContext(),
                        request, response);
            }
            else {
                // No security context or authentication present. Check for a session
                // timeout
                if (request.getRequestedSessionId() != null
                        && !request.isRequestedSessionIdValid()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Requested session ID "
                                + request.getRequestedSessionId() + " is invalid.");
                    }

                    if (invalidSessionStrategy != null) {
                        invalidSessionStrategy
                                .onInvalidSessionDetected(request, response);
                        return;
                    }
                }
            }  
    }  
}
  1. ExceptionTranslationFilter

如果在上述过滤器中出现Exception,都会在这个过滤器中拦截并跳转执行 handleSpringSecurityException()方法

  1. FilterSecurityInteceptor