Spring Security如何優雅的增加OAuth2協議授權模式


file

一、什么是OAuth2協議?

OAuth 2.0 是一個關於授權的開放的網絡協議,是目前最流行的授權機制。

數據的所有者告訴系統,同意授權第三方應用進入系統,獲取這些數據。系統從而產生一個短期的進入令牌(token),用來代替密碼,供第三方應用使用。

由於授權的場景眾多,OAuth 2.0 協議定義了獲取令牌的四種授權方式,分別是:

  • 授權碼模式:授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的后台服務器,與"服務提供商"的認證服務器進行互動。

  • 簡化模式:簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。

  • 密碼模式:密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要授權。

  • 客戶端模式:客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。

四種授權模式分別使用不同的 grant_type 來區分

 

二、為什么要自定義授權類型?

雖然 OAuth2 協議定義了4種標准的授權模式,但是在實際開發過程中還是遠遠滿足不了各種變態的業務場景,需要我們去擴展。

例如增加圖形驗證碼、手機驗證碼、手機號密碼登錄等等的場景

 

而常見的做法都是通過增加 過濾器Filter 的方式來擴展 Spring Security 授權,但是這樣的實現方式有兩個問題:

  1. 脫離了 OAuth2 的管理
  2. 不靈活:例如系統使用 密碼模式 授權,網頁版需要增加圖形驗證碼校驗,但是手機端APP又不需要的情況下,使用增加 Filter 的方式去實現就比較麻煩了。

 

所以目前在 Spring Security 中比較優雅和靈活的擴展方式就是通過自定義 grant_type 來增加授權模式。

 

三、實現思路

在擴展之前首先需要先了解 Spring Security 的整個授權流程,我以 密碼模式 為例去展開分析,如下圖所示

file

3.1. 流程分析

整個授權流程關鍵點分為以下兩個部分:

第一部分:關於授權類型 grant_type 的解析

  1. 每種 grant_type 都會有一個對應的 TokenGranter 實現類。
  2. 所有 TokenGranter 實現類都通過 CompositeTokenGranter 中的 tokenGranters 集合存起來。
  3. 然后通過判斷 grantType 參數來定位具體使用那個 TokenGranter 實現類來處理授權。

 

第二部分:關於授權登錄邏輯

  1. 每種 授權方式 都會有一個對應的 AuthenticationProvider 實現類來實現。
  2. 所有 AuthenticationProvider 實現類都通過 ProviderManager 中的 providers 集合存起來。
  3. TokenGranter 類會 new 一個 AuthenticationToken 實現類,如 UsernamePasswordAuthenticationToken 傳給 ProviderManager 類。
  4. 而 ProviderManager 則通過 AuthenticationToken 來判斷具體使用那個 AuthenticationProvider 實現類來處理授權。
  5. 具體的登錄邏輯由 AuthenticationProvider 實現類來實現,如 DaoAuthenticationProvider。

 

3.2. 擴展分析

根據上面的流程,擴展分為以下兩種場景

場景一:只對原有的授權邏輯進行增強或者擴展,如:用戶名密碼登錄前增加圖形驗證碼校驗。

該場景需要定義一個新的 grantType 類型,並新增對應的 TokenGranter 實現類 添加擴展內容,然后加到 CompositeTokenGranter 中的 tokenGranters 集合里即可。

參考代碼:PwdImgCodeGranter.java

 

場景二:新加一種授權方式,如:手機號加密碼登錄。

該場景需要實現以下內容:

  1. 定義一個新的 grantType 類型,並新增對應的 TokenGranter 實現類添加到 CompositeTokenGranter 中的 tokenGranters 集合里
  2. 新增一個 AuthenticationToken 實現類,用於存放該授權所需的信息。
  3. 新增一個 AuthenticationProvider 實現類 實現授權的邏輯,並重寫 supports 方法綁定步驟二的 AuthenticationToken 實現類

參考代碼:MobilePwdGranter.java

 

四、代碼實現

下面以 場景二 新增手機號加密碼授權方式為例,展示核心的代碼實現

4.1. 創建 AuthenticationToken 實現類

創建 MobileAuthenticationToken 類,用於存儲手機號和密碼信息

public class MobileAuthenticationToken extends AbstractAuthenticationToken {
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;
	private Object credentials;

	public MobileAuthenticationToken(String mobile, String password) {
		super(null);
		this.principal = mobile;
		this.credentials = password;
		setAuthenticated(false);
	}

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

	@Override
	public Object getCredentials() {
		return this.credentials;
	}

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

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

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

 

4.2. 創建 AuthenticationProvider 實現類

創建 MobileAuthenticationProvider 類,實現登錄邏輯,並綁定 MobileAuthenticationToken 類

@Setter
public class MobileAuthenticationProvider implements AuthenticationProvider {
    private ZltUserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) {
        MobileAuthenticationToken authenticationToken = (MobileAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String password = (String) authenticationToken.getCredentials();
        UserDetails user = userDetailsService.loadUserByMobile(mobile);
        if (user == null) {
            throw new InternalAuthenticationServiceException("手機號或密碼錯誤");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("手機號或密碼錯誤");
        }
        MobileAuthenticationToken authenticationResult = new MobileAuthenticationToken(user, password, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

 

4.3. 創建 TokenGranter 實現類

創建 MobilePwdGranter 類並定義 grant_type 的值為 mobile_password

public class MobilePwdGranter extends AbstractTokenGranter {
    private static final String GRANT_TYPE = "mobile_password";

    private final AuthenticationManager authenticationManager;

    public MobilePwdGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices
            , ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        String mobile = parameters.get("mobile");
        String password = parameters.get("password");
        parameters.remove("password");

        Authentication userAuth = new MobileAuthenticationToken(mobile, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        userAuth = authenticationManager.authenticate(userAuth);
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
        }

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

 

4.4. 加到 CompositeTokenGranter 中的集合里

// 添加手機號加密碼授權模式
tokenGranters.add(new MobilePwdGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));

 

4.5. 測試

使用以下地址,指定 grant_type 為 mobile_password 進行授權獲取 access_token

/oauth/token?grant_type=mobile_password&mobile={mobile}&password={password}

file

 

五、參考樣例

詳細的代碼實現可以參考

https://gitee.com/zlt2000/microservices-platform/tree/master/zlt-uaa

 
掃碼關注有驚喜!

file


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM