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

Spring API与Android Retrofit2的登录失败及错误处理方案咨询

问题背景

服务器端实现

我搭建了一个带认证的简易Spring API,仅引入了implementation("org.springframework.boot:spring-boot-starter-security")依赖,未登录时浏览器访问接口会跳转至登录页面。
当前使用基础认证,用户名和密码在配置文件resources/application.properties中设置:

spring.security.user.name=myusername
spring.security.user.password=mypassword
spring.security.user.roles=manager

同时使用了Spring Data REST,Spring会自动为项目中的JPA仓库生成API,为此我配置了数据库、创建了表对应的JPA仓库,并引入了implementation("org.springframework.boot:spring-boot-starter-data-rest")依赖。

Android端实现

通过以下Adapter和Client调用API:

interface ApiClient {
    @GET("inventoryItems/1")
    suspend fun getFirstInventoryItem(): Response<InventoryItemDto>
}

object ApiAdapter {

    private const val API_BASE_URL = "http://some.url/"
    private const val API_USERNAME = "myusername"
    private const val API_PASSWORD = "mypassword"

    val apiClient: ApiClient = Retrofit.Builder()
        .baseUrl(API_BASE_URL)
        .client(getHttpClient(API_USERNAME, API_PASSWORD))
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(ApiClient::class.java)

    private fun getHttpClient(user: String, pass: String): OkHttpClient = 
        OkHttpClient
            .Builder()
            .authenticator(getBasicAuth(user,pass))
            .build()

    private fun getBasicAuth(username: String?, password: String?): Authenticator? =
        object : Authenticator {
            override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
                return response
                    .request()
                    .newBuilder()
                    .addHeader("Authorization", Credentials.basic(username, password))
                    .build()
        }
    }
}

调用代码(在Fragment的onViewCreated中执行):

lifecycleScope.launch {
    val item: InventoryItemDto? = ApiAdapter.apiClient.getFirstInventoryItem().body()
    binding?.tvTest?.text = item.toString()
}    

问题现象

输入正确密码时一切正常,但密码错误时Android应用崩溃,抛出java.net.ProtocolException: Too many follow-up requests: 21异常。推测原因是:客户端请求inventoryItems/1后被重定向到登录页,客户端添加Authorization头再次尝试认证,失败后又被重定向,形成循环。

问题1:如何在Android和/或Spring端正确处理登录失败?

问题2:如何在Android和/或Spring端正确处理其他错误(如请求错误)?

已尝试方案

  • 在Android端禁用重定向:
    private fun getHttpClient(user: String, pass: String): OkHttpClient =
        OkHttpClient
            .Builder()
            .followRedirects(false)
            .followSslRedirects(false)
            .authenticator(getBasicAuth(user,pass))
            .build()
    
  • 添加请求头:
    private fun getBasicAuth(username: String?, password: String?):Authenticator? =
             object : Authenticator {
                 override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
                   return response
                     .request()
                     .newBuilder()
                     .addHeader("Authorization", Credentials.basic(username, password))
                     .addHeader("X-Requested-With", "XMLHttpRequest")
                     .build()
         }
     }
    

解决方案

针对问题1:解决登录失败循环崩溃

1. Spring端调整:返回标准HTTP错误而非重定向

Spring Security默认对浏览器请求重定向到登录页,但对于API请求(非浏览器),应该直接返回401 Unauthorized,从根源避免重定向循环。配置示例:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .httpBasic(basic -> basic
                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
            )
            // 禁用默认表单登录重定向
            .formLogin(form -> form.disable());
        return http.build();
    }
}

2. Android端优化:限制重试次数+处理401错误

即使Spring端调整,Android端也需要做好错误兜底:

  • 修改Authenticator限制重试次数:
private fun getBasicAuth(username: String?, password: String?): Authenticator? =
    object : Authenticator {
        override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
            // 仅重试1次,避免无限循环
            val retryCount = response.request().header("Retry-Count")?.toInt() ?: 0
            if (retryCount >= 1) {
                return null // 停止重试
            }
            return response.request()
                .newBuilder()
                .addHeader("Authorization", Credentials.basic(username, password))
                .addHeader("Retry-Count", (retryCount + 1).toString())
                .build()
        }
    }
  • 在调用时处理401状态:
lifecycleScope.launch {
    val response = ApiAdapter.apiClient.getFirstInventoryItem()
    if (response.isSuccessful) {
        val item = response.body()
        binding?.tvTest?.text = item.toString()
    } else {
        when (response.code()) {
            401 -> binding?.tvTest?.text = "用户名或密码错误"
            else -> binding?.tvTest?.text = "请求失败,错误码:${response.code()}"
        }
    }
}

针对问题2:处理其他请求错误

1. Spring端:全局异常处理

添加全局异常处理器,返回标准化错误响应:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        ErrorResponse error = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) {
        ErrorResponse error = new ErrorResponse(HttpStatus.FORBIDDEN.value(), "权限不足");
        return new ResponseEntity<>(error, HttpStatus.FORBIDDEN);
    }

    // 自定义错误响应实体
    public static class ErrorResponse {
        private int code;
        private String message;

        public ErrorResponse(int code, String message) {
            this.code = code;
            this.message = message;
        }

        // getter和setter方法
        public int getCode() { return code; }
        public void setCode(int code) { this.code = code; }
        public String getMessage() { return message; }
        public void setMessage(String message) { this.message = message; }
    }
}

2. Android端:完善错误处理逻辑

  • 捕获网络、解析等异常,分场景处理:
lifecycleScope.launch {
    try {
        val response = ApiAdapter.apiClient.getFirstInventoryItem()
        if (response.isSuccessful) {
            val item = response.body()
            binding?.tvTest?.text = item.toString()
        } else {
            handleHttpError(response.code())
        }
    } catch (e: IOException) {
        binding?.tvTest?.text = "网络连接失败,请检查网络"
    } catch (e: Exception) {
        binding?.tvTest?.text = "请求处理失败:${e.message}"
    }
}

private fun handleHttpError(code: Int) {
    val errorMsg = when(code) {
        403 -> "没有权限访问该资源"
        404 -> "请求的资源不存在"
        500 -> "服务器内部错误"
        else -> "请求失败,错误码:$code"
    }
    binding?.tvTest?.text = errorMsg
}
  • 可选:添加日志拦截器方便调试:
private fun getHttpClient(user: String, pass: String): OkHttpClient = 
    OkHttpClient
        .Builder()
        .authenticator(getBasicAuth(user,pass))
        .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
        .build()

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 08:40:35