Введение в 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)
}
}