使用JustAuth在第三方登录中如何实现校验state

前言

本文利用到的JustAuth的传送门。

本文纯属菜鸡视角。在开发者相当简略的官方使用文档的基础上,进入源码查看文档中使用的函数的具体实现,同时通过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);
    }

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了。

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据