如何向Django的端点发送带有CSRF令牌和JSON请求体的POST请求?
我正在开发一个与运行Django的服务器通信的Android Studio应用。我正在实现手机端的登录流程,但服务器对我的POST请求总是返回 403 Forbidden,即使在header设置了 X-CSRFToken。
在应用中,初始屏幕对 /phone/login 端点发起GET请求,以便从响应头中获取CSRF Token(我在Django视图上使用 @ensure_csrf_cookie 装饰器来确保它被发送)。根据响应,应用导航到登录屏幕,在那里用户可以输入信息。然后该屏幕对 /phone/login 端点发起POST请求,header中带有令牌,主体以JSON形式包含详细信息(由gson转换)。服务器收到后就会处理(尚未实现)。
我在日志中看到POST请求带有header和正确的csrf令牌(也就是我从服务器收到的那一个)。尽管如此,服务器仍然总是返回 403 Forbidden。
为了确保不是header名称的问题,我在Django的设置文件中将 CSRF_HEADER_NAME 显式设为默认值。我也尝试在body发送一个空字符串,获取一个带有 get_token(request) 的csrf令牌,并把它放进 LoginRequestBody 对象中,就像HTML表单那样包含进去,但结果仍然相同。浏览器的登录页工作正常,只有来自应用的请求会遇到这个问题。我当然可以在视图上使用 @crsf_exempt,但因为它仍可从浏览器访问,这会不会有安全隐患?非常感谢任何帮助。
ViewModel.kt:
fun getLogin() {
viewModelScope.launch {
try {
val response = CheckInApi.retrofitService.getLoggedIn()
val body = response.body() ?: LoginResponse()
if (response.code() == 200 ) {
if (body.result == "LOGGED IN") {
_uiState.update { currentState -> currentState.copy(result = AuthenticationState.SUCCESS)}
}
else if (body.result == "NOT LOGGED IN") {
val cookies = response.headers().get("Set-Cookie")
val csrfCookieHeader = cookies?.substring(10,42) ?: "No cookie"
_uiState.update { currentState -> currentState.copy(result = AuthenticationState.LOGIN_REQUIRED,csrfCookieHeader = csrfCookieHeader)}
}
}
}
catch (e: IOException) {
Log.d(TAG, e.toString())
}
}
}
fun postLogin() {
viewModelScope.launch {
val loginRequestBody = LoginRequestBody(_uiState.value.username, _uiState.value.password)
try {
val result = CheckInApi.retrofitService.requestLogin(
uiState.value.csrfCookieHeader,
loginRequestBody)
}
catch (e: IOException) {
Log.d(TAG, e.toString())
}
}
}
CheckInApiService.kt:
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface CheckInApiService {
@POST("phone/login/")
suspend fun requestLogin(@Header("X-CSRFToken") token : String, @Body loginBody : LoginRequestBody): Response<String>
@GET("phone/login/")
suspend fun getLoggedIn(): Response<LoginResponse>
}
object CheckInApi {
val retrofitService : CheckInApiService by lazy {
retrofit.create(CheckInApiService::class.java)
}
}
views.py:
@ensure_csrf_cookie
def login(request):
if request.method not in ["GET", "POST"]:
return HttpResponseNotAllowed(["GET", "POST"])
if request.user.is_authenticated:
return HttpResponse("LOGGED IN")
if request.method == "GET":
response = {
"result" : "NOT LOGGED IN"
}
return JsonResponse(response)
else:
# Log user in
pass
解决方案
我建议在生产API中改用一个REST API库来处理身份验证,尤其是对于移动应用,比如 django-rest-framework,因为它们简化了认证流程并且已经帮你完成了相当多繁重的工作;这将是对你当前的 csrftoken 问题一个很棒且永久的解决办法。