OAuth 2.0 и JWT авторизация на Kotlin: Полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в OAuth 2.0 и JWT на Kotlin

Современные веб-приложения и мобильные сервисы требуют надежной и безопасной системы аутентификации и авторизации. Два наиболее популярных стандарта в этой области — OAuth 2.0 и JWT (JSON Web Token). В этой статье мы подробно разберем, как реализовать OAuth 2.0 и JWT авторизацию на языке Kotlin с использованием фреймворка Spring Boot. Вы узнаете, как настроить сервер авторизации, генерировать и проверять токены, а также защитить свои API-эндпоинты.

Основы OAuth 2.0 и JWT

Что такое OAuth 2.0?

OAuth 2.0 — это протокол авторизации, который позволяет сторонним приложениям получать ограниченный доступ к ресурсам пользователя без передачи его учетных данных (логина и пароля). Вместо этого используется токен доступа, который выдается после успешной аутентификации. Основные участники OAuth 2.0:

  • Resource Owner (владелец ресурса) — пользователь.
  • Client (клиент) — приложение, запрашивающее доступ.
  • Authorization Server — сервер, выдающий токены.
  • Resource Server — сервер, предоставляющий защищенные данные.

Что такое JWT?

JWT (JSON Web Token) — это компактный и самодостаточный формат токена, который содержит информацию о пользователе (claims) в виде JSON-объекта. Токен подписывается с помощью секретного ключа или асимметричного шифрования, что гарантирует его целостность. Структура JWT состоит из трех частей, разделенных точками:

  • Header — заголовок, содержащий тип токена и алгоритм подписи.
  • Payload — полезная нагрузка с данными (например, userId, role, exp).
  • Signature — подпись, созданная на основе header и payload.

Пример JWT: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.ZL1mK9WpXoQ

Настройка проекта Kotlin с Spring Security

Подключение зависимостей

Для начала создадим новый проект Spring Boot на Kotlin. В файл build.gradle.kts добавьте следующие зависимости:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("io.jsonwebtoken:jjwt-api:0.11.5")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
    implementation("com.h2database:h2")
}

Мы используем библиотеку jjwt для работы с JWT и H2 в качестве базы данных для тестирования.

Конфигурация Security

Создадим класс SecurityConfig, который будет настраивать Spring Security и отключать стандартную форму входа:

@Configuration
@EnableWebSecurity
class SecurityConfig(private val jwtAuthFilter: JwtAuthFilter) {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth.requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated()
            }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
        return http.build()
    }
}

Мы разрешаем доступ к эндпоинтам /api/auth/** без аутентификации, а все остальные запросы требуют JWT токена.

Реализация OAuth 2.0 и JWT на Kotlin

Генерация и проверка JWT токена

Создадим сервис JwtService, который будет отвечать за создание и валидацию токенов:

@Service
class JwtService {

    @Value("\${jwt.secret}")
    private lateinit var secretKey: String

    private val expirationMs = 3600000L // 1 час

    fun generateToken(user: User): String {
        val claims = mutableMapOf(
            "id" to user.id.toString(),
            "role" to user.role.name
        )
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(user.email)
            .setIssuedAt(Date())
            .setExpiration(Date(System.currentTimeMillis() + expirationMs))
            .signWith(SignatureAlgorithm.HS256, secretKey.toByteArray())
            .compact()
    }

    fun extractUsername(token: String): String {
        return extractAllClaims(token).subject
    }

    fun isTokenValid(token: String, userDetails: UserDetails): Boolean {
        val username = extractUsername(token)
        return username == userDetails.username && !isTokenExpired(token)
    }

    private fun extractAllClaims(token: String): Claims {
        return Jwts.parser()
            .setSigningKey(secretKey.toByteArray())
            .parseClaimsJws(token)
            .body
    }

    private fun isTokenExpired(token: String): Boolean {
        return extractAllClaims(token).expiration.before(Date())
    }
}

Создание фильтра JWT аутентификации

Фильтр JwtAuthFilter будет перехватывать каждый запрос, извлекать JWT из заголовка Authorization и устанавливать контекст безопасности:

@Component
class JwtAuthFilter(
    private val jwtService: JwtService,
    private val userDetailsService: UserDetailsService
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authHeader = request.getHeader("Authorization")
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response)
            return
        }
        val token = authHeader.substring(7)
        val username = jwtService.extractUsername(token)
        if (username != null && SecurityContextHolder.getContext().authentication == null) {
            val userDetails = userDetailsService.loadUserByUsername(username)
            if (jwtService.isTokenValid(token, userDetails)) {
                val authToken = UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.authorities
                )
                authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
                SecurityContextHolder.getContext().authentication = authToken
            }
        }
        filterChain.doFilter(request, response)
    }
}

OAuth 2

Блоги

Рекомендаций по книгам