Spinrg Security Authentication(一)

1 認證流程

Spring Security是如何完成身份認證的?

具體攔截在UsernamePasswordAuthenticationFilter->AbstractAuthenticationProcessingFilter攔截器中;
具體認證流程在ProviderManager->AuthenticationManager中

UsernamePasswordAuthenticationFilter類

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;
	   //判斷是否需要進行驗證,其實就是判斷請求的路徑是否是/login,如果不是/login,說明不是form表單登錄請求,則不需要進行驗證
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;//如果不需要驗證,則直接return,下面的代碼邏輯都不會走了
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}
		Authentication authResult;
		try {
			 //調用attemptAuthentication方法進行驗證,並獲得驗證結果Authentication對象。
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				return;
			}
			//這段代碼主要是爲了防止session劫持(session fixation attacks)
			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) {
		     // 驗證失敗
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		 //驗證成功
		successfulAuthentication(request, response, chain, authResult);
	}

	.....省略
}

當驗證失敗時,需要拋出AuthenticationException異常。具體來說,驗證失敗可能分爲2種情況:

  1. 驗證服務失敗,例如我們從數據庫中查詢用戶名和密碼驗證用戶,但是數據庫服務掛了,此時拋出InternalAuthenticationServiceException異常
  2. 驗證參數失敗,例如用戶輸入的用戶名和密碼錯誤,此時拋出AuthenticationException異常

InternalAuthenticationServiceException是AuthenticationException的子類,因此我們看到在上面的catch代碼塊中,先捕獲前者,再捕獲後者。
無論是哪一種情況,都會調用unsuccessfulAuthentication方法,此方法內部會跳轉到我們定義的登錄失敗頁面。
如果驗證成功,會調用successfulAuthentication方法,默認情況下,這個方法內部會將用戶登錄信息放到Session中,然後跳轉到我們定義的登錄成功頁面。

上述代碼中,最重要的就是attemptAuthentication方法,這是一個抽象方法,UsernamePasswordAuthenticationFilter中進行了實現,以實現自己的驗證邏輯,也就是前面我看到的從HttpServletRequest對象中獲得用戶名和密碼,進行驗證。

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
		
	.....省略

		public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
	    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	    private boolean postOnly = true;
	 
	    public UsernamePasswordAuthenticationFilter() {
	        super(new AntPathRequestMatcher("/login", "POST"));//指定form表單的action屬性值爲/login,且提交方式必須爲post
	    }
	    
	     //登錄表單提交時,此方法會被回調,對用戶名和密碼進行校驗
		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);//內部調用request.getParameter(usernameParameter)獲得用戶名
		String password = obtainPassword(request);//內部調用request.getParameter(passwordParameter)獲得密碼

		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();
		// 用戶名和密碼被過濾器獲取到,封裝成Authentication
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		setDetails(request, authRequest);
		// AuthenticationManager 身份管理器負責驗證這個Authentication
		return this.getAuthenticationManager().authenticate(authRequest);
	}
	
	.....省略
}

在UsernamePasswordAuthenticationFilter中,指定了form表單的method屬性必須爲post,同時指定了form的action屬性值默認/login。當用戶表單提交時,attemptAuthentication方法會被回調,這個方法內部會通過HttpServletRequest.getParameter的方式,獲得表單中填寫的用戶名和密碼的值,封裝成一個UsernamePasswordAuthenticationToken對象,然後利用ProviderManager類進行校驗。

ProviderManager類

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
			
	.....省略
	
   public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (debug) {
				logger.debug("Authentication attempt using " + provider.getClass().getName());
			}
			try {
			  // 由具體的provider認證
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

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

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				((CredentialsContainer) result).eraseCredentials();
			}
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		prepareException(lastException, authentication);

		throw lastException;
	}
		
	.....省略
}

初次接觸Spring Security的朋友相信會被AuthenticationManager,ProviderManager ,AuthenticationProvider …這麼多相似的Spring認證類搞得暈頭轉向,但只要稍微梳理一下就可以理解清楚它們的聯繫和設計者的用意。AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,因爲在實際需求中,我們可能會允許用戶使用用戶名+密碼登錄,同時允許用戶使用郵箱+密碼,手機號碼+密碼登錄,甚至,可能允許用戶使用指紋登錄(還有這樣的操作?沒想到吧),所以說AuthenticationManager一般不直接認證,AuthenticationManager接口的常用實現類ProviderManager 內部會維護一個List列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。也就是說,核心的認證入口始終只有一個:AuthenticationManager,不同的認證方式:用戶名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機號碼+密碼登錄則對應了三個AuthenticationProvider。這樣一來四不四就好理解多了?熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默認策略下,只需要通過一個AuthenticationProvider的認證,即可被認爲是登錄成功。

具體流程總結

  1. 用戶名和密碼被過濾器獲取到,封裝成Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。
  2. AuthenticationManager 身份管理器負責驗證這個Authentication
  3. 在AuthenticationManager 通過AuthenticationProvider認證
  4. 認證成功後,AuthenticationManager身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼通常會被移除)Authentication實例。
  5. SecurityContextHolder安全上下文容器將第3步填充了信息的Authentication,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中。

時序圖
在這裏插入圖片描述

2 loginPage方法

FormLoginConfigurer的loginPage方法用於自定義登錄頁面。如果我們沒有調用這個方法,spring security將會註冊一個DefaultLoginPageGeneratingFilter,這個filter的generateLoginPageHtml方法會幫我們生成一個默認的登錄頁面,也就是我們前面章節看到的那樣。在本節案例中,我們自定義了登錄頁面地址爲/login.html,則DefaultLoginPageGeneratingFilter不會被註冊。
FormLoginConfigurer繼承了AbstractAuthenticationFilterConfigurer,事實上,loginPage方法定義在這個類中。AbstractAuthenticationFilterConfigurer中維護了一個customLoginPage字段,用於記錄用戶是否設置了自定義登錄頁面。

AbstractAuthenticationFilterConfigurer#loginPage

protected T loginPage(String loginPage) {
        setLoginPage(loginPage);//當spring security判斷用戶需要登錄時,會跳轉到loginPage指定的頁面中
        updateAuthenticationDefaults();
        this.customLoginPage = true; //標記用戶使用了自定義登錄頁面
        return getSelf();
}

而在FormLoginConfigurer初始化時,會根據customLoginPage的值判斷是否註冊DefaultLoginPageGeneratingFilter。參見:

FormLoginConfigurer#initDefaultLoginFilter

private void initDefaultLoginFilter(H http) {
        DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
                .getSharedObject(DefaultLoginPageGeneratingFilter.class);
                //如果沒有自定義登錄頁面,則使用DefaultLoginPageGeneratingFilter
        if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
            loginPageGeneratingFilter.setFormLoginEnabled(true);
            loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
            loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
            loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
            loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
            loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
        }
}

在前面的配置中,我們指定的loginPage是一個靜態頁面login.html,我們也可以定義一個Controller,返回一個ModelAndView對象跳轉到登錄頁面,注意Controller中方法的@RequestMapping註解的值,需要和loginPage方法中的值相對應。

可以看到這裏,還創建了一個LoginUrlAuthenticationEntryPoint對象,事實上,跳轉的邏輯就是在這個類中完成的。

LoginUrlAuthenticationEntryPoint#commence

//commence方法用戶判斷跳轉到登錄頁面時,是使用重定向(redirect)的方式,還是使用轉發(forward)的方式
public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        String redirectUrl = null;
        if (useForward) {//使用轉發的方法,用戶瀏覽器地址url不會發生改變
            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be
                // used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }
            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response,
                        authException);
                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }
                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
                dispatcher.forward(request, response);
                return;
            }
        }
        else {//使用重定向的方式,用戶瀏覽器地址url發生改變
            // redirect to login page. Use https if forceHttps true
            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
        }
        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }

而commence方法的調用是在ExceptionTranslationFilter#handleSpringSecurityException中進行的。spring security中的驗證錯誤會統一放到ExceptionTranslationFilter處理,其handleSpringSecurityException中,會判斷異常的類型,如果是AuthenticationException類型的異常,則會調用sendStartAuthentication方法,進行跳轉。如下

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);//進行頁面跳轉
    }

3 usernameParameter、passwordParameter方法

通過前面的分析,我們知道在UsernamePasswordAuthenticationFilter中,是通過HttpServletRequest的getParamter方法來獲得用戶性和密碼參數值的。默認的參數名是"username"、“password”。要求登錄頁面的form表單中的參數名,與此必須匹配。

我們可以通過調用FormLoginConfigurer的相關方法,重新定義參數名,例如

formLogin()
    .usernameParameter("user") //form表單密碼參數名
    .passwordParameter("pwd")//form表單用戶名參數名

此時login.html頁面的form表單也要進行相應的修改,如:

 <input type="text" name="user" placeholder="請輸入用戶名">
  <input type="password"  name="pwd" placeholder="請輸入密碼">

4 successForwardUrl、failureForwardUrl、defaultSuccessUrl方法

FormLoginConfigurer的successForwardUrl、failureForwardUrl方法分別用於定義登錄成功和失敗的跳轉地址。

.formLogin()
                    .successForwardUrl("/success.html")
                    .failureForwardUrl("/error.html")

這兩個方法內部實現如下:

public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
        successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
        return this;
}
 
public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) {
        failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));
        return this;
}

可以看到,內部利用跳轉路徑url分別構建了ForwardAuthenticationSuccessHandler、ForwardAuthenticationFailureHandler,用於跳轉。

其中successHandler和failureHandler方法都繼承自父類AbstractAuthenticationFilterConfigurer,用於給父類中維護的successHandler、failureHandler字段賦值。

因此FormLoginConfigurer的successForwardUrl、failureForwardUrl方法實際上只是AbstractAuthenticationFilterConfigurer的successHandler、failureHandler方法的一種快捷方式而已,我們可以直接調用successHandler、failureHandler方法,來定義跳轉方式

ForwardAuthenticationSuccessHandler和ForwardAuthenticationFailureHandler的實現類似,以前者爲例,其源碼如下:

public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final String forwardUrl;
    
    public ForwardAuthenticationSuccessHandler(String forwardUrl) {
        Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), "'"
                + forwardUrl + "' is not a valid forward URL");
        this.forwardUrl = forwardUrl;
    }
    
    //登錄成功時,通過調用此方法進行頁面的跳轉,forward方式
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
     throws IOException, ServletException {
        request.getRequestDispatcher(forwardUrl).forward(request, response);
    }
}

可以看到這個方法裏面就是利用request.getRequestDispatcher來進行轉發。

回顧1 我們分析formLogin方法源碼時,在AbstractAuthenticationProcessingFilter的doFilter方法中,驗證成功或者失敗分別會回調successfulAuthentication、unsuccessfulAuthentication方法。事實上ForwardAuthenticationSuccessHandler的onAuthenticationSuccess方法就是在AbstractAuthenticationProcessingFilter的successfulAuthentication中被回調的。

AbstractAuthenticationProcessingFilter#successfulAuthentication

//用戶驗證成功後,回調此方法   
 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);
        }
       //1、記錄用戶登錄成功信息,默認放入到Session中
        SecurityContextHolder.getContext().setAuthentication(authResult);
 
       //2、如果開啓了自動登錄(用於支持我們經常在各個網站登錄頁面上的"記住我"複選框)
        rememberMeServices.loginSuccess(request, response, authResult);
 
        //3、 發佈用戶登錄成功事件,我們可以定義一個bean,實現spring的ApplicationListener接口,則可以獲取到所有的用戶登錄事件
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
       //4、最後調用ForwardAuthenticationSuccessHandler的onAuthenticationSuccess方法,進行頁面的轉發
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

defaultSuccessUrl
如果這樣定義form表單登錄,如果沒有訪問受保護的資源,登錄成功後,默認跳轉到的頁面,默認值爲"/"

protected void configure(HttpSecurity http) throws Exception {
    	 http
         .authorizeRequests()
              .antMatchers("/index.html").permitAll()//訪問index.html不要權限驗證
              .anyRequest().authenticated()//其他所有路徑都需要權限校驗
         .and()
              .csrf().disable()//默認開啓,這裏先顯式關閉
         .formLogin()  //內部註冊 UsernamePasswordAuthenticationFilter
             .loginPage("/login.html") //表單登錄頁面地址
             .loginProcessingUrl("/login")//form表單POST請求url提交地址,默認爲/login
             .passwordParameter("password")//form表單用戶名參數名
             .usernameParameter("username")//form表單密碼參數名
 }

我們可以通過配置defaultSuccessUrl來修改默認跳轉到的頁面

.formLogin()
    .defaultSuccessUrl("/index.html")//如果沒有訪問受保護的資源,登錄成功後,默認跳轉到的頁面,默認值爲"/"

這裏需要注意的是,在登錄失敗後,瀏覽器的地址會變爲http://localhost:8080/login?error,也就是說,在loginProccessUrl方法指定的url後面加上?error。

由於這裏使用的靜態頁面,所以無法展示錯誤信息。事實上,spring security會將錯誤信息放到Session中,key爲SPRING_SECURITY_LAST_EXCEPTION,如果你使用jsp或者其他方式,則可以從session中把錯誤信息獲取出來,常見的錯誤信息包括:

  • 用戶名不存在:UsernameNotFoundException;
  • 密碼錯誤:BadCredentialException;
  • 帳戶被鎖:LockedException;
  • 帳戶未啓動:DisabledException;
  • 密碼過期:CredentialExpiredException;等等!

現在我們來分析,spring security是如何做到這種登錄成功/失敗跳轉的挑戰邏輯的:

登錄成功

在AbstractAuthenticationFilterConfigurer中,默認的successHandler是SavedRequestAwareAuthenticationSuccessHandler。當訪問受保護的目標頁面,登錄後直接跳轉到目標頁面,以及直接訪問登錄頁面,登錄後默認跳轉到首頁,就是通過SavedRequestAwareAuthenticationSuccessHandler這個類完成的。

public abstract class AbstractAuthenticationFilterConfigurer...{
    ....
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
}

由於前面的案例中,我們調用了successForwardUrl方法,因此successHandler的默認值被覆蓋爲ForwardAuthenticationSuccessHandler,因此失去了這個功能。

具體來說,SavedRequestAwareAuthenticationSuccessHandler有一個RequestCache對象,當用戶訪問受保護的頁面時,spring security會將當前請求HttpServletRequest對象信息放到這個RequestCache中。

參見ExceptionTranslationFilter#sendStartAuthentication方法

//需要進行登錄驗證    
protected void sendStartAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException {
        SecurityContextHolder.getContext().setAuthentication(null);
       //緩存當前request對象,其中包含了用戶想訪問的目標頁面登錄信息
        requestCache.saveRequest(request, response);
        logger.debug("Calling Authentication entry point.");
        //跳轉到登錄頁面,這個之前已經分析過,不再贅述
        authenticationEntryPoint.commence(request, response, reason);
    }

當用戶登錄成功後,AbstractAuthenticationProcessingFilter#successfulAuthentication方法會被回調,這個方法源碼之前也已經分析過,最後一步是調用

successHandler.onAuthenticationSuccess(request, response, authResult);

此時就是調用SavedRequestAwareAuthenticationSuccessHandler#onAuthenticationSuccess

public class SavedRequestAwareAuthenticationSuccessHandler extends
        SimpleUrlAuthenticationSuccessHandler {
    protected final Log logger = LogFactory.getLog(this.getClass());
    private RequestCache requestCache = new HttpSessionRequestCache();//這是一個session級別的緩存
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws ServletException, IOException {
        //1、從requestCache中獲得之前保存的HttpServletRequest對象,SavedRequest是對HttpServletRequest的封裝
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        //2、如果用戶直接訪問的登錄頁面,則savedRequest爲空,跳轉到默認頁面
        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        /*3 如果設置爲targetUrlParameter參數,會從當前請求request對象,查看請求url是否包含要跳轉到的路徑參數,如果有則跳轉到這個url,
        這個邏輯是在父類SimpleUrlAuthenticationSuccessHandler中進行的*/
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl()
                || (targetUrlParameter != null && StringUtils.hasText(request
                        .getParameter(targetUrlParameter)))) {
           //從requestCache中移除之前保存的request,以免緩存過多,內存溢出。
           //注意保存的是前一個request,移除的卻是當前request,因爲二者引用的是同一個session,內部只要找到這個session,移除對應的緩存key即可
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        //4、移除在session中緩存的之前的登錄錯誤信息,key爲:SPRING_SECURITY_LAST_EXCEPTION
        clearAuthenticationAttributes(request);
 
        //5、跳轉到之前保存的request對象中訪問的url
        String targetUrl = savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

登錄失敗

在AbstractAuthenticationFilterConfigurer中,failureHandler字段值默認爲空,在初始化時,updateAuthenticationDefaults方法會被調用:

AbstractAuthenticationFilterConfigurer#updateAuthenticationDefaults

....
private AuthenticationFailureHandler failureHandler;
....
private void updateAuthenticationDefaults() {
        ....
        if (failureHandler == null) {
            failureUrl(loginPage + "?error");
        }
       ....
    }

可以看到,如果發現failureHandler爲空,則會調用failureUrl方法創建一個AuthenticationFailureHandler實例,傳入的參數是是我們設置的loginPage+"?ERROR",這也是我們在前面的gif動態圖中,看到登錄失敗之後,登錄頁面變爲http://localhost:8080/login?error的原因。

failUrl方法如下所示:

public final T failureUrl(String authenticationFailureUrl) {
        T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(
                authenticationFailureUrl));
        this.failureUrl = authenticationFailureUrl;
        return result;
    }

可以看到這裏創建的AuthenticationFailureHandler實現類爲SimpleUrlAuthenticationFailureHandler。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章