
一、什麼是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
授權,但是這樣的實作方式有兩個問題:
- 脫離了
的管理OAuth2
- 不靈活:例如系統使用 密碼模式 授權,網頁版需要增加圖形驗證碼校驗,但是手機端APP又不需要的情況下,使用增加
的方式去實作就比較麻煩了。Filter
是以目前在
Spring Security
中比較優雅和靈活的擴充方式就是通過自定義 grant_type 來增加授權模式。
三、實作思路
在擴充之前首先需要先了解
Spring Security
的整個授權流程,我以 密碼模式 為例去展開分析,如下圖所示
3.1. 流程分析
整個授權流程關鍵點分為以下兩個部分:
第一部分:關于授權類型
grant_type
的解析
- 每種
都會有一個對應的grant_type
實作類。TokenGranter
- 所有
實作類都通過TokenGranter
中的CompositeTokenGranter
集合存起來。tokenGranters
- 然後通過判斷
參數來定位具體使用那個grantType
實作類來處理授權。TokenGranter
第二部分:關于授權登入邏輯
-
授權方式
實作類來實作。AuthenticationProvider
-
AuthenticationProvider
ProviderManager
providers
-
類會 new 一個TokenGranter
實作類,如AuthenticationToken
傳給UsernamePasswordAuthenticationToken
類。ProviderManager
- 而
則通過ProviderManager
來判斷具體使用那個AuthenticationToken
AuthenticationProvider
- 具體的登入邏輯由
實作類來實作,如AuthenticationProvider
。DaoAuthenticationProvider
3.2. 擴充分析
根據上面的流程,擴充分為以下兩種場景
場景一:隻對原有的授權邏輯進行增強或者擴充,如:使用者名密碼登入前增加圖形驗證碼校驗。
該場景需要定義一個新的
grantType
類型,并新增對應的
TokenGranter
實作類 添加擴充内容,然後加到
CompositeTokenGranter
tokenGranters
集合裡即可。
參考代碼: PwdImgCodeGranter.java
場景二:新加一種授權方式,如:手機号加密碼登入。
該場景需要實作以下内容:
- 定義一個新的
grantType
實作類添加到TokenGranter
CompositeTokenGranter
集合裡tokenGranters
- 新增一個
實作類,用于存放該授權所需的資訊。AuthenticationToken
-
實作類 實作授權的邏輯,并重寫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}
五、參考樣例
詳細的代碼實作可以參考
https://gitee.com/zlt2000/microservices-platform/tree/master/zlt-uaa掃碼關注有驚喜!