Bootstrap

SpringSecurity认证逻辑源码分析

SpringSecurity源码分析-认证逻辑

1. Spring-security-core包中的三个重要类

SecurityContext

  1. 这个类中就两个方法getAuthentication()和setAuthentication()
  2. 这个类用来存储Authentication对象
public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication var1);
}

Authentication

  1. 这个类是贯穿SpringSecurity整个流程的一个类。
  2. 它是一个接口,它的实现类中的UsernamePasswordAuthenticationToken是通过用户名密码认证的实现
  3. 登录成功后用来存储当前的登录信息。
  4. 其中三个方法:
  5. getCredentials():获取当前用户凭证
  6. getDetails():获取当前登录用户详情
  7. getPrincipal():获取当前登录用户对象
  8. isAuthenticated():是否登录
  9. GrantedAuthority类是用来存储权限的,它是一个接口,常用的SimpleGrantedAuthority实现类,用来存储用户包含的权限
public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

SecurityContextHolder

  1. 这个对象用来存储SecurityContext对象
  2. 其中有两个静态方法getContext()和setContext()
  3. 因此,获得SecurityContextHolder对象就能获得SecurityContext对象,也就可以获取Authentication对象,也就可以获取当前的登录信息。
  4. initialize():获取存储策略,全局、本地线程、父子线程三种,默认本地线程。
public class SecurityContextHolder {
    public static SecurityContext getContext() {
        return strategy.getContext();
    }
    private static void initialize() {
        if (!StringUtils.hasText(strategyName)) {
            strategyName = "MODE_THREADLOCAL";
        }

        if (strategyName.equals("MODE_THREADLOCAL")) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_GLOBAL")) {
            strategy = new GlobalSecurityContextHolderStrategy();
        } else {
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
            } catch (Exception var2) {
                ReflectionUtils.handleReflectionException(var2);
            }
        }
        ++initializeCount;
    }
    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }
}

小结:Authentication用来存储认证信息,SecurityContext用来存储认证信息的容器,SecurityContextHolder用来定义容器的存储策略。

2. 基于用户名密码认证的流程

  1. 找到UsernamePasswordAuthenticationFilter,找到doFilter方法
  1. 发现没有doFilter方法,去父类AbstractAuthenticationProcessingFilter中查看
  2. 其实是这样一个逻辑
  3. 所有AbstractAuthenticationProcessingFilter的实现类都调用父类中的doFilter方法
  4. 在doFilter方法中调用了attemptAuthentication方法
  5. attemptAuthentication方法是一个抽象方法,子类去实现AbstractAuthenticationProcessingFilter抽象方法

UsernamePasswordAuthenticationFilter类

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
                        //…… ……
	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();
        //封装成Authentication对象
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		setDetails(request, authRequest);
        //认证操作,ProviderManager中执行
		return this.getAuthenticationManager().authenticate(authRequest);
	}
    //…… ……
}

AbstractAuthenticationProcessingFilter

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	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;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		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);
	}
    //认证逻辑的抽象方法,交给子类去实现
	public abstract Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException, IOException,
			ServletException;
}
  1. 查看子类UsernamePasswordAuthenticationFilter中的attemptAuthentication方法
  1. 认证的逻辑在这里: this.getAuthenticationManager().authenticate(authRequest)
  2. 认证完毕后封装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);
	}
  1. ProviderManager类中的authenticate方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var8 = this.getProviders().iterator();

        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    // 认证逻辑
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (InternalAuthenticationServiceException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }
  1. AbstractUserDetailsAuthenticationProvider中的Authentication方法
    public Authentication authenticate(Authentication authentication)
        throws AuthenticationException{
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class,authentication,
        ()->messages.getMessage(
        "AbstractUserDetailsAuthenticationProvider.onlySupports",
        "Only UsernamePasswordAuthenticationToken is supported"));

        // 获取用户名
        String username=(authentication.getPrincipal()==null)?"NONE_PROVIDED"
        :authentication.getName();

        boolean cacheWasUsed=true;
        //缓存获取user
        UserDetails user=this.userCache.getUserFromCache(username);

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

        try{
            //自定义获取user,一般从数据库读取
        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){
        // There was a problem, so try again after checking
        // we're using latest data (i.e. not from the cache)
        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;

        if(forcePrincipalAsString){
        principalToReturn=user.getUsername();
        }
        //封装Authentication对象
        return createSuccessAuthentication(principalToReturn,authentication,user);
        }

  1. DaoAuthenticationProvider中的retrieveUser方法
	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //调用我们自己的loadUserByUsername方法获取user
			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);
		}
	}

小结:至此返回Authentication对象完成认证

3. 认证成功后的逻辑

	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);
		}
        //将认证后的对象放到SecurityContext中
		SecurityContextHolder.getContext().setAuthentication(authResult);
        //记住我的执行逻辑
		rememberMeServices.loginSuccess(request, response, authResult);

		// 发布认证成功后的时间,可以自定义监听器
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
        //认证成功后的执行逻辑,默认三种,可以通过实现AuthenticationSuccessHandler接口自定义认证成功后逻辑
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

4. 记住我是如何实现的

AbstractRememberMeServices的loginSuccess方法和rememberMeRequested方法

 public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
    //判断是否勾选remember
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        if (this.alwaysRemember) {
        return true;
        } else {
        String paramValue = request.getParameter(parameter);

        //判断是否勾选remember,传入的值可以为以下内容
        if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
        return true;
        } else {
        if (this.logger.isDebugEnabled()) {
        this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
        }

        return false;
        }
        }
        }

PersistentTokenBasedRememberMeServices中的onLoginSuccess方法完成持久化操作

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            //持久化操作
            this.tokenRepository.createNewToken(persistentToken);
            //将token放到cookie中,可以自定义
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

持久化操作有两个实现JdbcTokenRepositoryImpl存到数据库,InMemoryTokenRepositoryImpl存到内存中

5.Security中的ExceptionTranslationFilter过滤器

  1. 这个过滤器不处理逻辑
  2. 只捕获Security中的异常
  3. 捕获异常后处理异常信息
public class ExceptionTranslationFilter extends GenericFilterBean {

    // ~ Instance fields
    // ================================================================================================

    private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
    private AuthenticationEntryPoint authenticationEntryPoint;
    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

    private RequestCache requestCache = new HttpSessionRequestCache();

    private final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
        this(authenticationEntryPoint, new HttpSessionRequestCache());
    }

    public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
                                      RequestCache requestCache) {
        Assert.notNull(authenticationEntryPoint,
                "authenticationEntryPoint cannot be null");
        Assert.notNull(requestCache, "requestCache cannot be null");
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.requestCache = requestCache;
    }

    // ~ Methods
    // ========================================================================================================

    @Override
    public void afterPropertiesSet() {
        Assert.notNull(authenticationEntryPoint,
                "authenticationEntryPoint must be specified");
    }

    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) {
            /** 捕获Security中的异常,处理异常*/
            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) {
                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);
            }
        }
    }

    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return authenticationEntryPoint;
    }

    protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
        return authenticationTrustResolver;
    }

    private void handleSpringSecurityException(HttpServletRequest request,
                                               HttpServletResponse response, FilterChain chain, RuntimeException exception)
            throws IOException, ServletException {
        
        /** 根据不同的异常,做出不同的处理*/
        if (exception instanceof AuthenticationException) {
            logger.debug(
                    "Authentication exception occurred; redirecting to authentication entry point",
                    exception);

            sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);
        }
        else if (exception instanceof AccessDeniedException) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            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(
                                messages.getMessage(
                                        "ExceptionTranslationFilter.insufficientAuthentication",
                                        "Full authentication is required to access this resource")));
            }
            else {
                logger.debug(
                        "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                        exception);

                accessDeniedHandler.handle(request, response,
                        (AccessDeniedException) exception);
            }
        }
    }

    protected void sendStartAuthentication(HttpServletRequest request,
                                           HttpServletResponse response, FilterChain chain,
                                           AuthenticationException reason) throws ServletException, IOException {
        // SEC-112: Clear the SecurityContextHolder's Authentication, as the
        // existing Authentication is no longer considered valid
        SecurityContextHolder.getContext().setAuthentication(null);
        requestCache.saveRequest(request, response);
        logger.debug("Calling Authentication entry point.");
        authenticationEntryPoint.commence(request, response, reason);
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    public void setAuthenticationTrustResolver(
            AuthenticationTrustResolver authenticationTrustResolver) {
        Assert.notNull(authenticationTrustResolver,
                "authenticationTrustResolver must not be null");
        this.authenticationTrustResolver = authenticationTrustResolver;
    }

    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
        Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
        this.throwableAnalyzer = throwableAnalyzer;
    }

    /**
     * Default implementation of <code>ThrowableAnalyzer</code> which is capable of also
     * unwrapping <code>ServletException</code>s.
     */
    private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
        /**
         * @see org.springframework.security.web.util.ThrowableAnalyzer#initExtractorMap()
         */
        protected void initExtractorMap() {
            super.initExtractorMap();

            registerExtractor(ServletException.class, new ThrowableCauseExtractor() {
                public Throwable extractCause(Throwable throwable) {
                    ThrowableAnalyzer.verifyThrowableHierarchy(throwable,
                            ServletException.class);
                    return ((ServletException) throwable).getRootCause();
                }
            });
        }

    }
}

6.登录页面是如何产生的

答案在最后一个过滤器DefaultLoginPageGeneratingFilter中

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        boolean loginError = this.isErrorPage(request);
        boolean logoutSuccess = this.isLogoutSuccess(request);
        
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
            chain.doFilter(request, response);
        } else {
            //拼接生产html登录页面
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }
;