EN · DE · RU · FR · ES

#102: DataTransferPublicSession.kt

plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/restPublic/DataTransferPublicSession.kt Тип: Kotlin · Роль: Регистрация плагина · Источник: plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/restPublic/DataTransferPublicSession.kt 281 строка · 216 кода · 46 комментариев · 19 пустых
REST API конечная точка для DataTransferPublicSession. Обрабатывает HTTP-запросы и возвращает JSON-ответы для React-интерфейса.

Структура кода

Аннотации: param, Suppress, Service, JsonIgnore, Autowired

Классы: DataTransferPublicSession, TransferAreaData, CheckAccessResult

Функции (9): checkLogin, login, checkDataBaseEntry, register, unregister, logout, isOwnerOfFile, registerFileAsOwner, getSessionMap

Свойства (28): id, accessToken, password, userInfo, ownedFiles, dataTransferArea, failedAccessMessage, dataTransferAreaDao, data, area, errorMessage, loginProtection, clientIpAddress, offset, seconds, numberOfFailedAttempts, loginResultStatus, dbo, errorMessage, map, map, id, data, map, data...

Импорты: 12 пакетов

Пакет: org.projectforge.plugins.datatransfer.restPublic

Исходный код (сокращённый)

package org.projectforge.plugins.datatransfer.restPublic

import com.fasterxml.jackson.annotation.JsonIgnore
import mu.KotlinLogging
import org.projectforge.business.login.LoginProtection
import org.projectforge.business.login.LoginResultStatus
import org.projectforge.framework.ToStringUtil
import org.projectforge.framework.i18n.translate
import org.projectforge.plugins.datatransfer.DataTransferAreaDO
import org.projectforge.plugins.datatransfer.DataTransferAreaDao
import org.projectforge.rest.config.RestUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import jakarta.servlet.http.HttpServletRequest

private val log = KotlinLogging.logger {}

/**
 * Минимальная обработка сессий для предотвращения повторных входов для внешних пользователей инструмента передачи данных.
 */
@Service
class DataTransferPublicSession {
  internal class TransferAreaData(
    var id: Long,
    var accessToken: String,
    @JsonIgnore var password: String?,
    var userInfo: String?,
    var ownedFiles: MutableList<String> = mutableListOf()
  )

  internal class CheckAccessResult(
    val dataTransferArea: DataTransferAreaDO? = null,
    val failedAccessMessage: String? = null
  )


  @Autowired
  private lateinit var dataTransferAreaDao: DataTransferAreaDao

  /**
   * Проверяет, есть ли у пользователя действительная запись (accessToken/password) в его сессии.
   * @param areaId Поиск области по id в сессии пользователя.
   * @param accessToken Поиск области по accessToken в сессии пользователя.
   */
  internal fun checkLogin(
    request: HttpServletRequest,
    areaId: Long? = null,
    accessToken: String? = null
  ): Pair<DataTransferAreaDO, TransferAreaData>? {
    check(areaId != null || !accessToken.isNullOrBlank())
    val data = if (areaId != null) {
      getSessionMap(request)?.get(areaId)
    } else {
      getSessionMap(request)?.entries?.find { it.value.accessToken == accessToken }?.value
    } ?: return null
    val area = dataTransferAreaDao.getAnonymousArea(data.accessToken) ?: return null
    val errorMessage = checkDataBaseEntry(request, area, data.id, data.accessToken, data.password, data.userInfo)
    if (errorMessage != null) {
      unregister(request, data.id) // Отменить регистрацию, принудительный повторный вход.
      return null
    }
    log.info {
      "Информация о внешнем пользователе восстановлена из сессии: ${ToStringUtil.toJsonString(data)}, ip=${
        RestUtils.getClientIp(
          request
        )
      }"
    }
    return Pair(area, data)
  }

  /**
   * Пытается выполнить вход пользователя. Использует LoginProtection. Не проверяет, выполнен ли уже вход.
   * Идентификатор сессии будет изменён (фиксация сессии), но все предыдущие области входа будут помещены в новую сессию.
   */
  internal fun login(
    request: HttpServletRequest,
    accessToken: String?,
    password: String?,
    userInfo: String?
  ): CheckAccessResult {
    if (accessToken == null || password == null) {
      return CheckAccessResult(failedAccessMessage = LoginResultStatus.FAILED.localizedMessage)
    }
    val loginProtection = LoginProtection.instance()
    val clientIpAddress = RestUtils.getClientIp(request)
    val offset = loginProtection.getFailedLoginTimeOffsetIfExists(accessToken, clientIpAddress)
    if (offset > 0) {
      // Временная задержка всё ещё действует. Игнорируем попытку входа.
      val seconds = (offset / 1000).toString()
      log.warn("Учётная запись для '${accessToken}', ip=$clientIpAddress, userInfo='$userInfo' заблокирована на $seconds секунд из-за неудачных попыток входа. Пожалуйста, попробуйте позже.")
      val numberOfFailedAttempts = loginProtection.getNumberOfFailedLoginAttempts(accessToken, clientIpAddress)
      val loginResultStatus = LoginResultStatus.LOGIN_TIME_OFFSET
      loginResultStatus.setMsgParams(
        seconds,
        numberOfFailedAttempts.toString()
      )
      return CheckAccessResult(failedAccessMessage = loginResultStatus.localizedMessage)
    }

    val dbo = dataTransferAreaDao.getAnonymousArea(accessToken)
    if (dbo == null) {
      log.warn { "Область передачи данных с externalAccessToken '$accessToken' не найдена. Запрос от ip=$clientIpAddress, userInfo='$userInfo'." }
      loginProtection.incrementFailedLoginTimeOffset(accessToken, clientIpAddress)
      return CheckAccessResult(failedAccessMessage = LoginResultStatus.FAILED.localizedMessage)
    }
    val errorMessage = checkDataBaseEntry(request, dbo, dbo.id!!, accessToken, password, userInfo)
    if (errorMessage != null) {
      loginProtection.incrementFailedLoginTimeOffset(accessToken, clientIpAddress)
      return CheckAccessResult(failedAccessMessage = errorMessage)
    }
    // Успешный вход:
    loginProtection.clearLoginTimeOffset(accessToken, null, clientIpAddress)
    log.info { "Область передачи данных с externalAccessToken '$accessToken': вход выполнен успешно с ip=$clientIpAddress, userInfo='$userInfo'." }

    // Фиксация сессии: Изменение JSESSIONID после входа (по соображениям безопасности / XSS-атака на странице входа)
    request.getSession(false)?.let { session ->
      if (!session.isNew) {
        val map = getSessionMap(request)
// ... (сокращено, всего 259 строк)

История Git

868d6abb7 2025 -> 2026
63081666f Заголовки исходных файлов: 2024-> 2025.
4c04cfd65 MAJOR-CHANGE! Миграция целочисленных id на Long id (включая внешние ключи и т.д.)
77bade6df javax.* -> jakarta.*
b6092df09 Авторские права 2023 -> 2024