spring security使用和原理简析(3)

转自 https://www.colabug.com/4927991.html

假如没有spring security我们自己实现对url权限验证,则必然也是添加拦截器对于请求进行过滤,所以Spring Security 的核心之一就是它的过滤器链。

我们就从它的过滤器链入手,下图是Spring Security 过滤器链的一个执行过程,本文将依照该过程来逐步的剖析其认证过程

 

核心过滤器链简介

Spring Security 中的过滤器有很多,一般正常的项目中都有十几个过滤器,有时候还包含自定义的过滤器,当然我们不可能对每一个过滤器都进行分析,我们需要抓住重点,找比较关键的几个过滤器,它们在认证过程中扮演着重要角色,下面列举几个核心的过滤器:

  • SecurityContextPersistenceFilter : 整个Spring Security 过滤器链的开端,它有两个作用:一是当请求到来时,检查 Session 中是否存在 SecurityContext ,如果不存在,就创建一个新的 SecurityContext 。二是请求结束时将 SecurityContext 放入 Session 中,并清空 SecurityContextHolder
  • UsernamePasswordAuthenticationFilter : 继承自抽象类 AbstractAuthenticationProcessingFilter ,当进行表单登录时,该Filter将用户名和密码封装成一个 UsernamePasswordAuthentication 进行验证。
  • AnonymousAuthenticationFilter : 匿名身份过滤器,当前面的Filter认证后依然没有用户信息时,该Filter会生成一个匿名身份—— AnonymousAuthenticationToken 。一般的作用是用于匿名登录。
  • ExceptionTranslationFilter : 异常转换过滤器,用于处理 FilterSecurityInterceptor 抛出的异常。
  • FilterSecurityInterceptor : 过滤器链最后的关卡,从 SecurityContextHolder中获取 Authentication,比对用户拥有的权限和所访问资源需要的权限。

表单登录认证过程

当我们访问一个受保护的资源时,如果之前没有进行登录认证,那么系统将返回一个登录表单或者一个响应结果提示我们要先进行登录操作。我们这里的分析过程只针对表单登录,所以我们先在表单中填写用户名和密码进行登录验证。

上面已经简述了一堆核心过滤器,这里先从 SecurityContextPersistenceFilter 这个过滤器的开端开始分析整个表单登录的认证过程。

SecurityContextPersistenceFilter

当我们填写表单完毕后,点击登录按钮,请求先经过 SecurityContextPersistenceFilter 过滤器,在前面就曾提到,该Filter有两个作用,其中之一就是在请求到来时,创建 SecurityContext 安全上下文,我们来看看它内部是如何做的,部分源码如下:

public class SecurityContextPersistenceFilter extends GenericFilterBean {

    static final String FILTER_APPLIED = "__spring_security_scpf_applied";
    //安全上下文存储的仓库
    private SecurityContextRepository repo;

    private boolean forceEagerSessionCreation = false;

    public SecurityContextPersistenceFilter() {
        //使用HttpSession来存储 SecurityContext
        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;
        // 如果是第一次请求,request中肯定没有 FILTER_APPLIED属性
        if (request.getAttribute(FILTER_APPLIED) != null) {
            // 确保每个请求只应用一次过滤器
            chain.doFilter(request, response);
            return;
        }

        final boolean debug = logger.isDebugEnabled();
        // 在request 设置 FILTER_APPLIED 属性为 true,这样同一个请求再次访问时,就直接进入后续Filter的操作
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        
        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();

            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }
        // 封装 requset 和 response 
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        // 从存储安全上下文的仓库中载入 SecurityContext 安全上下文,其内部是从 Session中获取上下文信息
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

        try {
            //安全上下文信息设置到 SecurityContextHolder 中,以便在同一个线程中,后续访问 SecurityContextHolder 能获取到 SecuritContext
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            //进入下一个过滤器操作
            chain.doFilter(holder.getRequest(), holder.getResponse());

        }
        finally {
            // 请求结束后,清空安全上下文信息
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            SecurityContextHolder.clearContext();
            //将安全上下文信息存储到 Session中,相当于登录态的维护
            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;
    }
}
View Code

请求到来时,利用 HttpSessionSecurityContextRepository 读取安全上下文。我们这里是第一次请求,读取的安全上下文中是没有 Authentication 身份信息的,将安全上下文设置到 SecurityContextHolder 之后,进入下一个过滤器。

请求结束时,同样利用 HttpSessionSecurityContextRepository 该存储安全上下文的仓库将认证后的 SecurityContext 放入 Session 中,这也是 登录态维护 的关键,具体的操作这里就不细说了

UsernamePasswordAuthenticationFilter

经过 SecurityContextPersistenceFilter 过滤器后来到 UsernamePasswordAuthenticationFilter 过滤器,因为我们假定的是第一次请求,所以 SecurityContext 并没有包含认证过的 Authentication 。 从此过滤器开始的操作对于表单登录来说是非常关键的,包含了表单登录的核心认证步骤 ,下面画了一张在此过滤器中的认证过程图:

过程比较冗长,就不一一翻源码了,主要也是比较UserDetailsService.loadUserByUsername(String username)返回的详细信息中密码和表单提交中的密码做匹配

如一致则生成authentication对象,加入securitConetext中。

AnonymousAuthenticationFilter

匿名认证过滤器,它主要是针对匿名登录,如果前面的Filter,比如 UsernamePasswordAuthenticationFilter 执行完毕后,SecurityContext依旧没有用户信息,那么 AnonymousAuthenticationFilter 才会起作用,生成一个匿名身份信息——AnonymousAuthenticationToken

ExceptionTranslationFilter

ExceptionTranslationFilter 简单的说就是处理 FilterSecurityInterceptor 抛出的异常,其内部 doFilter 方法源码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            //直接进入下一个Filter
            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);
            //这里会处理 FilterSecurityInterceptor 抛出的AccessDeniedException
            if (ase == null) {
                ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                        AccessDeniedException.class, causeChain);
            }

            if (ase != null) {
                if (response.isCommitted()) {
                    throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
                }
                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);
            }
        }
    }
View Code

FilterSecurityInterceptor

FilterSecurityInterceptor 过滤器是最后的关卡,之前的请求最终会来到这里,它的大致工作流程就是

  • 封装请求信息
  • 从系统中读取配信息,即资源所需的权限信息
  • SecurityContextHolder 中获取之前认证过的 Authentication 对象,即表示当前用户所拥有的权限
  • 然后根据上面获取到的三种信息,传入一个权限校验器中,对于当前请求来说,比对用户拥有的权限和资源所需的权限。若比对成功,则进入真正系统的请求处理逻辑,反之,会抛出相应的异常

下面画一张简易的流程图来阐述 FilterSecurityInterceptor 的执行过程,如下: 

 

 根据上图内容,我们再来看看 FilterSecurityInterceptor 的源码, 

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
        Filter {
     ...
    public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    // 封装request、response请求
    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 {
        // 判断当前请求之前是否经历过该过滤器
        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);
    }
}

}
View Code

 

 源码中已经对请求进行了封装,然后进入核心部分, 调用父类的授权判断方法—— beforeInvocation(FilterInvocation) ,源码如下: 

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());
        }
        //读取Spring Security的配置信息,将其封装成 ConfigAttribute
        Collection 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'");
            }
                            ...
            return null; // no further work post-invocation
        }
                ...
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            credentialsNotFound(messages.getMessage(
                    "AbstractSecurityInterceptor.authenticationNotFound",
                    "An Authentication object was not found in the SecurityContext"),
                    object, attributes);
        }
        //从SecurityContextHolder中获取Authentication
        Authentication authenticated = authenticateIfRequired();

        // 启动授权匹配
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }
                ...
        
    }
View Code

 

beforeInvocation 的源码比较多,我这里只保留了相对核心的部分,从源码就可以看出,拿到配置信息和用户信息后,连同请求信息一同传入 AccessDecisionManagerdecide(Authentication authentication, Object object,Collection configAttributes) 方法。该方法是最终执行授权校验逻辑的地方。

AccessDecisionManager 本身是一个接口,它的 实现类是 AbstractAccessDecisionManager ,而 AbstractAccessDecisionManager 也是一个抽象类,

根据业务我们可以自己实现,主要通过重写decide方法来实现自定义的权限判断

   //decide 方法是判定是否拥有权限的决策方法
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        String url, method;
        //关闭鉴权
//        if (true){
//            return;
//        }
        if ( matchers("/api/v1/auth", request)
                || matchers("/swagger-resources/**", request)
                || matchers("/translate_a/**", request)
                || matchers("/doc.html", request)
                || matchers("/webjars/**", request)
                || matchers("/v2/**", request)
                || matchers("/META-INF/resources/webjars/**", request)
                || matchers("/META-INF/resources/**", request)) {
            return;
        } else {
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (ga instanceof UrlGrantedAuthority) {
                    UrlGrantedAuthority urlGrantedAuthority = (UrlGrantedAuthority) ga;
                    url = urlGrantedAuthority.getPermissionUrl();
                    method = urlGrantedAuthority.getMethod();
                    if (matchers(url, request)) {
                        if (method.equals(request.getMethod()) || "ALL".equals(method)) {
                            return;
                        }
                    }
                }
            }
        }
        throw new AccessDeniedException("no right");
    }
View Code

小结

上面从过滤器出发,对 Spring Security的认证过程做了一个还算详细的分析,当然还存在很多细节问题没有涉及到。

 

posted @ 2019-06-26 11:22  jiataoqin  阅读(300)  评论(0编辑  收藏  举报