You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

安卓应用对接MyAnimeList OAuth2授权使用Mal4J获token返回400错误求助

MyAnimeList安卓端OAuth2授权获取访问令牌返回400错误

我正在为我的安卓应用使用OAuth2协议完成MyAnimeList平台的用户身份认证,流程参考官方授权指南,目前遇到获取访问令牌时报400错误的问题,具体实现如下:

步骤1:获取授权码

该步骤我使用WebView引导用户输入账号密码完成授权,目前观测可正常获取返回的authorizationCode,实现代码如下:

private static final String REDIRECT_URL = "http://localhost/oauth";
private static final String CLIENT_ID = "9c..."; // 已省略
private static final String OAUTH_BASE_URL = "https://myanimelist.net/v1/oauth2/";

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);

    /*
     * 完成用户认证前,客户端需要生成Code Verifier和Code Challenge。Code Verifier是高熵加密随机字符串,仅允许包含[A-Z]/[a-z]/[0-9]/"-"/"."/"_"/"~"字符,长度需在43到128字符之间。
     * MAL平台仅支持Code Challenge的plain转换规则,即Code Challenge需和Code Verifier取值完全一致。
     */
    String codeChallenge = PKCEGenerator.generateVerifier(128);
    
    webview = findViewById(R.id.login_webview);
    webview.getSettings().setJavaScriptEnabled(true);
    webview.setWebViewClient(new WebViewClient(){
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
            Log.d(TAG, "重定向到: " + request.getUrl());
            Uri url = request.getUrl();
            if(url.toString().contains(REDIRECT_URL)){
                String authorizationCode = url.getQueryParameter("code");
                Log.d(TAG, "获取到授权码: " + authorizationCode);
                webview.setVisibility(View.GONE);
                getUserAccessToken(authorizationCode, codeChallenge);
            }
            return false; 
        }
    });
    authenticateMAL(codeChallenge);
}

private void authenticateMAL(String codeChallenge) {
    Log.d(TAG, "Code Challenge长度(" + codeChallenge.length() + "),取值: " + codeChallenge);
    String loginUrl = OAUTH_BASE_URL + "authorize" +
            "?response_type=code" +
            "&redirect_uri=" + REDIRECT_URL +
            "&client_id=" + CLIENT_ID +
            "&code_challenge=" + codeChallenge;
    Log.d(TAG, "登录地址: " + loginUrl);
    webview.loadUrl(loginUrl);
}

步骤2:获取用户访问令牌与刷新令牌

该步骤我使用Mal4J库完成后续认证流程,实现代码如下:

private void getUserAccessToken(String authorizationCode, String codeChallenge) {

    Single.fromCallable(() -> {
        MyAnimeListAuthenticator authenticator = new MyAnimeListAuthenticator(
                CLIENT_ID, null, authorizationCode, codeChallenge);
        return authenticator.getAccessToken();
    })
        .subscribeOn(Schedulers.io())
        .doOnError(throwable -> {
            Log.e(TAG, "获取令牌出错!", throwable);
        })
        .onErrorComplete()
        .subscribe(token -> {
            Log.d(TAG, "--> 访问令牌: " + token.getToken());
            Log.d(TAG, "--> 刷新令牌: " + token.getRefreshToken());
        });

}

执行后返回如下错误:

E/LoginActivity: Error while retrieving token!
    com.kttdevelopment.mal4j.HttpException: Server returned code 400 from 'https://myanimelist.net/v1/oauth2/token': 
        at com.kttdevelopment.mal4j.MyAnimeListAuthenticator.parseToken(MyAnimeListAuthenticator.java:505)
        at com.kttdevelopment.mal4j.MyAnimeListAuthenticator.<init>(MyAnimeListAuthenticator.java:139)
        at florian.baierl.daily_anime_news.ui.LoginActivity.lambda$getUserAccessToken$0(LoginActivity.java:99)
        at florian.baierl.daily_anime_news.ui.-$$Lambda$LoginActivity$-bBBIb9OKRzdaFNsFkQdJSeVW74.call(Unknown Source:4)
        at io.reactivex.rxjava3.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:43)
        at io.reactivex.rxjava3.core.Single.subscribe(Single.java:4813)
        at io.reactivex.rxjava3.internal.operators.single.SingleSubscribeOn$SubscribeOnObserver.run(SingleSubscribeOn.java:89)
        at io.reactivex.rxjava3.core.Scheduler$DisposeTask.run(Scheduler.java:614)
        at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:65)
        at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:56)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)

补充信息

目前已确认步骤1可正常获取授权码,尝试将code challenge/verifier硬编码为128位连续'A'(AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)后问题仍未解决。
抓取到的请求和响应如下:
请求详情
响应详情


排查方向

  • 回调地址不匹配:这是OAuth2流程中400错误最常见的原因,需要确认三个地方的回调地址完全一致:MAL API申请时填写的回调地址、授权请求中携带的redirect_uri、请求令牌时传入的redirect_uri,包括协议头(http/https)、域名、路径、末尾是否有斜杠都不能有任何差异。
  • 授权码过期:MAL的授权码有效期只有几分钟,拿到授权码后必须立刻发起令牌请求,中间不要插入任何耗时操作。
  • PKCE参数不匹配:要确认请求授权时的code_challenge和请求令牌时的code_verifier完全相同,中间没有被做任何编码、转义、截断处理,同时字符集必须落在MAL要求的[A-Z]/[a-z]/[0-9]/"-"/"."/"_"/"~"范围内。
  • 请求格式错误:令牌接口要求请求头的Content-Type必须为application/x-www-form-urlencoded,可确认下Mal4j发起请求时有没有正确携带这个请求头,参数有没有被错误序列化为JSON格式。
  • 客户端参数错误:确认填写的Client ID和MAL后台分配的完全一致,没有多余的空格、换行或者遗漏字符,应用状态在MAL后台为正常启用状态。

内容的提问来源于stack exchange,提问作者Florian Baierl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 12:27:03