zoukankan      html  css  js  c++  java
  • Spring Security 源码解析(一)

    上篇 Spring Security基本配置已讲述了Spring Security最简单的配置,本篇将开始分析其基本原理

    在上篇中可以看到,在访问 http://localhost:18081/user 时,直接跳转到登录页。那Security是怎么做的呢?本篇主要讲述跳转到登录页前的处理

    首先来看一张时序图:

    通过上图可以看到,请求顺序为AbstractAuthenticationProcessingFilter -> AnonymousAuthenticationFilter -> ExceptionTranslationFilter -> FilterSecurityInterceptor

    AbstractAuthenticationProcessingFilter

    请求先进入 AbstractAuthenticationProcessingFilter 的doFilter()方法。判断当前filter是否可以处理当前请求(也就是是否包含用户名密码信息),如果是,则调用其子类 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法进行验证(第一次请求时,没有用户名密码,是不会调用子类的)

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
        
            //判断当前的filter是否可以处理当前请求,不可以的话则交给下一个filter处理
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
    
                return;
            }
    
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }
    
            Authentication authResult;
    
            try {
                //抽象方法由子类UsernamePasswordAuthenticationFilter实现
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
                //认证成功后,处理一些与session相关的方法
                sessionStrategy.onAuthentication(authResult, request, response);
            }
            catch (InternalAuthenticationServiceException failed) {
                logger.error(
                        "An internal error occurred while trying to authenticate the user.",
                        failed);
                //认证失败后的的一些操作
                unsuccessfulAuthentication(request, response, failed);
    
                return;
            }
            catch (AuthenticationException failed) {
                // Authentication failed
                unsuccessfulAuthentication(request, response, failed);
    
                return;
            }
    
            // Authentication success
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            //认证成功后的相关回调方法,主要将当前的认证放到SecurityContextHolder中
            successfulAuthentication(request, response, chain, authResult);
        }

    认证成功后的回调方法:

    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中
            SecurityContextHolder.getContext().setAuthentication(authResult);
    
            rememberMeServices.loginSuccess(request, response, authResult);
    
            // Fire event
            if (this.eventPublisher != null) {
                eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                        authResult, this.getClass()));
            }
    
            //调用其它可扩展的 handlers 继续处理该认证成功以后的回调事件
            //实现AuthenticationSuccessHandler接口即可
            successHandler.onAuthenticationSuccess(request, response, authResult);
        }

    认证失败后的回调方法:

    protected void unsuccessfulAuthentication(HttpServletRequest request,
                HttpServletResponse response, AuthenticationException failed)
                throws IOException, ServletException {
            
            //清除SecurityContextHolder的中数据
            SecurityContextHolder.clearContext();
    
            if (logger.isDebugEnabled()) {
                logger.debug("Authentication request failed: " + failed.toString(), failed);
                logger.debug("Updated SecurityContextHolder to contain null Authentication");
                logger.debug("Delegating to authentication failure handler " + failureHandler);
            }
    
            rememberMeServices.loginFail(request, response);
    
            //调用其它可扩展的 handlers 处理该认证失败以后的回调事件
            //实现 AuthenticationFailureHandler 接口即可
            failureHandler.onAuthenticationFailure(request, response, failed);
        

    关于自定义 handlers ,可参考 Spring Security认证配置(三)

    UsernamePasswordAuthenticationFilter

    UsernamePasswordAuthenticationFilter本身不是过滤器,而是继承了AbstractAuthenticationProcessingFilter才拥有过滤器的性能,其主要是验证用户名密码。

    public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            
            //认证请求的方法必须为POST
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            //从request中获取 username 和 password
            String username = obtainUsername(request);
            String password = obtainPassword(request);
    
            if (username == null) {
                username = "";
            }
    
            if (password == null) {
                password = "";
            }
    
            username = username.trim();
    
            //封装Authenticaiton的实现类UsernamePasswordAuthenticationToken
            //传入用户名和密码,并将是否已经认证设为false
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
    
            //设置UsernamePasswordAuthenticationToken中的详细信息。如remoteAddress、sessionId
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    
            //调用 AuthenticationManager 的实现类 ProviderManager 进行验证
            return this.getAuthenticationManager().authenticate(authRequest);
        }

    验证的过程,可以参考AuthenticationManager、ProviderManager

    ExceptionTranslationFilter

    ExceptionTranslationFilter是异常处理过滤器,该过滤器用来处理在系统认证授权过程中抛出的异常(也就是FilterSecurityInterceptor抛出来的),主要是处理

    AccessDeniedException、AuthenticationException

    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) {
                //判断是不是AuthenticationException
                // Try to extract a SpringSecurityException from the stacktrace
                Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
                RuntimeException ase = (AuthenticationException) throwableAnalyzer
                        .getFirstThrowableOfType(AuthenticationException.class, causeChain);
    
                if (ase == null) {
                    //判断是不是AccessDeniedException
                    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);
                }
            }
        }

    在 handleSpringSecurityException 方法中,有一段:

           Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                //判断当前authentication是不是AnonymousAuthenticationToken(RememberMeAuthenticationToken)或者其子类
                if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                    logger.debug(
                            "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
                            exception);
                    
                    sendStartAuthentication(
                            request,
                            response,
                            chain,
                            new InsufficientAuthenticationException(
                                    "Full authentication is required to access this resource"));
                }

    其中sendStartAuthentication方法:

    protected void sendStartAuthentication(HttpServletRequest request,
                HttpServletResponse response, FilterChain chain,
                AuthenticationException reason) throws ServletException, IOException {
         //清空SecurityContext SecurityContextHolder.getContext().setAuthentication(null); //缓存当前请求 requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); //调用AuthenticationEntryPoint的实现类LoginUrlAuthenticationEntryPoint(可扩展,实现AuthenticationEntryPoint即可) //跳转到可配置的登录页(如果不配置,则跳转到spring security默认的登录页) authenticationEntryPoint.commence(request, response, reason); }

    FilterSecurityInterceptor 

    此过滤器为认证授权过滤器链中最后一个过滤器,该过滤器通过之后就是真正的 /user 服务

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
            ......
    else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //调用父类AbstractSecurityInterceptor.beforeInvocation方法,进行最后一次过滤 InterceptorStatusToken token = super.beforeInvocation(fi); try { //请求真正的 /user 服务 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }

    在beforeInvocation方法中,会调用AccessDecisionManager.decide方法来验证当前认证成功的用户是否有权限访问该资源

    protected InterceptorStatusToken beforeInvocation(Object object) {
            ......
    
            Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                    .getAttributes(object);
    
            ......
    
            Authentication authenticated = authenticateIfRequired();
    
            // Attempt authorization
            try {
                //授权认证
                this.accessDecisionManager.decide(authenticated, object, attributes);
            }
            catch (AccessDeniedException accessDeniedException) {
                publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                        accessDeniedException));
    
                throw accessDeniedException;
            }
        }

     上面的object和attributes是什么?调试一下:

     object为当前请求 URL:/user

     requestMap的值有两个,如下图:

    可以看到,这两个值对应SecurityConfig中的配置。第一个为antMatchs返回permitAll即不需要认证,第二个为anyRequest返回authenticated即其它请求需要认证

    所以 getAttributes 就是使用当前请求路径去匹配我们自定义的规则,attributes为匹配后的结果

    我们继续来看最核心的授权认证:

    this.accessDecisionManager.decide(authenticated, object, attributes)

    此时,authenticated为匿名AnonymousAuthenticationToken,attributes为authenticated

    AccessDecisionManager是如何授权的呢?

    Spring Security默认使用AccessDecisionManager的子类 AffirmativeBased,通过实现decide方法来鉴定用户是否有访问对应资源(方法或URL)的权限

    public void decide(Authentication authentication, Object object,
                Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
            int deny = 0;
    
            //调用AccessDecisionVoter进行vote(投票)
            for (AccessDecisionVoter voter : getDecisionVoters()) {
                int result = voter.vote(authentication, object, configAttributes);
    
                if (logger.isDebugEnabled()) {
                    logger.debug("Voter: " + voter + ", returned: " + result);
                }
    
                switch (result) {
                //voter投票为ACCESS_GRANTED,表示同意,直接返回
                case AccessDecisionVoter.ACCESS_GRANTED:
                    return;
                //voter投票为ACCESS_DENIED,表示反对,则记录一下
                case AccessDecisionVoter.ACCESS_DENIED:
                    deny++;
    
                    break;
                //voter投票为其它值,则表示弃权。都弃权也会通过
                default:
                    break;
                }
            }
    
            //只要有一个voter投票为ACCESS_DENIED,则直接就不通过了
            if (deny > 0) {
                throw new AccessDeniedException(messages.getMessage(
                        "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
            }
    
            // To get this far, every AccessDecisionVoter abstained
            checkAllowIfAllAbstainDecisions();
        }

    第一次请求,这里将抛出AccessDeniedException。然后被ExceptionTranslationFilter捕获,跳转到授权登录认证页面

  • 相关阅读:
    笨办法学习python之hashmap
    python实现三级菜单源代码
    ql的python学习之路-day3
    ql的python学习之路-day2
    Spring的数据库开发
    Spring学习之Aspectj开发实现AOP
    spring学习之依赖注入DI与控制反转IOC
    spring学习之第一个spring程序
    spring学习之spring入门
    Java线程(一)——创建线程的两种方法
  • 原文地址:https://www.cnblogs.com/xuwenjin/p/9552303.html
Copyright © 2011-2022 走看看