О чем эта статья?
В предыдущий раз мы разобрали как сделать авторизацию клиентского приложения в EDX Maple по протоколу Oauth2. Сегодня мы разберем как это сделать на практике с помощью Retrofit. Мы будем использовать Kotlin и Spring Framework.
Реализация
Для начала вам понадобятся зависимости для Spring, OkHttp и Retrofit. Подробно я их приводить не буду, только перечислю для нашего клиента Http:
com.squareup.retrofit2:retrofit
com.squareup.retrofit2:converter-jackson
com.squareup.okhttp3:logging-interceptor
com.squareup.okhttp3:okhttp
Далее мы сконфигурируем Retrofit как Bean, чтобы можно было его переиспользовать в различных сервисах. Однако, для начала нам нужно написать Interceptor, который будет перехватывать любой запрос и вставлять в него Bearer заголовок. Получать он его будет отправив запрос на эндпоинт /oauth2/access_token, про который я рассказывал в прошлой статье. Уже на этом этапе у вас может возникнуть диссонанс, как же мы отправим запрос, если у нас еще нет Retrofit клиента? Для этого мы будем использовать отдельный простой клиент, единственной целью которого будет отправка запроса для получения токена авторизации. Код для этого клиента приведен ниже:
private const val GRANT_TYPE = "client_credentials"
@Service
class AuthorizationService(
@Value("\${edx.client.id}")
private val edxClientId: String,
@Value("\${edx.client.secret}")
private val edxClientSecret: String,
@Value("\${edx.api.url}")
private val edxApiUrl: String,
) {
private val authEdxApi = Retrofit.Builder()
.baseUrl(edxApiUrl)
.addConverterFactory(JacksonConverterFactory.create())
.build()
.create(AuthorizationAPI::class.java)
fun getAuthHeader(): OauthJWTResponse {
val response = safeEdxRequest {
authEdxApi.getNewJWT(
jwtOauthRequest = mapOf(
Pair("client_id", edxClientId),
Pair("client_secret", edxClientSecret),
Pair("grant_type", GRANT_TYPE),
),
).execute()
}
return response.body() ?: throw InvalidDataException("Failed to update jwt")
}
}
interface AuthorizationAPI {
@POST("/oauth2/access_token")
@FormUrlEncoded
fun getNewJWT(
@FieldMap jwtOauthRequest: Map<String, String>,
): Call<OauthJWTResponse>
}
Также стоит обратить внимание на safeEdxRequest, который обрабатывает непредвиденные ошибки на EDX:
fun <T> safeEdxRequest(
request: () -> T,
): T {
try {
return request.invoke()
} catch (e: Exception) {
throw EDXUnreachableException(e)
}
}
После создания вспомогательного клиента Retrofit мы можем приступить к написанию и конфигурации основного:
@Configuration
class EdxAuthorizedRetrofitConfig(
@Value("\${edx.api.url}")
private val edxApiUrl: String,
private val authorizationService: AuthorizationService,
) {
@Bean
fun edxAuthorizedRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(edxApiUrl)
.client(getAuthInterceptorOkHttpClient())
.addConverterFactory(JacksonConverterFactory.create())
.build()
}
private fun getAuthInterceptorOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(AuthorizationInterceptor(authorizationService))
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.build()
}
private class AuthorizationInterceptor(
private val authorizationService: AuthorizationService,
) : Interceptor {
private var authHeader: String = ""
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer $authHeader")
.build()
val response = chain.proceed(request)
return when (response.code) {
401, 403 -> {
response.close()
authHeader = authorizationService.getAuthHeader().accessToken
val authRequest = chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer $authHeader")
.build()
chain.proceed(authRequest)
}
else -> {
response
}
}
}
}
}
Тем самым мы получили Http клиент, со встроенным перехватчиком запроса, который обрабатывает ответ с EDX и при получении кодов ответа 401 и 403 пытается обновить токен авторизации.
На этом статья подходит к концу, приведенный метод обновления токена является достаточно универсальным и может применяться не только при интеграции с EDX, но и в общем со сторонними сервисами (и даже без всяких таймеров). Спасибо за внимание!