Введение в OAuth 2.0 и JWT на Kotlin
Современные веб-приложения и API требуют надежных механизмов аутентификации и авторизации. Два ключевых стандарта — OAuth 2.0 (протокол делегирования доступа) и JWT (JSON Web Token, компактный формат токена) — часто используются вместе для построения безопасных систем. Kotlin, благодаря своей совместимости с Java и лаконичному синтаксису, отлично подходит для реализации этих протоколов.
В этой статье мы разберем, как на практике реализовать OAuth 2.0 и JWT авторизацию на Kotlin. Вы узнаете, как настроить сервер выдачи токенов, создать защищенные эндпоинты и обработать различные гранты (grant types), такие как Authorization Code и Client Credentials.
Основы: OAuth 2.0 и JWT
Что такое OAuth 2.0?
OAuth 2.0 — это протокол, который позволяет сторонним приложениям получать ограниченный доступ к ресурсам пользователя без передачи логина и пароля. Основные участники:
- Resource Owner (владелец ресурса) — обычно пользователь.
- Client (клиент) — приложение, запрашивающее доступ.
- Authorization Server (сервер авторизации) — выдает токены.
- Resource Server (сервер ресурсов) — хранит защищенные данные.
Что такое JWT?
JWT — это компактный, URL-безопасный токен, состоящий из трех частей (header, payload, signature), разделенных точками. Он используется для передачи утверждений (claims) между сторонами. Преимущества JWT:
- Не требует хранения сессии на сервере (stateless).
- Содержит всю необходимую информацию (например, ID пользователя, роли).
- Подписан цифровой подписью, что гарантирует его целостность.
Реализация OAuth 2.0 с JWT на Kotlin (Spring Boot)
Рассмотрим практический пример с использованием Spring Boot и библиотеки Spring Security. Мы создадим простой сервер авторизации и защищенный ресурс.
Шаг 1: Настройка зависимостей
Добавьте в build.gradle.kts необходимые зависимости:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-oauth2-authorization-server")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
}
Шаг 2: Конфигурация сервера авторизации
Создайте класс конфигурации для OAuth 2.0:
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
import java.util.UUID
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun registeredClientRepository(): RegisteredClientRepository {
val registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("my-client")
.clientSecret("{noop}my-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/my-client")
.scope("read")
.scope("write")
.build()
return InMemoryRegisteredClientRepository(registeredClient)
}
@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
@Bean
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
return http.formLogin { }.build()
}
}
Шаг 3: Генерация и проверка JWT
Создайте утилитарный класс для работы с JWT:
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Component
import java.security.Key
import java.util.Date
@Component
class JwtUtil {
private val secretKey: Key = Keys.hmacShaKeyFor("my-secret-key-which-must-be-at-least-256-bits-long".toByteArray())
private val expirationMs = 3600000L // 1 час
fun generateToken(username: String, roles: List<String>): String {
return Jwts.builder()
.subject(username)
.claim("roles", roles)
.issuedAt(Date())
.expiration(Date(System.currentTimeMillis() + expirationMs))
.signWith(secretKey)
.compact()
}
fun validateToken(token: String): Boolean {
return try {
Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.encoded))
.build()
.parseSignedClaims(token)
true
} catch (e: Exception) {
false
}
}
fun extractUsername(token: String): String {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.encoded))
.build()
.parseSignedClaims(token)
.payload
.subject
}
}
Шаг 4: Защита эндпоинтов
Настройте фильтр для проверки JWT на сервере ресурсов:
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
@Component
class JwtAuthFilter(private val jwtUtil: JwtUtil) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val authHeader = request.getHeader("Authorization")
if (authHeader != null && authHeader.startsWith("Bearer ")) {
val token = authHeader.substring(7)
if