本文利用到的JustAuth的傳送門。git
本文純屬菜雞視角。在開發者至關簡略的官方使用文檔的基礎上,進入源碼查看文檔中使用的函數的具體實現,同時經過QQ第三方登陸這一特例,工具開發者很是規範的命名和註釋,推測整個工具的實現邏輯。緩存
絕大部分第三方登陸採用OAuth2.0協議,其流程符合以下流程圖:
關於OAuth2.0流程複雜化了(用戶受權登陸後,服務器不能直接拿到能夠惟一標識用戶的id)登陸流程,到底在安全性上如何提供了好處,請自行谷歌。安全
A階段
跳轉到QQ的受權登陸網頁
必需參數 response_type client_id redirect_uri state
其中response_type爲必定值服務器
B階段
用戶受權登陸後,騰訊那邊帶上必要的數據以GET參數的模型經過GET訪問咱們設定的返回地址。
獲得的數據 code state
並要校驗發回的state與A階段的state是否相同微信
// 官方文檔中並未有此函數,只是我自用的。 private AuthQqRequest getAuthQqRequest(){ String client_id = 填入你本身的client_id; String redirect_uri = 填入你本身的redirect_url; String client_secret = 填入你本身的client_secret; AuthConfig build = AuthConfig.builder() .clientId(client_id) .clientSecret(client_secret) .redirectUri(redirect_uri) .build(); return new AuthQqRequest(build); }
/** * 官方僞代碼 */ @RequestMapping("/render/{source}") public void renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException { AuthRequest authRequest = getAuthRequest(source); String authorizeUrl = authRequest.authorize(AuthStateUtils.createState()); response.sendRedirect(authorizeUrl); }
/** * 個人具體到QQ上的實現 * 由於我胸無大志只想着QQ因此不須要用{source}來肯定我在用誰的(是微信啊,仍是QQ啊仍是gitee啊)的第三方登陸功能。 */ @RequestMapping("/render") public void render(HttpServletResponse resp) throws IOException { AuthQqRequest authQqRequest = getAuthQqRequest(); resp.sendRedirect(authQqRequest.authorize(AuthStateUtils.createState()); } }
/** * 官方文檔的僞代碼 */ @RequestMapping("/callback/{source}") public Object login(@PathVariable("source") String source, AuthCallback callback) { AuthRequest authRequest = getAuthRequest(source); AuthResponse response = authRequest.login(callback); return response; }
進行心無大志,醉心QQ的化簡app
/** * 官方文檔的僞代碼 */ @RequestMapping("/callback/QQ") public Object login(AuthCallback callback) { AuthRequest authRequest = getAuthQqRequest();//getAuthQqRequest()是準備階段我自用的那個函數 AuthResponse response = authRequest.login(callback); return response; }
問題來了:這一階段應當要完成的state校驗是如何處理的呢?
合理的推測是在authRequest.login(callback);
中的login函數中實現的(這裏的callback
是AuthCallback
類的實例,而AuthCallback
中有code,state等在B階段時會被以GET參數形式被第三方調用回調地址傳回的參數。由於SpringMVC致使這些參數直接被封裝到callback
中了。)。所以進入代碼探究:ide
default AuthResponse login(AuthCallback authCallback) { throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED); }
這是AuthRequest
類中的login方法,從異常信息可知,其依賴子類的具體實現。函數
public abstract class AuthDefaultRequest implements AuthRequest{ //省略 public AuthResponse login(AuthCallback authCallback) { try { AuthChecker.checkCode(source, authCallback); this.checkState(authCallback.getState()); AuthToken authToken = this.getAccessToken(authCallback); AuthUser user = this.getUserInfo(authToken); return AuthResponse.builder().code(AuthResponseStatus.SUCCESS.getCode()).data(user).build(); } catch (Exception e) { Log.error("Failed to login with oauth authorization.", e); return this.responseError(e); } } //省略 }
顯然,答案在this.checkState(authCallback.getState())
之中,去看看checkState方法。工具
public abstract class AuthDefaultRequest implements AuthRequest{ //省略 protected void checkState(String state) { if (StringUtils.isEmpty(state) || !authStateCache.containsKey(state)) { throw new AuthException(AuthResponseStatus.ILLEGAL_REQUEST); } } //省略 }
如今,問題進一步細化。第一,校驗state不爲空無需多言,以後這裏在檢驗state是否存在內存中,也就是說它以前就已經存入了內存,何時?怎麼作的?第二,authStateCache
是什麼?ui
關於第一個疑問,推測是在A階段中的String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
過程當中緩存了state狀態值是最合理的,由於緊接其後的B階段就已經要求校驗了。在此基礎上有兩個衍生推測:其一是AuthStateUtils.createState()
完成了緩存工做,其二是authRequest.authorize(...)
完成了緩存工做。
前往查看代碼
public class AuthStateUtils { public static String createState() { return UuidUtils.getUUID(); } }
猜想一否決。
public String authorize(String state) { return UrlBuilder.fromBaseUrl(source.authorize()) .queryParam("response_type", "code") .queryParam("client_id", config.getClientId()) .queryParam("redirect_uri", config.getRedirectUri()) .queryParam("state", getRealState(state)) .build(); }
進一步懷疑由getRealState(state)
實現
public abstract class AuthDefaultRequest implements AuthRequest{ //省略 protected String getRealState(String state) { if (StringUtils.isEmpty(state)) { state = UuidUtils.getUUID(); } // 緩存state authStateCache.cache(state, state); return state; } //省略 }
猜想證明。確實在這一步完成了state的緩存。接下來就是考慮`
authStateCache`究竟是個什麼東西。與問題二相同。
public abstract class AuthDefaultRequest implements AuthRequest { // 省略 protected AuthStateCache authStateCache; // 省略 }
AuthDefaultRequest
做爲子類繼承了父類AuthDefaultRequest
的成員變量authStateCache
。那麼authStateCache
是被如何賦值?在目前截取到的代碼中,authStateCache
均被直接使用而未見賦值,作出authStateCache
可能在構造器中被賦值的推測是合理的。
public abstract class AuthDefaultRequest implements AuthRequest { protected AuthConfig config; protected AuthSource source; protected AuthStateCache authStateCache; public AuthDefaultRequest(AuthConfig config, AuthSource source) { this(config, source, AuthDefaultStateCache.INSTANCE); } public AuthDefaultRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) { this.config = config; this.source = source; this.authStateCache = authStateCache; if (!AuthChecker.isSupportedAuth(config, source)) { throw new AuthException(AuthResponseStatus.PARAMETER_INCOMPLETE); } // 校驗配置合法性 AuthChecker.checkConfig(config, source); } //省略 }
AuthDefaultRequest
做爲一個抽象類,是不可能被new的,咱們new的通常都是它的具體實現類,具體到QQ上:
public class AuthQqRequest extends AuthDefaultRequest { public AuthQqRequest(AuthConfig config) { super(config, AuthDefaultSource.QQ); } public AuthQqRequest(AuthConfig config, AuthStateCache authStateCache) { super(config, AuthDefaultSource.QQ, authStateCache); } //省略 }
而回看準備階段,用於生成AuthQqRequest
的代碼,咱們是new AuthQqRequest(build)
這樣建立實例的。即
new AuthQqRequest(build) new AuthQqRequest(build,AuthDefaultSource.QQ) new AuthDefaultRequest(build, AuthDefaultSource.QQ) new AuthDefaultRequest(build, AuthDefaultSource.QQ,AuthDefaultStateCache.INSTANCE)
到第四個構造器時圖窮匕見,authStateCache
被賦值爲AuthDefaultStateCache.INSTANCE
,那麼接下來就要看AuthDefaultStateCache.INSTANCE
是個啥了。
public enum AuthDefaultStateCache implements AuthStateCache { INSTANCE; private AuthCache authCache; AuthDefaultStateCache() { authCache = new AuthDefaultCache(); } /** * 存入緩存 */ @Override public void cache(String key, String value) { authCache.set(key, value); } //省略 /** * 是否存在key,若是對應key的value值已過時,也返回false */ @Override public boolean containsKey(String key) { return authCache.containsKey(key); } //省略 }
注意AuthDefaultStateCache
是一個枚舉,再加INSTANCE
,這種寫法實際上是一種經過枚舉實現的單例模式。具體狀況能夠Google,換成常見的單例形式,應該如此:
public enum AuthDefaultStateCache implements AuthStateCache { /* INSTANCE; private AuthCache authCache; AuthDefaultStateCache() { authCache = new AuthDefaultCache(); } */ //如下內容的效用與上面原碼中上面的被註釋部分差很少。主要體現一個單例模式。 private AuthDefaultStateCache(){} // 私有構造 private static AuthDefaultStateCache INSTANCE = null; // 私有單例對象 // 靜態工廠 public static AuthDefaultStateCache getInstance(){ if (INSTANCE == null) { // 雙重檢測機制 synchronized (AuthDefaultStateCache.class) { // 同步鎖 if (INSTANCE == null) { // 雙重檢測機制 INSTANCE = new AuthDefaultStateCache(); } } } return INSTANCE; } //其它部分是一個單例類內部的成員變量和一些方法。不存在什麼等效。 }
既然是單例模式,那麼AuthDefaultStateCache.INSTANCE
在整個應用中都是那一個,天然而然地,在A階段得到它存儲以後,再在B階段得到,仍然是它,也所以天然能夠查詢以前被存下來的state了。