3. SpringSecurity 自定義手機號登錄

距離上一次更新,不知不覺已經過去了半個月了,人真的是不能放鬆,一放鬆就肆意妄爲了。希望這個月內可以把 SpringSecurity 系列更新完畢吧,加油!。

OK,言歸正傳上一章我們利用 SpringSecurity 提供的一些可選配置,實現了自定義表單登錄。但是在我們的日常需求中,僅僅是表單登錄時滿足不了的。所以這一章,我給大家帶來 SpringSecurity 下自定義登錄方式的示例。

首先我們選定我們的自定義登錄方式,這裏我們選擇手機短信登錄。顯而易見,SpringSecurity 並沒有給我們提供手機短信登錄的簡單配置集成方式,所以需要我們自己來進行實現。

我們先來分析一波,手機短信登錄我們可以分爲兩個部分:

  • 手機驗證碼校驗
    • 手機驗證碼校驗應該是一個複用模塊,因爲不光登錄可能會用到,註冊、綁定等很多場景也都可能用到,並且這一塊和 SpringSecurity 關係不大,我們放到後面,將其專門開發成一個 Lib。
  • 手機號登錄
    • 通過了手機驗證碼校驗,其實就是一個手機號登錄了,按用戶的手機號去數據庫查詢。所以我們現在主要完成第二塊,手機號登錄。

要自定義手機號登錄,我們這裏必須分析一下 SpringSecurity 的認證流程,具體源碼在後面的章節我會帶着大家去詳細看一下,這裏我們先來找一下 SpringSecurity 的認證流程,我們前面的章節已經可以使用表單登錄了,那麼我們就以表單登錄的方式來跟蹤一下原發,分析出認證流程,我們會以一下我們之前做了那些事:

  1. 我們指明瞭登錄方式爲 formLogin
  2. 我們通過設置配置,自定義認證路徑
  3. 我們自定義了 UserDetailService 從數據庫中查詢用戶信息
  4. 我們自定義了認證成功或失敗處理器

然後我們來猜測一下可能的認證流程

  1. 用戶發起認證請求,服務端從請求中取出參數

  2. 去數據庫按參數進行查詢,然後進行校驗

  3. 最後做認證結果處理。

我們從 IDEA 點擊查看 formLogin 方法

HttpSecurity 類

public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
    // 發現這裏 new 了一個 FormLoginConfigurer
    return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer());
}

繼續點擊 FormLoginConfigurer 類進行查看,發現在 FormLoginConfigurer 的構造函數中創建了一個 UsernamePasswordAuthenticationFilter,並且設置了表單登錄的參數名。

FormLoginConfigurer 類

public FormLoginConfigurer() {
    super(new UsernamePasswordAuthenticationFilter(), (String)null);
    this.usernameParameter("username");
    this.passwordParameter("password");
}

我們繼續進入 UsernamePasswordAuthenticationFilter,我們發現這是一個過濾器,其次在它的構造函數裏面指定了 /loginPost。(結合之前我們配置時說的,默認登錄地址是 login + post),我們猜測這裏是設置了攔截的 UrlMethod,那麼這個 Filter 應該就是認證的入口

UsernamePasswordAuthenticationFilter 類

public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}

我們繼續看 Filter 的 處理方法,可以出在這個方法裏面,從請求中取出了表單參數,並且將參數封裝到了 UsernamePasswordAuthenticationToken 中,最後使用getAuthenticationManager().authenticate(authRequest); 進行認證,getAuthenticationManager 獲取到的是一個 AuthenticationManager 對象,實際上是使用 AuthenticationManagerauthenticate 方法進行認證。

UsernamePasswordAuthenticationFilter 類

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

我們繼續進入 authenticate 方法,發現其是一個接口方法,有很多實現類。沒辦法,我們只好將應用啓動,進行 debug 斷點跟蹤。

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

經過斷點跟蹤,我們發現實際上調用的是 ProviderManagerauthenticate 方法,我們發現在該方法中,獲取所有的 Providers,然後遍歷,找出與封裝的 Token 匹配的 Provider,調用其 authenticate 方法。

ProviderManager 類

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    	// 獲取之前封裝的 Token 類型
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();

    	// 獲取所有 providers,遍歷之
		for (AuthenticationProvider provider : getProviders()) {
			// 判斷 Provider 是否支持封裝的 Token
            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);
				// 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;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
                // 認證沒有獲取到結果,使用 parent 進行認證
				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;
			}
		}

		if (result != null) {
            // 認證完畢後,調用 Token 的方法擦除掉敏感信息(eg: password...)
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// 認證通過,發佈認證成功消息
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

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

		prepareException(lastException, authentication);

		throw lastException;
	}

我們繼續追蹤 Providerauthenticate 方法,進入AbstractUserDetailsAuthenticationProviderauthenticate 方法,我們重點關注一下 retrieveUser 方法。

AbstractUserDetailsAuthenticationProvider 類

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

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

   boolean cacheWasUsed = true;
   // 從緩存中嘗試獲取用戶信息
   UserDetails user = this.userCache.getUserFromCache(username);

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

      try {
         // 緩存沒獲取到,使用封裝的 Token 嘗試獲取用戶信息
         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 {
      // UserDetail 支持設置賬戶凍結、啓用等四個狀態,這裏是對賬戶狀態進行校驗
      preAuthenticationChecks.check(user);
      // 進行密碼校驗,之前如果使用了 passwordEncoder 對密碼進行加密,那麼從數據庫中取出來的應該是加
      //密過的密碼,這裏會對參數中的明文密碼與數據庫密碼進行校驗
      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);
}

我們發現 retrieveUser 方法是一個抽象方法,具體實現應該在子類中,繼續追蹤,發現實現在 DaoAuthenticationProvider 中,在 retrieveUser 方法中調用 UserDetailServiceloadUserByUsername 方法,到了這裏,大概的流程我們基本上就知道了。PS: UserDetailService 在系統中有多個實現,這裏會使用哪個要看實際情況與設置,這個後面有機會說一下。

AbstractUserDetailAuthenticationProvider 類

protected abstract UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException;

DaoAuthenticationProcider 類

protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   UserDetails loadedUser;

   try {
      // 調用 UserDetailService 的 loadUserByUsername 方法
      loadedUser = this.getUserDetailsService().loadUserByUsername(username);
   }
   catch (UsernameNotFoundException notFound) {
      if (authentication.getCredentials() != null) {
         String presentedPassword = authentication.getCredentials().toString();
         passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
               presentedPassword, null);
      }
      throw notFound;
   }
   catch (Exception repositoryProblem) {
      throw new InternalAuthenticationServiceException(
            repositoryProblem.getMessage(), repositoryProblem);
   }

   if (loadedUser == null) {
      throw new InternalAuthenticationServiceException(
            "UserDetailsService returned null, which is an interface contract violation");
   }
   return loadedUser;
}

我們來總結一下整個認證流程,通過過濾器獲取到請求參數,封裝 Token,調用 ProviderManagerauthenticate 方法,該方法實際上調用的 ProviderManager 管理的 Provider (認證邏輯的實現類) 的 authenticate 方法,最後調用 UserDetailService 去獲取用戶的信息。

  1. 首先通過 Filter 攔截用戶請求,獲取到參數
  2. 將參數封裝成 Token
  3. 調用 AuthenticationManager 的 authenticated 方法。這裏 AuthenticationManager 是接口,實際上調用的是 ProviderManager 的 authenticated 方法。從名字我們可以猜測出 ProviderManager 管理了很多 Provider
  4. 在 ProviderManager 的 authenticated 方法中,獲取所有 Provider,遍歷,按 Token 匹配,調用匹配到的 Provider 的 authentication 方法(這裏表單登錄實際上調用的是 DaoAuthenticationProvider 的方法)
  5. 最終調用的是 UserDetailService 的 loadUserByUsername 方法
  6. 查詢出用戶信息後,進行校驗
  7. 校驗通過後,發佈認證成功信息。如果認證失敗,會拋出異常,最終也會發布認證失敗信息。

所以我們要自定義手機號登陸,需要做一下操作:

  1. 自定義一個 Filter 用來攔截手機號登陸
  2. 自定義一個 Token
  3. 自定義一個 Provider
  4. 自定義一個 UserDetailService 的實現
  5. 最終把上面這些自定義的類作爲配置,加入到 SpringSecurity 的校驗流程中去

OK,我們一步一步來:

自定義 SmsCodeAuthenticationToken

繼承 AbstractAuthenticationToken 類,父類裏面主要三個屬性

  • 權限集合 (Collection<GrantedAuthority)
  • 客戶端信息(Object detail)
  • 是否通過認證(authenticated)

自己實現的 Token 在此基礎上按照認證方式進行擴展,比如如果是表單登錄,需要添加用戶名、密碼等。我們這裏是短信驗證碼認證,只需要手機號即可。

  • Token 主要在認證流程中裝載數據。
  • 下面單參數的構造方法,傳遞一個 mobile,是認證前用來存儲認證參數的,此時默認將 authenticated 置爲 false
  • 雙參數的構造方法,是用來狀態獲取到的用戶信息,此時默認將 authenticate 置爲 true,但是並不代表當前認證已經通過了。因爲可能後面還有密碼校驗(表單登錄時)、賬號狀態校驗等。
/**
 * @author: hblolj
 * @Date: 2019/3/15 10:58
 * @Description: 認證前用來裝載認證參數,認證通過後用來裝載用戶信息,因爲短信驗證碼登錄沒有密碼,將 credentials 移除了
 * @Version:
 **/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken{

    private final Object principal;

    public SmsCodeAuthenticationToken(Object mobile) {
        super((Collection)null);
        this.principal = mobile;
        this.setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.principal;
    }
}

自定義 SmsCodeAuthenticationFilter

主要參考 UsernamePasswordAuthenticationFilter 來實現自定義 Filter。在 Filter 中主要要做的事情有以下幾點:

  • 指定 Filter 攔截的 Url 和 HttpMethod

  • 完成攔截的邏輯代碼

    • 從請求中獲取參數(手機號)

    • 將參數封裝成自定義的 Token,同時設置一下 Detail(主要是發起請求的客戶端信息)

    • 調用 AuthenticationManagerauthenticated 方法進行認證

/**
 * @author: hblolj
 * @Date: 2019/3/15 10:58
 * @Description:
 * @Version:
 **/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // TODO: 2019/3/15 該參數可以抽取成配置,最後通過配置文件進行修改,這樣作爲共用組件只需要實現一個 default,具體值可以有調用者指定
    private String mobileParameter = "mobile";

    private boolean postOnly = true;

    /**
     * 通過構造函數指定該 Filter 要攔截的 url 和 httpMethod
     */
    protected SmsCodeAuthenticationFilter() {
        // TODO: 2019/3/15 pattern 可以抽取成配置,最後通過配置文件進行修改,這樣作爲共用組件只需要實現一個 default,具體值可以有調用者指定
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        // 當設置該 filter 只攔截 post 請求時,符合 pattern 的非 post 請求會觸發異常
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {

            // 1. 從請求中獲取參數 mobile + smsCode
            String mobile = obtainMobile(request);
            if (mobile == null){
                mobile = "";
            }

            mobile = mobile.trim();

            // 2. 封裝成 Token 調用 AuthenticationManager 的 authenticated 方法,該方法中根據 Token 的類型去調用對應 Provider 的 authenticated
            SmsCodeAuthenticationToken token = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, token);

            // 3. 返回 authenticated 方法的返回值
            return this.getAuthenticationManager().authenticate(token);
        }
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "mobileParameter parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

自定義 SmsCodeAuthenticationProvider

SmsCodeAuthenticationProvider 實現 AuthenticationProvider 接口,其中有兩個方法:

  • authenticate: 主要是認證邏輯實現
    • 在 authenticate 方法中主要的邏輯
      • 從 token 取出參數,調用 UserDetailService 進行查詢用戶信息。UserDetailService 需要我們根據不同的業務實現不同的實現類,去數據庫做不同的查詢操作。
      • 使用查詢出的用戶信息構造新的 SmsCodeAuthenticationToken
      • 如果是表單登錄,還要使用 PasswordEncoder 進行密碼校驗
      • 如果系統有設置賬號凍結相關設置,這裏可以進行校驗(按取出的用戶信息)
      • 最後返回 token。(如果返回的 result 不爲 null,最後回去做密碼擦除等操作,然後調用登錄成功處理。)
  • supports: 對 authenticate 的 參數進行校驗,與 Provider 對應的 Token 進行比較,看是否是其子類或子接口。

PS: 這裏註明一下,短信驗證碼校驗應該在 SmsCodeAuthenticationFilter 之前就被校驗了

/**
 * @author: hblolj
 * @Date: 2019/3/15 10:58
 * @Description: 短信驗證碼認證的真正校驗邏輯,實際上是按手機號查詢用戶,短信驗證碼過濾器在這之前
 * @Version:
 **/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider{

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;

        // 對用戶進行認證
        UserDetails userDetails = userDetailsService.loadUserByUsername((String) token.getPrincipal());
        if (userDetails == null){
            throw new InternalAuthenticationServiceException("未找到對應的用戶信息!");
        }

        // 構造新的 Token,採用該構造函數時,會默認將 authenticated 參數置爲 true
        SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationToken.setDetails(token.getDetails());

        // TODO: 2019/3/15 如果認證方式與密碼相關,這裏可以對密碼進行校驗 @PasswordEncoder

        // TODO: 2019/3/15 可以校驗賬號狀態: 啓用、凍結等等
//        userDetails.isAccountNonExpired(); 賬號是否過期
//        userDetails.isAccountNonLocked(); 賬號有無凍結
//        userDetails.isCredentialsNonExpired(); 賬號密碼是否過期
//        userDetails.isEnabled(); 賬號是否啓用

        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        // aClass 是 authenticate 方法參數的類型
        // 此處判斷 aClass 是否是該 Provider 對應的 Token 的子類或者子接口,只有通過了,纔會調用 authenticate 方法去認證
        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

自定義 UserDetailService

重寫 loadUserByUsername 方法,按手機號查詢。在實際開發中,這裏可以提供一個默認缺省實現,真正的實現交給業務開發人員去實現。

/**
 * @author: hblolj
 * @Date: 2019/3/15 14:08
 * @Description:
 * @Version:
 **/
@Component("mobileUserDetailService")
public class MobileUserDetailService implements UserDetailsService{

    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        // TODO: 2019/3/15 按手機號查詢用戶信息
        return new User("4000368163", "123", true, true, true,
                true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

自定義 SmsCodeAuthenticationSecurityConfig

經過上面的幾步準備,現在萬事俱備,只欠東風。我們只需要將 FilterProvider 添加到 SpringSecurity 的認證鏈路當中(就可以召喚神龍了)即可。

  • 繼承 SecurityConfigurerAdapter 類,重寫該類中的 configure(HttpSecurity) 方法。(後面源碼分析時,會分析這個類是怎樣作用於配置的)
  • configure 方法中,首先初始化自定義的 Filter 和 Provider,最後使用 HttpSecurity 進行設置添加
/**
 * @author: hblolj
 * @Date: 2019/3/15 10:59
 * @Description:
 * @Version:
 **/
@Component
public class SmsCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>{

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Autowired
    private UserDetailsService mobileUserDetailService;

    @Override
    public void configure(HttpSecurity builder) throws Exception {

        // 1. 初始化 SmsCodeAuthenticationFilter
        SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
        filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        filter.setAuthenticationSuccessHandler(successHandler);
        filter.setAuthenticationFailureHandler(failureHandler);

        // 2. 初始化 SmsCodeAuthenticationProvider
        SmsCodeAuthenticationProvider provider = new SmsCodeAuthenticationProvider();
        provider.setUserDetailsService(mobileUserDetailService);

        // 3. 將設置完畢的 Filter 與 Provider 添加到配置中,將自定義的 Filter 加到 UsernamePasswordAuthenticationFilter 之前
        builder.authenticationProvider(provider).addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

最後,將自定義的 config 添加到配置中,主要使用 apply 方法將我們自定義的 config 加入到 SpringSecurity 中,同時設置手機登錄地址訪問不需要認證,不然就沒法使用了。

@Autowired
private SmsCodeAuthenticationConfig smsCodeAuthenticationConfig;

 @Override
protected void configure(HttpSecurity http) throws Exception {

    http.apply(smsCodeAuthenticationConfig) // 加載短信驗證
        .and()
        .formLogin() // 指定登錄認證方式爲表單登錄
      //.loginPage("http://www.baidu.com") //指定自定義登錄頁面地址,一般前後端分離,這裏就用不到了
        .loginProcessingUrl("/authentication/form") // 自定義表單登錄的 action 地址,默認是 /login
        .successHandler(authenticationSuccessHandler)
        .failureHandler(authenticationFailureHandler)
        .and()
        .authorizeRequests()
        // 設置手機登錄地址不需要校驗
        .antMatchers("/authentication/mobile").permitAll()
        .antMatchers(
        securityProperties.getBrowser().getSignUpUrl()).permitAll() // 允許登錄頁面不需要認證就可以訪問,不然會死循環導致重定向次數過多
        .anyRequest() // 對所有的請求
        .authenticated() // 都進行認證
        .and()
        .csrf()
        .disable();
}

然後啓動服務,因爲是 post 請求,我們打開 postman 進行模擬,這裏我對DefaultAuthenticationSuccessHandler 做了一下處理,使其返回 principal.toString()

3-1

如圖所示,證明我們配置的手機號認證流程已經生效了。

同理,除了手機號的自定義登錄,我們還可以自定義其他的登錄方式,比如微信公衆號開發中,我們需要使用用戶的 OpenId 來登錄,就可以按這個模式來處理(最終更新完後,我會將代碼實現上傳到 Github 上,到時候會包含這個 weixin openId 登錄,這裏大家感興趣的話,可以自己先實現以下)。

不過 QQ 登錄和 WeiXin 的快捷登錄又不一樣,屬於第三方登錄,要使用 SpringSocial + OAuth2 來開發。這個我會放到後面幾章去講解。

下一章的話,會帶大家處理一下在 SpringSecurity 下的 Session 管理與登出。To Be Continue!

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