使用JustAuth在第三方登錄中如何實現校驗state

前言

本文利用到的JustAuth的傳送門

本文純屬菜雞視角。在開發者相當簡略的官方使用文檔的基礎上,進入源碼查看文檔中使用的函數的具體實現,同時通過QQ第三方登錄這一特例,工具開發者非常規範的命名和註釋,推測整個工具的實現邏輯。

絕大部分第三方登錄採用OAuth2.0協議,其流程符合如下流程圖:
流程圖.png
關於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);
    }


A階段

/**
 * 官方僞代碼
 */
@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());
    }
    }


B階段

/**
 * 官方文檔的僞代碼
 */
@RequestMapping("/callback/{source}")
public Object login(@PathVariable("source") String source, AuthCallback callback) {
    AuthRequest authRequest = getAuthRequest(source);
    AuthResponse response = authRequest.login(callback);
    return response;
}

進行心無大志,醉心QQ的化簡

/**
 * 官方文檔的僞代碼
 */
@RequestMapping("/callback/QQ")
public Object login(AuthCallback callback) {
    AuthRequest authRequest = getAuthQqRequest();//getAuthQqRequest()是準備階段我自用的那個函數
    AuthResponse response = authRequest.login(callback);
    return response;
}

問題來了:這一階段應當要完成的state校驗是如何處理的呢?
合理的推測是在authRequest.login(callback);中的login函數中實現的(這裏的callbackAuthCallback類的實例,而AuthCallback中有code,state等在B階段時會被以GET參數形式被第三方調用回調地址傳回的參數。因爲SpringMVC導致這些參數直接被封裝到callback中了。)。因此進入代碼探究:

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是什麼?

關於第一個疑問,推測是在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了。

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