Spring Security 登录框架源码逻辑剖析

若无特殊配置登录认证会通过UsernamePasswordAuthenticationFilter来处理

UsernamePasswordAuthenticationFilter继承了抽象类AbstractAuthenticationProcessingFilter

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
}

AbstractAuthenticationProcessingFilter已经定义了doFilter方法,UsernamePasswordAuthenticationFilter对请求的拦截是通过其父类方法实现的。

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

    ......

   Authentication authResult;

   try {
      // 划重点划重点划重点划重点划重点划重点
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      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);
   }

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

doFilter中核心便是那个attemptAuthentication()方法了,这个AbstractAuthenticationProcessingFilter的抽象方法被UsernamePasswordAuthenticationFilter继承实现了

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
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        // 获取AuthenticationManager对象,并对Token进行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }

UsernamePasswordAuthenticationFilter在这个方法中基于用户的用户名和密码生成了token,并将这个token传给AuthenticationManager进行认证。Spring Security有许多AuthenticationManager的实现类,通过Debug可以发现,默认情况下被set到UsernamePasswordAuthenticationFilter中的AuthenticationManagerProviderManager

ProviderManager对token进行认证的代码逻辑如下

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
   Class<? extends Authentication> toTest = authentication.getClass();
   AuthenticationException lastException = null;
   Authentication result = null;
   boolean debug = logger.isDebugEnabled();
   
   // 调用getProviders()获取所有AuthenticationProvider
   for (AuthenticationProvider provider : getProviders()) {
      // 若这个provider不支持此类型的token,则continue
      if (!provider.supports(toTest)) {
         continue;
      }

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

      try {
         // 使用这个provider对token进行认证
         result = provider.authenticate(authentication);

         if (result != null) {
            // 将authentication的Details拷贝到result
            copyDetails(authentication, result);
            break;
         }
      }
      catch (AccountStatusException e) {
         prepareException(e, authentication);
         // SEC-546: Avoid polling additional providers if auth failure is due to
         // invalid account status
         throw e;
      }
      catch (InternalAuthenticationServiceException e) {
         prepareException(e, authentication);
         throw e;
      }
      catch (AuthenticationException e) {
         lastException = e;
      }
   }
   // result为null且这个ProviderManager的parent不为null
   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;
      }
   }

   // 如果result不为null
   if (result != null) {
      if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
         // 认证完成. 从authentication中移除密码以及其他敏感信息
         ((CredentialsContainer) result).eraseCredentials();
      }

      eventPublisher.publishAuthenticationSuccess(result);
      return result;
   }

   // 执行到这里,可能是因为parent为null或认证未通过或抛出了异常
   // 如果未抛出exception,则是由于provider为null的原因
   if (lastException == null) {
      lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] { toTest.getName() },
            "No AuthenticationProvider found for {0}"));
   }

   prepareException(lastException, authentication);

   throw lastException;
}

上面的代码中,若provider对token的认证未通过,则会调用它的parentAuthenticationManager执行authenticate()

那么这个过程究竟会经过哪些和AuthenticationProvider呢?经过debug跟踪可以发现

第一个ProviderManager有一个AuthenticationProvider,它就是AnonymousAuthenticationProvider,不支持AnonymousAuthenticationProvider类型为UsernamePasswordAuthenticationTokenAuthentication。因此会continue到下一个foreach循环,又由于providers列表中只有这一个provider,因此会跳出for循环。判断if (result == null && parent != null),由于第一个ProviderManager有parent,因此调用parent.authenticate(authentication)对authentication进行认证。

第二个ProviderManager也只有一个AuthenticationProvider,这个Provider便是DaoAuthenticationProviderDaoAuthenticationProvider支持UsernamePasswordAuthenticationToken此类型的token认证,因此会执行它的authenticate()方法对token进行认证。

DaoAuthenticationProvider继承了抽象类AbstractUserDetailsAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
}

DaoAuthenticationProviderauthenticate方法也是在其父类抽象类中定义的

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // 判断 username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        // 从userCache取用户详细
        UserDetails user = this.userCache.getUserFromCache(username);

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

            try {
                // 从缓存取user失败,调用retrieveUser()取用户详细
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        // 如果认证失败
        catch (AuthenticationException exception) {
            // 如果是使用缓存的用户详细,考虑是因为缓存的原因导致的
            if (cacheWasUsed) {
                // 将缓存置位false,再取用户信息进行一次认证
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
        // 进行后置认证,判断密码是否过期
        postAuthenticationChecks.check(user);

        // 如果认证过程不是使用缓存获取最终用户详细的,将用户详细存入缓存
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        // 如果要求PrincipalAsString是字符串,则将用户的用户名传入
        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        
        // 创建成功的认证对象并返回
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

authenticate方法会先从缓存取用户详细,若取不到,则会调用retrieveUser()方法取用户详细

protected final UserDetails retrieveUser(String username,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
    catch (UsernameNotFoundException ex) {
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    }
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    }
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

获取用户详细的方法是调用UserDetailsService.loadUserByUsername()方法,UserDetailsService是接口类,在自己的系统中,可以通过实现这个接口来定义如何获取UserDetails

在调用retrieveUser()方法获得了用户详细后,会利用这个用户详细和登录信息进行比对

preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);

DefaultPreAuthenticationChecksUserDetailsChecker的默认前置认证实现类


    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();


    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            // 如果账号被锁定
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");

                throw new LockedException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"));
            }
            // 如果账号不可用
            if (!user.isEnabled()) {
                logger.debug("User account is disabled");

                throw new DisabledException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"));
            }
            // 如果账号已过期
            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");

                throw new AccountExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"));
            }
        }
    }

前置确认通过后,会进入additionalAuthenticationChecks()方法进行额外的认证确认,这个抽象方法被DaoAuthenticationProvider实现。

    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

其实就是判断密码是否匹配的过程,会调用用户指定的passwordEncoderUserDetailspassword做解密,然后和客户端提交的密码做比对,以BCryptPasswordEncoder为例

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword == null || encodedPassword.length() == 0) {
            logger.warn("Empty encoded password");
            return false;
        }

        if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
            logger.warn("Encoded password does not look like BCrypt");
            return false;
        }

        return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
    }

在前置认证和额外认证都通过之后,会进行后置认证,DefaultPostAuthenticationChecksUserDetailsChecker的默认后置认证实现类

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            // 判断密码是否过期
            if (!user.isCredentialsNonExpired()) {
                logger.debug("User account credentials have expired");

                throw new CredentialsExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                        "User credentials have expired"));
            }
        }
    }

至此,认证成功后,DaoAuthenticationProvider会生成成功的UsernamePasswordAuthenticationToken并返回给ProviderManager

    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(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

因为result不为null,因此跳出providers的遍历认证。认证完成后. ProviderManager从authentication中移除密码以及其他敏感信息并返回给UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter返回认证结果,从attemptAuthentication()方法跳回doFilter()

接着执行session策略

sessionStrategy.onAuthentication(authResult, request, response);

CompositeSessionAuthenticationStrategysessionStrategy的具体实现

    public void onAuthentication(Authentication authentication,
            HttpServletRequest request, HttpServletResponse response)
                    throws SessionAuthenticationException {
        // 遍历delegateStrategies
        for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Delegating to " + delegate);
            }
            delegate.onAuthentication(authentication, request, response);
        }
    }

delegateStrategies的列表默认情况下只有一个,实现了SessionAuthenticationStrategyAbstractSessionFixationProtectionStrategy

    /**
     * Called when a user is newly authenticated.
     * <p>
     * If a session already exists, and matches the session Id from the client, a new
     * session will be created, and the session attributes copied to it (if
     * {@code migrateSessionAttributes} is set). If the client's requested session Id is
     * invalid, nothing will be done, since there is no need to change the session Id if
     * it doesn't match the current session.
     * <p>
     * If there is no session, no action is taken unless the {@code alwaysCreateSession}
     * property is set, in which case a session will be created if one doesn't already
     * exist.
     */
    public void onAuthentication(Authentication authentication,
            HttpServletRequest request, HttpServletResponse response) {
        boolean hadSessionAlready = request.getSession(false) != null;

        if (!hadSessionAlready && !alwaysCreateSession) {
            // Session fixation isn't a problem if there's no session

            return;
        }

        // Create new session if necessary
        HttpSession session = request.getSession();

        if (hadSessionAlready && request.isRequestedSessionIdValid()) {

            String originalSessionId;
            String newSessionId;
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized (mutex) {
                // We need to migrate to a new session
                originalSessionId = session.getId();

                session = applySessionFixation(request);
                newSessionId = session.getId();
            }

            if (originalSessionId.equals(newSessionId)) {
                logger.warn("Your servlet container did not change the session ID when a new session was created. You will"
                        + " not be adequately protected against session-fixation attacks");
            }

            onSessionChange(originalSessionId, session, authentication);
        }
    }

默认情况下alwaysCreateSessionfalse,代码中只需要关注hadSessionAlready这个字段即可

最后,执行doFilter()中的最后一行代码successfulAuthentication(request, response, chain, authResult)successfulAuthentication也是在UsernamePasswordAuthenticationFilter的抽象父类中定义好的方法

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

        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

大体就是向SecurityContextHolder中放入认证的信息,然后调用successHandler.onAuthenticationSuccess()处理认证成功操作,successHandler的默认实现是SavedRequestAwareAuthenticationSuccessHandler

@Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws ServletException, IOException {
        // 将请求存入请求缓存session中
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);

            return;
        }
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl()
                || (targetUrlParameter != null && StringUtils.hasText(request
                        .getParameter(targetUrlParameter)))) {
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);

            return;
        }

        clearAuthenticationAttributes(request);

        // Use the DefaultSavedRequest URL
        String targetUrl = savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

默认不存session,因此if (savedRequest == null)条件成立,调用super.onAuthenticationSuccess()处理,然后return

SavedRequestAwareAuthenticationSuccessHandler其父类是SimpleUrlAuthenticationSuccessHandler

    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {

        handle(request, response, authentication);
        clearAuthenticationAttributes(request);
    }

handle()方法代码如下,其实就是将页面重定向回登录跳转之前的请求页

    protected void handle(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        String targetUrl = determineTargetUrl(request, response);

        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to "
                    + targetUrl);
            return;
        }

        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

handle()方法执行结束以后,会执行clearAuthenticationAttributes()

    /**
     * Removes temporary authentication-related data which may have been stored in the
     * session during the authentication process.
     */
    protected final void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        if (session == null) {
            return;
        }

        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }

其实就是从session中移除认证的信息,至此登录处理逻辑结束,收工

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258