您的位置:首页 > 编程语言 > Java开发

Spring Security原理学习--核心过滤器Filter(二)

2018-11-11 09:28 232 查看

绍一下Spring Security相关的实现机制  。首先Spring Security的认证功能是依赖Filter实现的,当然在认证功能基础上还提供了一些安全的验证等都是依赖Filter来实现完成的,如下截图Spring Security提供了13个功能Filter,并且是按照如下顺序依次执行的。当然配合Spring web的Filter注入实现,Spring Security提供了另外一个Filter的实现类FilterChainProxy,其对外包装了以下13个Filter,其实13个Filter是在接口SecurityFilterChain的实现类DefaultSecurityFilterChain中通过List保存的。

接下来我们依次介绍一下这个13个Filter。

1、WebAsyncManagerIntegrationFilter

1、WebAsyncManagerIntegrationFilter
public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
    private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
 
        SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
                .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
        if (securityProcessingInterceptor == null) {
            asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
                    new SecurityContextCallableProcessingInterceptor());
        }
 
        filterChain.doFilter(request, response);
    }
}
根据请求封装获取WebAsyncManager
从WebAsyncManager获取/注册SecurityContextCallableProcessingInterceptor
2、SecurityContextPersistenceFilter
两个主要职责:请求来临时,创建SecurityContext安全上下文信息,请求结束时清空SecurityContextHolder。

public class SecurityContextPersistenceFilter extends GenericFilterBean {
 
    static final String FILTER_APPLIED = "__spring_security_scpf_applied";
 
    private SecurityContextRepository repo;
 
    private boolean forceEagerSessionCreation = false;
 
    public SecurityContextPersistenceFilter() {
        this(new HttpSessionSecurityContextRepository());
    }
 
    public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
        this.repo = repo;
    }
 
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        
        //判断是否已经处理过
        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);
            return;
        }
 
        final boolean debug = logger.isDebugEnabled();
 
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
 
        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();
 
            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }
 
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        //获取SecurityContext
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
 
        try {
            SecurityContextHolder.setContext(contextBeforeChainExecution);
 
            chain.doFilter(holder.getRequest(), holder.getResponse());
 
        }
        finally {
            //结束后清理SecurityContext
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);
 
            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }
 
    public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
        this.forceEagerSessionCreation = forceEagerSessionCreation;
    }
}
3、HeaderWriterFilter
用来给http响应添加一些Header,比如X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options.

public class HeaderWriterFilter extends OncePerRequestFilter {
 
    /**
     * Collection of {@link HeaderWriter} instances to write out the headers to the
     * response.
     */
    private final List<HeaderWriter> headerWriters;
 
    /**
     * Creates a new instance.
     *
     * @param headerWriters the {@link HeaderWriter} instances to write out headers to the
     * {@link HttpServletResponse}.
     */
    public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
        Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
        this.headerWriters = headerWriters;
    }
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
 
        HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
                response, this.headerWriters);
        try {
            filterChain.doFilter(request, headerWriterResponse);
        }
        finally {
            headerWriterResponse.writeHeaders();
        }
    }
 
    static class HeaderWriterResponse extends OnCommittedResponseWrapper {
        private final HttpServletRequest request;
        private final List<HeaderWriter> headerWriters;
 
        HeaderWriterResponse(HttpServletRequest request, HttpServletResponse response,
                List<HeaderWriter> headerWriters) {
            super(response);
            this.request = request;
            this.headerWriters = headerWriters;
        }
 
        /*
         * (non-Javadoc)
         *
         * @see org.springframework.security.web.util.OnCommittedResponseWrapper#
         * onResponseCommitted()
         */
        @Override
        protected void onResponseCommitted() {
            writeHeaders();
            this.disableOnResponseCommitted();
        }
 
        protected void writeHeaders() {
            if (isDisableOnResponseCommitted()) {
                return;
            }
            for (HeaderWriter headerWriter : this.headerWriters) {
                headerWriter.writeHeaders(this.request, getHttpResponse());
            }
        }
 
        private HttpServletResponse getHttpResponse() {
            return (HttpServletResponse) getResponse();
        }
    }
}
4、LogoutFilter
退出拦截器,退出的简单操作就是删除Session,根据Spring Security初始化配置的退出地址来匹配请求。

public class LogoutFilter extends GenericFilterBean {
 
    // ~ Instance fields
    // ================================================================================================
 
    private RequestMatcher logoutRequestMatcher;
 
    private final List<LogoutHandler> handlers;
    private final LogoutSuccessHandler logoutSuccessHandler;
 
    // ~ Constructors
    // ===================================================================================================
 
    /**
     * Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
     * target destination after logging out. The list of <tt>LogoutHandler</tt>s are
     * intended to perform the actual logout functionality (such as clearing the security
     * context, invalidating the session, etc.).
     */
    public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler,
            LogoutHandler... handlers) {
        Assert.notEmpty(handlers, "LogoutHandlers are required");
        this.handlers = Arrays.asList(handlers);
        Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
        this.logoutSuccessHandler = logoutSuccessHandler;
        setFilterProcessesUrl("/logout");
    }
 
    public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
        Assert.notEmpty(handlers, "LogoutHandlers are required");
        this.handlers = Arrays.asList(handlers);
        Assert.isTrue(
                !StringUtils.hasLength(logoutSuccessUrl)
                        || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
                logoutSuccessUrl + " isn't a valid redirect URL");
        SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
        if (StringUtils.hasText(logoutSuccessUrl)) {
            urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
        }
        logoutSuccessHandler = urlLogoutSuccessHandler;
        setFilterProcessesUrl("/logout");
    }
 
    // ~ Methods
    // ========================================================================================================
 
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        //判断是不是退出请求
        if (requiresLogout(request, response)) {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
 
            if (logger.isDebugEnabled()) {
                logger.debug("Logging out user '" + auth
                        + "' and transferring to logout destination");
            }
 
            for (LogoutHandler handler : handlers) {
                //在logoutHandler中进行删除session操作
                handler.logout(request, response, auth);
            }
 
            logoutSuccessHandler.onLogoutSuccess(request, response, auth);
 
            return;
        }
 
        chain.doFilter(request, response);
    }
 
    /**
     * Allow subclasses to modify when a logout should take place.
     *
     * @param request the request
     * @param response the response
     *
     * @return <code>true</code> if logout should occur, <code>false</code> otherwise
     */
    protected boolean requiresLogout(HttpServletRequest request,
            HttpServletResponse response) {
        return logoutRequestMatcher.matches(request);
    }
 
    public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
        Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
        this.logoutRequestMatcher = logoutRequestMatcher;
    }
 
    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
    }
}
当判断请求是退出时,会调用LogoutHandler的logout删除session,具体实现在SecurityContextLogoutHandler的logout方法中。

public void logout(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {
        Assert.notNull(request, "HttpServletRequest required");
        if (invalidateHttpSession) {
            //设置 20000 session无效
            HttpSession session = request.getSession(false);
            if (session != null) {
                logger.debug("Invalidating session: " + session.getId());
                session.invalidate();
            }
        }
 
        if (clearAuthentication) {
            SecurityContext context = SecurityContextHolder.getContext();
            context.setAuthentication(null);
        }
        //清理信息
        SecurityContextHolder.clearContext();
    }
5、UsernamePasswordAuthenticationFilter
        用户名和密码校验Filter,是特别重要的一个Filter,我们会在用户登录验证博客中专门进行分析学习一下,当然这个Filter只会对配置的登录请求/login进行业务处理,其他请求不做任何业务处理,其处理逻辑在父类AbstractAuthenticationProcessingFilter中。

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;
        }
        //是登录请求进行校验工作
        //省略部分代码
        authResult = attemptAuthentication(request, response);
        //省略部分代码
        
    }
6、DefaultLoginPageGeneratingFilter
         当是登录地址请求,登录失败请求或者登出请求则跳转到登录页面。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
 
        boolean loginError = isErrorPage(request);
        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.length());
            response.getWriter().write(loginPageHtml);
 
            return;
        }
 
        chain.doFilter(request, response);
    }
7、BasicAuthenticationFilter
        处理BASIC authentication认证方式,简单理解和UsernamePasswordAuthenticationFilter类似,不过UsernamePasswordAuthenticationFilter是通过表单提交的,而Authorization认证方式是将用户名密码数据添加到请求header中罢了。

@Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
                    throws IOException, ServletException {
        final boolean debug = this.logger.isDebugEnabled();
        //请求头中获取Authorization
        String header = request.getHeader("Authorization");
        //如果不存在或者不是以Basic开头直接跳过处理
        if (header == null || !header.startsWith("Basic ")) {
            chain.doFilter(request, response);
            return;
        }
        //从header中获取相关用户名密码数据进行验证
        try {
            String[] tokens = extractAndDecodeHeader(header, request);
            assert tokens.length == 2;
 
            String username = tokens[0];
 
            if (debug) {
                this.logger
                        .debug("Basic Authentication Authorization header found for user '"
                                + username + "'");
            }
 
            if (authenticationIsRequired(username)) {
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                        username, tokens[1]);
                authRequest.setDetails(
                        this.authenticationDetailsSource.buildDetails(request));
                Authentication authResult = this.authenticationManager
                        .authenticate(authRequest);
 
                if (debug) {
                    this.logger.debug("Authentication success: " + authResult);
                }
 
                SecurityContextHolder.getContext().setAuthentication(authResult);
 
                this.rememberMeServices.loginSuccess(request, response, authResult);
 
                onSuccessfulAuthentication(request, response, authResult);
            }
 
        }
        catch (AuthenticationException failed) {
            SecurityContextHolder.clearContext();
 
            if (debug) {
                this.logger.debug("Authentication request for failed: " + failed);
            }
 
            this.rememberMeServices.loginFail(request, response);
 
            onUnsuccessfulAuthentication(request, response, failed);
 
            if (this.ignoreFailure) {
                chain.doFilter(request, response);
            }
            else {
                this.authenticationEntryPoint.commence(request, response, failed);
            }
 
            return;
        }
 
        chain.doFilter(request, response);
    }
8、RequestCacheAwareFilter 
内部维护了一个RequestCache,用于缓存request请求

public class RequestCacheAwareFilter extends GenericFilterBean {
 
    private RequestCache requestCache;
 
    public RequestCacheAwareFilter() {
        this(new HttpSessionRequestCache());
    }
 
    public RequestCacheAwareFilter(RequestCache requestCache) {
        Assert.notNull(requestCache, "requestCache cannot be null");
        this.requestCache = requestCache;
    }
 
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
 
        HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
                (HttpServletRequest) request, (HttpServletResponse) response);
 
        chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
                response);
    }
 
}
9、SecurityContextHolderAwareRequestFilter 
        此过滤器对ServletRequest进行了一次包装,使得request具有更加丰富的API

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
                (HttpServletResponse) res), res);
    }
10、AnonymousAuthenticationFilter
           匿名身份过滤器,这个过滤器个人认为很重要,需要将它与UsernamePasswordAuthenticationFilter 放在一起比较理解,spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
 
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //创建一个匿名身份
            SecurityContextHolder.getContext().setAuthentication(
                    createAuthentication((HttpServletRequest) req));
 
            if (logger.isDebugEnabled()) {
                logger.debug("Populated SecurityContextHolder with anonymous token: '"
                        + SecurityContextHolder.getContext().getAuthentication() + "'");
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
                        + SecurityContextHolder.getContext().getAuthentication() + "'");
            }
        }
 
        chain.doFilter(req, res);
    }
 
    protected Authentication createAuthentication(HttpServletRequest request) {
        AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
                principal, authorities);
        auth.setDetails(authenticationDetailsSource.buildDetails(request));
 
        return auth;
    }
11、SessionManagementFilter
           和session相关的过滤器,内部维护了一个SessionAuthenticationStrategy,两者组合使用,常用来防止session-fixation protection attack,以及限制同一用户开启多个会话的数量

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
 
        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }
 
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
 
        if (!securityContextRepository.containsContext(request)) {
            Authentication authentication = SecurityContextHolder.getContext()
                    .getAuthentication();
 
            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;
                    }
                }
            }
        }
 
        chain.doFilter(request, response);
    }
12、ExceptionTranslationFilter
        ExceptionTranslationFilter异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常,将其转化,顾名思义,转化以意味本身并不处理。一般其只处理两大类异常:AccessDeniedException访问异常和AuthenticationException认证异常。这个过滤器非常重要,因为它将Java中的异常和HTTP的响应连接在了一起,这样在处理异常时,我们不用考虑密码错误该跳到什么页面,账号锁定该如何,只需要关注自己的业务逻辑,抛出相应的异常便可。如果该过滤器检测到AuthenticationException,则将会交给内部的AuthenticationEntryPoint去处理,如果检测到AccessDeniedException,需要先判断当前用户是不是匿名用户,如果是匿名访问,则和前面一样运行AuthenticationEntryPoint,否则会委托给AccessDeniedHandler去处理,而AccessDeniedHandler的默认实现,是AccessDeniedHandlerImpl。所以ExceptionTranslationFilter内部的AuthenticationEntryPoint是至关重要的,顾名思义:认证的入口点。

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);
 
            logger.debug("Chain processed normally");
        }
        catch (IOException ex) {
            throw ex;
        }
        //捕捉一些异常处理,身份校验判断错误会抛出异常,由这里处理
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
            RuntimeException ase = (AuthenticationException) throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
 
            if (ase == null) {
                ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                        AccessDeniedException.class, causeChain);
            }
 
            if (ase != null) {
                handleSpringSecurityException(request, response, chain, ase);
            }
            else {
                // Rethrow ServletExceptions and RuntimeExceptions as-is
                if (ex instanceof ServletException) {
                    throw (ServletException) ex;
                }
                else if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                }
 
                // Wrap other Exceptions. This shouldn't actually happen
                // as we've already covered all the possibilities for doFilter
                throw new RuntimeException(ex);
            }
        }
    }
13、FilterSecurityInterceptor
        个人理解任务这个拦截器是Spring Security中最重要的,首先这个拦截器就是进行角色相关的处理,也是登录结果是否成功最后一个判断的拦截器,针对不需要认证和需要认证的请求会进行不同的验证处理,同样在登录博客中会对其做的功能进行详细的说明。

public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
 
        invoke(fi);
    }
 
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) {
                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);
        }
    }
 
 

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐