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

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

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

Начать курс

OAuth 2.0 и JWT авторизация на Kotlin: для начинающих

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

Что такое OAuth 2.0 и JWT?

Прежде чем писать код, важно понять теорию.

OAuth 2.0 — это протокол авторизации, который позволяет приложениям получать ограниченный доступ к ресурсам пользователя без передачи логина и пароля. Представьте, что вы входите на сайт через "Войти через Google" — это и есть OAuth 2.0.

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

Основные потоки OAuth 2.0:

  • Authorization Code Flow — для серверных приложений (самый безопасный)
  • Client Credentials Flow — для сервер-серверной коммуникации
  • Implicit Flow — устарел, не рекомендуется
  • Resource Owner Password Flow — только если доверяете клиенту

В этой статье мы сосредоточимся на Client Credentials Flow, так как он проще всего для понимания и часто применяется в микросервисах.

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

Создадим новый проект 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-oauth2-resource-server")
    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.fasterxml.jackson.module:jackson-module-kotlin")
}

Эти зависимости обеспечивают поддержку Spring Security, OAuth 2.0 Resource Server и библиотеку JJWT для работы с JWT.

Создание JWT токенов на Kotlin

Напишем сервис для генерации и валидации JWT. Это основа нашей авторизации.

import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Service
import java.util.Date

@Service
class JwtService {
    private val secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
    private val expirationMs = 3600000L // 1 час

    fun generateToken(clientId: String): String {
        val now = Date()
        val expiryDate = Date(now.time + expirationMs)

        return Jwts.builder()
            .setSubject(clientId)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(secretKey)
            .compact()
    }

    fun validateToken(token: String): Boolean {
        return try {
            Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
            true
        } catch (e: Exception) {
            false
        }
    }

    fun getClientIdFromToken(token: String): String {
        val claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .body
        return claims.subject
    }
}

В этом сервисе мы используем симметричный ключ. В продакшене лучше использовать асимметричное шифрование (RS256) и хранить ключи в безопасном хранилище.

Реализация OAuth 2.0 Client Credentials Flow

Теперь настроим Spring Security для работы с 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.http.SessionCreationPolicy
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.config.annotation.web.invoke

@Configuration
class SecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            csrf { disable() }
            sessionManagement {
                sessionCreationPolicy = SessionCreationPolicy.STATELESS
            }
            authorizeHttpRequests {
                authorize("/api/public/**", permitAll)
                authorize("/api/token", permitAll)
                authorize("/api/**", authenticated)
            }
            oauth2ResourceServer {
                jwt { }
            }
        }
        return http.build()
    }
}

Обратите внимание: мы используем DSL синтаксис Kotlin для конфигурации. Это делает код более читаемым.

Создадим контроллер для выдачи токенов:

import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api")
class AuthController(private val jwtService: JwtService) {

    @PostMapping("/token")
    fun getToken(@RequestParam clientId: String,
                 @RequestParam clientSecret: String): ResponseEntity<Map<String, String>> {
        // В реальном проекте здесь проверка clientSecret
        // Для простоты пропустим
        val token = jwtService.generateToken(clientId)
        return ResponseEntity.ok(mapOf("access_token" to token, "token_type" to "Bearer"))
    }

    @GetMapping("/secure")
    fun secureEndpoint(@AuthenticationPrincipal jwt: Jwt): String {
        val clientId = jwt.subject
        return "Hello, $clientId! You have access to this secure endpoint."
    }
}

Защита эндпоинтов и работа с токенами

Теперь добавим публичный эндпоинт и защищённый. Пользователь сначала получает токен через /api/token, затем использует его для доступа к /api/secure.

Пример запроса на получение токена:

curl -X POST http://localhost:8080/api/token \
  -d "clientId=my-client" \
  -d "clientSecret=my-secret"

Ответ будет содержать JWT токен. Затем используем его для доступа к защищённому ресурсу:

curl -H "Authorization: Bearer <ваш_токен>" \
  http://localhost:8080/api/secure

Чтобы сделать эндпоинт

Блоги

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