feat: 扩展前端认证服务,支持短信验证码
- 扩展AuthService添加短信验证码接口 - 更新AuthInterceptor认证拦截器 - 更新AuthModels数据模型
This commit is contained in:
@@ -1,13 +1,7 @@
|
||||
package com.huaga.life_echo.network
|
||||
|
||||
import com.huaga.life_echo.config.AppConfig
|
||||
import com.huaga.life_echo.network.models.ErrorResponse
|
||||
import com.huaga.life_echo.network.models.LoginRequest
|
||||
import com.huaga.life_echo.network.models.RefreshTokenRequest
|
||||
import com.huaga.life_echo.network.models.RegisterRequest
|
||||
import com.huaga.life_echo.network.models.TokenResponse
|
||||
import com.huaga.life_echo.network.models.UserResponse
|
||||
import com.huaga.life_echo.network.models.ValidationErrorResponse
|
||||
import com.huaga.life_echo.network.models.*
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.android.Android
|
||||
@@ -285,4 +279,248 @@ class AuthService {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 短信验证码相关方法
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
suspend fun sendVerificationCode(phone: String, purpose: String): Result<SmsResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/sms/send") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(SmsRequest(phone, purpose))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val smsResponse = response.body<SmsResponse>()
|
||||
Result.success(smsResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("发送验证码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.TooManyRequests -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("发送过于频繁,请稍后再试"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("发送验证码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录
|
||||
*/
|
||||
suspend fun loginWithSms(phone: String, code: String): Result<TokenResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/login/sms") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(SmsLoginRequest(phone, code))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val tokenResponse = response.body<TokenResponse>()
|
||||
Result.success(tokenResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("登录失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("登录失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码注册
|
||||
*/
|
||||
suspend fun registerWithSms(
|
||||
phone: String,
|
||||
code: String,
|
||||
password: String,
|
||||
nickname: String,
|
||||
email: String? = null
|
||||
): Result<TokenResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/register/sms") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(SmsRegisterRequest(phone, code, password, nickname, email))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.Created -> {
|
||||
val tokenResponse = response.body<TokenResponse>()
|
||||
Result.success(tokenResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("注册失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("注册失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
suspend fun resetPassword(phone: String, code: String, newPassword: String): Result<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/password/reset") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(ResetPasswordRequest(phone, code, newPassword))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val messageResponse = response.body<MessageResponse>()
|
||||
Result.success(messageResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest, HttpStatusCode.NotFound -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("重置密码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("重置密码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码(已登录)
|
||||
*/
|
||||
suspend fun changePassword(accessToken: String, oldPassword: String, newPassword: String): Result<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/password/change") {
|
||||
contentType(ContentType.Application.Json)
|
||||
header("Authorization", "Bearer $accessToken")
|
||||
setBody(ChangePasswordRequest(oldPassword, newPassword))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val messageResponse = response.body<MessageResponse>()
|
||||
Result.success(messageResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("修改密码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("修改密码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改手机号
|
||||
*/
|
||||
suspend fun changePhone(accessToken: String, newPhone: String, code: String): Result<UserResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/phone/change") {
|
||||
contentType(ContentType.Application.Json)
|
||||
header("Authorization", "Bearer $accessToken")
|
||||
setBody(ChangePhoneRequest(newPhone, code))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val userResponse = response.body<UserResponse>()
|
||||
Result.success(userResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest -> {
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("修改手机号失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("修改手机号失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出所有设备
|
||||
*/
|
||||
suspend fun logoutAll(accessToken: String): Result<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/logout/all") {
|
||||
header("Authorization", "Bearer $accessToken")
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val messageResponse = response.body<MessageResponse>()
|
||||
Result.success(messageResponse)
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("登出失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.network.AuthService
|
||||
import io.ktor.client.plugins.api.createClientPlugin
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* 认证拦截器配置
|
||||
@@ -17,7 +20,8 @@ data class AuthInterceptorConfig(
|
||||
/**
|
||||
* 认证拦截器插件
|
||||
* 自动在请求头添加Authorization
|
||||
* 注意:401响应处理需要在调用方实现,因为Ktor的响应管道中无法直接重试请求
|
||||
* 当收到401错误时,尝试刷新令牌
|
||||
* 如果刷新失败,清除令牌并触发回调
|
||||
*/
|
||||
val AuthInterceptorPlugin = createClientPlugin("AuthInterceptor", ::AuthInterceptorConfig) {
|
||||
// 在外部作用域捕获配置
|
||||
@@ -34,4 +38,51 @@ val AuthInterceptorPlugin = createClientPlugin("AuthInterceptor", ::AuthIntercep
|
||||
request.headers.append(HttpHeaders.Authorization, "Bearer $accessToken")
|
||||
}
|
||||
}
|
||||
|
||||
onResponse { response ->
|
||||
// 检查是否是401未授权错误
|
||||
if (response.status == HttpStatusCode.Unauthorized) {
|
||||
// 尝试刷新令牌
|
||||
val refreshToken = runBlocking {
|
||||
config.tokenManager?.getRefreshToken()
|
||||
}
|
||||
|
||||
if (!refreshToken.isNullOrBlank() && config.authService != null) {
|
||||
// 尝试刷新令牌
|
||||
val refreshResult = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
config.authService?.refreshToken(refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
refreshResult?.fold(
|
||||
onSuccess = { tokenResponse ->
|
||||
// 刷新成功,保存新令牌
|
||||
runBlocking {
|
||||
config.tokenManager?.saveTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token,
|
||||
config.tokenManager?.getUserId() ?: ""
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
// 刷新失败,清除令牌并触发回调
|
||||
runBlocking {
|
||||
config.tokenManager?.clearTokens()
|
||||
}
|
||||
// 通知 TokenManager,触发回调导航到登录页面
|
||||
config.tokenManager?.notifyTokenRefreshFailed()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 没有刷新令牌,清除令牌并触发回调
|
||||
runBlocking {
|
||||
config.tokenManager?.clearTokens()
|
||||
}
|
||||
// 通知 TokenManager,触发回调导航到登录页面
|
||||
config.tokenManager?.notifyTokenRefreshFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,3 +68,66 @@ data class ValidationErrorDetail(
|
||||
data class ValidationErrorResponse(
|
||||
val detail: List<ValidationErrorDetail>
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 短信验证码相关模型
|
||||
// ============================================================================
|
||||
|
||||
// 发送短信验证码请求
|
||||
@Serializable
|
||||
data class SmsRequest(
|
||||
val phone: String,
|
||||
val purpose: String // register/login/reset_password/change_phone
|
||||
)
|
||||
|
||||
// 短信验证码响应
|
||||
@Serializable
|
||||
data class SmsResponse(
|
||||
val message: String,
|
||||
val expires_in: Int
|
||||
)
|
||||
|
||||
// 验证码登录请求
|
||||
@Serializable
|
||||
data class SmsLoginRequest(
|
||||
val phone: String,
|
||||
val code: String
|
||||
)
|
||||
|
||||
// 验证码注册请求
|
||||
@Serializable
|
||||
data class SmsRegisterRequest(
|
||||
val phone: String,
|
||||
val code: String,
|
||||
val password: String,
|
||||
val nickname: String,
|
||||
val email: String? = null
|
||||
)
|
||||
|
||||
// 重置密码请求
|
||||
@Serializable
|
||||
data class ResetPasswordRequest(
|
||||
val phone: String,
|
||||
val code: String,
|
||||
val new_password: String
|
||||
)
|
||||
|
||||
// 修改密码请求
|
||||
@Serializable
|
||||
data class ChangePasswordRequest(
|
||||
val old_password: String,
|
||||
val new_password: String
|
||||
)
|
||||
|
||||
// 修改手机号请求
|
||||
@Serializable
|
||||
data class ChangePhoneRequest(
|
||||
val new_phone: String,
|
||||
val code: String
|
||||
)
|
||||
|
||||
// 通用消息响应
|
||||
@Serializable
|
||||
data class MessageResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user